summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.babelrc6
-rw-r--r--.gitlab-ci.yml52
-rw-r--r--.gitlab/issue_templates/Security Developer Workflow.md70
-rw-r--r--.gitlab/merge_request_templates/Database Changes.md2
-rw-r--r--.rubocop_todo.yml8
-rw-r--r--CHANGELOG.md235
-rw-r--r--CONTRIBUTING.md60
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile19
-rw-r--r--Gemfile.lock63
-rw-r--r--Gemfile.rails5.lock38
-rw-r--r--LICENSE26
-rw-r--r--README.md6
-rw-r--r--VERSION2
-rw-r--r--app/assets/images/ext_snippet_icons/ext_snippet_icons.pngbin0 -> 1018 bytes
-rw-r--r--app/assets/images/ext_snippet_icons/logo.pngbin0 -> 494 bytes
-rw-r--r--app/assets/javascripts/boards/models/list.js2
-rw-r--r--app/assets/javascripts/branches/branches_delete_modal.js11
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue185
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js33
-rw-r--r--app/assets/javascripts/due_date_select.js9
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight.js17
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_helper.js29
-rw-r--r--app/assets/javascripts/ide/components/changed_file_icon.vue74
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue58
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue93
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue153
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue121
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue60
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/message_field.vue130
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue88
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue59
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue45
-rw-r--r--app/assets/javascripts/ide/components/file_finder/index.vue245
-rw-r--r--app/assets/javascripts/ide/components/file_finder/item.vue113
-rw-r--r--app/assets/javascripts/ide/components/ide.vue114
-rw-r--r--app/assets/javascripts/ide/components/ide_context_bar.vue42
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue7
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue123
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue13
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue5
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue13
-rw-r--r--app/assets/javascripts/ide/constants.js8
-rw-r--r--app/assets/javascripts/ide/ide_router.js4
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js37
-rw-r--r--app/assets/javascripts/ide/lib/common/model_manager.js4
-rw-r--r--app/assets/javascripts/ide/lib/decorations/controller.js9
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js25
-rw-r--r--app/assets/javascripts/ide/lib/editor.js37
-rw-r--r--app/assets/javascripts/ide/lib/keymap.json11
-rw-r--r--app/assets/javascripts/ide/stores/actions.js31
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js38
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js100
-rw-r--r--app/assets/javascripts/ide/stores/getters.js28
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js108
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js14
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js7
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js26
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js71
-rw-r--r--app/assets/javascripts/ide/stores/mutations/tree.js8
-rw-r--r--app/assets/javascripts/ide/stores/state.js2
-rw-r--r--app/assets/javascripts/ide/stores/utils.js4
-rw-r--r--app/assets/javascripts/issuable_context.js4
-rw-r--r--app/assets/javascripts/jobs/components/header.vue150
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_details_block.vue185
-rw-r--r--app/assets/javascripts/jobs/job_details_bundle.js5
-rw-r--r--app/assets/javascripts/labels_select.js5
-rw-r--r--app/assets/javascripts/lib/utils/keycodes.js4
-rw-r--r--app/assets/javascripts/merge_request_tabs.js49
-rw-r--r--app/assets/javascripts/milestone.js22
-rw-r--r--app/assets/javascripts/milestone_select.js14
-rw-r--r--app/assets/javascripts/notes.js52
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue4
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue9
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue11
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue1
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue4
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue1
-rw-r--r--app/assets/javascripts/notes/stores/actions.js3
-rw-r--r--app/assets/javascripts/notes/stores/getters.js3
-rw-r--r--app/assets/javascripts/pages/dashboard/milestones/show/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/milestones/show/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/new/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/shared/project_new.js152
-rw-r--r--app/assets/javascripts/pages/projects/shared/save_project_loader.js12
-rw-r--r--app/assets/javascripts/pages/projects/snippets/show/index.js6
-rw-r--r--app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js2
-rw-r--r--app/assets/javascripts/pages/snippets/show/index.js10
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js205
-rw-r--r--app/assets/javascripts/performance_bar/services/performance_bar_service.js30
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue25
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue53
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue128
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue28
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue21
-rw-r--r--app/assets/javascripts/pipelines/constants.js2
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js39
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js17
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediator.js6
-rw-r--r--app/assets/javascripts/pipelines/services/pipeline_service.js13
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js5
-rw-r--r--app/assets/javascripts/registry/stores/actions.js3
-rw-r--r--app/assets/javascripts/registry/stores/getters.js3
-rw-r--r--app/assets/javascripts/right_sidebar.js12
-rw-r--r--app/assets/javascripts/shared/popover.js33
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js11
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue20
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue12
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue19
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue21
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue18
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue32
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js17
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.vue20
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/help_state.vue (renamed from app/assets/javascripts/sidebar/components/time_tracking/help_state.js)49
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue8
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js1
-rw-r--r--app/assets/javascripts/snippet/snippet_embed.js23
-rw-r--r--app/assets/javascripts/users_select.js7
-rw-r--r--app/assets/javascripts/visibility_select.js21
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue25
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue (renamed from app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js)267
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/callout.vue27
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue88
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue75
-rw-r--r--app/assets/javascripts/vue_shared/components/clipboard_button.vue76
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue217
-rw-r--r--app/assets/javascripts/vue_shared/components/expand_button.vue54
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue132
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue118
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue39
-rw-r--r--app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue2
-rw-r--r--app/assets/stylesheets/application.scss6
-rw-r--r--app/assets/stylesheets/framework/animations.scss80
-rw-r--r--app/assets/stylesheets/framework/banner.scss19
-rw-r--r--app/assets/stylesheets/framework/buttons.scss46
-rw-r--r--app/assets/stylesheets/framework/common.scss1
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss33
-rw-r--r--app/assets/stylesheets/framework/mobile.scss4
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss4
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss9
-rw-r--r--app/assets/stylesheets/framework/snippets.scss27
-rw-r--r--app/assets/stylesheets/framework/variables.scss17
-rw-r--r--app/assets/stylesheets/highlight/embedded.scss3
-rw-r--r--app/assets/stylesheets/highlight/white.scss291
-rw-r--r--app/assets/stylesheets/highlight/white_base.scss290
-rw-r--r--app/assets/stylesheets/pages/builds.scss41
-rw-r--r--app/assets/stylesheets/pages/commits.scss4
-rw-r--r--app/assets/stylesheets/pages/diff.scss6
-rw-r--r--app/assets/stylesheets/pages/issuable.scss28
-rw-r--r--app/assets/stylesheets/pages/labels.scss4
-rw-r--r--app/assets/stylesheets/pages/login.scss20
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss17
-rw-r--r--app/assets/stylesheets/pages/milestone.scss49
-rw-r--r--app/assets/stylesheets/pages/note_form.scss13
-rw-r--r--app/assets/stylesheets/pages/notes.scss16
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss21
-rw-r--r--app/assets/stylesheets/pages/projects.scss5
-rw-r--r--app/assets/stylesheets/pages/repo.scss193
-rw-r--r--app/assets/stylesheets/snippets.scss156
-rw-r--r--app/controllers/admin/application_settings_controller.rb21
-rw-r--r--app/controllers/application_controller.rb1
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb3
-rw-r--r--app/controllers/concerns/checks_collaboration.rb21
-rw-r--r--app/controllers/concerns/issuable_collections.rb4
-rw-r--r--app/controllers/concerns/notes_actions.rb8
-rw-r--r--app/controllers/concerns/renders_notes.rb2
-rw-r--r--app/controllers/concerns/snippets_actions.rb4
-rw-r--r--app/controllers/dashboard/todos_controller.rb2
-rw-r--r--app/controllers/groups/variables_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb6
-rw-r--r--app/controllers/ldap/omniauth_callbacks_controller.rb31
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb144
-rw-r--r--app/controllers/projects/application_controller.rb14
-rw-r--r--app/controllers/projects/commit_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb8
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb4
-rw-r--r--app/controllers/projects/notes_controller.rb2
-rw-r--r--app/controllers/projects/repositories_controller.rb11
-rw-r--r--app/controllers/projects/snippets_controller.rb3
-rw-r--r--app/controllers/projects/variables_controller.rb2
-rw-r--r--app/controllers/projects/wikis_controller.rb19
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/controllers/snippets_controller.rb4
-rw-r--r--app/controllers/users_controller.rb2
-rw-r--r--app/finders/group_descendants_finder.rb6
-rw-r--r--app/finders/issuable_finder.rb11
-rw-r--r--app/finders/merge_request_target_project_finder.rb1
-rw-r--r--app/finders/users_finder.rb12
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/blob_helper.rb8
-rw-r--r--app/helpers/ci_status_helper.rb4
-rw-r--r--app/helpers/commits_helper.rb2
-rw-r--r--app/helpers/compare_helper.rb2
-rw-r--r--app/helpers/diff_helper.rb2
-rw-r--r--app/helpers/gitlab_routing_helper.rb8
-rw-r--r--app/helpers/icons_helper.rb4
-rw-r--r--app/helpers/issuables_helper.rb38
-rw-r--r--app/helpers/issues_helper.rb15
-rw-r--r--app/helpers/markup_helper.rb2
-rw-r--r--app/helpers/merge_requests_helper.rb12
-rw-r--r--app/helpers/milestones_helper.rb91
-rw-r--r--app/helpers/nav_helper.rb2
-rw-r--r--app/helpers/notes_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb69
-rw-r--r--app/helpers/safe_params_helper.rb11
-rw-r--r--app/helpers/snippets_helper.rb35
-rw-r--r--app/helpers/tree_helper.rb2
-rw-r--r--app/mailers/emails/issues.rb6
-rw-r--r--app/models/ability.rb4
-rw-r--r--app/models/broadcast_message.rb6
-rw-r--r--app/models/ci/build.rb45
-rw-r--r--app/models/ci/group_variable.rb2
-rw-r--r--app/models/ci/job_artifact.rb40
-rw-r--r--app/models/ci/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb2
-rw-r--r--app/models/commit.rb2
-rw-r--r--app/models/commit_status.rb2
-rw-r--r--app/models/concerns/atomic_internal_id.rb5
-rw-r--r--app/models/concerns/avatarable.rb5
-rw-r--r--app/models/concerns/awardable.rb14
-rw-r--r--app/models/concerns/cache_markdown_field.rb30
-rw-r--r--app/models/concerns/group_descendant.rb15
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/models/concerns/milestoneish.rb4
-rw-r--r--app/models/concerns/nonatomic_internal_id.rb22
-rw-r--r--app/models/concerns/protected_ref.rb2
-rw-r--r--app/models/concerns/resolvable_discussion.rb2
-rw-r--r--app/models/concerns/routable.rb7
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb30
-rw-r--r--app/models/concerns/uniquify.rb22
-rw-r--r--app/models/deploy_key.rb2
-rw-r--r--app/models/deploy_token.rb7
-rw-r--r--app/models/deployment.rb4
-rw-r--r--app/models/event.rb3
-rw-r--r--app/models/fork_network.rb2
-rw-r--r--app/models/group.rb10
-rw-r--r--app/models/internal_id.rb27
-rw-r--r--app/models/issue.rb19
-rw-r--r--app/models/label.rb4
-rw-r--r--app/models/lfs_object.rb8
-rw-r--r--app/models/merge_request.rb4
-rw-r--r--app/models/merge_request_diff.rb4
-rw-r--r--app/models/milestone.rb7
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/models/notification_recipient.rb12
-rw-r--r--app/models/notification_setting.rb3
-rw-r--r--app/models/project.rb64
-rw-r--r--app/models/project_ci_cd_setting.rb16
-rw-r--r--app/models/project_services/flowdock_service.rb48
-rw-r--r--app/models/project_statistics.rb20
-rw-r--r--app/models/project_wiki.rb8
-rw-r--r--app/models/protected_branch.rb9
-rw-r--r--app/models/repository.rb16
-rw-r--r--app/models/storage/hashed_project.rb4
-rw-r--r--app/models/storage/legacy_project.rb8
-rw-r--r--app/models/todo.rb2
-rw-r--r--app/models/user.rb33
-rw-r--r--app/models/wiki_page.rb18
-rw-r--r--app/policies/ci/build_policy.rb4
-rw-r--r--app/policies/ci/pipeline_schedule_policy.rb14
-rw-r--r--app/policies/group_policy.rb8
-rw-r--r--app/policies/issuable_policy.rb2
-rw-r--r--app/policies/note_policy.rb11
-rw-r--r--app/policies/personal_snippet_policy.rb2
-rw-r--r--app/policies/project_policy.rb96
-rw-r--r--app/policies/project_policy/class_methods.rb19
-rw-r--r--app/presenters/ci/build_presenter.rb21
-rw-r--r--app/presenters/merge_request_presenter.rb11
-rw-r--r--app/presenters/project_presenter.rb19
-rw-r--r--app/serializers/entity_date_helper.rb27
-rw-r--r--app/serializers/issue_entity.rb4
-rw-r--r--app/serializers/job_entity.rb18
-rw-r--r--app/serializers/note_entity.rb6
-rw-r--r--app/services/ci/register_job_service.rb21
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb2
-rw-r--r--app/services/clusters/gcp/verify_provision_status_service.rb2
-rw-r--r--app/services/create_deployment_service.rb4
-rw-r--r--app/services/events/render_service.rb12
-rw-r--r--app/services/import_export_clean_up_service.rb2
-rw-r--r--app/services/labels/transfer_service.rb11
-rw-r--r--app/services/merge_requests/create_service.rb4
-rw-r--r--app/services/notes/render_service.rb13
-rw-r--r--app/services/notification_recipient_service.rb11
-rw-r--r--app/services/notification_service.rb14
-rw-r--r--app/services/projects/create_from_template_service.rb3
-rw-r--r--app/services/projects/destroy_service.rb8
-rw-r--r--app/services/projects/hashed_storage/migrate_repository_service.rb6
-rw-r--r--app/services/projects/transfer_service.rb2
-rw-r--r--app/services/quick_actions/interpret_service.rb26
-rw-r--r--app/services/repository_archive_clean_up_service.rb2
-rw-r--r--app/services/system_note_service.rb2
-rw-r--r--app/services/test_hooks/base_service.rb2
-rw-r--r--app/uploaders/gitlab_uploader.rb4
-rw-r--r--app/uploaders/job_artifact_uploader.rb8
-rw-r--r--app/uploaders/object_storage.rb22
-rw-r--r--app/views/admin/application_settings/_repository_storage.html.haml2
-rw-r--r--app/views/admin/application_settings/_signin.html.haml1
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml1
-rw-r--r--app/views/admin/dashboard/index.html.haml1
-rw-r--r--app/views/admin/projects/show.html.haml2
-rw-r--r--app/views/admin/users/_user.html.haml2
-rw-r--r--app/views/award_emoji/_awards_block.html.haml4
-rw-r--r--app/views/dashboard/issues.atom.builder2
-rw-r--r--app/views/dashboard/issues.html.haml4
-rw-r--r--app/views/devise/shared/_tab_single.html.haml2
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml2
-rw-r--r--app/views/devise/shared/_tabs_normal.html.haml2
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml7
-rw-r--r--app/views/groups/edit.html.haml2
-rw-r--r--app/views/groups/issues.atom.builder2
-rw-r--r--app/views/groups/issues.html.haml2
-rw-r--r--app/views/layouts/header/_new_dropdown.haml4
-rw-r--r--app/views/layouts/mailer.text.erb2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml2
-rw-r--r--app/views/layouts/notify.text.erb2
-rw-r--r--app/views/notify/issue_due_email.html.haml12
-rw-r--r--app/views/notify/issue_due_email.text.erb7
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml2
-rw-r--r--app/views/projects/_last_push.html.haml7
-rw-r--r--app/views/projects/_visibility_select.html.haml9
-rw-r--r--app/views/projects/blob/_viewer.html.haml5
-rw-r--r--app/views/projects/blob/viewers/_highlight_embed.html.haml7
-rw-r--r--app/views/projects/branches/_branch.html.haml6
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml22
-rw-r--r--app/views/projects/clusters/_empty_state.html.haml5
-rw-r--r--app/views/projects/clusters/new.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml11
-rw-r--r--app/views/projects/commit/show.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml5
-rw-r--r--app/views/projects/diffs/_collapsed.html.haml2
-rw-r--r--app/views/projects/edit.html.haml11
-rw-r--r--app/views/projects/empty.html.haml16
-rwxr-xr-x[-rw-r--r--]app/views/projects/forks/new.html.haml2
-rw-r--r--app/views/projects/issues/_issue.html.haml2
-rw-r--r--app/views/projects/issues/_nav_btns.html.haml15
-rw-r--r--app/views/projects/issues/_new_branch.html.haml8
-rw-r--r--app/views/projects/issues/index.atom.builder2
-rw-r--r--app/views/projects/issues/index.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml13
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml9
-rw-r--r--app/views/projects/jobs/index.html.haml2
-rw-r--r--app/views/projects/jobs/show.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_submit.html.haml8
-rw-r--r--app/views/projects/merge_requests/index.html.haml2
-rw-r--r--app/views/projects/notes/_actions.html.haml2
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml2
-rw-r--r--app/views/projects/pipelines/new.html.haml15
-rw-r--r--app/views/projects/protected_branches/_create_protected_branch.html.haml4
-rw-r--r--app/views/projects/protected_branches/_update_protected_branch.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_branches_list.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_dropdown.html.haml4
-rw-r--r--app/views/projects/protected_branches/shared/_index.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_protected_branch.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_tags_list.html.haml2
-rw-r--r--app/views/projects/registry/repositories/index.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml40
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml38
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml14
-rw-r--r--app/views/projects/show.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml6
-rw-r--r--app/views/projects/tags/show.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml33
-rw-r--r--app/views/shared/_auto_devops_callout.html.haml10
-rw-r--r--app/views/shared/_group_form.html.haml7
-rw-r--r--app/views/shared/_label.html.haml26
-rw-r--r--app/views/shared/_ref_switcher.html.haml4
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml13
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar_todo.html.haml6
-rw-r--r--app/views/shared/issuable/form/_merge_request_assignee.html.haml2
-rw-r--r--app/views/shared/members/_group.html.haml2
-rw-r--r--app/views/shared/members/_member.html.haml2
-rw-r--r--app/views/shared/milestones/_deprecation_message.html.haml14
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml37
-rw-r--r--app/views/shared/milestones/_top.html.haml13
-rw-r--r--app/views/shared/notes/_form.html.haml35
-rw-r--r--app/views/shared/notes/_note.html.haml2
-rw-r--r--app/views/shared/snippets/_embed.html.haml24
-rw-r--r--app/views/shared/snippets/_header.html.haml25
-rw-r--r--app/views/shared/snippets/show.js.haml2
-rw-r--r--app/workers/all_queues.yml3
-rw-r--r--app/workers/concerns/mail_scheduler_queue.rb7
-rw-r--r--app/workers/issue_due_scheduler_worker.rb10
-rw-r--r--app/workers/mail_scheduler/issue_due_worker.rb14
-rw-r--r--app/workers/post_receive.rb2
-rw-r--r--app/workers/repository_fork_worker.rb4
-rw-r--r--app/workers/stuck_ci_jobs_worker.rb2
-rwxr-xr-xbin/secpick47
-rwxr-xr-xbin/spinach5
-rw-r--r--changelogs/no-rm-rf-gitlab-basics.yml5
-rw-r--r--changelogs/unreleased/10244-add-project-ci-cd-settings.yml5
-rw-r--r--changelogs/unreleased/16957-issue-due-email.yml5
-rw-r--r--changelogs/unreleased/17516-nested-restore-changelog.yml5
-rw-r--r--changelogs/unreleased/17939-osw-patch-support-gfm.yml5
-rw-r--r--changelogs/unreleased/20394-protected-branches-wildcard.yml5
-rw-r--r--changelogs/unreleased/21677-run-pipeline-word.yml5
-rw-r--r--changelogs/unreleased/23460-send-email-when-pushing-more-commits-to-the-merge-request.yml5
-rw-r--r--changelogs/unreleased/25010-collapsed-sidebar-tooltips.yml5
-rw-r--r--changelogs/unreleased/27210-add-cancel-btn-to-new-page-domain.yml5
-rw-r--r--changelogs/unreleased/30739-fix-joined-information-on-project-members-page.yml5
-rw-r--r--changelogs/unreleased/31114-internal-ids-are-not-atomic.yml5
-rw-r--r--changelogs/unreleased/31591-project-deploy-tokens-to-allow-permanent-access.yml5
-rw-r--r--changelogs/unreleased/33697-remove-ujs-action-big-graph.yml5
-rw-r--r--changelogs/unreleased/33803-drop-json-support-in-project-milestone.yml5
-rw-r--r--changelogs/unreleased/34262-show-current-labels-when-editing.yml5
-rw-r--r--changelogs/unreleased/34604-fix-generated-url-for-external-repository.yml5
-rw-r--r--changelogs/unreleased/35475-lazy-diff.yml5
-rw-r--r--changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml5
-rw-r--r--changelogs/unreleased/39584-nesting-depth-5-framework-dropdowns.yml5
-rw-r--r--changelogs/unreleased/39880-merge-method-api.yml5
-rw-r--r--changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml5
-rw-r--r--changelogs/unreleased/40781-os-to-ce.yml5
-rw-r--r--changelogs/unreleased/41059-calculate-artifact-size-more-efficiently.yml5
-rw-r--r--changelogs/unreleased/41224-pipeline-icons.yml5
-rw-r--r--changelogs/unreleased/41748-vertical-misalignment-login-box.yml5
-rw-r--r--changelogs/unreleased/41758-after-changing-username-url-still-redirects-to-old-route.yml5
-rw-r--r--changelogs/unreleased/41902-add-api-option-to-overwrite-project-description-on-project-export.yml5
-rw-r--r--changelogs/unreleased/41967_issue_api_closed_by_info.yml5
-rw-r--r--changelogs/unreleased/42028-xss-diffs.yml5
-rw-r--r--changelogs/unreleased/42037-long-instance-names-group-names-covers-namespace-dropdown.yml5
-rw-r--r--changelogs/unreleased/42448-change-commit-row-actions-and-sha-design-for-project-commit-list.yml6
-rw-r--r--changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml5
-rw-r--r--changelogs/unreleased/42568-pipeline-empty-state.yml5
-rw-r--r--changelogs/unreleased/42579-fix-sidebar-dropdown-hover-style.yml5
-rw-r--r--changelogs/unreleased/42803-show-new-branch-mr-button.yml5
-rw-r--r--changelogs/unreleased/42880-loss-of-input-text-on-comments-after-preview.yml5
-rw-r--r--changelogs/unreleased/42889-avoid-return-inside-block.yml5
-rw-r--r--changelogs/unreleased/42936-improve-ns-factory-avoid-duplicates.yml6
-rw-r--r--changelogs/unreleased/43098-controller-projects-issuescontroller-show-executes-more-than-100-sql-queries.yml5
-rw-r--r--changelogs/unreleased/43111-controller-projects-mergerequestscontroller-index-executes-more-than-100-sql-queries.yml5
-rw-r--r--changelogs/unreleased/43215-update-design-for-verifying-domains.yml5
-rw-r--r--changelogs/unreleased/43246-checkfilter.yml6
-rw-r--r--changelogs/unreleased/43316-controller-parameters-handling-sensitive-information-should-use-a-more-specific-name.yml5
-rw-r--r--changelogs/unreleased/43404-pipelines-commit.yml5
-rw-r--r--changelogs/unreleased/43466-make-auto-devops-settings-first-class.yml5
-rw-r--r--changelogs/unreleased/43482-enabling-auto-devops-on-an-empty-project-gives-you-wrong-information.yml5
-rw-r--r--changelogs/unreleased/43512-add-support-for-omniauth-jwt-provider.yml5
-rw-r--r--changelogs/unreleased/43525-limit-number-of-failed-logins-using-ldap.yml5
-rw-r--r--changelogs/unreleased/43552-user-owned-projects-query-performance-improvement.yml5
-rw-r--r--changelogs/unreleased/43567-replace-gke.yml5
-rw-r--r--changelogs/unreleased/43603-ci-lint-support.yml5
-rw-r--r--changelogs/unreleased/43617-mailsig.yml5
-rw-r--r--changelogs/unreleased/43702-update-label-dropdown-wording.yml5
-rw-r--r--changelogs/unreleased/43717-breadcrumb-on-admin-runner-page.yml5
-rw-r--r--changelogs/unreleased/43720-update-fe-webpack-docs.yml6
-rw-r--r--changelogs/unreleased/43745-store-metadata-checksum-for-artifacts.yml5
-rw-r--r--changelogs/unreleased/43771-improve-avatar-error-message.yml5
-rw-r--r--changelogs/unreleased/43786-on-the-issuable-list-add-tooltips-to-icons.yml5
-rw-r--r--changelogs/unreleased/43805-list-gitaly-calls-and-arguments-in-the-performance-bar.yml5
-rw-r--r--changelogs/unreleased/43806-update-ruby-saml-to-1-7-2.yml5
-rw-r--r--changelogs/unreleased/43933-always-notify-mentions.yml6
-rw-r--r--changelogs/unreleased/43949-verify-job-artifacts.yml5
-rw-r--r--changelogs/unreleased/43976-fix-access-token-clipboard-button-style.yml5
-rw-r--r--changelogs/unreleased/44022-singular-1-diff.yml5
-rw-r--r--changelogs/unreleased/44139-fix-issue-boards-dup-keys.yml6
-rw-r--r--changelogs/unreleased/44160-update-foreman-to-0-84-0.yml5
-rw-r--r--changelogs/unreleased/44191-reduce-redis-usage-from-merge-request-diffs-caching.yml5
-rw-r--r--changelogs/unreleased/44218-add-internationalization-support-for-the-prometheus-merge-request-widget.yml5
-rw-r--r--changelogs/unreleased/44235-update-knapsack-to-1-16-0.yml5
-rw-r--r--changelogs/unreleased/44257-viewing-a-particular-commit-gives-500-error-error-undefined-method-binary.yml5
-rw-r--r--changelogs/unreleased/44269-show-failure-reason-on-upgrade-tooltip-of-jobs.yml5
-rw-r--r--changelogs/unreleased/44280-fix-code-search.yml5
-rw-r--r--changelogs/unreleased/44291-usage-ping-for-kubernetes-integration.yml5
-rw-r--r--changelogs/unreleased/44382-ui-breakdown-for-create-merge-request.yml5
-rw-r--r--changelogs/unreleased/44383-cleanup-framework-header.yml5
-rw-r--r--changelogs/unreleased/44384-cleanup-css-for-nested-lists.yml5
-rw-r--r--changelogs/unreleased/44386-better-ux-for-long-name-branches.yml5
-rw-r--r--changelogs/unreleased/44388-update-rack-protection-to-2-0-1.yml5
-rw-r--r--changelogs/unreleased/44389-always-allow-http-for-ci-git-operations.yml5
-rw-r--r--changelogs/unreleased/44392-resolve-projects-creation-silently-failing-on-after-create-error.yml5
-rw-r--r--changelogs/unreleased/44425-use-gitlab_environment.yml5
-rw-r--r--changelogs/unreleased/44447-expose-deploy-token-to-ci-cd.yml5
-rw-r--r--changelogs/unreleased/44508-fix-fork-namespace-images.yml5
-rw-r--r--changelogs/unreleased/44541-fix-file-tree-commit-status-cache.yml5
-rw-r--r--changelogs/unreleased/44582-clear-pipeline-status-cache.yml5
-rw-r--r--changelogs/unreleased/44657-reuse-root_ref_hash-on-branches.yml5
-rw-r--r--changelogs/unreleased/44665-fix-db-trace-stream-by-raw-access.yml5
-rw-r--r--changelogs/unreleased/44697-prevue.yml5
-rw-r--r--changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml5
-rw-r--r--changelogs/unreleased/44774-migrate-upload-task-fails-for-upload-with-store-nil.yml5
-rw-r--r--changelogs/unreleased/44775-avatar-on-os-fails-with-cdn.yml5
-rw-r--r--changelogs/unreleased/44776-fix-upload-migrate-fails-for-group.yml5
-rw-r--r--changelogs/unreleased/44834-ide-remove-branch-from-bottom-bar.yml5
-rw-r--r--changelogs/unreleased/44870-remove-extra-space-around-comment-form-on-merge-requests.yml5
-rw-r--r--changelogs/unreleased/44878-update-brakeman-3-6-1-to-4-2-1.yml5
-rw-r--r--changelogs/unreleased/44902-remove-rake-test-ci.yml5
-rw-r--r--changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml5
-rw-r--r--changelogs/unreleased/45159-fix-illustration.yml5
-rw-r--r--changelogs/unreleased/45397-update-faraday_middleware-to-0-12-2.yml5
-rw-r--r--changelogs/unreleased/45398-fix-rss-button.yml5
-rw-r--r--changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml5
-rw-r--r--changelogs/unreleased/45576-fix-create-project-for-user-endpoint.yml5
-rw-r--r--changelogs/unreleased/45666-project-ci-lint-links.yml5
-rw-r--r--changelogs/unreleased/4950-unassign-slash-command-preview-fix.yml5
-rw-r--r--changelogs/unreleased/8088_embedded_snippets_support.yml5
-rw-r--r--changelogs/unreleased/Link_to_project_labels_page.yml5
-rw-r--r--changelogs/unreleased/ab-37462-cache-personal-projects-count.yml5
-rw-r--r--changelogs/unreleased/ab-43150-users-controller-show-query-limit.yml5
-rw-r--r--changelogs/unreleased/ab-44259-atomic-internal-ids-for-all-models.yml5
-rw-r--r--changelogs/unreleased/ab-44467-remove-index.yml5
-rw-r--r--changelogs/unreleased/ac-fix-use_file-race.yml5
-rw-r--r--changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml5
-rw-r--r--changelogs/unreleased/ac-pages-port.yml5
-rwxr-xr-xchangelogs/unreleased/accessible-text.yml6
-rw-r--r--changelogs/unreleased/adamco-gitlab-ce-move-issue-command.yml5
-rw-r--r--changelogs/unreleased/add-canary-favicon.yml5
-rw-r--r--changelogs/unreleased/add-copy-metadata-command.yml5
-rw-r--r--changelogs/unreleased/add-cpu-mem-totals.yml5
-rw-r--r--changelogs/unreleased/add-milestone-path-to-dashboard-milestones-breadcrumb-link.yml5
-rw-r--r--changelogs/unreleased/add-per-runner-job-timeout.yml5
-rw-r--r--changelogs/unreleased/add-query-counts-to-profiler-output.yml5
-rw-r--r--changelogs/unreleased/ajax-requests-in-performance-bar.yml5
-rw-r--r--changelogs/unreleased/align-project-avatar-on-small-viewports.yml5
-rw-r--r--changelogs/unreleased/ash-mckenzie-include-sha-with-version.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-add-missing-changelog-type-to-docs.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-bump-html-pipeline-gem.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-rails5-update-state_machines-activerecord-gem.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-commits-branches-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-commits-comments-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-issues-issues-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-issues-labels-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-issues-milestones-feature.yml5
-rw-r--r--changelogs/unreleased/bvl-export-import-lfs.yml5
-rw-r--r--changelogs/unreleased/bvl-fix-maintainer-push-error.yml5
-rw-r--r--changelogs/unreleased/bvl-import-zip-multiple-assignees.yml5
-rw-r--r--changelogs/unreleased/bvl-no-permanent-redirect.yml5
-rw-r--r--changelogs/unreleased/bvl-override-import-params.yml5
-rw-r--r--changelogs/unreleased/bvl-shared-groups-on-group-page.yml5
-rw-r--r--changelogs/unreleased/ci-pipeline-commit-lookup.yml5
-rw-r--r--changelogs/unreleased/da-gitaly-calculate-repository-checksum.yml5
-rw-r--r--changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml5
-rw-r--r--changelogs/unreleased/deploy-tokens-container-registry-specs.yml5
-rw-r--r--changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml5
-rw-r--r--changelogs/unreleased/direct-upload-of-uploads.yml5
-rw-r--r--changelogs/unreleased/dm-deploy-keys-default-user.yml5
-rw-r--r--changelogs/unreleased/dm-flatten-tree-plus-chars.yml5
-rw-r--r--changelogs/unreleased/dm-internal-user-namespace.yml5
-rw-r--r--changelogs/unreleased/dz-add-2fa-filter-admin-api.yml5
-rw-r--r--changelogs/unreleased/dz-improve-app-settings-2.yml5
-rw-r--r--changelogs/unreleased/escape-autocomplete-values-for-markdown.yml5
-rw-r--r--changelogs/unreleased/expose-commits-mr-api.yml5
-rw-r--r--changelogs/unreleased/feature-add-language-in-repository-to-api.yml5
-rw-r--r--changelogs/unreleased/feature-gb-variables-expressions-in-only-except.yml5
-rw-r--r--changelogs/unreleased/feature_detect_co_authored_commits.yml6
-rw-r--r--changelogs/unreleased/fix-40798-namespace-forking.yml5
-rw-r--r--changelogs/unreleased/fix-42459---in-branch.yml5
-rw-r--r--changelogs/unreleased/fix-500-error-when-mr-ref-is-not-yet-fetched.yml6
-rw-r--r--changelogs/unreleased/fix-auth0-unsafe-login.yml5
-rw-r--r--changelogs/unreleased/fix-dashboard-sorting.yml5
-rw-r--r--changelogs/unreleased/fix-emoji-popup.yml5
-rw-r--r--changelogs/unreleased/fix-mattermost-delete-team.yml5
-rw-r--r--changelogs/unreleased/fix-n-plus-one-when-getting-notification-settings-for-recipients.yml5
-rw-r--r--changelogs/unreleased/fix-projects-no-repository-placeholder.yml5
-rw-r--r--changelogs/unreleased/fj-15329-services-callbacks-ssrf.yml5
-rw-r--r--changelogs/unreleased/fj-174-better-ldap-connection-handling.yml5
-rw-r--r--changelogs/unreleased/fj-41900-import-endpoint-with-overwrite-support.yml5
-rw-r--r--changelogs/unreleased/fj-42354-custom-hooks-not-triggered-by-UI-wiki-edit.yml5
-rw-r--r--changelogs/unreleased/fj-42685-extend-project-export-endpoint.yml5
-rw-r--r--changelogs/unreleased/fj-45057-improve-ssrf-documentation.yml5
-rw-r--r--changelogs/unreleased/fj-change-gollum-gems-to-custom-ones.yml5
-rw-r--r--changelogs/unreleased/fl-fix-annoying-actions.yml5
-rw-r--r--changelogs/unreleased/fl-pipelines-details-axios.yml5
-rw-r--r--changelogs/unreleased/ide-file-finder.yml5
-rw-r--r--changelogs/unreleased/ide-file-row-hover-style.yml5
-rw-r--r--changelogs/unreleased/ide-folder-button-path.yml5
-rw-r--r--changelogs/unreleased/ide-project-avatar-identicon.yml5
-rw-r--r--changelogs/unreleased/improve-jobs-queuing-time-metric.yml5
-rw-r--r--changelogs/unreleased/increase-unicorn-memory-killer-limits.yml5
-rw-r--r--changelogs/unreleased/issue_25542.yml5
-rw-r--r--changelogs/unreleased/issue_40915.yml5
-rw-r--r--changelogs/unreleased/issue_42443.yml5
-rw-r--r--changelogs/unreleased/issue_44270.yml5
-rw-r--r--changelogs/unreleased/issue_45463.yml5
-rw-r--r--changelogs/unreleased/jej-commit-api-tracks-lfs.yml5
-rw-r--r--changelogs/unreleased/jej-mattermost-notification-confidentiality.yml5
-rw-r--r--changelogs/unreleased/jivl-realtime-update-adding-file.yml5
-rw-r--r--changelogs/unreleased/jivl-refactor-activity-calendar.yml5
-rw-r--r--changelogs/unreleased/jivl-summary-statistics-prometheus-dashboard.yml5
-rw-r--r--changelogs/unreleased/jprovazn-issueref.yml6
-rw-r--r--changelogs/unreleased/jramsay-38830-tarball.yml5
-rw-r--r--changelogs/unreleased/label-links-on-project-transfer.yml5
-rw-r--r--changelogs/unreleased/merge-request-widget-source-branch-improvements.yml6
-rw-r--r--changelogs/unreleased/move-email-footer-info-to-single-line.yml5
-rw-r--r--changelogs/unreleased/move-estimate-only-pane-vue-component.yml5
-rw-r--r--changelogs/unreleased/move-help-state-vue-component.yml5
-rw-r--r--changelogs/unreleased/move-pipeline-failed-vue-component.yml5
-rw-r--r--changelogs/unreleased/move-registry-after-cicd-project-nav-sidebar.yml5
-rw-r--r--changelogs/unreleased/optional-api-delimiter.yml5
-rw-r--r--changelogs/unreleased/osw-41401-render-mr-commit-sha-instead-diffs.yml5
-rw-r--r--changelogs/unreleased/osw-44295-adjust-authorization-for-discussions-show.yml5
-rw-r--r--changelogs/unreleased/pages_force_https.yml5
-rw-r--r--changelogs/unreleased/poc-upload-hashing-path.yml5
-rw-r--r--changelogs/unreleased/rd-44635-error-500-when-clicking-ghost-user-or-gitlab-support-bot-in-ui.yml5
-rw-r--r--changelogs/unreleased/rd-45502-uploading-project-export-with-lfs-file-locks-fails.yml5
-rw-r--r--changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml5
-rw-r--r--changelogs/unreleased/refactor-move-assignee-title-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-mr-widget-memory-usage-and-graph-components.yml5
-rw-r--r--changelogs/unreleased/refactor-move-mr-widget-nothing-to-merge-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-mr-widget-ready-to-merge-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-mr-widget-sha-mismatch-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-mr-widget-unresolved-discussions-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-time-tracking-comparison-pane-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-time-tracking-vue-components.yml5
-rw-r--r--changelogs/unreleased/remove-pages-tar-support.yml5
-rw-r--r--changelogs/unreleased/rename-overview-project-sidenav.yml5
-rw-r--r--changelogs/unreleased/restore-label-underline-color.yml5
-rw-r--r--changelogs/unreleased/sh-add-cleanup-rpc-gitaly.yml5
-rw-r--r--changelogs/unreleased/sh-appearance-cache-key-version.yml5
-rw-r--r--changelogs/unreleased/sh-bump-lograge.yml5
-rw-r--r--changelogs/unreleased/sh-gitlab-sidekiq-logger.yml5
-rw-r--r--changelogs/unreleased/sh-move-sidekiq-exporter-logs.yml5
-rw-r--r--changelogs/unreleased/show-group-id-in-group-settings.yml5
-rw-r--r--changelogs/unreleased/show-runners-description-on-jobs-page.yml5
-rw-r--r--changelogs/unreleased/tc-re-add-read-only-banner.yml5
-rw-r--r--changelogs/unreleased/ui-mr-counter-cache.yml5
-rw-r--r--changelogs/unreleased/unresolved-discussions-vue-component-i18n-and-tests.yml5
-rw-r--r--changelogs/unreleased/update-doorkeeper-changelog.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-ci-yml-services-docs.yml5
-rw-r--r--changelogs/unreleased/update-spec-import-path-for-vue-mount-component-helper.yml5
-rw-r--r--changelogs/unreleased/update-unresolved-discussions-vue-component.yml5
-rw-r--r--changelogs/unreleased/use-chronic-duration-attribute-for-project-build-timeout.yml5
-rw-r--r--changelogs/unreleased/winh-41174-projects-groups-badges-ui.yml5
-rw-r--r--changelogs/unreleased/winh-deprecate-old-modal.yml5
-rw-r--r--changelogs/unreleased/winh-dropdown-entry-unlocking.yml5
-rw-r--r--changelogs/unreleased/workhorse-gitaly-mandatory.yml5
-rw-r--r--changelogs/unreleased/zj-branch-containing-sha-opt-out.yml5
-rw-r--r--changelogs/unreleased/zj-bump-gitaly.yml5
-rw-r--r--changelogs/unreleased/zj-feature-gate-remove-http-api.yml5
-rw-r--r--changelogs/unreleased/zj-opt-out-delete-refs.yml5
-rw-r--r--changelogs/unreleased/zj-opt-out-list-commits-by-oid.yml5
-rw-r--r--changelogs/unreleased/zj-ref-exists-opt-out.yml5
-rw-r--r--changelogs/unreleased/zj-remote-repo-exists.yml5
-rw-r--r--changelogs/unreleased/zj-tag-containing-sha-opt-out.yml5
-rw-r--r--config/application.rb1
-rw-r--r--config/initializers/1_settings.rb133
-rw-r--r--config/initializers/2_app.rb8
-rw-r--r--config/initializers/2_gitlab.rb1
-rw-r--r--config/initializers/active_record_avoid_type_casting_in_uniqueness_validator.rb98
-rw-r--r--config/initializers/deprecations.rb5
-rw-r--r--config/initializers/forbid_sidekiq_in_transactions.rb12
-rw-r--r--config/initializers/gollum.rb133
-rw-r--r--config/initializers/lograge.rb18
-rw-r--r--config/karma.config.js21
-rw-r--r--config/routes/user.rb18
-rw-r--r--config/settings.rb126
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--db/fixtures/development/01_admin.rb20
-rw-r--r--db/fixtures/development/10_merge_requests.rb8
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb2
-rw-r--r--db/fixtures/development/19_environments.rb6
-rw-r--r--db/migrate/20161220141214_remove_dot_git_from_group_names.rb12
-rw-r--r--db/migrate/20161226122833_remove_dot_git_from_usernames.rb22
-rw-r--r--db/migrate/20180330121048_add_issue_due_to_notification_settings.rb9
-rw-r--r--db/migrate/20180403035759_create_project_ci_cd_settings.rb68
-rw-r--r--db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb66
-rw-r--r--db/migrate/20180416155103_add_further_scope_columns_to_internal_id_table.rb15
-rw-r--r--db/migrate/20180417090132_add_index_constraints_to_internal_id_table.rb40
-rw-r--r--db/migrate/20180418053107_add_index_to_ci_job_artifacts_file_store.rb15
-rw-r--r--db/migrate/20180425131009_assure_commits_count_for_merge_request_diff.rb27
-rw-r--r--db/post_migrate/20180409170809_populate_missing_project_ci_cd_settings.rb34
-rw-r--r--db/schema.rb19
-rw-r--r--doc/README.md9
-rw-r--r--doc/administration/high_availability/nfs.md94
-rw-r--r--doc/administration/high_availability/redis.md41
-rw-r--r--doc/administration/index.md2
-rw-r--r--doc/administration/pages/index.md36
-rw-r--r--doc/administration/plugins.md5
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/notification_settings.md4
-rw-r--r--doc/api/project_import_export.md8
-rw-r--r--doc/api/projects.md48
-rw-r--r--doc/api/repositories.md2
-rw-r--r--doc/api/todos.md2
-rw-r--r--doc/api/users.md1
-rw-r--r--doc/ci/docker/using_docker_build.md22
-rw-r--r--doc/ci/docker/using_docker_images.md2
-rw-r--r--doc/ci/environments.md2
-rw-r--r--doc/ci/examples/browser_performance.md4
-rw-r--r--doc/ci/examples/code_climate.md6
-rw-r--r--doc/ci/examples/container_scanning.md4
-rw-r--r--doc/ci/examples/dast.md6
-rw-r--r--doc/ci/img/job_failure_reason.pngbin0 -> 5346 bytes
-rw-r--r--doc/ci/pipelines.md19
-rw-r--r--doc/ci/runners/README.md22
-rw-r--r--doc/ci/variables/README.md7
-rw-r--r--doc/ci/yaml/README.md85
-rw-r--r--doc/development/background_migrations.md15
-rw-r--r--doc/development/changelog.md2
-rw-r--r--doc/development/doc_styleguide.md33
-rw-r--r--doc/development/fe_guide/development_process.md77
-rw-r--r--doc/development/fe_guide/index.md50
-rw-r--r--doc/development/fe_guide/vue.md12
-rw-r--r--doc/development/i18n/externalization.md17
-rw-r--r--doc/development/i18n/proofreader.md1
-rw-r--r--doc/development/new_fe_guide/development/components.md20
-rw-r--r--doc/development/testing_guide/best_practices.md67
-rw-r--r--doc/development/testing_guide/end_to_end_tests.md6
-rw-r--r--doc/development/testing_guide/frontend_testing.md76
-rw-r--r--doc/development/testing_guide/testing_rake_tasks.md2
-rw-r--r--doc/gitlab-basics/command-line-commands.md2
-rw-r--r--doc/install/README.md4
-rw-r--r--doc/install/installation.md8
-rw-r--r--doc/install/kubernetes/gitlab_runner_chart.md22
-rw-r--r--doc/integration/github.md4
-rw-r--r--doc/raketasks/backup_restore.md7
-rw-r--r--doc/security/img/outbound_requests_section.pngbin0 -> 18064 bytes
-rw-r--r--doc/security/webhooks.md13
-rw-r--r--doc/topics/autodevops/index.md43
-rw-r--r--doc/update/10.6-to-10.7.md361
-rw-r--r--doc/user/discussions/index.md5
-rw-r--r--doc/user/group/img/groups.pngbin202498 -> 249533 bytes
-rw-r--r--doc/user/group/img/new_group_from_groups.pngbin97271 -> 109302 bytes
-rw-r--r--doc/user/group/img/new_group_from_other_pages.pngbin70899 -> 90423 bytes
-rw-r--r--doc/user/group/index.md11
-rw-r--r--doc/user/permissions.md4
-rw-r--r--doc/user/project/deploy_tokens/index.md12
-rw-r--r--doc/user/project/index.md7
-rw-r--r--doc/user/project/integrations/custom_issue_tracker.md6
-rw-r--r--doc/user/project/integrations/prometheus_library/cloudwatch.md2
-rw-r--r--doc/user/project/integrations/prometheus_library/haproxy.md2
-rw-r--r--doc/user/project/integrations/prometheus_library/metrics.md3
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx.md2
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx_ingress.md6
-rw-r--r--doc/user/project/issues/due_dates.md4
-rw-r--r--doc/user/project/issues/issues_functionalities.md12
-rw-r--r--doc/user/project/labels.md3
-rw-r--r--doc/user/project/merge_requests/index.md12
-rw-r--r--doc/user/project/pages/getting_started_part_three.md2
-rw-r--r--doc/user/project/quick_actions.md7
-rw-r--r--doc/user/project/settings/import_export.md3
-rw-r--r--doc/user/project/settings/index.md17
-rw-r--r--doc/user/project/web_ide/img/commit_changes.pngbin0 -> 672321 bytes
-rw-r--r--doc/user/project/web_ide/img/enable_web_ide.pngbin0 -> 11364 bytes
-rw-r--r--doc/user/project/web_ide/img/open_web_ide.pngbin0 -> 28574 bytes
-rw-r--r--doc/user/project/web_ide/index.md33
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md17
-rw-r--r--doc/workflow/notifications.md14
-rw-r--r--doc/workflow/todos.md4
-rw-r--r--ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb22
-rw-r--r--ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb29
-rw-r--r--features/project/builds/permissions.feature54
-rw-r--r--features/project/commits/branches.feature42
-rw-r--r--features/project/commits/comments.feature51
-rw-r--r--features/project/find_file.feature42
-rw-r--r--features/project/issues/milestones.feature43
-rw-r--r--features/steps/project/builds/permissions.rb7
-rw-r--r--features/steps/project/commits/branches.rb57
-rw-r--r--features/steps/project/issues/milestones.rb58
-rw-r--r--features/steps/project/project_find_file.rb72
-rw-r--r--features/steps/shared/builds.rb25
-rw-r--r--features/steps/shared/markdown.rb4
-rw-r--r--features/steps/shared/note.rb122
-rw-r--r--features/steps/shared/paths.rb28
-rw-r--r--features/steps/shared/project.rb29
-rw-r--r--features/support/capybara.rb8
-rw-r--r--features/support/env.rb6
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/discussions.rb10
-rw-r--r--lib/api/group_variables.rb4
-rw-r--r--lib/api/helpers.rb4
-rw-r--r--lib/api/helpers/notes_helpers.rb4
-rw-r--r--lib/api/helpers/project_snapshots_helpers.rb25
-rw-r--r--lib/api/internal.rb10
-rw-r--r--lib/api/issues.rb2
-rw-r--r--lib/api/job_artifacts.rb2
-rw-r--r--lib/api/jobs.rb4
-rw-r--r--lib/api/merge_requests.rb2
-rw-r--r--lib/api/project_snapshots.rb19
-rw-r--r--lib/api/project_snippets.rb2
-rw-r--r--lib/api/projects.rb17
-rw-r--r--lib/api/repositories.rb4
-rw-r--r--lib/api/runner.rb6
-rw-r--r--lib/api/snippets.rb8
-rw-r--r--lib/api/triggers.rb8
-rw-r--r--lib/api/users.rb2
-rw-r--r--lib/api/v3/builds.rb8
-rw-r--r--lib/api/v3/merge_requests.rb2
-rw-r--r--lib/api/v3/projects.rb2
-rw-r--r--lib/api/v3/snippets.rb6
-rw-r--r--lib/api/v3/triggers.rb4
-rw-r--r--lib/api/variables.rb4
-rw-r--r--lib/backup/files.rb2
-rw-r--r--lib/backup/helper.rb14
-rw-r--r--lib/backup/repository.rb2
-rw-r--r--lib/banzai/commit_renderer.rb2
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb26
-rw-r--r--lib/banzai/filter/commit_range_reference_filter.rb2
-rw-r--r--lib/banzai/filter/commit_reference_filter.rb2
-rw-r--r--lib/banzai/filter/issuable_state_filter.rb3
-rw-r--r--lib/banzai/filter/label_reference_filter.rb4
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb6
-rw-r--r--lib/banzai/filter/redactor_filter.rb6
-rw-r--r--lib/banzai/filter/snippet_reference_filter.rb2
-rw-r--r--lib/banzai/issuable_extractor.rb14
-rw-r--r--lib/banzai/object_renderer.rb32
-rw-r--r--lib/banzai/redactor.rb20
-rw-r--r--lib/banzai/reference_extractor.rb4
-rw-r--r--lib/banzai/reference_parser/base_parser.rb16
-rw-r--r--lib/banzai/reference_parser/commit_range_parser.rb2
-rw-r--r--lib/banzai/reference_parser/issue_parser.rb39
-rw-r--r--lib/banzai/reference_parser/user_parser.rb3
-rw-r--r--lib/banzai/render_context.rb32
-rw-r--r--lib/banzai/renderer/common_mark/html.rb2
-rw-r--r--lib/banzai/renderer/redcarpet/html.rb2
-rw-r--r--lib/declarative_policy/runner.rb2
-rw-r--r--lib/gitlab.rb29
-rw-r--r--lib/gitlab/auth.rb2
-rw-r--r--lib/gitlab/auth/ldap/user.rb9
-rw-r--r--lib/gitlab/auth/o_auth/identity_linker.rb8
-rw-r--r--lib/gitlab/auth/o_auth/user.rb14
-rw-r--r--lib/gitlab/auth/omniauth_identity_linker_base.rb47
-rw-r--r--lib/gitlab/auth/saml/identity_linker.rb8
-rw-r--r--lib/gitlab/auth/saml/user.rb13
-rw-r--r--lib/gitlab/bare_repository_import/importer.rb9
-rw-r--r--lib/gitlab/cache/ci/project_pipeline_status.rb2
-rw-r--r--lib/gitlab/ci/status/build/common.rb8
-rw-r--r--lib/gitlab/ci/status/build/erased.rb2
-rw-r--r--lib/gitlab/ci/trace.rb2
-rw-r--r--lib/gitlab/ci/trace/http_io.rb22
-rw-r--r--lib/gitlab/ci/trace/stream.rb6
-rw-r--r--lib/gitlab/ci/variables/collection/item.rb7
-rw-r--r--lib/gitlab/daemon.rb4
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb13
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb2
-rw-r--r--lib/gitlab/diff/highlight.rb5
-rw-r--r--lib/gitlab/ee_compat_check.rb2
-rw-r--r--lib/gitlab/email/handler/create_merge_request_handler.rb3
-rw-r--r--lib/gitlab/etag_caching/middleware.rb2
-rw-r--r--lib/gitlab/gfm/uploads_rewriter.rb2
-rw-r--r--lib/gitlab/git.rb6
-rw-r--r--lib/gitlab/git/attributes_parser.rb12
-rw-r--r--lib/gitlab/git/commit.rb2
-rw-r--r--lib/gitlab/git/committer_with_hooks.rb47
-rw-r--r--lib/gitlab/git/diff.rb2
-rw-r--r--lib/gitlab/git/info_attributes.rb49
-rw-r--r--lib/gitlab/git/popen.rb4
-rw-r--r--lib/gitlab/git/raw_diff_change.rb60
-rw-r--r--lib/gitlab/git/repository.rb125
-rw-r--r--lib/gitlab/git/repository_mirroring.rb2
-rwxr-xr-xlib/gitlab/git/support/format-git-cat-file-input21
-rw-r--r--lib/gitlab/git/wiki.rb60
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb10
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb25
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb4
-rw-r--r--lib/gitlab/gl_id.rb8
-rw-r--r--lib/gitlab/import_export.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml5
-rw-r--r--lib/gitlab/import_export/relation_factory.rb3
-rw-r--r--lib/gitlab/optimistic_locking.rb19
-rw-r--r--lib/gitlab/sentry.rb23
-rw-r--r--lib/gitlab/shell.rb30
-rw-r--r--lib/gitlab/sidekiq_middleware/shutdown.rb2
-rw-r--r--lib/gitlab/user_access.rb10
-rw-r--r--lib/gitlab/utils.rb4
-rw-r--r--lib/gitlab/view/presenter/base.rb4
-rw-r--r--lib/gitlab/workhorse.rb14
-rw-r--r--lib/rspec_flaky/config.rb4
-rw-r--r--lib/rspec_flaky/flaky_examples_collection.rb10
-rw-r--r--lib/rspec_flaky/listener.rb39
-rw-r--r--lib/rspec_flaky/report.rb54
-rw-r--r--lib/tasks/cache.rake23
-rw-r--r--lib/tasks/gitlab/check.rake5
-rw-r--r--lib/tasks/gitlab/list_repos.rake5
-rw-r--r--lib/tasks/gitlab/setup.rake11
-rw-r--r--lib/tasks/gitlab/storage.rake4
-rw-r--r--locale/gitlab.pot317
-rw-r--r--package.json13
-rw-r--r--qa/qa.rb2
-rw-r--r--qa/qa/factory/base.rb2
-rw-r--r--qa/qa/factory/resource/branch.rb73
-rw-r--r--qa/qa/git/repository.rb6
-rw-r--r--qa/qa/page/README.md4
-rw-r--r--qa/qa/page/group/show.rb2
-rw-r--r--qa/qa/page/merge_request/show.rb2
-rw-r--r--qa/qa/page/project/pipeline/show.rb4
-rw-r--r--qa/qa/page/project/settings/protected_branches.rb69
-rw-r--r--qa/qa/page/project/settings/repository.rb6
-rw-r--r--qa/qa/page/project/show.rb17
-rw-r--r--qa/qa/scenario/template.rb2
-rw-r--r--qa/qa/scenario/test/sanity/selectors.rb2
-rw-r--r--qa/qa/specs/features/repository/protected_branches_spec.rb63
-rw-r--r--rubocop/cop/avoid_break_from_strong_memoize.rb48
-rw-r--r--rubocop/cop/avoid_return_from_blocks.rb77
-rw-r--r--rubocop/cop/gitlab/has_many_through_scope.rb45
-rw-r--r--rubocop/cop/migration/safer_boolean_column.rb4
-rw-r--r--rubocop/rubocop.rb5
-rw-r--r--rubocop/spec_helpers.rb2
-rwxr-xr-xscripts/prune-old-flaky-specs24
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb5
-rw-r--r--spec/controllers/concerns/checks_collaboration_spec.rb55
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb5
-rw-r--r--spec/controllers/import/gitlab_controller_spec.rb5
-rw-r--r--spec/controllers/ldap/omniauth_callbacks_controller_spec.rb58
-rw-r--r--spec/controllers/profiles_controller_spec.rb4
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb6
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb18
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb24
-rw-r--r--spec/db/production/settings_spec.rb7
-rw-r--r--spec/factories/award_emoji.rb4
-rw-r--r--spec/factories/ci/builds.rb8
-rw-r--r--spec/factories/commits.rb2
-rw-r--r--spec/factories/deploy_tokens.rb8
-rw-r--r--spec/factories/gpg_key_subkeys.rb2
-rw-r--r--spec/factories/gpg_keys.rb2
-rw-r--r--spec/factories/gpg_signature.rb2
-rw-r--r--spec/factories/groups.rb8
-rw-r--r--spec/factories/namespaces.rb18
-rw-r--r--spec/factories/notes.rb2
-rw-r--r--spec/factories/projects.rb15
-rw-r--r--spec/fast_spec_helper.rb16
-rw-r--r--spec/features/admin/admin_broadcast_messages_spec.rb2
-rw-r--r--spec/features/admin/admin_settings_spec.rb53
-rw-r--r--spec/features/boards/new_issue_spec.rb7
-rw-r--r--spec/features/boards/sidebar_spec.rb20
-rw-r--r--spec/features/dashboard/projects_spec.rb4
-rw-r--r--spec/features/groups/show_spec.rb2
-rw-r--r--spec/features/ide_spec.rb25
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb9
-rw-r--r--spec/features/issues/todo_spec.rb4
-rw-r--r--spec/features/labels_hierarchy_spec.rb14
-rw-r--r--spec/features/merge_request/user_awards_emoji_spec.rb8
-rw-r--r--spec/features/merge_request/user_cherry_picks_spec.rb8
-rw-r--r--spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb11
-rw-r--r--spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb17
-rw-r--r--spec/features/milestone_spec.rb14
-rw-r--r--spec/features/milestones/show_spec.rb26
-rw-r--r--spec/features/milestones/user_creates_milestone_spec.rb29
-rw-r--r--spec/features/milestones/user_deletes_milestone_spec.rb25
-rw-r--r--spec/features/milestones/user_views_milestone_spec.rb31
-rw-r--r--spec/features/milestones/user_views_milestones_spec.rb35
-rw-r--r--spec/features/oauth_login_spec.rb49
-rw-r--r--spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb70
-rw-r--r--spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb52
-rw-r--r--spec/features/projects/branches/user_creates_branch_spec.rb46
-rw-r--r--spec/features/projects/branches/user_deletes_branch_spec.rb23
-rw-r--r--spec/features/projects/branches/user_views_branches_spec.rb34
-rw-r--r--spec/features/projects/branches_spec.rb22
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb8
-rw-r--r--spec/features/projects/clusters_spec.rb2
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb11
-rw-r--r--spec/features/projects/commit/user_comments_on_commit_spec.rb110
-rw-r--r--spec/features/projects/commit/user_reverts_commit_spec.rb17
-rw-r--r--spec/features/projects/files/user_edits_files_spec.rb23
-rw-r--r--spec/features/projects/files/user_find_file_spec.rb66
-rw-r--r--spec/features/projects/files/user_reads_pipeline_status_spec.rb46
-rw-r--r--spec/features/projects/files/user_uploads_files_spec.rb4
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin341299 -> 343091 bytes
-rw-r--r--spec/features/projects/issues/user_views_issue_spec.rb18
-rw-r--r--spec/features/projects/jobs/permissions_spec.rb130
-rw-r--r--spec/features/projects/jobs/user_browses_jobs_spec.rb2
-rw-r--r--spec/features/projects/jobs_spec.rb19
-rw-r--r--spec/features/projects/merge_request_button_spec.rb12
-rw-r--r--spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb8
-rw-r--r--spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb12
-rw-r--r--spec/features/projects/new_project_spec.rb4
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb8
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb30
-rw-r--r--spec/features/projects/show/user_sees_collaboration_links_spec.rb87
-rw-r--r--spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb8
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb2
-rw-r--r--spec/features/projects/tree/create_file_spec.rb2
-rw-r--r--spec/features/projects/user_uses_shortcuts_spec.rb6
-rw-r--r--spec/features/projects/user_views_empty_project_spec.rb43
-rw-r--r--spec/features/snippets/embedded_snippet_spec.rb25
-rw-r--r--spec/features/users/login_spec.rb2
-rw-r--r--spec/finders/group_descendants_finder_spec.rb9
-rw-r--r--spec/finders/merge_request_target_project_finder_spec.rb6
-rw-r--r--spec/fixtures/big-image.pngbin0 -> 324444 bytes
-rw-r--r--spec/fixtures/exported-project.gzbin2306 -> 2560 bytes
-rw-r--r--spec/fixtures/trace/sample_trace3480
-rw-r--r--spec/helpers/gitlab_routing_helper_spec.rb15
-rw-r--r--spec/helpers/icons_helper_spec.rb7
-rw-r--r--spec/helpers/issuables_helper_spec.rb8
-rw-r--r--spec/helpers/issues_helper_spec.rb45
-rw-r--r--spec/helpers/milestones_helper_spec.rb54
-rw-r--r--spec/helpers/projects_helper_spec.rb74
-rw-r--r--spec/helpers/snippets_helper_spec.rb33
-rw-r--r--spec/initializers/gollum_spec.rb62
-rw-r--r--spec/javascripts/.eslintrc1
-rw-r--r--spec/javascripts/activities_spec.js75
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js2
-rw-r--r--spec/javascripts/blob/blob_file_dropzone_spec.js2
-rw-r--r--spec/javascripts/branches/branches_delete_modal_spec.js40
-rw-r--r--spec/javascripts/collapsed_sidebar_todo_spec.js10
-rw-r--r--spec/javascripts/comment_type_toggle_spec.js5
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js2
-rw-r--r--spec/javascripts/commits_spec.js12
-rw-r--r--spec/javascripts/droplab/hook_spec.js5
-rw-r--r--spec/javascripts/feature_highlight/feature_highlight_helper_spec.js159
-rw-r--r--spec/javascripts/feature_highlight/feature_highlight_spec.js16
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js17
-rw-r--r--spec/javascripts/filtered_search/recent_searches_root_spec.js6
-rw-r--r--spec/javascripts/fixtures/linked_tabs.html.haml2
-rw-r--r--spec/javascripts/fixtures/signin_tabs.html.haml8
-rw-r--r--spec/javascripts/gl_dropdown_spec.js7
-rw-r--r--spec/javascripts/groups/components/app_spec.js5
-rw-r--r--spec/javascripts/groups/components/group_item_spec.js5
-rw-r--r--spec/javascripts/helpers/class_spec_helper_spec.js2
-rw-r--r--spec/javascripts/helpers/vue_component_helper.js21
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js95
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js54
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_item_spec.js15
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_spec.js42
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/message_field_spec.js174
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js13
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js46
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js39
-rw-r--r--spec/javascripts/ide/components/file_finder/index_spec.js308
-rw-r--r--spec/javascripts/ide/components/file_finder/item_spec.js140
-rw-r--r--spec/javascripts/ide/components/ide_spec.js65
-rw-r--r--spec/javascripts/ide/components/repo_commit_section_spec.js101
-rw-r--r--spec/javascripts/ide/components/repo_editor_spec.js57
-rw-r--r--spec/javascripts/ide/components/repo_loading_file_spec.js2
-rw-r--r--spec/javascripts/ide/lib/common/model_spec.js30
-rw-r--r--spec/javascripts/ide/lib/decorations/controller_spec.js29
-rw-r--r--spec/javascripts/ide/lib/diff/controller_spec.js68
-rw-r--r--spec/javascripts/ide/lib/editor_spec.js2
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js65
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js61
-rw-r--r--spec/javascripts/ide/stores/getters_spec.js34
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js146
-rw-r--r--spec/javascripts/ide/stores/modules/commit/getters_spec.js10
-rw-r--r--spec/javascripts/ide/stores/mutations/file_spec.js48
-rw-r--r--spec/javascripts/ide/stores/mutations/tree_spec.js10
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js18
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js25
-rw-r--r--spec/javascripts/issue_show/components/description_spec.js15
-rw-r--r--spec/javascripts/issue_spec.js1
-rw-r--r--spec/javascripts/job_spec.js3
-rw-r--r--spec/javascripts/jobs/header_spec.js34
-rw-r--r--spec/javascripts/jobs/sidebar_details_block_spec.js61
-rw-r--r--spec/javascripts/lib/utils/csrf_token_spec.js2
-rw-r--r--spec/javascripts/lib/utils/image_utility_spec.js8
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js13
-rw-r--r--spec/javascripts/monitoring/monitoring_store_spec.js2
-rw-r--r--spec/javascripts/notes/components/note_actions_spec.js4
-rw-r--r--spec/javascripts/notes/components/note_awards_list_spec.js34
-rw-r--r--spec/javascripts/notes/components/note_body_spec.js1
-rw-r--r--spec/javascripts/notes/mock_data.js15
-rw-r--r--spec/javascripts/notes_spec.js7
-rw-r--r--spec/javascripts/pager_spec.js43
-rw-r--r--spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js5
-rw-r--r--spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js5
-rw-r--r--spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js2
-rw-r--r--spec/javascripts/pipelines/graph/action_component_spec.js47
-rw-r--r--spec/javascripts/pipelines/graph/dropdown_action_component_spec.js32
-rw-r--r--spec/javascripts/pipelines/graph/job_component_spec.js11
-rw-r--r--spec/javascripts/pipelines/mock_data.js326
-rw-r--r--spec/javascripts/pipelines/pipeline_details_mediator_spec.js36
-rw-r--r--spec/javascripts/pipelines/pipelines_spec.js76
-rw-r--r--spec/javascripts/pipelines/stage_spec.js88
-rw-r--r--spec/javascripts/right_sidebar_spec.js4
-rw-r--r--spec/javascripts/search_autocomplete_spec.js4
-rw-r--r--spec/javascripts/settings_panels_spec.js4
-rw-r--r--spec/javascripts/shared/popover_spec.js162
-rw-r--r--spec/javascripts/shortcuts_dashboard_navigation_spec.js23
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js2
-rw-r--r--spec/javascripts/sidebar/sidebar_mediator_spec.js7
-rw-r--r--spec/javascripts/sidebar/sidebar_move_issue_spec.js2
-rw-r--r--spec/javascripts/sidebar/sidebar_store_spec.js2
-rw-r--r--spec/javascripts/signin_tabs_memoizer_spec.js8
-rw-r--r--spec/javascripts/test_bundle.js47
-rw-r--r--spec/javascripts/todos_spec.js5
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js2
-rw-r--r--spec/javascripts/u2f/register_spec.js2
-rw-r--r--spec/javascripts/visibility_select_spec.js98
-rw-r--r--spec/javascripts/vue_mr_widget/components/deployment_spec.js5
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js3
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js11
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js25
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js50
-rw-r--r--spec/javascripts/vue_shared/components/callout_spec.js45
-rw-r--r--spec/javascripts/vue_shared/components/ci_icon_spec.js149
-rw-r--r--spec/javascripts/vue_shared/components/clipboard_button_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js93
-rw-r--r--spec/javascripts/vue_shared/components/expand_button_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/markdown/header_spec.js33
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js16
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js10
-rw-r--r--spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js4
-rw-r--r--spec/lib/api/helpers_spec.rb42
-rw-r--r--spec/lib/backup/files_spec.rb14
-rw-r--r--spec/lib/backup/repository_spec.rb12
-rw-r--r--spec/lib/banzai/commit_renderer_spec.rb5
-rw-r--r--spec/lib/banzai/filter/commit_range_reference_filter_spec.rb16
-rw-r--r--spec/lib/banzai/filter/commit_reference_filter_spec.rb16
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb12
-rw-r--r--spec/lib/banzai/filter/snippet_reference_filter_spec.rb6
-rw-r--r--spec/lib/banzai/issuable_extractor_spec.rb2
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb11
-rw-r--r--spec/lib/banzai/redactor_spec.rb4
-rw-r--r--spec/lib/banzai/reference_parser/base_parser_spec.rb18
-rw-r--r--spec/lib/banzai/reference_parser/commit_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/commit_range_parser_spec.rb16
-rw-r--r--spec/lib/banzai/reference_parser/external_issue_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/issue_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/label_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/merge_request_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/milestone_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/snippet_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/user_parser_spec.rb2
-rw-r--r--spec/lib/banzai/render_context_spec.rb37
-rw-r--r--spec/lib/gitlab/auth/ldap/user_spec.rb8
-rw-r--r--spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb62
-rw-r--r--spec/lib/gitlab/auth/saml/identity_linker_spec.rb48
-rw-r--r--spec/lib/gitlab/bare_repository_import/importer_spec.rb21
-rw-r--r--spec/lib/gitlab/bare_repository_import/repository_spec.rb2
-rw-r--r--spec/lib/gitlab/bitbucket_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/common_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb5
-rw-r--r--spec/lib/gitlab/ci/status/build/play_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/trace_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/variables/collection/item_spec.rb35
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb9
-rw-r--r--spec/lib/gitlab/email/handler_spec.rb10
-rw-r--r--spec/lib/gitlab/git/attributes_parser_spec.rb12
-rw-r--r--spec/lib/gitlab/git/committer_with_hooks_spec.rb154
-rw-r--r--spec/lib/gitlab/git/info_attributes_spec.rb43
-rw-r--r--spec/lib/gitlab/git/raw_diff_change_spec.rb66
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb51
-rw-r--r--spec/lib/gitlab/git/wiki_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb8
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb22
-rw-r--r--spec/lib/gitlab/gitlab_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/google_code_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml8
-rw-r--r--spec/lib/gitlab/import_export/wiki_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/legacy_github_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/sentry_spec.rb44
-rw-r--r--spec/lib/gitlab/shell_spec.rb54
-rw-r--r--spec/lib/gitlab/user_access_spec.rb12
-rw-r--r--spec/lib/gitlab/utils_spec.rb11
-rw-r--r--spec/lib/gitlab/view/presenter/base_spec.rb7
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb22
-rw-r--r--spec/lib/gitlab_spec.rb16
-rw-r--r--spec/lib/rspec_flaky/config_spec.rb30
-rw-r--r--spec/lib/rspec_flaky/flaky_examples_collection_spec.rb14
-rw-r--r--spec/lib/rspec_flaky/listener_spec.rb114
-rw-r--r--spec/lib/rspec_flaky/report_spec.rb125
-rw-r--r--spec/migrations/assure_commits_count_for_merge_request_diff_spec.rb32
-rw-r--r--spec/migrations/create_missing_namespace_for_internal_users_spec.rb42
-rw-r--r--spec/models/ability_spec.rb56
-rw-r--r--spec/models/broadcast_message_spec.rb6
-rw-r--r--spec/models/ci/build_spec.rb90
-rw-r--r--spec/models/ci/job_artifact_spec.rb95
-rw-r--r--spec/models/commit_spec.rb5
-rw-r--r--spec/models/commit_status_spec.rb32
-rw-r--r--spec/models/concerns/avatarable_spec.rb16
-rw-r--r--spec/models/concerns/awardable_spec.rb25
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb183
-rw-r--r--spec/models/concerns/group_descendant_spec.rb17
-rw-r--r--spec/models/concerns/uniquify_spec.rb9
-rw-r--r--spec/models/deploy_token_spec.rb19
-rw-r--r--spec/models/deployment_spec.rb9
-rw-r--r--spec/models/environment_spec.rb4
-rw-r--r--spec/models/internal_id_spec.rb37
-rw-r--r--spec/models/issue_spec.rb63
-rw-r--r--spec/models/lfs_object_spec.rb39
-rw-r--r--spec/models/merge_request_spec.rb8
-rw-r--r--spec/models/milestone_spec.rb32
-rw-r--r--spec/models/namespace_spec.rb9
-rw-r--r--spec/models/note_spec.rb17
-rw-r--r--spec/models/project_ci_cd_setting_spec.rb24
-rw-r--r--spec/models/project_spec.rb117
-rw-r--r--spec/models/project_statistics_spec.rb80
-rw-r--r--spec/models/project_wiki_spec.rb16
-rw-r--r--spec/models/repository_spec.rb6
-rw-r--r--spec/models/user_spec.rb8
-rw-r--r--spec/models/wiki_page_spec.rb2
-rw-r--r--spec/policies/group_policy_spec.rb27
-rw-r--r--spec/policies/note_policy_spec.rb4
-rw-r--r--spec/policies/personal_snippet_policy_spec.rb11
-rw-r--r--spec/policies/project_policy_spec.rb93
-rw-r--r--spec/presenters/ci/build_presenter_spec.rb35
-rw-r--r--spec/presenters/project_presenter_spec.rb13
-rw-r--r--spec/requests/api/merge_requests_spec.rb4
-rw-r--r--spec/requests/api/project_snapshots_spec.rb51
-rw-r--r--spec/requests/api/projects_spec.rb51
-rw-r--r--spec/requests/api/repositories_spec.rb15
-rw-r--r--spec/requests/api/runner_spec.rb2
-rw-r--r--spec/requests/api/users_spec.rb12
-rw-r--r--spec/requests/api/v3/merge_requests_spec.rb4
-rw-r--r--spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb74
-rw-r--r--spec/rubocop/cop/avoid_return_from_blocks_spec.rb127
-rw-r--r--spec/rubocop/cop/gitlab/has_many_through_scope_spec.rb74
-rw-r--r--spec/serializers/entity_date_helper_spec.rb55
-rw-r--r--spec/serializers/job_entity_spec.rb63
-rw-r--r--spec/services/ci/register_job_service_spec.rb105
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb2
-rw-r--r--spec/services/events/render_service_spec.rb6
-rw-r--r--spec/services/groups/destroy_service_spec.rb16
-rw-r--r--spec/services/groups/nested_create_service_spec.rb7
-rw-r--r--spec/services/labels/transfer_service_spec.rb10
-rw-r--r--spec/services/merge_requests/create_service_spec.rb24
-rw-r--r--spec/services/notes/render_service_spec.rb25
-rw-r--r--spec/services/notification_service_spec.rb40
-rw-r--r--spec/services/projects/create_from_template_service_spec.rb22
-rw-r--r--spec/services/projects/create_service_spec.rb5
-rw-r--r--spec/services/projects/destroy_service_spec.rb16
-rw-r--r--spec/services/projects/fork_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/migrate_repository_service_spec.rb14
-rw-r--r--spec/services/projects/transfer_service_spec.rb6
-rw-r--r--spec/services/projects/update_service_spec.rb2
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb76
-rw-r--r--spec/services/system_note_service_spec.rb8
-rw-r--r--spec/services/users/destroy_service_spec.rb4
-rw-r--r--spec/spec_helper.rb63
-rw-r--r--spec/support/capybara.rb29
-rw-r--r--spec/support/controllers/githubish_import_controller_shared_examples.rb9
-rw-r--r--spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb33
-rw-r--r--spec/support/factory_bot.rb3
-rwxr-xr-xspec/support/generate-seed-repo-rb2
-rw-r--r--spec/support/gitlab-git-test.git/README.md2
-rw-r--r--spec/support/helpers/api_helpers.rb (renamed from spec/support/api_helpers.rb)0
-rw-r--r--spec/support/helpers/bare_repo_operations.rb (renamed from spec/support/bare_repo_operations.rb)10
-rw-r--r--spec/support/helpers/board_helpers.rb (renamed from spec/support/board_helpers.rb)0
-rw-r--r--spec/support/helpers/capybara_helpers.rb (renamed from spec/support/capybara_helpers.rb)4
-rw-r--r--spec/support/helpers/cookie_helper.rb (renamed from spec/support/cookie_helper.rb)0
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb (renamed from spec/support/cycle_analytics_helpers.rb)4
-rw-r--r--spec/support/helpers/database_connection_helpers.rb (renamed from spec/support/database_connection_helpers.rb)0
-rw-r--r--spec/support/helpers/devise_helpers.rb (renamed from spec/support/devise_helpers.rb)0
-rw-r--r--spec/support/helpers/drag_to_helper.rb (renamed from spec/support/drag_to_helper.rb)0
-rw-r--r--spec/support/helpers/dropzone_helper.rb (renamed from spec/support/dropzone_helper.rb)0
-rw-r--r--spec/support/helpers/email_helpers.rb (renamed from spec/support/email_helpers.rb)0
-rw-r--r--spec/support/helpers/fake_migration_classes.rb (renamed from spec/support/fake_migration_classes.rb)0
-rw-r--r--spec/support/helpers/fake_u2f_device.rb (renamed from spec/support/fake_u2f_device.rb)0
-rw-r--r--spec/support/helpers/features/branches_helpers.rb33
-rw-r--r--spec/support/helpers/filter_item_select_helper.rb (renamed from spec/support/filter_item_select_helper.rb)0
-rw-r--r--spec/support/helpers/filter_spec_helper.rb (renamed from spec/support/filter_spec_helper.rb)5
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb (renamed from spec/support/filtered_search_helpers.rb)0
-rw-r--r--spec/support/helpers/fixture_helpers.rb (renamed from spec/support/fixture_helpers.rb)4
-rw-r--r--spec/support/helpers/git_http_helpers.rb (renamed from spec/support/git_http_helpers.rb)0
-rw-r--r--spec/support/helpers/gitlab_verify_helpers.rb25
-rw-r--r--spec/support/helpers/gpg_helpers.rb (renamed from spec/support/gpg_helpers.rb)0
-rw-r--r--spec/support/helpers/import_spec_helper.rb (renamed from spec/support/import_spec_helper.rb)0
-rw-r--r--spec/support/helpers/input_helper.rb (renamed from spec/support/input_helper.rb)0
-rw-r--r--spec/support/helpers/inspect_requests.rb (renamed from spec/support/inspect_requests.rb)0
-rw-r--r--spec/support/helpers/issue_helpers.rb (renamed from spec/support/issue_helpers.rb)0
-rw-r--r--spec/support/helpers/javascript_fixtures_helpers.rb (renamed from spec/support/javascript_fixtures_helpers.rb)2
-rw-r--r--spec/support/helpers/jira_service_helper.rb (renamed from spec/support/jira_service_helper.rb)0
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb (renamed from spec/support/kubernetes_helpers.rb)0
-rw-r--r--spec/support/helpers/ldap_helpers.rb (renamed from spec/support/ldap_helpers.rb)4
-rw-r--r--spec/support/helpers/live_debugger.rb (renamed from spec/support/live_debugger.rb)0
-rw-r--r--spec/support/helpers/login_helpers.rb (renamed from spec/support/login_helpers.rb)4
-rw-r--r--spec/support/helpers/markdown_feature.rb (renamed from spec/support/markdown_feature.rb)0
-rw-r--r--spec/support/helpers/merge_request_helpers.rb (renamed from spec/support/merge_request_helpers.rb)0
-rw-r--r--spec/support/helpers/migrations_helpers.rb (renamed from spec/support/migrations_helpers.rb)0
-rw-r--r--spec/support/helpers/mobile_helpers.rb (renamed from spec/support/mobile_helpers.rb)0
-rw-r--r--spec/support/helpers/project_forks_helper.rb (renamed from spec/support/project_forks_helper.rb)0
-rw-r--r--spec/support/helpers/prometheus_helpers.rb (renamed from spec/support/prometheus_helpers.rb)0
-rw-r--r--spec/support/helpers/query_recorder.rb38
-rw-r--r--spec/support/helpers/quick_actions_helpers.rb10
-rw-r--r--spec/support/helpers/rake_helpers.rb (renamed from spec/support/rake_helpers.rb)0
-rw-r--r--spec/support/helpers/reactive_caching_helpers.rb (renamed from spec/support/reactive_caching_helpers.rb)0
-rw-r--r--spec/support/helpers/redis_without_keys.rb (renamed from spec/support/redis_without_keys.rb)0
-rw-r--r--spec/support/helpers/reference_parser_helpers.rb (renamed from spec/support/reference_parser_helpers.rb)8
-rw-r--r--spec/support/helpers/repo_helpers.rb (renamed from spec/support/repo_helpers.rb)0
-rw-r--r--spec/support/helpers/search_helpers.rb (renamed from spec/support/search_helpers.rb)0
-rw-r--r--spec/support/helpers/seed_helper.rb (renamed from spec/support/seed_helper.rb)10
-rw-r--r--spec/support/helpers/seed_repo.rb (renamed from spec/support/seed_repo.rb)0
-rw-r--r--spec/support/helpers/select2_helper.rb (renamed from spec/support/select2_helper.rb)0
-rw-r--r--spec/support/helpers/selection_helper.rb (renamed from spec/support/selection_helper.rb)0
-rw-r--r--spec/support/helpers/sorting_helper.rb18
-rw-r--r--spec/support/helpers/stub_configuration.rb (renamed from spec/support/stub_configuration.rb)3
-rw-r--r--spec/support/helpers/stub_env.rb (renamed from spec/support/stub_env.rb)0
-rw-r--r--spec/support/helpers/stub_feature_flags.rb (renamed from spec/support/stub_feature_flags.rb)0
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb (renamed from spec/support/stub_gitlab_calls.rb)0
-rw-r--r--spec/support/helpers/stub_gitlab_data.rb (renamed from spec/support/stub_gitlab_data.rb)0
-rw-r--r--spec/support/helpers/stub_object_storage.rb (renamed from spec/support/stub_object_storage.rb)2
-rw-r--r--spec/support/helpers/test_env.rb (renamed from spec/support/test_env.rb)5
-rw-r--r--spec/support/helpers/upload_helpers.rb (renamed from spec/support/upload_helpers.rb)0
-rw-r--r--spec/support/helpers/user_activities_helpers.rb (renamed from spec/support/user_activities_helpers.rb)0
-rw-r--r--spec/support/helpers/wait_for_requests.rb (renamed from spec/support/wait_for_requests.rb)0
-rw-r--r--spec/support/helpers/workhorse_helpers.rb (renamed from spec/support/workhorse_helpers.rb)0
-rw-r--r--spec/support/http_io/http_io_helpers.rb3
-rw-r--r--spec/support/issuables_list_metadata_shared_examples.rb46
-rw-r--r--spec/support/json_response.rb (renamed from spec/support/json_response_helpers.rb)4
-rw-r--r--spec/support/matchers/background_migrations_matchers.rb (renamed from spec/support/background_migrations_matchers.rb)0
-rw-r--r--spec/support/matchers/exceed_query_limit.rb (renamed from spec/support/query_recorder.rb)39
-rw-r--r--spec/support/matchers/have_emoji.rb5
-rwxr-xr-xspec/support/prepare-gitlab-git-test-for-commit2
-rw-r--r--spec/support/routing_helpers.rb3
-rw-r--r--spec/support/rspec.rb12
-rw-r--r--spec/support/seed.rb7
-rw-r--r--spec/support/shared_contexts/json_response_shared_context.rb3
-rw-r--r--spec/support/shared_contexts/services_shared_context.rb (renamed from spec/support/services_shared_context.rb)0
-rw-r--r--spec/support/shared_examples/chat_slash_commands_shared_examples.rb (renamed from spec/support/chat_slash_commands_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/email_format_shared_examples.rb (renamed from spec/support/email_format_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/gitlab_verify.rb (renamed from spec/support/gitlab_verify.rb)26
-rw-r--r--spec/support/shared_examples/group_members_shared_example.rb (renamed from spec/support/group_members_shared_example.rb)0
-rw-r--r--spec/support/shared_examples/issuable_shared_examples.rb (renamed from spec/support/issuable_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/issuables_list_metadata_shared_examples.rb62
-rw-r--r--spec/support/shared_examples/issue_tracker_service_shared_example.rb (renamed from spec/support/issue_tracker_service_shared_example.rb)0
-rw-r--r--spec/support/shared_examples/ldap_shared_examples.rb (renamed from spec/support/ldap_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/legacy_path_redirect_shared_examples.rb (renamed from spec/support/legacy_path_redirect_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/malicious_regexp_shared_examples.rb (renamed from spec/support/malicious_regexp_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/mentionable_shared_examples.rb (renamed from spec/support/mentionable_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/milestone_tabs_examples.rb (renamed from spec/support/milestone_tabs_examples.rb)0
-rw-r--r--spec/support/shared_examples/models/atomic_internal_id_spec.rb8
-rw-r--r--spec/support/shared_examples/notify_shared_examples.rb (renamed from spec/support/notify_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/reference_parser_shared_examples.rb (renamed from spec/support/reference_parser_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb (renamed from spec/support/slack_mattermost_notifications_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/snippet_visibility.rb (renamed from spec/support/snippet_visibility.rb)0
-rw-r--r--spec/support/shared_examples/snippets_shared_examples.rb (renamed from spec/support/snippets_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/taskable_shared_examples.rb (renamed from spec/support/taskable_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/time_tracking_shared_examples.rb (renamed from spec/support/time_tracking_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/unique_ip_check_shared_examples.rb (renamed from spec/support/unique_ip_check_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/update_invalid_issuable.rb (renamed from spec/support/update_invalid_issuable.rb)0
-rw-r--r--spec/support/shared_examples/updating_mentions_shared_examples.rb (renamed from spec/support/updating_mentions_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb2
-rw-r--r--spec/tasks/cache/clear/redis_spec.rb19
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb21
-rw-r--r--spec/uploaders/object_storage_spec.rb101
-rw-r--r--spec/views/admin/dashboard/index.html.haml_spec.rb6
-rw-r--r--spec/views/projects/buttons/_dropdown.html.haml_spec.rb3
-rw-r--r--spec/views/projects/commit/_commit_box.html.haml_spec.rb8
-rw-r--r--spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb (renamed from spec/views/projects/settings/ci_cd/_form.html.haml_spec.rb)2
-rw-r--r--spec/views/shared/milestones/_top.html.haml.rb35
-rw-r--r--spec/workers/issue_due_scheduler_worker_spec.rb24
-rw-r--r--spec/workers/mail_scheduler/issue_due_worker_spec.rb21
-rw-r--r--vendor/gitignore/Android.gitignore2
-rw-r--r--vendor/gitignore/Elixir.gitignore1
-rw-r--r--vendor/gitignore/Global/JetBrains.gitignore15
-rw-r--r--vendor/gitignore/Global/Windows.gitignore1
-rw-r--r--vendor/gitignore/Godot.gitignore8
-rw-r--r--vendor/gitignore/Joomla.gitignore1
-rw-r--r--vendor/gitignore/KiCad.gitignore5
-rw-r--r--vendor/gitignore/Leiningen.gitignore1
-rw-r--r--vendor/gitignore/Node.gitignore2
-rw-r--r--vendor/gitignore/Python.gitignore3
-rw-r--r--vendor/gitignore/Rails.gitignore1
-rw-r--r--vendor/gitignore/Rust.gitignore2
-rw-r--r--vendor/gitignore/TeX.gitignore2
-rw-r--r--vendor/gitignore/Unity.gitignore2
-rw-r--r--vendor/gitignore/VisualStudio.gitignore8
-rw-r--r--vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml56
-rw-r--r--vendor/gitlab-ci-yml/Chef.gitlab-ci.yml2
-rw-r--r--vendor/gitlab-ci-yml/Docker.gitlab-ci.yml2
-rw-r--r--vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml2
-rw-r--r--vendor/gitlab-ci-yml/Pages/Gatsby.gitlab-ci.yml17
-rw-r--r--vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml2
-rw-r--r--vendor/gitlab-ci-yml/Python.gitlab-ci.yml23
-rw-r--r--vendor/licenses.csv129
-rw-r--r--vendor/project_templates/express.tar.gzbin5614 -> 5608 bytes
-rw-r--r--vendor/project_templates/rails.tar.gzbin25007 -> 25004 bytes
-rw-r--r--vendor/project_templates/spring.tar.gzbin50945 -> 50938 bytes
-rw-r--r--yarn.lock309
1368 files changed, 21075 insertions, 9175 deletions
diff --git a/.babelrc b/.babelrc
index 8cf07b73420..50d85f58d69 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,6 +1,9 @@
{
"presets": [["latest", { "es2015": { "modules": false } }], "stage-2"],
"env": {
+ "karma": {
+ "plugins": ["rewire"]
+ },
"coverage": {
"plugins": [
[
@@ -14,7 +17,8 @@
{
"process.env.BABEL_ENV": "coverage"
}
- ]
+ ],
+ "rewire"
]
}
}
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4659722854e..5bc2f1f3a0f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-65.0-node-8.x-yarn-1.2-postgresql-9.6"
.dedicated-runner: &dedicated-runner
retry: 1
@@ -75,7 +75,7 @@ stages:
.use-mysql: &use-mysql
services:
- - mysql:latest
+ - mysql:5.7
- redis:alpine
.rails5-variables: &rails5-variables
@@ -289,7 +289,6 @@ stages:
# Trigger a package build in omnibus-gitlab repository
#
package-and-qa:
- <<: *dedicated-runner
image: ruby:2.4-alpine
before_script: []
stage: build
@@ -364,10 +363,11 @@ update-tests-metadata:
- rspec_flaky/
policy: push
script:
- - retry gem install fog-aws mime-types
+ - retry gem install fog-aws mime-types activesupport
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
- scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json
- scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json
+ - FLAKY_RSPEC_GENERATE_REPORT=1 scripts/prune-old-flaky-specs ${FLAKY_RSPEC_SUITE_REPORT_PATH}
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
@@ -720,7 +720,7 @@ codequality:
tags: []
before_script: []
services:
- - docker:dind
+ - docker:stable-dind
variables:
SETUP_DB: "false"
DOCKER_DRIVER: overlay2
@@ -735,16 +735,50 @@ codequality:
expire_in: 1 week
sast:
- <<: *except-docs
- image: registry.gitlab.com/gitlab-org/gl-sast:latest
+ <<: *dedicated-no-docs-no-db-pull-cache-job
+ image: docker:stable
variables:
- CONFIDENCE_LEVEL: 2
+ SAST_CONFIDENCE_LEVEL: 2
+ DOCKER_DRIVER: overlay2
+ allow_failure: true
+ tags: []
before_script: []
+ cache: {}
+ dependencies: []
+ services:
+ - docker:stable-dind
script:
- - /app/bin/run .
+ - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
+ - docker run
+ --env SAST_CONFIDENCE_LEVEL="${SAST_CONFIDENCE_LEVEL:-3}"
+ --volume "$PWD:/code"
+ --volume /var/run/docker.sock:/var/run/docker.sock
+ "registry.gitlab.com/gitlab-org/security-products/sast:$SP_VERSION" /app/bin/run /code
artifacts:
paths: [gl-sast-report.json]
+dependency_scanning:
+ <<: *dedicated-no-docs-no-db-pull-cache-job
+ image: docker:stable
+ variables:
+ DOCKER_DRIVER: overlay2
+ allow_failure: true
+ tags: []
+ before_script: []
+ cache: {}
+ dependencies: []
+ services:
+ - docker:stable-dind
+ script:
+ - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
+ - docker run
+ --env DEP_SCAN_DISABLE_REMOTE_CHECKS="${DEP_SCAN_DISABLE_REMOTE_CHECKS:-false}"
+ --volume "$PWD:/code"
+ --volume /var/run/docker.sock:/var/run/docker.sock
+ "registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$SP_VERSION" /code
+ artifacts:
+ paths: [gl-dependency-scanning-report.json]
+
qa:internal:
<<: *dedicated-no-docs-no-db-pull-cache-job
services: []
diff --git a/.gitlab/issue_templates/Security Developer Workflow.md b/.gitlab/issue_templates/Security Developer Workflow.md
new file mode 100644
index 00000000000..8dd447ed121
--- /dev/null
+++ b/.gitlab/issue_templates/Security Developer Workflow.md
@@ -0,0 +1,70 @@
+<!--
+# Read me first!
+
+Create this issue under https://dev.gitlab.org/gitlab/gitlabhq
+
+Set the title to: `[Security] Description of the original issue`
+-->
+
+### Prior to the security release
+
+- [ ] Read the [security process for developers] if you are not familiar with it.
+- [ ] Link to the original issue adding it to the [links section](#links)
+- [ ] Run `scripts/security-harness` in the CE, EE, and/or Omnibus to prevent pushing to any remote besides `dev.gitlab.org`
+- [ ] Create an MR targetting `org` `master`, prefixing your branch with `security-`
+- [ ] Label your MR with the ~security label, prefix the title with `WIP: [master]`
+- [ ] Add a link to the MR to the [links section](#links)
+- [ ] Add a link to an EE MR if required
+- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**.
+- [ ] Assign the MR to a RM once is reviewed and ready to be merged. Check the [RM list] to see who to ping.
+
+#### Backports
+
+- [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases
+ - [ ] At this point, it might be easy to squash the commits from the MR into one
+ - You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [seckpick documentation]
+ - [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable)
+ - [ ] Create each MR targetting the security branch `security-X-Y`
+ - [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
+- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager.
+
+[seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/process.md#secpick-script
+
+#### Documentation and final details
+
+- [ ] Check the topic on #security to see when the next release is going ot happen and add a link to the [links section](#links)
+- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
+- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
+- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
+- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
+
+### Summary
+#### Links
+
+| Description | Link |
+| -------- | -------- |
+| Original issue | #TODO |
+| Security release issue | #TODO |
+| `master` MR | !TODO |
+| `master` MR (EE) | !TODO |
+| `Backport X.Y` MR | !TODO |
+| `Backport X.Y` MR | !TODO |
+| `Backport X.Y` MR | !TODO |
+| `Backport X.Y` MR (EE) | !TODO |
+| `Backport X.Y` MR (EE) | !TODO |
+| `Backport X.Y` MR (EE) | !TODO |
+
+#### Details
+
+| Description | Details | Further details|
+| -------- | -------- | -------- |
+| Versions affected | X.Y | |
+| Upgrade notes | | |
+| GitLab Settings updated | Yes/No| |
+| Migration required | Yes/No | |
+| Thanks | | |
+
+[security process for developers]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md
+[RM list]: https://about.gitlab.com/release-managers/
+
+/label ~security
diff --git a/.gitlab/merge_request_templates/Database Changes.md b/.gitlab/merge_request_templates/Database Changes.md
index 68bc0fd1c7f..2bb1f374e98 100644
--- a/.gitlab/merge_request_templates/Database Changes.md
+++ b/.gitlab/merge_request_templates/Database Changes.md
@@ -45,4 +45,4 @@ When removing columns, tables, indexes or other structures:
- [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
- [ ] Internationalization required/considered
- [ ] If paid feature, have we considered GitLab.com plan and how it works for groups and is there a design for promoting it to users who aren't on the correct plan
-- [ ] End-to-end tests pass (`package-qa` manual pipeline job)
+- [ ] End-to-end tests pass (`package-and-qa` manual pipeline job)
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index d443238b9e1..16b0b5c95e2 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -143,7 +143,7 @@ Lint/MissingCopEnableDirective:
Lint/NestedPercentLiteral:
Exclude:
- 'lib/gitlab/git/repository.rb'
- - 'spec/support/email_format_shared_examples.rb'
+ - 'spec/support/shared_examples/email_format_shared_examples.rb'
# Offense count: 1
Lint/ReturnInVoidContext:
@@ -195,8 +195,8 @@ Naming/HeredocDelimiterCase:
- 'spec/lib/gitlab/diff/parser_spec.rb'
- 'spec/lib/json_web_token/rsa_token_spec.rb'
- 'spec/models/commit_spec.rb'
- - 'spec/support/repo_helpers.rb'
- - 'spec/support/seed_repo.rb'
+ - 'spec/support/helpers/repo_helpers.rb'
+ - 'spec/support/helpers/seed_repo.rb'
# Offense count: 112
# Configuration parameters: Blacklist.
@@ -496,7 +496,7 @@ Style/EmptyLiteral:
- 'spec/lib/gitlab/request_context_spec.rb'
- 'spec/lib/gitlab/workhorse_spec.rb'
- 'spec/requests/api/jobs_spec.rb'
- - 'spec/support/chat_slash_commands_shared_examples.rb'
+ - 'spec/support/shared_examples/chat_slash_commands_shared_examples.rb'
# Offense count: 102
# Cop supports --auto-correct.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d56c86523f5..8278119cf10 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,241 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 10.7.1 (2018-04-23)
+
+### Fixed (11 changes)
+
+- [API] Fix URLs in the `Link` header for `GET /projects/:id/repository/contributors` when no value is passed for `order_by` or `sort`. !18393
+- Fix a case with secret variables being empty sometimes. !18400
+- Fix `Trace::HttpIO` can not render multi-byte chars. !18417
+- Fix specifying a non-default ref when requesting an archive using the legacy URL. !18468
+- Respect visibility options and description when importing project from template. !18473
+- Removes 'No Job log' message from build trace. !18523
+- Align action icons in pipeline graph.
+- Fix direct_upload when records with null file_store are used.
+- Removed alert box in IDE when redirecting to new merge request.
+- Fixed IDE not loading for sub groups.
+- Fixed IDE not showing loading state when tree is loading.
+
+### Performance (4 changes)
+
+- Validate project path prior to hitting the database. !18322
+- Add index to file_store on ci_job_artifacts. !18444
+- Fix N+1 queries when loading participants for a commit note.
+- Support Markdown rendering using multiple projects.
+
+### Added (1 change)
+
+- Add an API endpoint to download git repository snapshots. !18173
+
+
+## 10.7.0 (2018-04-22)
+
+### Security (6 changes, 2 of them are from the community)
+
+- Fixed some SSRF vulnerabilities in services, hooks and integrations. !2337
+- Update ruby-saml to 1.7.2 and omniauth-saml to 1.10.0. !17734 (Takuya Noguchi)
+- Update rack-protection to 2.0.1. !17835 (Takuya Noguchi)
+- Adds confidential notes channel for Slack/Mattermost.
+- Fix XSS on diff view stored on filenames.
+- Fix GitLab Auth0 integration signing in the wrong user.
+
+### Fixed (65 changes, 20 of them are from the community)
+
+- File uploads in remote storage now support project renaming. !4597
+- Fixed bug in dropdown selector when selecting the same selection again. !14631 (bitsapien)
+- Fixed group deletion linked to Mattermost. !16209 (Julien Millau)
+- Create commit API and Web IDE obey LFS filters. !16718
+- Set breadcrumb for admin/runners/show. !17431 (Takuya Noguchi)
+- Enable restore rake task to handle nested storage directories. !17516 (Balasankar C)
+- Fix hover style of dropdown items in the right sidebar. !17519
+- Improve empty state for canceled job. !17646
+- Fix generated URL when listing repoitories for import. !17692
+- Use singular in the diff stats if only one line has been changed. !17697 (Jan Beckmann)
+- Long instance urls do not overflow anymore during project creation. !17717
+- Fix importing multiple assignees from GitLab export. !17718
+- Correct copy text for the promote milestone and label modals. !17726
+- Fix search results stripping last endline when parsing the results. !17777 (Jasper Maes)
+- Add read-only banner to all pages. !17798
+- Fix viewing diffs on old merge requests. !17805
+- Fix forking to subgroup via API when namespace is given by name. !17815 (Jan Beckmann)
+- Fix UI breakdown for Create merge request button. !17821 (Takuya Noguchi)
+- Unify format for nested non-task lists. !17823 (Takuya Noguchi)
+- UX re-design branch items with flexbox. !17832 (Takuya Noguchi)
+- Use porcelain commit lookup method on CI::CreatePipelineService. !17911
+- Update dashboard milestones breadcrumb link. !17933 (George Tsiolis)
+- Deleting a MR you are assigned to should decrements counter. !17951 (m b)
+- Update no repository placeholder. !17964 (George Tsiolis)
+- Drop JSON response in Project Milestone along with avoiding error. !17977 (Takuya Noguchi)
+- Fix personal access token clipboard button style. !17978 (Fabian Schneider)
+- Avoid validation errors when running the Pages domain verification service. !17992
+- Project creation will now raise an error if a service template is invalid. !18013
+- Add better LDAP connection handling. !18039
+- Fix autolinking URLs containing ampersands. !18045
+- Fix exceptions raised when migrating pipeline stages in the background. !18076
+- Always display Labels section in issuable sidebar, even when the project has no labels. !18081 (Branka Martinovic)
+- Fixed gitlab:uploads:migrate task ignoring some uploads. !18082
+- Fixed gitlab:uploads:migrate task failing for Groups' avatar. !18088
+- Increase dropdown width in pipeline graph & center action icon. !18089
+- Fix `JobsController#raw` endpoint can not read traces in database. !18101
+- Fix `gitlab-rake gitlab:two_factor:disable_for_all_users`. !18154
+- Adjust 404's for LegacyDiffNote discussion rendering. !18201
+- Work around Prometheus Helm chart name changes to fix integration. !18206 (joshlambert)
+- Prioritize weight over title when sorting charts. !18233
+- Verify that deploy token has valid access when pulling container registry image. !18260
+- Stop redirecting the page in pipeline main actions.
+- Fixed IDE button opening the wrong URL in tree list.
+- Ensure hooks run when a deploy key without a user pushes.
+- Fix 404 in group boards when moving issue between lists.
+- Display state indicator for issuable references in non-project scope (e.g. when referencing issuables from group scope).
+- Add missing port to artifact links.
+- Fix data race between ObjectStorage background_upload and Pages publishing.
+- Fixes unresolved discussions rendering the error state instead of the diff.
+- Don't show Jump to Discussion button on Issues.
+- Fix bug rendering group icons when forking.
+- Automatically cleanup stale worktrees and lock files upon a push.
+- Use the GitLab version as part of the appearances cache key.
+- Fix Firefox stealing formatting characters on issue notes.
+- Include matching branches and tags in protected branches / tags count. (Jan Beckmann)
+- Fix 500 error when a merge request from a fork has conflicts and has not yet been updated.
+- Test if remote repository exists when importing wikis.
+- Hide emoji popup after multiple spaces. (Jan Beckmann)
+- Fix relative uri when "#" is in branch name. (Jan)
+- Escape Markdown characters properly when using autocomplete.
+- Ignore project internal references in group context.
+- Fix finding wiki file when Gitaly is enabled.
+- Fix listing commit branch/tags that contain special characters.
+- Ensure internal users (ghost, support bot) get assigned a namespace.
+- Fix links to subdirectories of a directory with a plus character in its path.
+
+### Deprecated (1 change)
+
+- Remove support for legacy tar.gz pages artifacts. !18090
+
+### Changed (22 changes, 2 of them are from the community)
+
+- Add yellow favicon when `CANARY=true` to differientate canary environment. !12477
+- Use human readable value build_timeout in Project. !17386
+- Improved visual styles and consistency for commit hash and possible actions across commit lists. !17406
+- Don't create permanent redirect routes. !17521
+- Add empty repo check before running AutoDevOps pipeline. !17605
+- Update wording to specify create/manage project vs group labels in labels dropdown. !17640
+- Add tooltips to icons in lists of issues and merge requests. !17700
+- Change avatar error message to include allowed file formats. !17747 (Fabian Schneider)
+- Polish design for verifying domains. !17767
+- Move email footer info to a single line. !17916
+- Add average and maximum summary statistics to the prometheus dashboard. !17921
+- Add additional cluster usage metrics to usage ping. !17922
+- Move 'Registry' after 'CI/CD' in project navigation sidebar. !18018 (Elias Werberich)
+- Redesign application settings to match project settings. !18019
+- Allow HTTP(s) when git request is made by GitLab CI. !18021
+- Added hover background color to IDE file list rows.
+- Make project avatar in IDE consistent with the rest of GitLab.
+- Show issues of subgroups in group-level issue board.
+- Repository checksum calculation is handled by Gitaly when feature is enabled.
+- Allow viewing timings for AJAX requests in the performance bar.
+- Fixes remove source branch checkbox being visible when user cannot remove the branch.
+- Make /-/ delimiter optional for search endpoints.
+
+### Performance (24 changes, 11 of them are from the community)
+
+- Move AssigneeTitle vue component. !17397 (George Tsiolis)
+- Move TimeTrackingCollapsedState vue component. !17399 (George Tsiolis)
+- Move MemoryGraph and MemoryUsage vue components. !17533 (George Tsiolis)
+- Move UnresolvedDiscussions vue component. !17538 (George Tsiolis)
+- Move NothingToMerge vue component. !17544 (George Tsiolis)
+- Move ShaMismatch vue component. !17546 (George Tsiolis)
+- Stop caching highlighted diffs in Redis unnecessarily. !17746
+- Add i18n and update specs for ShaMismatch vue component. !17870 (George Tsiolis)
+- Update spec import path for vue mount component helper. !17880 (George Tsiolis)
+- Move TimeTrackingComparisonPane vue component. !17931 (George Tsiolis)
+- Improves the performance of projects list page. !17934
+- Remove N+1 query for Noteable association. !17956
+- Improve performance of loading issues with lots of references to merge requests. !17986
+- Reuse root_ref_hash for performance on Branches. !17998 (Takuya Noguchi)
+- Update asciidoctor-plantuml to 0.0.8. !18022 (Takuya Noguchi)
+- Cache personal projects count. !18197
+- Reduce complexity of issuable finder query. !18219
+- Reduce number of queries when viewing a merge request.
+- Free open file descriptors and libgit2 buffers in UpdatePagesService.
+- Memoize Git::Repository#has_visible_content?.
+- Require at least one filter when listing issues or merge requests on dashboard page.
+- lazy load diffs on merge request discussions.
+- Bulk deleting refs is handled by Gitaly by default.
+- ListCommitsByOid is executed by Gitaly by default.
+
+### Added (38 changes, 7 of them are from the community)
+
+- Add HTTPS-only pages. !16273 (rfwatson)
+- adds closed by informations in issue api. !17042 (haseebeqx)
+- Projects and groups badges settings UI. !17114
+- Add per-runner configured job timeout. !17221
+- Add alternate archive route for simplified packaging. !17225
+- Add support for pipeline variables expressions in only/except. !17316
+- Add object storage support for LFS objects, CI artifacts, and uploads. !17358
+- Added confirmation modal for changing username. !17405
+- Implement foreground verification of CI artifacts. !17578
+- Extend API for exporting a project with direct upload URL. !17686
+- Move ci/lint under project's namespace. !17729
+- Add Total CPU/Memory consumption metrics for Kubernetes. !17731
+- Adds the option to the project export API to override the project description and display GitLab export description once imported. !17744
+- Port direct upload of LFS artifacts from EE. !17752
+- Adds support for OmniAuth JWT provider. !17774
+- Display error message on job's tooltip if this one fails. !17782
+- Add 'Assigned Issues' and 'Assigned Merge Requests' as dashboard view choices for users. !17860 (Elias Werberich)
+- Extend API for importing a project export with overwrite support. !17883
+- Create Deploy Tokens to allow permanent access to repository and registry. !17894
+- Detect commit message trailers and link users properly to their accounts on Gitlab. !17919 (cousine)
+- Adds cancel btn to new pages domain page. !18026 (Jacopo Beschi @jacopo-beschi)
+- API: Add parameter merge_method to projects. !18031 (Jan Beckmann)
+- Introduce simpler env vars for auto devops REPLICAS and CANARY_REPLICAS #41436. !18036
+- Allow overriding params on project import through API. !18086
+- Support LFS objects when importing/exporting GitLab project archives. !18115
+- Store sha256 checksum of artifact metadata. !18149
+- Limit the number of failed logins when using LDAP for authentication. !43525
+- Allow assigning and filtering issuables by ancestor group labels.
+- Include subgroup issues when searching for group issues using the API.
+- Allow to store uploads by default on Object Storage.
+- Add slash command for moving issues. (Adam Pahlevi)
+- Render MR commit SHA instead "diffs" when viable.
+- Send @mention notifications even if a user has explicitly unsubscribed from item.
+- Add support for Sidekiq JSON logging.
+- Add Gitaly call details to performance bar.
+- Add support for patch link extension for commit links on GitLab Flavored Markdown.
+- Allow feature gates to be removed through the API.
+- Allow merge requests related to a commit to be found via API.
+
+### Other (27 changes, 11 of them are from the community)
+
+- Send notification emails when push to a merge request. !7610 (YarNayar)
+- Rename modal.vue to deprecated_modal.vue. !17438
+- Atomic generation of internal ids for issues. !17580
+- Use object ID to prevent duplicate keys Vue warning on Issue Boards page during development. !17682
+- Update foreman from 0.78.0 to 0.84.0. !17690 (Takuya Noguchi)
+- Add realtime pipeline status for adding/viewing files. !17705
+- Update documentation to reflect current minimum required versions of node and yarn. !17706
+- Update knapsack to 1.16.0. !17735 (Takuya Noguchi)
+- Update CI services documnetation. !17749
+- Added i18n support for the prometheus memory widget. !17753
+- Use specific names for filtered CI variable controller parameters. !17796
+- Apply NestingDepth (level 5) (framework/dropdowns.scss). !17820 (Takuya Noguchi)
+- Clean up selectors in framework/header.scss. !17822 (Takuya Noguchi)
+- Bump `state_machines-activerecord` to 0.5.1. !17924 (blackst0ne)
+- Increase the memory limits used in the unicorn killer. !17948
+- Replace the spinach test with an rspec analog. !17950 (blackst0ne)
+- Remove unused index from events table. !18014
+- Make all workhorse gitaly calls opt-out, take 2. !18043
+- Update brakeman 3.6.1 to 4.2.1. !18122 (Takuya Noguchi)
+- Replace the `project/issues/labels.feature` spinach test with an rspec analog. !18126 (blackst0ne)
+- Bump html-pipeline to 2.7.1. !18132 (@blackst0ne)
+- Remove test_ci rake task. !18139 (Takuya Noguchi)
+- Add documentation for Pipelines failure reasons. !18352
+- Improve JIRA event descriptions.
+- Add query counts to profiler output.
+- Move Sidekiq exporter logs to log/sidekiq_exporter.log.
+- Upgrade Gitaly to upgrade its charlock_holmes.
+
+
## 10.6.4 (2018-04-09)
### Fixed (8 changes, 1 of them is from the community)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9c8fdc1275b..29009010f0e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -26,7 +26,9 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
- - [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#priority-labels-deliverable-stretch-next-patch-release)
+ - [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release)
+ - [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-priority-labels-p1-p2-p3-etc)
+ - [Severity labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-severity-labels-s1-s2-s3-etc)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design-ui-elements)
- [Issue tracker](#issue-tracker)
@@ -126,7 +128,9 @@ Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, etc.
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.
-- Priority: ~Deliverable, ~Stretch, ~"Next Patch Release"
+- Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release"
+- Priority: ~P1, ~P2, ~P3, ~P4
+- Severity: ~S1, ~S2, ~S3, ~S4
All labels, their meaning and priority are defined on the
[labels page][labels-page].
@@ -185,10 +189,10 @@ indicate if an issue needs backend work, frontend work, or both.
Team labels are always capitalized so that they show up as the first label for
any issue.
-### Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")
+### Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")
-Priority labels help us clearly communicate expectations of the work for the
-release. There are two levels of priority labels:
+Milestone labels help us clearly communicate expectations of the work for the
+release. There are three levels of Milestone labels:
- ~Deliverable: Issues that are expected to be delivered in the current
milestone.
@@ -203,16 +207,46 @@ Each issue scheduled for the current milestone should be labeled ~Deliverable
or ~"Stretch". Any open issue for a previous milestone should be labeled
~"Next Patch Release", or otherwise rescheduled to a different milestone.
-### Severity labels (~S1, ~S2, etc.)
+### Bug Priority labels (~P1, ~P2, ~P3 & etc.)
-Severity labels help us clearly communicate the impact of a ~bug on users.
+Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be. If there are multiple defects, the priority decides which defect has to be fixed immediately versus later.
+This label documents the planned timeline & urgency which is used to measure against our actual SLA on delivering ~bug fixes.
-| Label | Meaning | Example |
-|-------|------------------------------------------|---------|
-| ~S1 | Feature broken, no workaround | Unable to create an issue |
-| ~S2 | Feature broken, workaround unacceptable | Can push commits, but only via the command line |
-| ~S3 | Feature broken, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue |
-| ~S4 | Cosmetic issue | Label colors are incorrect / not being displayed |
+| Label | Meaning | Estimate time to fix | Guidance |
+|-------|-----------------|------------------------------------------------------------------|----------|
+| ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com | |
+| ~P2 | High Priority | The next release | |
+| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
+| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
+
+#### Specific Priority guidance
+
+| Label | Availability / Performance |
+|-------|--------------------------------------------------------------|
+| ~P1 | |
+| ~P2 | The issue is (almost) guaranteed to occur in the near future |
+| ~P3 | The issue is likely to occur in the near future |
+| ~P4 | The issue _may_ occur but it's not likely |
+
+### Bug Severity labels (~S1, ~S2, ~S3 & etc.)
+
+Severity labels help us clearly communicate the impact of a ~bug on users.
+
+| Label | Meaning | Impact of the defect | Example |
+|-------|-------------------|-------------------------------------------------------|---------|
+| ~S1 | Blocker | Outage, broken feature with no workaround | Unable to create an issue. Data corruption/loss. Security breach. |
+| ~S2 | Critical Severity | Broken Feature, workaround too complex & unacceptable | Can push commits, but only via the command line. |
+| ~S3 | Major Severity | Broken Feature, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue. |
+| ~S4 | Low Severity | Functionality inconvenience or cosmetic issue | Label colors are incorrect / not being displayed. |
+
+#### Specific Severity guidance
+
+| Label | Security Impact |
+|-------|-------------------------------------------------------------------|
+| ~S1 | >50% customers impacted (possible company extinction level event) |
+| ~S2 | Multiple customers impacted (but not apocalyptic) |
+| ~S3 | A single customer impacted |
+| ~S4 | No customer impact, or expected impact within 30 days |
### Label for community contributors (~"Accepting Merge Requests")
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 5f8cbfdb7d7..483b7719418 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.95.0
+0.96.1
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index fcdb2e109f6..ee74734aa22 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-4.0.0
+4.1.0
diff --git a/Gemfile b/Gemfile
index 647138cd90f..71f27e0f6de 100644
--- a/Gemfile
+++ b/Gemfile
@@ -62,7 +62,7 @@ gem 'akismet', '~> 2.0'
# Two-factor authentication
gem 'devise-two-factor', '~> 3.0.0'
gem 'rqrcode-rails3', '~> 0.1.7'
-gem 'attr_encrypted', '~> 3.0.0'
+gem 'attr_encrypted', '~> 3.1.0'
gem 'u2f', '~> 0.2.1'
# GitLab Pages
@@ -82,16 +82,9 @@ gem 'net-ldap'
# Git Wiki
# Required manually in config/initializers/gollum.rb to control load order
-# Before updating this gem, check if
-# https://github.com/gollum/gollum-lib/pull/292 has been merged.
-# If it has, then remove the monkey patch for update_page, rename_page and raw_data_in_committer
-# in config/initializers/gollum.rb
-gem 'gollum-lib', '~> 4.2', require: false
+gem 'gitlab-gollum-lib', '~> 4.2', require: false
-# Before updating this gem, check if
-# https://github.com/gollum/rugged_adapter/pull/28 has been merged.
-# If it has, then remove the monkey patch for tree_entry in config/initializers/gollum.rb
-gem 'gollum-rugged_adapter', '~> 0.4.4', require: false
+gem 'gitlab-gollum-rugged_adapter', '~> 0.4.4', require: false
# Language detection
gem 'github-linguist', '~> 5.3.3', require: 'linguist'
@@ -147,7 +140,7 @@ gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.6'
gem 'asciidoctor-plantuml', '0.0.8'
-gem 'rouge', '~> 2.0'
+gem 'rouge', '~> 3.1'
gem 'truncato', '~> 0.7.9'
gem 'bootstrap_form', '~> 2.7.0'
gem 'nokogiri', '~> 1.8.2'
@@ -422,7 +415,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.94.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.97.0', require: 'gitaly'
gem 'grpc', '~> 1.10.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
@@ -441,5 +434,3 @@ gem 'grape_logging', '~> 1.7'
# Asset synchronization
gem 'asset_sync', '~> 2.2.0'
-
-gem 'goldiloader', '~> 2.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 76e1a17155f..d5e1c428e25 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -66,7 +66,7 @@ GEM
unf
ast (2.4.0)
atomic (1.1.99)
- attr_encrypted (3.0.3)
+ attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
attr_required (1.0.0)
autoprefixer-rails (6.2.3)
@@ -178,7 +178,7 @@ GEM
docile (1.1.5)
domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0)
- doorkeeper (4.3.1)
+ doorkeeper (4.3.2)
railties (>= 4.2)
doorkeeper-openid_connect (1.3.0)
doorkeeper (~> 4.3)
@@ -206,7 +206,7 @@ GEM
railties (>= 3.0.0)
faraday (0.12.2)
multipart-post (>= 1.2, < 3)
- faraday_middleware (0.11.0.1)
+ faraday_middleware (0.12.2)
faraday (>= 0.7.4, < 1.0)
faraday_middleware-multi_json (0.0.6)
faraday_middleware
@@ -290,19 +290,30 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.94.0)
+ gitaly-proto (0.97.0)
google-protobuf (~> 3.1)
- grpc (~> 1.0)
+ grpc (~> 1.10)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
mime-types (>= 1.19)
rugged (>= 0.25.1)
- github-markup (1.6.1)
+ github-markup (1.7.0)
gitlab-flowdock-git-hook (1.0.1)
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
+ gitlab-gollum-lib (4.2.7.2)
+ gemojione (~> 3.2)
+ github-markup (~> 1.6)
+ gollum-grit_adapter (~> 1.0)
+ nokogiri (>= 1.6.1, < 2.0)
+ rouge (~> 3.1)
+ sanitize (~> 2.1)
+ stringex (~> 2.6)
+ gitlab-gollum-rugged_adapter (0.4.4)
+ mime-types (>= 1.15)
+ rugged (~> 0.25)
gitlab-grit (2.8.2)
charlock_holmes (~> 0.6)
diff-lcs (~> 1.1)
@@ -320,22 +331,8 @@ GEM
rubyntlm (~> 0.5)
globalid (0.4.1)
activesupport (>= 4.2.0)
- goldiloader (2.0.1)
- activerecord (>= 4.2, < 5.2)
- activesupport (>= 4.2, < 5.2)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
- gollum-lib (4.2.7)
- gemojione (~> 3.2)
- github-markup (~> 1.6)
- gollum-grit_adapter (~> 1.0)
- nokogiri (>= 1.6.1, < 2.0)
- rouge (~> 2.1)
- sanitize (~> 2.1)
- stringex (~> 2.6)
- gollum-rugged_adapter (0.4.4)
- mime-types (>= 1.15)
- rugged (~> 0.25)
gon (6.1.0)
actionpack (>= 3.0)
json
@@ -486,10 +483,11 @@ GEM
logging (2.2.2)
little-plugger (~> 1.1)
multi_json (~> 1.10)
- lograge (0.5.1)
- actionpack (>= 4, < 5.2)
- activesupport (>= 4, < 5.2)
- railties (>= 4, < 5.2)
+ lograge (0.10.0)
+ actionpack (>= 4)
+ activesupport (>= 4)
+ railties (>= 4)
+ request_store (~> 1.0)
loofah (2.2.2)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
@@ -590,7 +588,7 @@ GEM
orm_adapter (0.5.0)
os (0.9.6)
parallel (1.12.1)
- parser (2.5.0.5)
+ parser (2.5.1.0)
ast (~> 2.4.0)
parslet (1.5.0)
blankslate (~> 2.0)
@@ -750,7 +748,7 @@ GEM
retriable (3.1.1)
rinku (2.0.0)
rotp (2.1.2)
- rouge (2.2.1)
+ rouge (3.1.1)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
@@ -910,7 +908,7 @@ GEM
state_machines-activerecord (0.5.1)
activerecord (>= 4.1, < 6.0)
state_machines-activemodel (>= 0.5.0)
- stringex (2.7.1)
+ stringex (2.8.4)
sys-filesystem (1.1.6)
ffi
sysexits (1.2.0)
@@ -1004,7 +1002,7 @@ DEPENDENCIES
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.2.0)
- attr_encrypted (~> 3.0.0)
+ attr_encrypted (~> 3.1.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
base32 (~> 0.3.0)
@@ -1064,15 +1062,14 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.94.0)
+ gitaly-proto (~> 0.97.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
+ gitlab-gollum-lib (~> 4.2)
+ gitlab-gollum-rugged_adapter (~> 0.4.4)
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
- goldiloader (~> 2.0)
- gollum-lib (~> 4.2)
- gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)
@@ -1164,7 +1161,7 @@ DEPENDENCIES
redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
- rouge (~> 2.0)
+ rouge (~> 3.1)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.6.0)
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index 03fe5f2ed26..a0330cbdd02 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -69,7 +69,7 @@ GEM
unf
ast (2.4.0)
atomic (1.1.100)
- attr_encrypted (3.0.3)
+ attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
attr_required (1.0.1)
autoprefixer-rails (8.1.0.1)
@@ -291,9 +291,9 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.94.0)
+ gitaly-proto (0.97.0)
google-protobuf (~> 3.1)
- grpc (~> 1.0)
+ grpc (~> 1.10)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
@@ -304,6 +304,17 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
+ gitlab-gollum-lib (4.2.7.1)
+ gemojione (~> 3.2)
+ github-markup (~> 1.6)
+ gollum-grit_adapter (~> 1.0)
+ nokogiri (>= 1.6.1, < 2.0)
+ rouge (~> 2.1)
+ sanitize (~> 2.1)
+ stringex (~> 2.6)
+ gitlab-gollum-rugged_adapter (0.4.4)
+ mime-types (>= 1.15)
+ rugged (~> 0.25)
gitlab-grit (2.8.2)
charlock_holmes (~> 0.6)
diff-lcs (~> 1.1)
@@ -323,17 +334,6 @@ GEM
activesupport (>= 4.2.0)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
- gollum-lib (4.2.7)
- gemojione (~> 3.2)
- github-markup (~> 1.6)
- gollum-grit_adapter (~> 1.0)
- nokogiri (>= 1.6.1, < 2.0)
- rouge (~> 2.1)
- sanitize (~> 2.1)
- stringex (~> 2.6)
- gollum-rugged_adapter (0.4.4)
- mime-types (>= 1.15)
- rugged (~> 0.25)
gon (6.1.0)
actionpack (>= 3.0)
json
@@ -878,7 +878,7 @@ GEM
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
slack-notifier (1.5.1)
- spinach (0.10.1)
+ spinach (0.8.10)
colorize
gherkin-ruby (>= 0.3.2)
json
@@ -1006,7 +1006,7 @@ DEPENDENCIES
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.2.0)
- attr_encrypted (~> 3.0.0)
+ attr_encrypted (~> 3.1.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
base32 (~> 0.3.0)
@@ -1066,14 +1066,14 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.94.0)
+ gitaly-proto (~> 0.97.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
+ gitlab-gollum-lib (~> 4.2)
+ gitlab-gollum-rugged_adapter (~> 0.4.4)
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
- gollum-lib (~> 4.2)
- gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)
diff --git a/LICENSE b/LICENSE
index 15c423e1416..a76372fad2c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,25 +1,7 @@
-Copyright (c) 2011-2017 GitLab B.V.
+Copyright GitLab B.V.
-With regard to the GitLab Software:
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal 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:
-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 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.
-
-For all third party components incorporated into the GitLab Software, those
-components are licensed under the original license provided by the owner of the
-applicable component. \ No newline at end of file
+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. \ No newline at end of file
diff --git a/README.md b/README.md
index 9ead6d51c5d..9c1aad65307 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,12 @@ You can access a new installation with the login **`root`** and password **`5ive
GitLab is an open source project and we are very happy to accept community contributions. Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details.
+## Licensing
+
+GitLab Community Edition (CE) is available freely under the MIT Expat license.
+
+All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component.
+
## Install a development environment
To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit).
diff --git a/VERSION b/VERSION
index 7a86eda5728..60919325d67 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-10.7.0-pre
+10.8.0-pre
diff --git a/app/assets/images/ext_snippet_icons/ext_snippet_icons.png b/app/assets/images/ext_snippet_icons/ext_snippet_icons.png
new file mode 100644
index 00000000000..20380adc4e5
--- /dev/null
+++ b/app/assets/images/ext_snippet_icons/ext_snippet_icons.png
Binary files differ
diff --git a/app/assets/images/ext_snippet_icons/logo.png b/app/assets/images/ext_snippet_icons/logo.png
new file mode 100644
index 00000000000..794c9cc2dbc
--- /dev/null
+++ b/app/assets/images/ext_snippet_icons/logo.png
Binary files differ
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index e210d69895e..7144f4190e7 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -113,6 +113,8 @@ class List {
issue.id = data.id;
issue.iid = data.iid;
issue.project = data.project;
+ issue.path = data.real_path;
+ issue.referencePath = data.reference_path;
if (this.issuesSize > 1) {
const moveBeforeId = this.issues[1].id;
diff --git a/app/assets/javascripts/branches/branches_delete_modal.js b/app/assets/javascripts/branches/branches_delete_modal.js
index 839e369eaf6..f34496f84c6 100644
--- a/app/assets/javascripts/branches/branches_delete_modal.js
+++ b/app/assets/javascripts/branches/branches_delete_modal.js
@@ -16,6 +16,7 @@ class DeleteModal {
bindEvents() {
this.$toggleBtns.on('click', this.setModalData.bind(this));
this.$confirmInput.on('input', this.setDeleteDisabled.bind(this));
+ this.$deleteBtn.on('click', this.setDisableDeleteButton.bind(this));
}
setModalData(e) {
@@ -30,6 +31,16 @@ class DeleteModal {
this.$deleteBtn.attr('disabled', e.currentTarget.value !== this.branchName);
}
+ setDisableDeleteButton(e) {
+ if (this.$deleteBtn.is('[disabled]')) {
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ }
+
+ return true;
+ }
+
updateModal() {
this.$branchName.text(this.branchName);
this.$confirmInput.val('');
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index f8dcdf3f60a..9c12b89240c 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -1,96 +1,102 @@
<script>
- import _ from 'underscore';
- import { s__, sprintf } from '../../locale';
- import applicationRow from './application_row.vue';
- import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
- import {
- APPLICATION_INSTALLED,
- INGRESS,
- } from '../constants';
+import _ from 'underscore';
+import { s__, sprintf } from '../../locale';
+import applicationRow from './application_row.vue';
+import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import { APPLICATION_INSTALLED, INGRESS } from '../constants';
- export default {
- components: {
- applicationRow,
- clipboardButton,
+export default {
+ components: {
+ applicationRow,
+ clipboardButton,
+ },
+ props: {
+ applications: {
+ type: Object,
+ required: false,
+ default: () => ({}),
},
- props: {
- applications: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- helpPath: {
- type: String,
- required: false,
- default: '',
- },
- ingressHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- ingressDnsHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- managePrometheusPath: {
- type: String,
- required: false,
- default: '',
- },
+ helpPath: {
+ type: String,
+ required: false,
+ default: '',
},
- computed: {
- generalApplicationDescription() {
- return sprintf(
- _.escape(s__(
+ ingressHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ ingressDnsHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ managePrometheusPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ generalApplicationDescription() {
+ return sprintf(
+ _.escape(
+ s__(
`ClusterIntegration|Install applications on your Kubernetes cluster.
Read more about %{helpLink}`,
- )), {
- helpLink: `<a href="${this.helpPath}">
+ ),
+ ),
+ {
+ helpLink: `<a href="${this.helpPath}">
${_.escape(s__('ClusterIntegration|installing applications'))}
</a>`,
- },
- false,
- );
- },
- ingressId() {
- return INGRESS;
- },
- ingressInstalled() {
- return this.applications.ingress.status === APPLICATION_INSTALLED;
- },
- ingressExternalIp() {
- return this.applications.ingress.externalIp;
- },
- ingressDescription() {
- const extraCostParagraph = sprintf(
- _.escape(s__(
+ },
+ false,
+ );
+ },
+ ingressId() {
+ return INGRESS;
+ },
+ ingressInstalled() {
+ return this.applications.ingress.status === APPLICATION_INSTALLED;
+ },
+ ingressExternalIp() {
+ return this.applications.ingress.externalIp;
+ },
+ ingressDescription() {
+ const extraCostParagraph = sprintf(
+ _.escape(
+ s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources
like a load balancer, which may incur additional costs depending on
- the hosting provider your Kubernetes cluster is installed on. If you are using GKE,
- you can %{pricingLink}.`,
- )), {
- boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
- pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
+ the hosting provider your Kubernetes cluster is installed on. If you are using
+ Google Kubernetes Engine, you can %{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|check the pricing here'))}</a>`,
- },
- false,
- );
+ },
+ false,
+ );
- const externalIpParagraph = sprintf(
- _.escape(s__(
+ const externalIpParagraph = sprintf(
+ _.escape(
+ s__(
`ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS
at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}`,
- )), {
- ingressHelpLink: `<a href="${this.ingressHelpPath}">
+ ),
+ ),
+ {
+ ingressHelpLink: `<a href="${this.ingressHelpPath}">
${_.escape(s__('ClusterIntegration|More information'))}
</a>`,
- },
- false,
- );
+ },
+ false,
+ );
- return `
+ return `
<p>
${extraCostParagraph}
</p>
@@ -98,22 +104,25 @@
${externalIpParagraph}
</p>
`;
- },
- prometheusDescription() {
- return sprintf(
- _.escape(s__(
+ },
+ prometheusDescription() {
+ return sprintf(
+ _.escape(
+ s__(
`ClusterIntegration|Prometheus is an open-source monitoring system
with %{gitlabIntegrationLink} to monitor deployed applications.`,
- )), {
- gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
+ ),
+ ),
+ {
+ gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`,
- },
- false,
- );
- },
+ },
+ false,
+ );
},
- };
+ },
+};
</script>
<template>
@@ -205,7 +214,7 @@
>
{{ s__(`ClusterIntegration|The IP address is in
the process of being assigned. Please check your Kubernetes
- cluster or Quotas on GKE if it takes a long time.`) }}
+ cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }}
<a
:href="ingressHelpPath"
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index fb1fc9cd32e..a88b6971f90 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -84,20 +84,21 @@ export default class CreateMergeRequestDropdown {
if (data.can_create_branch) {
this.available();
this.enable();
+ this.updateBranchName(data.suggested_branch_name);
if (!this.droplabInitialized) {
this.droplabInitialized = true;
this.initDroplab();
this.bindEvents();
}
- } else if (data.has_related_branch) {
+ } else {
this.hide();
}
})
.catch(() => {
this.unavailable();
this.disable();
- Flash('Failed to check if a new branch can be created.');
+ Flash(__('Failed to check related branches.'));
});
}
@@ -409,13 +410,16 @@ export default class CreateMergeRequestDropdown {
this.unavailableButton.classList.remove('hide');
}
+ updateBranchName(suggestedBranchName) {
+ this.branchInput.value = suggestedBranchName;
+ this.updateCreatePaths('branch', suggestedBranchName);
+ }
+
updateInputState(target, ref, result) {
// target - 'branch' or 'ref' - which the input field we are searching a ref for.
// ref - string - what a user typed.
// result - string - what has been found on backend.
- const pathReplacement = `$1${ref}`;
-
// If a found branch equals exact the same text a user typed,
// that means a new branch cannot be created as it already exists.
if (ref === result) {
@@ -426,18 +430,12 @@ export default class CreateMergeRequestDropdown {
this.refIsValid = true;
this.refInput.dataset.value = ref;
this.showAvailableMessage('ref');
- this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath,
- pathReplacement);
- this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath,
- pathReplacement);
+ this.updateCreatePaths(target, ref);
}
} else if (target === 'branch') {
this.branchIsValid = true;
this.showAvailableMessage('branch');
- this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath,
- pathReplacement);
- this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath,
- pathReplacement);
+ this.updateCreatePaths(target, ref);
} else {
this.refIsValid = false;
this.refInput.dataset.value = ref;
@@ -457,4 +455,15 @@ export default class CreateMergeRequestDropdown {
this.disableCreateAction();
}
}
+
+ // target - 'branch' or 'ref'
+ // ref - string - the new value to use as branch or ref
+ updateCreatePaths(target, ref) {
+ const pathReplacement = `$1${ref}`;
+
+ this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath,
+ pathReplacement);
+ this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath,
+ pathReplacement);
+ }
}
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index 842a4255f08..4164149dd06 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -2,7 +2,9 @@
import $ from 'jquery';
import Pikaday from 'pikaday';
+import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
+import { timeFor } from './lib/utils/datetime_utility';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
class DueDateSelect {
@@ -14,6 +16,7 @@ class DueDateSelect {
this.$dropdownParent = $dropdownParent;
this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
this.$block = $block;
+ this.$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
this.$selectbox = $dropdown.closest('.selectbox');
this.$value = $block.find('.value');
this.$valueContent = $block.find('.value-content');
@@ -128,7 +131,8 @@ class DueDateSelect {
submitSelectedDate(isDropdown) {
const selectedDateValue = this.datePayload[this.abilityName].due_date;
- const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
+ const hasDueDate = this.displayedDate !== 'No due date';
+ const displayedDateStyle = hasDueDate ? 'bold' : 'no-value';
this.$loading.removeClass('hidden').fadeIn();
@@ -145,10 +149,13 @@ class DueDateSelect {
return axios.put(this.issueUpdateURL, this.datePayload)
.then(() => {
+ const tooltipText = hasDueDate ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})` : __('Due date');
if (isDropdown) {
this.$dropdown.trigger('loaded.gl.dropdown');
this.$dropdown.dropdown('toggle');
}
+ this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
+
return this.$loading.fadeOut();
});
}
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js
index c50ac667c20..2d5bae9a9c4 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight.js
+++ b/app/assets/javascripts/feature_highlight/feature_highlight.js
@@ -1,19 +1,19 @@
import $ from 'jquery';
-import _ from 'underscore';
import {
getSelector,
- togglePopover,
inserted,
- mouseenter,
- mouseleave,
} from './feature_highlight_helper';
+import {
+ togglePopover,
+ mouseenter,
+ debouncedMouseleave,
+} from '../shared/popover';
export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
const $selector = $(getSelector(id));
const $parent = $selector.parent();
const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
const hideOnScroll = togglePopover.bind($selector, false);
- const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout);
$selector
// Setup popover
@@ -29,13 +29,10 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
`,
})
.on('mouseenter', mouseenter)
- .on('mouseleave', debouncedMouseleave)
+ .on('mouseleave', debouncedMouseleave(debounceTimeout))
.on('inserted.bs.popover', inserted)
.on('show.bs.popover', () => {
- window.addEventListener('scroll', hideOnScroll);
- })
- .on('hide.bs.popover', () => {
- window.removeEventListener('scroll', hideOnScroll);
+ window.addEventListener('scroll', hideOnScroll, { once: true });
})
// Display feature highlight
.removeAttr('disabled');
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
index f480e72961c..d5b97ebb264 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
+++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
@@ -3,20 +3,10 @@ import axios from '../lib/utils/axios_utils';
import { __ } from '../locale';
import Flash from '../flash';
import LazyLoader from '../lazy_loader';
+import { togglePopover } from '../shared/popover';
export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`;
-export function togglePopover(show) {
- const isAlreadyShown = this.hasClass('js-popover-show');
- if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) {
- return false;
- }
- this.popover(show ? 'show' : 'hide');
- this.toggleClass('disable-animation js-popover-show', show);
-
- return true;
-}
-
export function dismiss(highlightId) {
axios.post(this.attr('data-dismiss-endpoint'), {
feature_name: highlightId,
@@ -27,23 +17,6 @@ export function dismiss(highlightId) {
this.hide();
}
-export function mouseleave() {
- if (!$('.popover:hover').length > 0) {
- const $featureHighlight = $(this);
- togglePopover.call($featureHighlight, false);
- }
-}
-
-export function mouseenter() {
- const $featureHighlight = $(this);
-
- const showedPopover = togglePopover.call($featureHighlight, true);
- if (showedPopover) {
- $('.popover')
- .on('mouseleave', mouseleave.bind($featureHighlight));
- }
-}
-
export function inserted() {
const popoverId = this.getAttribute('aria-describedby');
const highlightId = this.dataset.highlight;
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue
index 037e3efb4ce..1fc11c84639 100644
--- a/app/assets/javascripts/ide/components/changed_file_icon.vue
+++ b/app/assets/javascripts/ide/components/changed_file_icon.vue
@@ -1,31 +1,87 @@
<script>
-import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+import { pluralize } from '~/lib/utils/text_utility';
+import { __, sprintf } from '~/locale';
export default {
components: {
- icon,
+ Icon,
+ },
+ directives: {
+ tooltip,
},
props: {
file: {
type: Object,
required: true,
},
+ showTooltip: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showStagedIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
changedIcon() {
- return this.file.tempFile ? 'file-addition' : 'file-modified';
+ const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : '';
+ return this.file.tempFile ? `file-addition${suffix}` : `file-modified${suffix}`;
+ },
+ stagedIcon() {
+ return `${this.changedIcon}-solid`;
},
changedIconClass() {
- return `multi-${this.changedIcon}`;
+ return `multi-${this.changedIcon} prepend-left-5 pull-left`;
+ },
+ tooltipTitle() {
+ if (!this.showTooltip) return undefined;
+
+ const type = this.file.tempFile ? 'addition' : 'modification';
+
+ if (this.file.changed && !this.file.staged) {
+ return sprintf(__('Unstaged %{type}'), {
+ type,
+ });
+ } else if (!this.file.changed && this.file.staged) {
+ return sprintf(__('Staged %{type}'), {
+ type,
+ });
+ } else if (this.file.changed && this.file.staged) {
+ return sprintf(__('Unstaged and staged %{type}'), {
+ type: pluralize(type),
+ });
+ }
+
+ return undefined;
},
},
};
</script>
<template>
- <icon
- :name="changedIcon"
- :size="12"
- :css-classes="`ide-file-changed-icon ${changedIconClass}`"
- />
+ <span
+ v-tooltip
+ :title="tooltipTitle"
+ data-container="body"
+ data-placement="right"
+ class="ide-file-changed-icon"
+ >
+ <icon
+ v-if="file.staged && showStagedIcon"
+ :name="stagedIcon"
+ :size="12"
+ :css-classes="changedIconClass"
+ />
+ <icon
+ v-if="file.changed || file.tempFile || (file.staged && !showStagedIcon)"
+ :name="changedIcon"
+ :size="12"
+ :css-classes="changedIconClass"
+ />
+ </span>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index 2cbd982af19..45321df191c 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -1,41 +1,27 @@
<script>
- import { mapState } from 'vuex';
- import { sprintf, __ } from '~/locale';
- import * as consts from '../../stores/modules/commit/constants';
- import RadioGroup from './radio_group.vue';
+import { mapState } from 'vuex';
+import { sprintf, __ } from '~/locale';
+import * as consts from '../../stores/modules/commit/constants';
+import RadioGroup from './radio_group.vue';
- export default {
- components: {
- RadioGroup,
+export default {
+ components: {
+ RadioGroup,
+ },
+ computed: {
+ ...mapState(['currentBranchId']),
+ commitToCurrentBranchText() {
+ return sprintf(
+ __('Commit to %{branchName} branch'),
+ { branchName: `<strong class="monospace">${this.currentBranchId}</strong>` },
+ false,
+ );
},
- computed: {
- ...mapState([
- 'currentBranchId',
- ]),
- newMergeRequestHelpText() {
- return sprintf(
- __('Creates a new branch from %{branchName} and re-directs to create a new merge request'),
- { branchName: this.currentBranchId },
- );
- },
- commitToCurrentBranchText() {
- return sprintf(
- __('Commit to %{branchName} branch'),
- { branchName: `<strong>${this.currentBranchId}</strong>` },
- false,
- );
- },
- commitToNewBranchText() {
- return sprintf(
- __('Creates a new branch from %{branchName}'),
- { branchName: this.currentBranchId },
- );
- },
- },
- commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
- commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
- commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
- };
+ },
+ commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
+ commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
+ commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
+};
</script>
<template>
@@ -53,13 +39,11 @@
:value="$options.commitToNewBranch"
:label="__('Create a new branch')"
:show-input="true"
- :help-text="commitToNewBranchText"
/>
<radio-group
:value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')"
:show-input="true"
- :help-text="newMergeRequestHelpText"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
new file mode 100644
index 00000000000..6424b93ce54
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
@@ -0,0 +1,93 @@
+<script>
+import { mapActions, mapState, mapGetters } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ noChangesStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ committedStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['lastCommitMsg', 'rightPanelCollapsed']),
+ ...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
+ statusSvg() {
+ return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
+ },
+ },
+ methods: {
+ ...mapActions(['toggleRightPanelCollapsed']),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state"
+ >
+ <header
+ class="multi-file-commit-panel-header"
+ :class="{
+ 'is-collapsed': rightPanelCollapsed,
+ }"
+ >
+ <button
+ v-tooltip
+ :title="collapseButtonTooltip"
+ data-container="body"
+ data-placement="left"
+ type="button"
+ class="btn btn-transparent multi-file-commit-panel-collapse-btn"
+ :aria-label="__('Toggle sidebar')"
+ @click.stop="toggleRightPanelCollapsed"
+ >
+ <icon
+ :name="collapseButtonIcon"
+ :size="18"
+ />
+ </button>
+ </header>
+ <div
+ class="ide-commit-empty-state-container"
+ v-if="!rightPanelCollapsed"
+ >
+ <div class="svg-content svg-80">
+ <img :src="statusSvg" />
+ </div>
+ <div class="append-right-default prepend-left-default">
+ <div
+ class="text-content text-center"
+ v-if="!lastCommitMsg"
+ >
+ <h4>
+ {{ __('No changes') }}
+ </h4>
+ <p>
+ {{ __('Edit files in the editor and commit changes here') }}
+ </p>
+ </div>
+ <div
+ class="text-content text-center"
+ v-else
+ >
+ <h4>
+ {{ __('All changes are committed') }}
+ </h4>
+ <p v-html="lastCommitMsg"></p>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index 453208f3f19..ff05ee8682a 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -1,56 +1,132 @@
<script>
- import { mapState } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
- import listItem from './list_item.vue';
- import listCollapsed from './list_collapsed.vue';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import { __, sprintf } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import ListItem from './list_item.vue';
+import ListCollapsed from './list_collapsed.vue';
- export default {
- components: {
- icon,
- listItem,
- listCollapsed,
+export default {
+ components: {
+ Icon,
+ ListItem,
+ ListCollapsed,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
},
- props: {
- title: {
- type: String,
- required: true,
- },
- fileList: {
- type: Array,
- required: true,
- },
+ fileList: {
+ type: Array,
+ required: true,
},
- computed: {
- ...mapState([
- 'currentProjectId',
- 'currentBranchId',
- 'rightPanelCollapsed',
- ]),
- isCommitInfoShown() {
- return this.rightPanelCollapsed || this.fileList.length;
- },
+ showToggle: {
+ type: Boolean,
+ required: false,
+ default: true,
},
- methods: {
- toggleCollapsed() {
- this.$emit('toggleCollapsed');
- },
+ iconName: {
+ type: String,
+ required: true,
},
- };
+ action: {
+ type: String,
+ required: true,
+ },
+ actionBtnText: {
+ type: String,
+ required: true,
+ },
+ itemActionComponent: {
+ type: String,
+ required: true,
+ },
+ stagedList: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapState(['rightPanelCollapsed']),
+ ...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
+ titleText() {
+ return sprintf(__('%{title} changes'), {
+ title: this.title,
+ });
+ },
+ },
+ methods: {
+ ...mapActions(['toggleRightPanelCollapsed', 'stageAllChanges', 'unstageAllChanges']),
+ actionBtnClicked() {
+ this[this.action]();
+ },
+ },
+};
</script>
<template>
<div
+ class="ide-commit-list-container"
:class="{
- 'multi-file-commit-list': isCommitInfoShown
+ 'is-collapsed': rightPanelCollapsed,
}"
>
+ <header
+ class="multi-file-commit-panel-header"
+ >
+ <div
+ v-if="!rightPanelCollapsed"
+ class="multi-file-commit-panel-header-title"
+ :class="{
+ 'append-right-10': showToggle,
+ }"
+ >
+ <icon
+ v-once
+ :name="iconName"
+ :size="18"
+ />
+ {{ titleText }}
+ <button
+ type="button"
+ class="btn btn-blank btn-link ide-staged-action-btn"
+ @click="actionBtnClicked"
+ >
+ {{ actionBtnText }}
+ </button>
+ </div>
+ <button
+ v-if="showToggle"
+ v-tooltip
+ :title="collapseButtonTooltip"
+ data-container="body"
+ data-placement="left"
+ type="button"
+ class="btn btn-transparent multi-file-commit-panel-collapse-btn"
+ :aria-label="__('Toggle sidebar')"
+ @click.stop="toggleRightPanelCollapsed"
+ >
+ <icon
+ :name="collapseButtonIcon"
+ :size="18"
+ />
+ </button>
+ </header>
<list-collapsed
v-if="rightPanelCollapsed"
+ :files="fileList"
+ :icon-name="iconName"
+ :title="title"
/>
<template v-else>
<ul
v-if="fileList.length"
- class="list-unstyled append-bottom-0"
+ class="multi-file-commit-list list-unstyled append-bottom-0"
>
<li
v-for="file in fileList"
@@ -58,9 +134,18 @@
>
<list-item
:file="file"
+ :action-component="itemActionComponent"
+ :key-prefix="title"
+ :staged-list="stagedList"
/>
</li>
</ul>
+ <p
+ v-else
+ class="multi-file-commit-list help-block"
+ >
+ {{ __('No changes') }}
+ </p>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
index 15918ac9631..2254271c679 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
@@ -1,35 +1,110 @@
<script>
- import { mapGetters } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { sprintf, n__, __ } from '~/locale';
- export default {
- components: {
- icon,
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ files: {
+ type: Array,
+ required: true,
},
- computed: {
- ...mapGetters([
- 'addedFiles',
- 'modifiedFiles',
- ]),
+ iconName: {
+ type: String,
+ required: true,
},
- };
+ title: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ addedFilesLength() {
+ return this.files.filter(f => f.tempFile).length;
+ },
+ modifiedFilesLength() {
+ return this.files.filter(f => !f.tempFile).length;
+ },
+ addedFilesIconClass() {
+ return this.addedFilesLength ? 'multi-file-addition' : '';
+ },
+ modifiedFilesClass() {
+ return this.modifiedFilesLength ? 'multi-file-modified' : '';
+ },
+ additionsTooltip() {
+ return sprintf(n__('1 %{type} addition', '%d %{type} additions', this.addedFilesLength), {
+ type: this.title.toLowerCase(),
+ });
+ },
+ modifiedTooltip() {
+ return sprintf(
+ n__('1 %{type} modification', '%d %{type} modifications', this.modifiedFilesLength),
+ { type: this.title.toLowerCase() },
+ );
+ },
+ titleTooltip() {
+ return sprintf(__('%{title} changes'), { title: this.title });
+ },
+ additionIconName() {
+ return this.title.toLowerCase() === 'staged' ? 'file-addition-solid' : 'file-addition';
+ },
+ modifiedIconName() {
+ return this.title.toLowerCase() === 'staged' ? 'file-modified-solid' : 'file-modified';
+ },
+ },
+};
</script>
<template>
<div
class="multi-file-commit-list-collapsed text-center"
>
- <icon
- name="file-addition"
- :size="18"
- css-classes="multi-file-addition append-bottom-10"
- />
- {{ addedFiles.length }}
- <icon
- name="file-modified"
- :size="18"
- css-classes="multi-file-modified prepend-top-10 append-bottom-10"
- />
- {{ modifiedFiles.length }}
+ <div
+ v-tooltip
+ :title="titleTooltip"
+ data-container="body"
+ data-placement="left"
+ class="append-bottom-15"
+ >
+ <icon
+ v-once
+ :name="iconName"
+ :size="18"
+ />
+ </div>
+ <div
+ v-tooltip
+ :title="additionsTooltip"
+ data-container="body"
+ data-placement="left"
+ class="append-bottom-10"
+ >
+ <icon
+ :name="additionIconName"
+ :size="18"
+ :css-classes="addedFilesIconClass"
+ />
+ </div>
+ {{ addedFilesLength }}
+ <div
+ v-tooltip
+ :title="modifiedTooltip"
+ data-container="body"
+ data-placement="left"
+ class="prepend-top-10 append-bottom-10"
+ >
+ <icon
+ :name="modifiedIconName"
+ :size="18"
+ :css-classes="modifiedFilesClass"
+ />
+ </div>
+ {{ modifiedFilesLength }}
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 560cdd941cd..ad4713c40d5 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -1,34 +1,69 @@
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
+import StageButton from './stage_button.vue';
+import UnstageButton from './unstage_button.vue';
export default {
components: {
Icon,
+ StageButton,
+ UnstageButton,
},
props: {
file: {
type: Object,
required: true,
},
+ actionComponent: {
+ type: String,
+ required: true,
+ },
+ keyPrefix: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ stagedList: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
iconName() {
- return this.file.tempFile ? 'file-addition' : 'file-modified';
+ const prefix = this.stagedList ? '-solid' : '';
+ return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`;
},
iconClass() {
- return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
+ return `multi-file-${this.file.tempFile ? 'additions' : 'modified'} append-right-8`;
},
},
methods: {
- ...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']),
- openFileInEditor(file) {
- return this.openPendingTab(file).then(changeViewer => {
+ ...mapActions([
+ 'discardFileChanges',
+ 'updateViewer',
+ 'openPendingTab',
+ 'unstageChange',
+ 'stageChange',
+ ]),
+ openFileInEditor() {
+ return this.openPendingTab({
+ file: this.file,
+ keyPrefix: this.keyPrefix.toLowerCase(),
+ }).then(changeViewer => {
if (changeViewer) {
this.updateViewer('diff');
}
});
},
+ fileAction() {
+ if (this.file.staged) {
+ this.unstageChange(this.file.path);
+ } else {
+ this.stageChange(this.file.path);
+ }
+ },
},
};
</script>
@@ -38,7 +73,9 @@ export default {
<button
type="button"
class="multi-file-commit-list-path"
- @click="openFileInEditor(file)">
+ @dblclick="fileAction"
+ @click="openFileInEditor"
+ >
<span class="multi-file-commit-list-file-path">
<icon
:name="iconName"
@@ -47,12 +84,9 @@ export default {
/>{{ file.path }}
</span>
</button>
- <button
- type="button"
- class="btn btn-blank multi-file-discard-btn"
- @click="discardFileChanges(file.path)"
- >
- Discard
- </button>
+ <component
+ :is="actionComponent"
+ :path="file.path"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
new file mode 100644
index 00000000000..dcd934f76b7
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
@@ -0,0 +1,130 @@
+<script>
+import { __, sprintf } from '../../../locale';
+import Icon from '../../../vue_shared/components/icon.vue';
+import popover from '../../../vue_shared/directives/popover';
+import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants';
+
+export default {
+ directives: {
+ popover,
+ },
+ components: {
+ Icon,
+ },
+ props: {
+ text: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ scrollTop: 0,
+ isFocused: false,
+ };
+ },
+ computed: {
+ allLines() {
+ return this.text.split('\n').map((line, i) => ({
+ text: line.substr(0, this.getLineLength(i)) || ' ',
+ highlightedText: line.substr(this.getLineLength(i)),
+ }));
+ },
+ },
+ methods: {
+ handleScroll() {
+ if (this.$refs.textarea) {
+ this.$nextTick(() => {
+ this.scrollTop = this.$refs.textarea.scrollTop;
+ });
+ }
+ },
+ getLineLength(i) {
+ return i === 0 ? MAX_TITLE_LENGTH : MAX_BODY_LENGTH;
+ },
+ onInput(e) {
+ this.$emit('input', e.target.value);
+ },
+ updateIsFocused(isFocused) {
+ this.isFocused = isFocused;
+ },
+ },
+ popoverOptions: {
+ trigger: 'hover',
+ placement: 'top',
+ content: sprintf(
+ __(`
+ The character highligher helps you keep the subject line to %{titleLength} characters
+ and wrap the body at %{bodyLength} so they are readable in git.
+ `),
+ { titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH },
+ ),
+ },
+};
+</script>
+
+<template>
+ <fieldset class="common-note-form ide-commit-message-field">
+ <div
+ class="md-area"
+ :class="{
+ 'is-focused': isFocused
+ }"
+ >
+ <div
+ v-once
+ class="md-header"
+ >
+ <ul class="nav-links">
+ <li>
+ {{ __('Commit Message') }}
+ <span
+ v-popover="$options.popoverOptions"
+ class="help-block prepend-left-10"
+ >
+ <icon
+ name="question"
+ />
+ </span>
+ </li>
+ </ul>
+ </div>
+ <div class="ide-commit-message-textarea-container">
+ <div class="ide-commit-message-highlights-container">
+ <div
+ class="note-textarea highlights monospace"
+ :style="{
+ transform: `translate3d(0, ${-scrollTop}px, 0)`
+ }"
+ >
+ <div
+ v-for="(line, index) in allLines"
+ :key="index"
+ >
+ <span
+ v-text="line.text"
+ >
+ </span><mark
+ v-show="line.highlightedText"
+ v-text="line.highlightedText"
+ >
+ </mark>
+ </div>
+ </div>
+ </div>
+ <textarea
+ class="note-textarea ide-commit-message-textarea"
+ name="commit-message"
+ :placeholder="__('Write a commit message...')"
+ :value="text"
+ @scroll="handleScroll"
+ @input="onInput"
+ @focus="updateIsFocused(true)"
+ @blur="updateIsFocused(false)"
+ ref="textarea"
+ >
+ </textarea>
+ </div>
+ </div>
+ </fieldset>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
index 4310d762c78..b660a2961cb 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
@@ -1,52 +1,40 @@
<script>
- import { mapActions, mapState, mapGetters } from 'vuex';
- import tooltip from '~/vue_shared/directives/tooltip';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import tooltip from '~/vue_shared/directives/tooltip';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ props: {
+ value: {
+ type: String,
+ required: true,
},
- props: {
- value: {
- type: String,
- required: true,
- },
- label: {
- type: String,
- required: false,
- default: null,
- },
- checked: {
- type: Boolean,
- required: false,
- default: false,
- },
- showInput: {
- type: Boolean,
- required: false,
- default: false,
- },
- helpText: {
- type: String,
- required: false,
- default: null,
- },
+ label: {
+ type: String,
+ required: false,
+ default: null,
},
- computed: {
- ...mapState('commit', [
- 'commitAction',
- ]),
- ...mapGetters('commit', [
- 'newBranchName',
- ]),
+ checked: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- methods: {
- ...mapActions('commit', [
- 'updateCommitAction',
- 'updateBranchName',
- ]),
+ showInput: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- };
+ },
+ computed: {
+ ...mapState('commit', ['commitAction']),
+ ...mapGetters('commit', ['newBranchName']),
+ },
+ methods: {
+ ...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
+ },
+};
</script>
<template>
@@ -65,18 +53,6 @@
{{ label }}
</template>
<slot v-else></slot>
- <span
- v-if="helpText"
- v-tooltip
- class="help-block inline"
- :title="helpText"
- >
- <i
- class="fa fa-question-circle"
- aria-hidden="true"
- >
- </i>
- </span>
</span>
</label>
<div
@@ -85,7 +61,7 @@
>
<input
type="text"
- class="form-control"
+ class="form-control monospace"
:placeholder="newBranchName"
@input="updateBranchName($event.target.value)"
/>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
new file mode 100644
index 00000000000..52dce8412ab
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
@@ -0,0 +1,59 @@
+<script>
+import { mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ ...mapActions(['stageChange', 'discardFileChanges']),
+ },
+};
+</script>
+
+<template>
+ <div
+ v-once
+ class="multi-file-discard-btn"
+ >
+ <button
+ v-tooltip
+ type="button"
+ class="btn btn-blank append-right-5"
+ :aria-label="__('Stage changes')"
+ :title="__('Stage changes')"
+ data-container="body"
+ @click.stop="stageChange(path)"
+ >
+ <icon
+ name="mobile-issue-close"
+ :size="12"
+ />
+ </button>
+ <button
+ v-tooltip
+ type="button"
+ class="btn btn-blank"
+ :aria-label="__('Discard changes')"
+ :title="__('Discard changes')"
+ data-container="body"
+ @click.stop="discardFileChanges(path)"
+ >
+ <icon
+ name="remove"
+ :size="12"
+ />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
new file mode 100644
index 00000000000..123d60da47e
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
@@ -0,0 +1,45 @@
+<script>
+import { mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ ...mapActions(['unstageChange']),
+ },
+};
+</script>
+
+<template>
+ <div
+ v-once
+ class="multi-file-discard-btn"
+ >
+ <button
+ v-tooltip
+ type="button"
+ class="btn btn-blank"
+ :aria-label="__('Unstage changes')"
+ :title="__('Unstage changes')"
+ data-container="body"
+ @click="unstageChange(path)"
+ >
+ <icon
+ name="history"
+ :size="12"
+ />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue
new file mode 100644
index 00000000000..ea2b13a8b21
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_finder/index.vue
@@ -0,0 +1,245 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import VirtualList from 'vue-virtual-scroll-list';
+import Item from './item.vue';
+import router from '../../ide_router';
+import {
+ MAX_FILE_FINDER_RESULTS,
+ FILE_FINDER_ROW_HEIGHT,
+ FILE_FINDER_EMPTY_ROW_HEIGHT,
+} from '../../constants';
+import {
+ UP_KEY_CODE,
+ DOWN_KEY_CODE,
+ ENTER_KEY_CODE,
+ ESC_KEY_CODE,
+} from '../../../lib/utils/keycodes';
+
+export default {
+ components: {
+ Item,
+ VirtualList,
+ },
+ data() {
+ return {
+ focusedIndex: 0,
+ searchText: '',
+ mouseOver: false,
+ cancelMouseOver: false,
+ };
+ },
+ computed: {
+ ...mapGetters(['allBlobs']),
+ ...mapState(['fileFindVisible', 'loading']),
+ filteredBlobs() {
+ const searchText = this.searchText.trim();
+
+ if (searchText === '') {
+ return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS);
+ }
+
+ return fuzzaldrinPlus
+ .filter(this.allBlobs, searchText, {
+ key: 'path',
+ maxResults: MAX_FILE_FINDER_RESULTS,
+ })
+ .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
+ },
+ filteredBlobsLength() {
+ return this.filteredBlobs.length;
+ },
+ listShowCount() {
+ return this.filteredBlobsLength ? Math.min(this.filteredBlobsLength, 5) : 1;
+ },
+ listHeight() {
+ return this.filteredBlobsLength ? FILE_FINDER_ROW_HEIGHT : FILE_FINDER_EMPTY_ROW_HEIGHT;
+ },
+ showClearInputButton() {
+ return this.searchText.trim() !== '';
+ },
+ },
+ watch: {
+ fileFindVisible() {
+ this.$nextTick(() => {
+ if (!this.fileFindVisible) {
+ this.searchText = '';
+ } else {
+ this.focusedIndex = 0;
+
+ if (this.$refs.searchInput) {
+ this.$refs.searchInput.focus();
+ }
+ }
+ });
+ },
+ searchText() {
+ this.focusedIndex = 0;
+ },
+ focusedIndex() {
+ if (!this.mouseOver) {
+ this.$nextTick(() => {
+ const el = this.$refs.virtualScrollList.$el;
+ const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT;
+ const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT;
+
+ if (this.focusedIndex === 0) {
+ // if index is the first index, scroll straight to start
+ el.scrollTop = 0;
+ } else if (this.focusedIndex === this.filteredBlobsLength - 1) {
+ // if index is the last index, scroll to the end
+ el.scrollTop = this.filteredBlobsLength * FILE_FINDER_ROW_HEIGHT;
+ } else if (scrollTop >= bottom + el.scrollTop) {
+ // if element is off the bottom of the scroll list, scroll down one item
+ el.scrollTop = scrollTop - bottom + FILE_FINDER_ROW_HEIGHT;
+ } else if (scrollTop < el.scrollTop) {
+ // if element is off the top of the scroll list, scroll up one item
+ el.scrollTop = scrollTop;
+ }
+ });
+ }
+ },
+ },
+ methods: {
+ ...mapActions(['toggleFileFinder']),
+ clearSearchInput() {
+ this.searchText = '';
+
+ this.$nextTick(() => {
+ this.$refs.searchInput.focus();
+ });
+ },
+ onKeydown(e) {
+ switch (e.keyCode) {
+ case UP_KEY_CODE:
+ e.preventDefault();
+ this.mouseOver = false;
+ this.cancelMouseOver = true;
+ if (this.focusedIndex > 0) {
+ this.focusedIndex -= 1;
+ } else {
+ this.focusedIndex = this.filteredBlobsLength - 1;
+ }
+ break;
+ case DOWN_KEY_CODE:
+ e.preventDefault();
+ this.mouseOver = false;
+ this.cancelMouseOver = true;
+ if (this.focusedIndex < this.filteredBlobsLength - 1) {
+ this.focusedIndex += 1;
+ } else {
+ this.focusedIndex = 0;
+ }
+ break;
+ default:
+ break;
+ }
+ },
+ onKeyup(e) {
+ switch (e.keyCode) {
+ case ENTER_KEY_CODE:
+ this.openFile(this.filteredBlobs[this.focusedIndex]);
+ break;
+ case ESC_KEY_CODE:
+ this.toggleFileFinder(false);
+ break;
+ default:
+ break;
+ }
+ },
+ openFile(file) {
+ this.toggleFileFinder(false);
+ router.push(`/project${file.url}`);
+ },
+ onMouseOver(index) {
+ if (!this.cancelMouseOver) {
+ this.mouseOver = true;
+ this.focusedIndex = index;
+ }
+ },
+ onMouseMove(index) {
+ this.cancelMouseOver = false;
+ this.onMouseOver(index);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-file-finder-overlay"
+ @mousedown.self="toggleFileFinder(false)"
+ >
+ <div
+ class="dropdown-menu diff-file-changes ide-file-finder show"
+ >
+ <div class="dropdown-input">
+ <input
+ type="search"
+ class="dropdown-input-field"
+ :placeholder="__('Search files')"
+ autocomplete="off"
+ v-model="searchText"
+ ref="searchInput"
+ @keydown="onKeydown($event)"
+ @keyup="onKeyup($event)"
+ />
+ <i
+ aria-hidden="true"
+ class="fa fa-search dropdown-input-search"
+ :class="{
+ hidden: showClearInputButton
+ }"
+ ></i>
+ <i
+ role="button"
+ :aria-label="__('Clear search input')"
+ class="fa fa-times dropdown-input-clear"
+ :class="{
+ show: showClearInputButton
+ }"
+ @click="clearSearchInput"
+ ></i>
+ </div>
+ <div>
+ <virtual-list
+ :size="listHeight"
+ :remain="listShowCount"
+ wtag="ul"
+ ref="virtualScrollList"
+ >
+ <template v-if="filteredBlobsLength">
+ <li
+ v-for="(file, index) in filteredBlobs"
+ :key="file.key"
+ >
+ <item
+ class="disable-hover"
+ :file="file"
+ :search-text="searchText"
+ :focused="index === focusedIndex"
+ :index="index"
+ @click="openFile"
+ @mouseover="onMouseOver"
+ @mousemove="onMouseMove"
+ />
+ </li>
+ </template>
+ <li
+ v-else
+ class="dropdown-menu-empty-item"
+ >
+ <div class="append-right-default prepend-left-default prepend-top-8 append-bottom-8">
+ <template v-if="loading">
+ {{ __('Loading...') }}
+ </template>
+ <template v-else>
+ {{ __('No files found.') }}
+ </template>
+ </div>
+ </li>
+ </virtual-list>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue
new file mode 100644
index 00000000000..d4427420207
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_finder/item.vue
@@ -0,0 +1,113 @@
+<script>
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import FileIcon from '../../../vue_shared/components/file_icon.vue';
+import ChangedFileIcon from '../changed_file_icon.vue';
+
+const MAX_PATH_LENGTH = 60;
+
+export default {
+ components: {
+ ChangedFileIcon,
+ FileIcon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ focused: {
+ type: Boolean,
+ required: true,
+ },
+ searchText: {
+ type: String,
+ required: true,
+ },
+ index: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ pathWithEllipsis() {
+ const path = this.file.path;
+
+ return path.length < MAX_PATH_LENGTH
+ ? path
+ : `...${path.substr(path.length - MAX_PATH_LENGTH)}`;
+ },
+ nameSearchTextOccurences() {
+ return fuzzaldrinPlus.match(this.file.name, this.searchText);
+ },
+ pathSearchTextOccurences() {
+ return fuzzaldrinPlus.match(this.pathWithEllipsis, this.searchText);
+ },
+ },
+ methods: {
+ clickRow() {
+ this.$emit('click', this.file);
+ },
+ mouseOverRow() {
+ this.$emit('mouseover', this.index);
+ },
+ mouseMove() {
+ this.$emit('mousemove', this.index);
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ type="button"
+ class="diff-changed-file"
+ :class="{
+ 'is-focused': focused,
+ }"
+ @click.prevent="clickRow"
+ @mouseover="mouseOverRow"
+ @mousemove="mouseMove"
+ >
+ <file-icon
+ :file-name="file.name"
+ :size="16"
+ css-classes="diff-file-changed-icon append-right-8"
+ />
+ <span class="diff-changed-file-content append-right-8">
+ <strong
+ class="diff-changed-file-name"
+ >
+ <span
+ v-for="(char, index) in file.name.split('')"
+ :key="index + char"
+ :class="{
+ highlighted: nameSearchTextOccurences.indexOf(index) >= 0,
+ }"
+ v-text="char"
+ >
+ </span>
+ </strong>
+ <span
+ class="diff-changed-file-path prepend-top-5"
+ >
+ <span
+ v-for="(char, index) in pathWithEllipsis.split('')"
+ :key="index + char"
+ :class="{
+ highlighted: pathSearchTextOccurences.indexOf(index) >= 0,
+ }"
+ v-text="char"
+ >
+ </span>
+ </span>
+ </span>
+ <span
+ v-if="file.changed || file.tempFile"
+ class="diff-changed-stats"
+ >
+ <changed-file-icon
+ :file="file"
+ />
+ </span>
+ </button>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 1c237c0ec97..0274fc7d299 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,55 +1,91 @@
<script>
-import { mapState, mapGetters } from 'vuex';
-import ideSidebar from './ide_side_bar.vue';
-import ideContextbar from './ide_context_bar.vue';
-import repoTabs from './repo_tabs.vue';
-import ideStatusBar from './ide_status_bar.vue';
-import repoEditor from './repo_editor.vue';
+ import { mapActions, mapState, mapGetters } from 'vuex';
+ import Mousetrap from 'mousetrap';
+ import ideSidebar from './ide_side_bar.vue';
+ import ideContextbar from './ide_context_bar.vue';
+ import repoTabs from './repo_tabs.vue';
+ import ideStatusBar from './ide_status_bar.vue';
+ import repoEditor from './repo_editor.vue';
+ import FindFile from './file_finder/index.vue';
-export default {
- components: {
- ideSidebar,
- ideContextbar,
- repoTabs,
- ideStatusBar,
- repoEditor,
- },
- props: {
- emptyStateSvgPath: {
- type: String,
- required: true,
+ const originalStopCallback = Mousetrap.stopCallback;
+
+ export default {
+ components: {
+ ideSidebar,
+ ideContextbar,
+ repoTabs,
+ ideStatusBar,
+ repoEditor,
+ FindFile,
},
- noChangesStateSvgPath: {
- type: String,
- required: true,
+ props: {
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ noChangesStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ committedStateSvgPath: {
+ type: String,
+ required: true,
+ },
},
- committedStateSvgPath: {
- type: String,
- required: true,
+ computed: {
+ ...mapState([
+ 'changedFiles',
+ 'openFiles',
+ 'viewer',
+ 'currentMergeRequestId',
+ 'fileFindVisible',
+ ]),
+ ...mapGetters(['activeFile', 'hasChanges']),
},
- },
- computed: {
- ...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
- ...mapGetters(['activeFile', 'hasChanges']),
- },
- mounted() {
- const returnValue = 'Are you sure you want to lose unsaved changes?';
- window.onbeforeunload = e => {
- if (!this.changedFiles.length) return undefined;
+ mounted() {
+ const returnValue = 'Are you sure you want to lose unsaved changes?';
+ window.onbeforeunload = e => {
+ if (!this.changedFiles.length) return undefined;
+
+ Object.assign(e, {
+ returnValue,
+ });
+ return returnValue;
+ };
- Object.assign(e, {
- returnValue,
+ Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
+
+ this.toggleFileFinder(!this.fileFindVisible);
});
- return returnValue;
- };
- },
-};
+
+ Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
+ },
+ methods: {
+ ...mapActions(['toggleFileFinder']),
+ mousetrapStopCallback(e, el, combo) {
+ if (combo === 't' && el.classList.contains('dropdown-input-field')) {
+ return true;
+ } else if (combo === 'command+p' || combo === 'ctrl+p') {
+ return false;
+ }
+
+ return originalStopCallback(e, el, combo);
+ },
+ },
+ };
</script>
<template>
<div
class="ide-view"
>
+ <find-file
+ v-show="fileFindVisible"
+ />
<ide-sidebar />
<div
class="multi-file-edit-pane"
diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue
index 79a83b47994..627fbeb9adf 100644
--- a/app/assets/javascripts/ide/components/ide_context_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_context_bar.vue
@@ -1,5 +1,4 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import repoCommitSection from './repo_commit_section.vue';
@@ -22,13 +21,6 @@ export default {
required: true,
},
},
- computed: {
- ...mapState(['changedFiles', 'rightPanelCollapsed']),
- ...mapGetters(['currentIcon']),
- },
- methods: {
- ...mapActions(['setPanelCollapsedStatus']),
- },
};
</script>
@@ -41,40 +33,6 @@ export default {
<div
class="multi-file-commit-panel-section"
>
- <header
- class="multi-file-commit-panel-header"
- :class="{
- 'is-collapsed': rightPanelCollapsed,
- }"
- >
- <div
- class="multi-file-commit-panel-header-title"
- v-if="!rightPanelCollapsed"
- >
- <div
- v-if="changedFiles.length"
- >
- <icon
- name="list-bulleted"
- :size="18"
- />
- Staged
- </div>
- </div>
- <button
- type="button"
- class="btn btn-transparent multi-file-commit-panel-collapse-btn"
- @click.stop="setPanelCollapsedStatus({
- side: 'right',
- collapsed: !rightPanelCollapsed,
- })"
- >
- <icon
- :name="currentIcon"
- :size="18"
- />
- </button>
- </header>
<repo-commit-section
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 152a5f632ad..c13eeeace3f 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -22,13 +22,6 @@ export default {
<template>
<div class="ide-status-bar">
- <div class="ref-name">
- <icon
- name="branch"
- :size="12"
- />
- {{ file.branchId }}
- </div>
<div>
<div v-if="file.lastCommit && file.lastCommit.id">
Last commit:
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index d885ed5e301..877d1b5e026 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -1,20 +1,24 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
-import icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
-import commitFilesList from './commit_sidebar/list.vue';
+import CommitFilesList from './commit_sidebar/list.vue';
+import EmptyState from './commit_sidebar/empty_state.vue';
+import CommitMessageField from './commit_sidebar/message_field.vue';
import * as consts from '../stores/modules/commit/constants';
import Actions from './commit_sidebar/actions.vue';
export default {
components: {
DeprecatedModal,
- icon,
- commitFilesList,
+ Icon,
+ CommitFilesList,
+ EmptyState,
Actions,
LoadingButton,
+ CommitMessageField,
},
directives: {
tooltip,
@@ -30,43 +34,19 @@ export default {
},
},
computed: {
- ...mapState([
- 'currentProjectId',
- 'currentBranchId',
- 'rightPanelCollapsed',
- 'lastCommitMsg',
- 'changedFiles',
- ]),
+ ...mapState(['changedFiles', 'stagedFiles', 'rightPanelCollapsed']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
- ...mapGetters('commit', [
- 'commitButtonDisabled',
- 'discardDraftButtonDisabled',
- 'branchName',
- ]),
- statusSvg() {
- return this.lastCommitMsg
- ? this.committedStateSvgPath
- : this.noChangesStateSvgPath;
- },
+ ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled', 'branchName']),
},
methods: {
- ...mapActions(['setPanelCollapsedStatus']),
...mapActions('commit', [
'updateCommitMessage',
'discardDraft',
'commitChanges',
'updateCommitAction',
]),
- toggleCollapsed() {
- this.setPanelCollapsedStatus({
- side: 'right',
- collapsed: !this.rightPanelCollapsed,
- });
- },
forceCreateNewBranch() {
- return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() =>
- this.commitChanges(),
- );
+ return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
},
},
};
@@ -75,9 +55,6 @@ export default {
<template>
<div
class="multi-file-commit-panel-section"
- :class="{
- 'multi-file-commit-empty-state-container': !changedFiles.length
- }"
>
<deprecated-modal
id="ide-create-branch-modal"
@@ -91,30 +68,36 @@ export default {
Would you like to create a new branch?`) }}
</template>
</deprecated-modal>
- <commit-files-list
- title="Staged"
- :file-list="changedFiles"
- :collapsed="rightPanelCollapsed"
- @toggleCollapsed="toggleCollapsed"
- />
<template
- v-if="changedFiles.length"
+ v-if="changedFiles.length || stagedFiles.length"
>
+ <commit-files-list
+ icon-name="unstaged"
+ :title="__('Unstaged')"
+ :file-list="changedFiles"
+ action="stageAllChanges"
+ :action-btn-text="__('Stage all')"
+ item-action-component="stage-button"
+ />
+ <commit-files-list
+ icon-name="staged"
+ :title="__('Staged')"
+ :file-list="stagedFiles"
+ action="unstageAllChanges"
+ :action-btn-text="__('Unstage all')"
+ item-action-component="unstage-button"
+ :show-toggle="false"
+ :staged-list="true"
+ />
<form
class="form-horizontal multi-file-commit-form"
@submit.prevent.stop="commitChanges"
v-if="!rightPanelCollapsed"
>
- <div class="multi-file-commit-fieldset">
- <textarea
- class="form-control multi-file-commit-message"
- name="commit-message"
- :value="commitMessage"
- :placeholder="__('Write a commit message...')"
- @input="updateCommitMessage($event.target.value)"
- >
- </textarea>
- </div>
+ <commit-message-field
+ :text="commitMessage"
+ @input="updateCommitMessage"
+ />
<div class="clearfix prepend-top-15">
<actions />
<loading-button
@@ -135,38 +118,10 @@ export default {
</div>
</form>
</template>
- <div
- v-else-if="!rightPanelCollapsed"
- class="row js-empty-state"
- >
- <div class="col-xs-10 col-xs-offset-1">
- <div class="svg-content svg-80">
- <img :src="statusSvg" />
- </div>
- </div>
- <div class="col-xs-10 col-xs-offset-1">
- <div
- class="text-content text-center"
- v-if="!lastCommitMsg"
- >
- <h4>
- {{ __('No changes') }}
- </h4>
- <p>
- {{ __('Edit files in the editor and commit changes here') }}
- </p>
- </div>
- <div
- class="text-content text-center"
- v-else
- >
- <h4>
- {{ __('All changes are committed') }}
- </h4>
- <p v-html="lastCommitMsg">
- </p>
- </div>
- </div>
- </div>
+ <empty-state
+ v-else
+ :no-changes-state-svg-path="noChangesStateSvgPath"
+ :committed-state-svg-path="committedStateSvgPath"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 6aa44ca2c11..3a04cdd8e46 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -20,9 +20,9 @@ export default {
},
computed: {
...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
- ...mapGetters(['currentMergeRequest']),
+ ...mapGetters(['currentMergeRequest', 'getStagedFile']),
shouldHideEditor() {
- return this.file && this.file.binary && !this.file.raw;
+ return this.file && this.file.binary && !this.file.content;
},
editTabCSS() {
return {
@@ -120,7 +120,12 @@ export default {
setupEditor() {
if (!this.file || !this.editor.instance) return;
- this.model = this.editor.createModel(this.file);
+ const head = this.getStagedFile(this.file.path);
+
+ this.model = this.editor.createModel(
+ this.file,
+ this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null,
+ );
if (this.viewer === 'mrdiff') {
this.editor.attachMergeRequestModel(this.model);
@@ -212,7 +217,7 @@ export default {
<content-viewer
v-if="shouldHideEditor || file.viewMode === 'preview'"
:content="file.content || file.raw"
- :path="file.rawPath"
+ :path="file.rawPath || file.path"
:file-size="file.size"
:project-path="file.projectId"/>
</div>
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 3b5068d4910..8b18c7d28b4 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -102,8 +102,11 @@ export default {
v-if="file.mrChange"
/>
<changed-file-icon
+ v-if="file.changed || file.tempFile || file.staged"
:file="file"
- v-if="file.changed || file.tempFile"
+ :show-tooltip="true"
+ :show-staged-icon="true"
+ class="prepend-top-5 pull-right"
/>
</span>
<new-dropdown
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index 304a73ed1ad..35a362b01e0 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -26,13 +26,16 @@ export default {
},
computed: {
closeLabel() {
- if (this.tab.changed || this.tab.tempFile) {
+ if (this.fileHasChanged) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
showChangedIcon() {
- return this.tab.changed ? !this.tabMouseOver : false;
+ return this.fileHasChanged ? !this.tabMouseOver : false;
+ },
+ fileHasChanged() {
+ return this.tab.changed || this.tab.tempFile || this.tab.staged;
},
},
@@ -42,18 +45,18 @@ export default {
this.updateDelayViewerUpdated(true);
if (tab.pending) {
- this.openPendingTab(tab);
+ this.openPendingTab({ file: tab, keyPrefix: tab.staged ? 'staged' : 'unstaged' });
} else {
this.$router.push(`/project${tab.url}`);
}
},
mouseOverTab() {
- if (this.tab.changed) {
+ if (this.fileHasChanged) {
this.tabMouseOver = true;
}
},
mouseOutTab() {
- if (this.tab.changed) {
+ if (this.fileHasChanged) {
this.tabMouseOver = false;
}
},
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
new file mode 100644
index 00000000000..b06da9f95d1
--- /dev/null
+++ b/app/assets/javascripts/ide/constants.js
@@ -0,0 +1,8 @@
+// Fuzzy file finder
+export const MAX_FILE_FINDER_RESULTS = 40;
+export const FILE_FINDER_ROW_HEIGHT = 55;
+export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
+
+// Commit message textarea
+export const MAX_TITLE_LENGTH = 50;
+export const MAX_BODY_LENGTH = 72;
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 20983666b4a..4a0a303d5a6 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -36,11 +36,11 @@ const router = new VueRouter({
base: `${gon.relative_url_root}/-/ide/`,
routes: [
{
- path: '/project/:namespace/:project',
+ path: '/project/:namespace/:project+',
component: EmptyRouterComponent,
children: [
{
- path: ':targetmode/:branch/*',
+ path: ':targetmode(edit|tree|blob)/:branch/*',
component: EmptyRouterComponent,
},
{
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index e47adae99ed..016dcda1fa1 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -3,15 +3,16 @@ import Disposable from './disposable';
import eventHub from '../../eventhub';
export default class Model {
- constructor(monaco, file) {
+ constructor(monaco, file, head = null) {
this.monaco = monaco;
this.disposable = new Disposable();
this.file = file;
+ this.head = head;
this.content = file.content !== '' ? file.content : file.raw;
this.disposable.add(
(this.originalModel = this.monaco.editor.createModel(
- this.file.raw,
+ head ? head.content : this.file.raw,
undefined,
new this.monaco.Uri(null, null, `original/${this.file.key}`),
)),
@@ -31,13 +32,15 @@ export default class Model {
);
}
- this.events = new Map();
+ this.events = new Set();
this.updateContent = this.updateContent.bind(this);
+ this.updateNewContent = this.updateNewContent.bind(this);
this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.key}`, this.dispose);
- eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
+ eventHub.$on(`editor.update.model.content.${this.file.key}`, this.updateContent);
+ eventHub.$on(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent);
}
get url() {
@@ -73,22 +76,36 @@ export default class Model {
}
onChange(cb) {
- this.events.set(
- this.path,
- this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))),
- );
+ this.events.add(this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))));
+ }
+
+ onDispose(cb) {
+ this.events.add(cb);
}
- updateContent(content) {
+ updateContent({ content, changed }) {
this.getOriginalModel().setValue(content);
+
+ if (!changed) {
+ this.getModel().setValue(content);
+ }
+ }
+
+ updateNewContent(content) {
this.getModel().setValue(content);
}
dispose() {
this.disposable.dispose();
+
+ this.events.forEach(cb => {
+ if (typeof cb === 'function') cb();
+ });
+
this.events.clear();
eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose);
- eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
+ eventHub.$off(`editor.update.model.content.${this.file.key}`, this.updateContent);
+ eventHub.$off(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent);
}
}
diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js
index 0e7b563b5d6..7f643969480 100644
--- a/app/assets/javascripts/ide/lib/common/model_manager.js
+++ b/app/assets/javascripts/ide/lib/common/model_manager.js
@@ -17,12 +17,12 @@ export default class ModelManager {
return this.models.get(key);
}
- addModel(file) {
+ addModel(file, head = null) {
if (this.hasCachedModel(file.key)) {
return this.getModel(file.key);
}
- const model = new Model(this.monaco, file);
+ const model = new Model(this.monaco, file, head);
this.models.set(model.path, model);
this.disposable.add(model);
diff --git a/app/assets/javascripts/ide/lib/decorations/controller.js b/app/assets/javascripts/ide/lib/decorations/controller.js
index 42904774747..13d477bb2cf 100644
--- a/app/assets/javascripts/ide/lib/decorations/controller.js
+++ b/app/assets/javascripts/ide/lib/decorations/controller.js
@@ -38,6 +38,15 @@ export default class DecorationsController {
);
}
+ hasDecorations(model) {
+ return this.decorations.has(model.url);
+ }
+
+ removeDecorations(model) {
+ this.decorations.delete(model.url);
+ this.editorDecorations.delete(model.url);
+ }
+
dispose() {
this.decorations.clear();
this.editorDecorations.clear();
diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
index b136545ad11..f579424cf33 100644
--- a/app/assets/javascripts/ide/lib/diff/controller.js
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
@@ -3,7 +3,7 @@ import { throttle } from 'underscore';
import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable';
-export const getDiffChangeType = (change) => {
+export const getDiffChangeType = change => {
if (change.modified) {
return 'modified';
} else if (change.added) {
@@ -16,12 +16,7 @@ export const getDiffChangeType = (change) => {
};
export const getDecorator = change => ({
- range: new monaco.Range(
- change.lineNumber,
- 1,
- change.endLineNumber,
- 1,
- ),
+ range: new monaco.Range(change.lineNumber, 1, change.endLineNumber, 1),
options: {
isWholeLine: true,
linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
@@ -31,6 +26,7 @@ export const getDecorator = change => ({
export default class DirtyDiffController {
constructor(modelManager, decorationsController) {
this.disposable = new Disposable();
+ this.models = new Map();
this.editorSimpleWorker = null;
this.modelManager = modelManager;
this.decorationsController = decorationsController;
@@ -42,7 +38,15 @@ export default class DirtyDiffController {
}
attachModel(model) {
+ if (this.models.has(model.url)) return;
+
model.onChange(() => this.throttledComputeDiff(model));
+ model.onDispose(() => {
+ this.decorationsController.removeDecorations(model);
+ this.models.delete(model.url);
+ });
+
+ this.models.set(model.url, model);
}
computeDiff(model) {
@@ -54,7 +58,11 @@ export default class DirtyDiffController {
}
reDecorate(model) {
- this.decorationsController.decorate(model);
+ if (this.decorationsController.hasDecorations(model)) {
+ this.decorationsController.decorate(model);
+ } else {
+ this.computeDiff(model);
+ }
}
decorate({ data }) {
@@ -65,6 +73,7 @@ export default class DirtyDiffController {
dispose() {
this.disposable.dispose();
+ this.models.clear();
this.dirtyDiffWorker.removeEventListener('message', this.decorate);
this.dirtyDiffWorker.terminate();
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 001737d6ee8..b65d9c68a0b 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -1,10 +1,12 @@
import _ from 'underscore';
+import store from '../stores';
import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller';
import Disposable from './common/disposable';
import ModelManager from './common/model_manager';
import editorOptions, { defaultEditorOptions } from './editor_options';
import gitlabTheme from './themes/gl_theme';
+import keymap from './keymap.json';
export const clearDomElement = el => {
if (!el || !el.firstChild) return;
@@ -53,6 +55,8 @@ export default class Editor {
)),
);
+ this.addCommands();
+
window.addEventListener('resize', this.debouncedUpdate, false);
}
}
@@ -73,12 +77,14 @@ export default class Editor {
})),
);
+ this.addCommands();
+
window.addEventListener('resize', this.debouncedUpdate, false);
}
}
- createModel(file) {
- return this.modelManager.addModel(file);
+ createModel(file, head = null) {
+ return this.modelManager.addModel(file, head);
}
attachModel(model) {
@@ -189,4 +195,31 @@ export default class Editor {
static renderSideBySide(domElement) {
return domElement.offsetWidth >= 700;
}
+
+ addCommands() {
+ const getKeyCode = key => {
+ const monacoKeyMod = key.indexOf('KEY_') === 0;
+
+ return monacoKeyMod ? this.monaco.KeyCode[key] : this.monaco.KeyMod[key];
+ };
+
+ keymap.forEach(command => {
+ const keybindings = command.bindings.map(binding => {
+ const keys = binding.split('+');
+
+ // eslint-disable-next-line no-bitwise
+ return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]);
+ });
+
+ this.instance.addAction({
+ id: command.id,
+ label: command.label,
+ keybindings,
+ run() {
+ store.dispatch(command.action.name, command.action.params);
+ return null;
+ },
+ });
+ });
+ }
}
diff --git a/app/assets/javascripts/ide/lib/keymap.json b/app/assets/javascripts/ide/lib/keymap.json
new file mode 100644
index 00000000000..131abfebbed
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/keymap.json
@@ -0,0 +1,11 @@
+[
+ {
+ "id": "file-finder",
+ "label": "File finder",
+ "bindings": ["CtrlCmd+KEY_P"],
+ "action": {
+ "name": "toggleFileFinder",
+ "params": true
+ }
+ }
+]
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index c6ba679d99c..cbe43f5f7f2 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -1,3 +1,4 @@
+import $ from 'jquery';
import Vue from 'vue';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
@@ -32,6 +33,22 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
}
};
+export const toggleRightPanelCollapsed = (
+ { dispatch, state },
+ e = undefined,
+) => {
+ if (e) {
+ $(e.currentTarget)
+ .tooltip('hide')
+ .blur();
+ }
+
+ dispatch('setPanelCollapsedStatus', {
+ side: 'right',
+ collapsed: !state.rightPanelCollapsed,
+ });
+};
+
export const setResizingStatus = ({ commit }, resizing) => {
commit(types.SET_RESIZING_STATUS, resizing);
};
@@ -104,6 +121,14 @@ export const scrollToTab = () => {
});
};
+export const stageAllChanges = ({ state, commit }) => {
+ state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
+};
+
+export const unstageAllChanges = ({ state, commit }) => {
+ state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path));
+};
+
export const updateViewer = ({ commit }, viewer) => {
commit(types.UPDATE_VIEWER, viewer);
};
@@ -112,7 +137,13 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
};
+export const toggleFileFinder = ({ commit }, fileFindVisible) =>
+ commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
+
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
export * from './actions/merge_request';
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 1a17320a1ea..d782e0a84d2 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -24,7 +24,10 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
if (nextFileToOpen.pending) {
dispatch('updateViewer', 'diff');
- dispatch('openPendingTab', nextFileToOpen);
+ dispatch('openPendingTab', {
+ file: nextFileToOpen,
+ keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
+ });
} else {
dispatch('updateDelayViewerUpdated', true);
router.push(`/project${nextFileToOpen.url}`);
@@ -153,7 +156,7 @@ export const setFileViewMode = ({ state, commit }, { file, viewMode }) => {
commit(types.SET_FILE_VIEWMODE, { file, viewMode });
};
-export const discardFileChanges = ({ state, commit }, path) => {
+export const discardFileChanges = ({ dispatch, state, commit, getters }, path) => {
const file = state.entries[path];
commit(types.DISCARD_FILE_CHANGES, path);
@@ -161,17 +164,40 @@ export const discardFileChanges = ({ state, commit }, path) => {
if (file.tempFile && file.opened) {
commit(types.TOGGLE_FILE_OPEN, path);
+ } else if (getters.activeFile && file.path === getters.activeFile.path) {
+ dispatch('updateDelayViewerUpdated', true)
+ .then(() => {
+ router.push(`/project${file.url}`);
+ })
+ .catch(e => {
+ throw e;
+ });
+ }
+
+ eventHub.$emit(`editor.update.model.new.content.${file.key}`, file.content);
+ eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content);
+};
+
+export const stageChange = ({ commit, state }, path) => {
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
+ commit(types.STAGE_CHANGE, path);
+
+ if (stagedFile) {
+ eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
}
+};
- eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
+export const unstageChange = ({ commit }, path) => {
+ commit(types.UNSTAGE_CHANGE, path);
};
-export const openPendingTab = ({ commit, getters, dispatch, state }, file) => {
- if (getters.activeFile && getters.activeFile.path === file.path && state.viewer === 'diff') {
+export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => {
+ if (getters.activeFile && getters.activeFile === file && state.viewer === 'diff') {
return false;
}
- commit(types.ADD_PENDING_TAB, { file });
+ commit(types.ADD_PENDING_TAB, { file, keyPrefix });
dispatch('scrollToTab');
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index b3882cb8d21..4eb23b2ee0f 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -5,45 +5,71 @@ import * as types from '../mutation_types';
export const getProjectData = (
{ commit, state, dispatch },
{ namespace, projectId, force = false } = {},
-) => new Promise((resolve, reject) => {
- if (!state.projects[`${namespace}/${projectId}`] || force) {
- commit(types.TOGGLE_LOADING, { entry: state });
- service.getProjectData(namespace, projectId)
- .then(res => res.data)
- .then((data) => {
+) =>
+ new Promise((resolve, reject) => {
+ if (!state.projects[`${namespace}/${projectId}`] || force) {
commit(types.TOGGLE_LOADING, { entry: state });
- commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
- if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
- resolve(data);
- })
- .catch(() => {
- flash('Error loading project data. Please try again.', 'alert', document, null, false, true);
- reject(new Error(`Project not loaded ${namespace}/${projectId}`));
- });
- } else {
- resolve(state.projects[`${namespace}/${projectId}`]);
- }
-});
+ service
+ .getProjectData(namespace, projectId)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.TOGGLE_LOADING, { entry: state });
+ commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
+ if (!state.currentProjectId)
+ commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
+ resolve(data);
+ })
+ .catch(() => {
+ flash(
+ 'Error loading project data. Please try again.',
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+ reject(new Error(`Project not loaded ${namespace}/${projectId}`));
+ });
+ } else {
+ resolve(state.projects[`${namespace}/${projectId}`]);
+ }
+ });
export const getBranchData = (
{ commit, state, dispatch },
{ projectId, branchId, force = false } = {},
-) => new Promise((resolve, reject) => {
- if ((typeof state.projects[`${projectId}`] === 'undefined' ||
- !state.projects[`${projectId}`].branches[branchId])
- || force) {
- service.getBranchData(`${projectId}`, branchId)
- .then(({ data }) => {
- const { id } = data.commit;
- commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
- commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
- resolve(data);
- })
- .catch(() => {
- flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
- reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
- });
- } else {
- resolve(state.projects[`${projectId}`].branches[branchId]);
- }
-});
+) =>
+ new Promise((resolve, reject) => {
+ if (
+ typeof state.projects[`${projectId}`] === 'undefined' ||
+ !state.projects[`${projectId}`].branches[branchId] ||
+ force
+ ) {
+ service
+ .getBranchData(`${projectId}`, branchId)
+ .then(({ data }) => {
+ const { id } = data.commit;
+ commit(types.SET_BRANCH, {
+ projectPath: `${projectId}`,
+ branchName: branchId,
+ branch: data,
+ });
+ commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
+ commit(types.SET_CURRENT_BRANCH, branchId);
+ resolve(data);
+ })
+ .catch(() => {
+ flash(
+ 'Error loading branch data. Please try again.',
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+ reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
+ });
+ } else {
+ resolve(state.projects[`${projectId}`].branches[branchId]);
+ }
+ });
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index a77cdbc13c8..ec1ea155aee 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
@@ -29,9 +31,31 @@ export const currentMergeRequest = state => {
};
// eslint-disable-next-line no-confusing-arrow
-export const currentIcon = state =>
+export const collapseButtonIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
-export const hasChanges = state => !!state.changedFiles.length;
+export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
+
+// eslint-disable-next-line no-confusing-arrow
+export const collapseButtonTooltip = state =>
+ state.rightPanelCollapsed ? __('Expand sidebar') : __('Collapse sidebar');
export const hasMergeRequest = state => !!state.currentMergeRequestId;
+
+export const allBlobs = state =>
+ Object.keys(state.entries)
+ .reduce((acc, key) => {
+ const entry = state.entries[key];
+
+ if (entry.type === 'blob') {
+ acc.push(entry);
+ }
+
+ return acc;
+ }, [])
+ .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
+
+export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index f536ce6344b..119debaf5f3 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -37,9 +37,9 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => {
const commitMsg = sprintf(
__('Your changes have been committed. Commit %{commitId} %{commitStats}'),
{
- commitId: `<a href="${currentProject.web_url}/commit/${
+ commitId: `<a href="${currentProject.web_url}/commit/${data.short_id}" class="commit-sha">${
data.short_id
- }" class="commit-sha">${data.short_id}</a>`,
+ }</a>`,
commitStats,
},
false,
@@ -54,9 +54,7 @@ export const checkCommitStatus = ({ rootState }) =>
.then(({ data }) => {
const { id } = data.commit;
const selectedBranch =
- rootState.projects[rootState.currentProjectId].branches[
- rootState.currentBranchId
- ];
+ rootState.projects[rootState.currentProjectId].branches[rootState.currentBranchId];
if (selectedBranch.workingReference !== id) {
return true;
@@ -100,67 +98,35 @@ export const updateFilesAfterCommit = (
{ root: true },
);
- rootState.changedFiles.forEach(entry => {
- commit(
- rootTypes.SET_LAST_COMMIT_DATA,
- {
- entry,
- lastCommit,
- },
- { root: true },
- );
-
- eventHub.$emit(`editor.update.model.content.${entry.path}`, entry.content);
+ rootState.stagedFiles.forEach(file => {
+ const changedFile = rootState.changedFiles.find(f => f.path === file.path);
commit(
- rootTypes.SET_FILE_RAW_DATA,
+ rootTypes.UPDATE_FILE_AFTER_COMMIT,
{
- file: entry,
- raw: entry.content,
+ file,
+ lastCommit,
},
{ root: true },
);
- commit(
- rootTypes.TOGGLE_FILE_CHANGED,
- {
- file: entry,
- changed: false,
- },
- { root: true },
- );
+ eventHub.$emit(`editor.update.model.content.${file.key}`, {
+ content: file.content,
+ changed: !!changedFile,
+ });
});
- commit(rootTypes.REMOVE_ALL_CHANGES_FILES, null, { root: true });
-
- if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) {
+ if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH && rootGetters.activeFile) {
router.push(
- `/project/${rootState.currentProjectId}/blob/${branch}/${
- rootGetters.activeFile.path
- }`,
+ `/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`,
);
}
-
- dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH);
};
-export const commitChanges = ({
- commit,
- state,
- getters,
- dispatch,
- rootState,
-}) => {
+export const commitChanges = ({ commit, state, getters, dispatch, rootState }) => {
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
- const payload = createCommitPayload(
- getters.branchName,
- newBranch,
- state,
- rootState,
- );
- const getCommitStatus = newBranch
- ? Promise.resolve(false)
- : dispatch('checkCommitStatus');
+ const payload = createCommitPayload(getters.branchName, newBranch, state, rootState);
+ const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus');
commit(types.UPDATE_LOADING, true);
@@ -182,28 +148,31 @@ export const commitChanges = ({
if (!data.short_id) {
flash(data.message, 'alert', document, null, false, true);
- return;
+ return null;
}
dispatch('setLastCommitMessage', data);
dispatch('updateCommitMessage', '');
+ return dispatch('updateFilesAfterCommit', {
+ data,
+ branch: getters.branchName,
+ })
+ .then(() => {
+ if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) {
+ dispatch(
+ 'redirectToUrl',
+ createNewMergeRequestUrl(
+ rootState.projects[rootState.currentProjectId].web_url,
+ getters.branchName,
+ rootState.currentBranchId,
+ ),
+ { root: true },
+ );
+ }
- if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) {
- dispatch(
- 'redirectToUrl',
- createNewMergeRequestUrl(
- rootState.projects[rootState.currentProjectId].web_url,
- getters.branchName,
- rootState.currentBranchId,
- ),
- { root: true },
- );
- } else {
- dispatch('updateFilesAfterCommit', {
- data,
- branch: getters.branchName,
- });
- }
+ commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
+ })
+ .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH));
})
.catch(err => {
let errMsg = __('Error committing changes. Please try again.');
@@ -216,3 +185,6 @@ export const commitChanges = ({
commit(types.UPDATE_LOADING, false);
});
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js
index f7cdd6adb0c..d01060201f2 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js
@@ -1,12 +1,17 @@
import * as consts from './constants';
-export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading;
+const BRANCH_SUFFIX_COUNT = 5;
+
+export const discardDraftButtonDisabled = state =>
+ state.commitMessage === '' || state.submitCommitLoading;
export const commitButtonDisabled = (state, getters, rootState) =>
- getters.discardDraftButtonDisabled || !rootState.changedFiles.length;
+ getters.discardDraftButtonDisabled || !rootState.stagedFiles.length;
export const newBranchName = (state, _, rootState) =>
- `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(-5)}`;
+ `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(
+ -BRANCH_SUFFIX_COUNT,
+ )}`;
export const branchName = (state, getters, rootState) => {
if (
@@ -22,3 +27,6 @@ export const branchName = (state, getters, rootState) => {
return rootState.currentBranchId;
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index e3f504e5ab0..c7f08449d03 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -51,5 +51,12 @@ export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
+export const CLEAR_STAGED_CHANGES = 'CLEAR_STAGED_CHANGES';
+export const STAGE_CHANGE = 'STAGE_CHANGE';
+export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE';
+
+export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
+
+export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 5e5eb831662..2a6c136aeed 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -49,6 +49,11 @@ export default {
lastCommitMsg,
});
},
+ [types.CLEAR_STAGED_CHANGES](state) {
+ Object.assign(state, {
+ stagedFiles: [],
+ });
+ },
[types.SET_ENTRIES](state, entries) {
Object.assign(state, {
entries,
@@ -95,6 +100,27 @@ export default {
delayViewerUpdated,
});
},
+ [types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
+ Object.assign(state, {
+ fileFindVisible,
+ });
+ },
+ [types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) {
+ const changedFile = state.changedFiles.find(f => f.path === file.path);
+
+ Object.assign(state.entries[file.path], {
+ raw: file.content,
+ changed: !!changedFile,
+ staged: false,
+ lastCommit: Object.assign(state.entries[file.path].lastCommit, {
+ id: lastCommit.commit.id,
+ url: lastCommit.commit_path,
+ message: lastCommit.commit.message,
+ author: lastCommit.commit.author_name,
+ updatedAt: lastCommit.commit.authored_date,
+ }),
+ });
+ },
...projectMutations,
...mergeRequestMutation,
...fileMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index eeb14b5490c..c3041c77199 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -4,6 +4,7 @@ export default {
[types.SET_FILE_ACTIVE](state, { path, active }) {
Object.assign(state.entries[path], {
active,
+ lastOpenedAt: new Date().getTime(),
});
if (active && !state.entries[path].pending) {
@@ -57,7 +58,9 @@ export default {
});
},
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
- const changed = content !== state.entries[path].raw;
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+ const rawContent = stagedFile ? stagedFile.content : state.entries[path].raw;
+ const changed = content !== rawContent;
Object.assign(state.entries[path], {
content,
@@ -91,8 +94,10 @@ export default {
});
},
[types.DISCARD_FILE_CHANGES](state, path) {
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
Object.assign(state.entries[path], {
- content: state.entries[path].raw,
+ content: stagedFile ? stagedFile.content : state.entries[path].raw,
changed: false,
});
},
@@ -106,16 +111,67 @@ export default {
changedFiles: state.changedFiles.filter(f => f.path !== path),
});
},
+ [types.STAGE_CHANGE](state, path) {
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
+ Object.assign(state, {
+ changedFiles: state.changedFiles.filter(f => f.path !== path),
+ entries: Object.assign(state.entries, {
+ [path]: Object.assign(state.entries[path], {
+ staged: true,
+ changed: false,
+ }),
+ }),
+ });
+
+ if (stagedFile) {
+ Object.assign(stagedFile, {
+ ...state.entries[path],
+ });
+ } else {
+ Object.assign(state, {
+ stagedFiles: state.stagedFiles.concat({
+ ...state.entries[path],
+ }),
+ });
+ }
+ },
+ [types.UNSTAGE_CHANGE](state, path) {
+ const changedFile = state.changedFiles.find(f => f.path === path);
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
+ if (!changedFile && stagedFile) {
+ Object.assign(state.entries[path], {
+ ...stagedFile,
+ key: state.entries[path].key,
+ active: state.entries[path].active,
+ opened: state.entries[path].opened,
+ changed: true,
+ });
+
+ Object.assign(state, {
+ changedFiles: state.changedFiles.concat(state.entries[path]),
+ });
+ }
+
+ Object.assign(state, {
+ stagedFiles: state.stagedFiles.filter(f => f.path !== path),
+ entries: Object.assign(state.entries, {
+ [path]: Object.assign(state.entries[path], {
+ staged: false,
+ }),
+ }),
+ });
+ },
[types.TOGGLE_FILE_CHANGED](state, { file, changed }) {
Object.assign(state.entries[file.path], {
changed,
});
},
[types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
- const pendingTab = state.openFiles.find(f => f.path === file.path && f.pending);
- let openFiles = state.openFiles.map(f =>
- Object.assign(f, { active: f.path === file.path, opened: false }),
- );
+ const key = `${keyPrefix}-${file.key}`;
+ const pendingTab = state.openFiles.find(f => f.key === key && f.pending);
+ let openFiles = state.openFiles.map(f => Object.assign(f, { active: false, opened: false }));
if (!pendingTab) {
const openFile = openFiles.find(f => f.path === file.path);
@@ -126,10 +182,11 @@ export default {
if (f.path === file.path) {
return acc.concat({
...f,
+ content: file.content,
active: true,
pending: true,
opened: true,
- key: `${keyPrefix}-${f.key}`,
+ key,
});
}
diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js
index 7f7e470c9bb..1176c040fb9 100644
--- a/app/assets/javascripts/ide/stores/mutations/tree.js
+++ b/app/assets/javascripts/ide/stores/mutations/tree.js
@@ -17,12 +17,8 @@ export default {
});
},
[types.SET_DIRECTORY_DATA](state, { data, treePath }) {
- Object.assign(state, {
- trees: Object.assign(state.trees, {
- [treePath]: {
- tree: data,
- },
- }),
+ Object.assign(state.trees[treePath], {
+ tree: data,
});
},
[types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index e5cc8814000..3470bb9aec0 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -3,6 +3,7 @@ export default () => ({
currentBranchId: '',
currentMergeRequestId: '',
changedFiles: [],
+ stagedFiles: [],
endpoints: {},
lastCommitMsg: '',
lastCommitPath: '',
@@ -17,4 +18,5 @@ export default () => ({
entries: {},
viewer: 'editor',
delayViewerUpdated: false,
+ fileFindVisible: false,
});
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 05a019de54f..86612d845e0 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -15,6 +15,7 @@ export const dataStructure = () => ({
opened: false,
active: false,
changed: false,
+ staged: false,
lastCommitPath: '',
lastCommit: {
id: '',
@@ -41,6 +42,7 @@ export const dataStructure = () => ({
viewMode: 'edit',
previewMode: null,
size: 0,
+ lastOpenedAt: 0,
});
export const decorateData = entity => {
@@ -101,7 +103,7 @@ export const setPageTitle = title => {
export const createCommitPayload = (branch, newBranch, state, rootState) => ({
branch,
commit_message: state.commitMessage,
- actions: rootState.changedFiles.map(f => ({
+ actions: rootState.stagedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.content,
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 7470d634b99..f3d722409b0 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -30,10 +30,10 @@ export default class IssuableContext {
const $selectbox = $block.find('.selectbox');
if ($selectbox.is(':visible')) {
$selectbox.hide();
- $block.find('.value').show();
+ $block.find('.value:not(.dont-hide)').show();
} else {
$selectbox.show();
- $block.find('.value').hide();
+ $block.find('.value:not(.dont-hide)').hide();
}
if ($selectbox.is(':visible')) {
diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue
index 357bc9aab17..21b545d6cab 100644
--- a/app/assets/javascripts/jobs/components/header.vue
+++ b/app/assets/javascripts/jobs/components/header.vue
@@ -1,82 +1,94 @@
<script>
- import ciHeader from '../../vue_shared/components/header_ci_component.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import ciHeader from '../../vue_shared/components/header_ci_component.vue';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import callout from '../../vue_shared/components/callout.vue';
- export default {
- name: 'JobHeaderSection',
- components: {
- ciHeader,
- loadingIcon,
+export default {
+ name: 'JobHeaderSection',
+ components: {
+ ciHeader,
+ loadingIcon,
+ callout,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
},
- props: {
- job: {
- type: Object,
- required: true,
- },
- isLoading: {
- type: Boolean,
- required: true,
- },
+ isLoading: {
+ type: Boolean,
+ required: true,
},
- data() {
- return {
- actions: this.getActions(),
- };
+ },
+ data() {
+ return {
+ actions: this.getActions(),
+ };
+ },
+ computed: {
+ status() {
+ return this.job && this.job.status;
},
- computed: {
- status() {
- return this.job && this.job.status;
- },
- shouldRenderContent() {
- return !this.isLoading && Object.keys(this.job).length;
- },
- /**
- * When job has not started the key will be `false`
- * When job started the key will be a string with a date.
- */
- jobStarted() {
- return !this.job.started === false;
- },
+ shouldRenderContent() {
+ return !this.isLoading && Object.keys(this.job).length;
},
- watch: {
- job() {
- this.actions = this.getActions();
- },
+ shouldRenderReason() {
+ return !!(this.job.status && this.job.callout_message);
},
- methods: {
- getActions() {
- const actions = [];
+ /**
+ * When job has not started the key will be `false`
+ * When job started the key will be a string with a date.
+ */
+ jobStarted() {
+ return !this.job.started === false;
+ },
+ },
+ watch: {
+ job() {
+ this.actions = this.getActions();
+ },
+ },
+ methods: {
+ getActions() {
+ const actions = [];
- if (this.job.new_issue_path) {
- actions.push({
- label: 'New issue',
- path: this.job.new_issue_path,
- cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
- type: 'link',
- });
- }
- return actions;
- },
+ if (this.job.new_issue_path) {
+ actions.push({
+ label: 'New issue',
+ path: this.job.new_issue_path,
+ cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
+ type: 'link',
+ });
+ }
+ return actions;
},
- };
+ },
+};
</script>
<template>
- <div class="js-build-header build-header top-area">
- <ci-header
- v-if="shouldRenderContent"
- :status="status"
- item-name="Job"
- :item-id="job.id"
- :time="job.created_at"
- :user="job.user"
- :actions="actions"
- :has-sidebar-button="true"
- :should-render-triggered-label="jobStarted"
- />
- <loading-icon
- v-if="isLoading"
- size="2"
- class="prepend-top-default append-bottom-default"
+ <header>
+ <div class="js-build-header build-header top-area">
+ <ci-header
+ v-if="shouldRenderContent"
+ :status="status"
+ item-name="Job"
+ :item-id="job.id"
+ :time="job.created_at"
+ :user="job.user"
+ :actions="actions"
+ :has-sidebar-button="true"
+ :should-render-triggered-label="jobStarted"
+ />
+ <loading-icon
+ v-if="isLoading"
+ size="2"
+ class="prepend-top-default append-bottom-default"
+ />
+ </div>
+
+ <callout
+ v-if="shouldRenderReason"
+ :message="job.callout_message"
/>
- </div>
+ </header>
</template>
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
index af47056d98f..db19dc9b238 100644
--- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
@@ -1,80 +1,119 @@
<script>
- import detailRow from './sidebar_detail_row.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import timeagoMixin from '../../vue_shared/mixins/timeago';
- import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
+import detailRow from './sidebar_detail_row.vue';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import timeagoMixin from '../../vue_shared/mixins/timeago';
+import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
- export default {
- name: 'SidebarDetailsBlock',
- components: {
- detailRow,
- loadingIcon,
+export default {
+ name: 'SidebarDetailsBlock',
+ components: {
+ detailRow,
+ loadingIcon,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ job: {
+ type: Object,
+ required: true,
},
- mixins: [
- timeagoMixin,
- ],
- props: {
- job: {
- type: Object,
- required: true,
- },
- isLoading: {
- type: Boolean,
- required: true,
- },
- runnerHelpUrl: {
- type: String,
- required: false,
- default: '',
- },
+ isLoading: {
+ type: Boolean,
+ required: true,
},
- computed: {
- shouldRenderContent() {
- return !this.isLoading && Object.keys(this.job).length > 0;
- },
- coverage() {
- return `${this.job.coverage}%`;
- },
- duration() {
- return timeIntervalInWords(this.job.duration);
- },
- queued() {
- return timeIntervalInWords(this.job.queued);
- },
- runnerId() {
- return `#${this.job.runner.id}`;
- },
- hasTimeout() {
- return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
- },
- timeout() {
- if (this.job.metadata == null) {
- return '';
- }
+ canUserRetry: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ runnerHelpUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ shouldRenderContent() {
+ return !this.isLoading && Object.keys(this.job).length > 0;
+ },
+ coverage() {
+ return `${this.job.coverage}%`;
+ },
+ duration() {
+ return timeIntervalInWords(this.job.duration);
+ },
+ queued() {
+ return timeIntervalInWords(this.job.queued);
+ },
+ runnerId() {
+ return `${this.job.runner.description} (#${this.job.runner.id})`;
+ },
+ retryButtonClass() {
+ let className = 'js-retry-button pull-right btn btn-retry visible-md-block visible-lg-block';
+ className +=
+ this.job.status && this.job.recoverable
+ ? ' btn-primary'
+ : ' btn-inverted-secondary';
+ return className;
+ },
+ hasTimeout() {
+ return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
+ },
+ timeout() {
+ if (this.job.metadata == null) {
+ return '';
+ }
- let t = this.job.metadata.timeout_human_readable;
- if (this.job.metadata.timeout_source !== '') {
- t += ` (from ${this.job.metadata.timeout_source})`;
- }
+ let t = this.job.metadata.timeout_human_readable;
+ if (this.job.metadata.timeout_source !== '') {
+ t += ` (from ${this.job.metadata.timeout_source})`;
+ }
- return t;
- },
- renderBlock() {
- return this.job.merge_request ||
- this.job.duration ||
- this.job.finished_data ||
- this.job.erased_at ||
- this.job.queued ||
- this.job.runner ||
- this.job.coverage ||
- this.job.tags.length ||
- this.job.cancel_path;
- },
+ return t;
},
- };
+ renderBlock() {
+ return (
+ this.job.merge_request ||
+ this.job.duration ||
+ this.job.finished_data ||
+ this.job.erased_at ||
+ this.job.queued ||
+ this.job.runner ||
+ this.job.coverage ||
+ this.job.tags.length ||
+ this.job.cancel_path
+ );
+ },
+ },
+};
</script>
<template>
<div>
+ <div class="block">
+ <strong class="inline prepend-top-8">
+ {{ job.name }}
+ </strong>
+ <a
+ v-if="canUserRetry"
+ :class="retryButtonClass"
+ :href="job.retry_path"
+ data-method="post"
+ rel="nofollow"
+ >
+ {{ __('Retry') }}
+ </a>
+ <button
+ type="button"
+ :aria-label="__('Toggle Sidebar')"
+ class="btn btn-blank gutter-toggle pull-right
+ visible-xs-block visible-sm-block js-sidebar-build-toggle"
+ >
+ <i
+ aria-hidden="true"
+ data-hidden="true"
+ class="fa fa-angle-double-right"
+ ></i>
+ </button>
+ </div>
<template v-if="shouldRenderContent">
<div
class="block retry-link"
@@ -85,16 +124,16 @@
class="js-new-issue btn btn-new btn-inverted"
:href="job.new_issue_path"
>
- New issue
+ {{ __('New issue') }}
</a>
<a
- v-if="job.retry_path"
+ v-if="canUserRetry"
class="js-retry-job btn btn-inverted-secondary"
:href="job.retry_path"
data-method="post"
rel="nofollow"
>
- Retry
+ {{ __('Retry') }}
</a>
</div>
<div :class="{block : renderBlock }">
@@ -103,7 +142,7 @@
v-if="job.merge_request"
>
<span class="build-light-text">
- Merge Request:
+ {{ __('Merge Request:') }}
</span>
<a :href="job.merge_request.path">
!{{ job.merge_request.iid }}
@@ -158,7 +197,7 @@
v-if="job.tags.length"
>
<span class="build-light-text">
- Tags:
+ {{ __('Tags:') }}
</span>
<span
v-for="(tag, i) in job.tags"
@@ -178,7 +217,7 @@
data-method="post"
rel="nofollow"
>
- Cancel
+ {{ __('Cancel') }}
</a>
</div>
</div>
diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js
index 656676ead91..f2939ad4dbe 100644
--- a/app/assets/javascripts/jobs/job_details_bundle.js
+++ b/app/assets/javascripts/jobs/job_details_bundle.js
@@ -35,9 +35,11 @@ export default () => {
});
// Sidebar information block
+ const detailsBlockElement = document.getElementById('js-details-block-vue');
+ const detailsBlockDataset = detailsBlockElement.dataset;
// eslint-disable-next-line
new Vue({
- el: '#js-details-block-vue',
+ el: detailsBlockElement,
components: {
detailsBlock,
},
@@ -50,6 +52,7 @@ export default () => {
return createElement('details-block', {
props: {
isLoading: this.mediator.state.isLoading,
+ canUserRetry: !!('canUserRetry' in detailsBlockDataset),
job: this.mediator.store.state.job,
runnerHelpUrl: dataset.runnerHelpUrl,
},
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index d0050abb8e9..9b62cfb8206 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -83,7 +83,7 @@ export default class LabelsSelect {
$dropdown.trigger('loading.gl.dropdown');
axios.put(issueUpdateURL, data)
.then(({ data }) => {
- var labelCount, template, labelTooltipTitle, labelTitles;
+ var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels;
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
@@ -115,8 +115,7 @@ export default class LabelsSelect {
labelTooltipTitle = labelTitles.join(', ');
}
else {
- labelTooltipTitle = '';
- $sidebarLabelTooltip.tooltip('destroy');
+ labelTooltipTitle = __('Labels');
}
$sidebarLabelTooltip
diff --git a/app/assets/javascripts/lib/utils/keycodes.js b/app/assets/javascripts/lib/utils/keycodes.js
new file mode 100644
index 00000000000..5e0f9b612a2
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/keycodes.js
@@ -0,0 +1,4 @@
+export const UP_KEY_CODE = 38;
+export const DOWN_KEY_CODE = 40;
+export const ENTER_KEY_CODE = 13;
+export const ESC_KEY_CODE = 27;
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index e77318fef46..3f84f4b9499 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -7,11 +7,7 @@ import flash from './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
-import {
- parseUrlPathname,
- handleLocationHash,
- isMetaClick,
-} from './lib/utils/common_utils';
+import { parseUrlPathname, handleLocationHash, isMetaClick } from './lib/utils/common_utils';
import { getLocationHash } from './lib/utils/url_utility';
import initDiscussionTab from './image_diff/init_discussion_tab';
import Diff from './diff';
@@ -69,11 +65,10 @@ import Notes from './notes';
let location = window.location;
export default class MergeRequestTabs {
-
constructor({ action, setUrl, stubLocation } = {}) {
const mergeRequestTabs = document.querySelector('.js-tabs-affix');
const navbar = document.querySelector('.navbar-gitlab');
- const peek = document.getElementById('peek');
+ const peek = document.getElementById('js-peek');
const paddingTop = 16;
this.diffsLoaded = false;
@@ -109,8 +104,7 @@ export default class MergeRequestTabs {
.on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.on('click', '.js-show-tab', this.showTab);
- $('.merge-request-tabs a[data-toggle="tab"]')
- .on('click', this.clickTab);
+ $('.merge-request-tabs a[data-toggle="tab"]').on('click', this.clickTab);
}
// Used in tests
@@ -119,8 +113,7 @@ export default class MergeRequestTabs {
.off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.off('click', '.js-show-tab', this.showTab);
- $('.merge-request-tabs a[data-toggle="tab"]')
- .off('click', this.clickTab);
+ $('.merge-request-tabs a[data-toggle="tab"]').off('click', this.clickTab);
}
destroyPipelinesView() {
@@ -183,10 +176,7 @@ export default class MergeRequestTabs {
scrollToElement(container) {
if (location.hash) {
- const offset = 0 - (
- $('.navbar-gitlab').outerHeight() +
- $('.js-tabs-affix').outerHeight()
- );
+ const offset = 0 - ($('.navbar-gitlab').outerHeight() + $('.js-tabs-affix').outerHeight());
const $el = $(`${container} ${location.hash}:not(.match)`);
if ($el.length) {
$.scrollTo($el[0], { offset });
@@ -240,9 +230,13 @@ export default class MergeRequestTabs {
// Turbolinks' history.
//
// See https://github.com/rails/turbolinks/issues/363
- window.history.replaceState({
- url: newState,
- }, document.title, newState);
+ window.history.replaceState(
+ {
+ url: newState,
+ },
+ document.title,
+ newState,
+ );
return newState;
}
@@ -258,7 +252,8 @@ export default class MergeRequestTabs {
this.toggleLoading(true);
- axios.get(`${source}.json`)
+ axios
+ .get(`${source}.json`)
.then(({ data }) => {
document.querySelector('div#commits').innerHTML = data.html;
localTimeAgo($('.js-timeago', 'div#commits'));
@@ -303,7 +298,8 @@ export default class MergeRequestTabs {
this.toggleLoading(true);
- axios.get(`${urlPathname}.json${location.search}`)
+ axios
+ .get(`${urlPathname}.json${location.search}`)
.then(({ data }) => {
const $container = $('#diffs');
$container.html(data.html);
@@ -332,8 +328,7 @@ export default class MergeRequestTabs {
cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'),
suggestionSections: $(el).find('.js-file-fork-suggestion-section'),
actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'),
- })
- .init();
+ }).init();
});
// Scroll any linked note into view
@@ -388,8 +383,7 @@ export default class MergeRequestTabs {
resetViewContainer() {
if (this.fixedLayoutPref !== null) {
- $('.content-wrapper .container-fluid')
- .toggleClass('container-limited', this.fixedLayoutPref);
+ $('.content-wrapper .container-fluid').toggleClass('container-limited', this.fixedLayoutPref);
}
}
@@ -438,12 +432,11 @@ export default class MergeRequestTabs {
const $diffTabs = $('#diff-notes-app');
- $tabs.off('affix.bs.affix affix-top.bs.affix')
+ $tabs
+ .off('affix.bs.affix affix-top.bs.affix')
.affix({
offset: {
- top: () => (
- $diffTabs.offset().top - $tabs.height() - $fixedNav.height()
- ),
+ top: () => $diffTabs.offset().top - $tabs.height() - $fixedNav.height(),
},
})
.on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() }))
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index e6e3a66aa20..325fa570f37 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import flash from './flash';
+import { mouseenter, debouncedMouseleave, togglePopover } from './shared/popover';
export default class Milestone {
constructor() {
@@ -43,4 +44,25 @@ export default class Milestone {
.catch(() => flash('Error loading milestone tab'));
}
}
+
+ static initDeprecationMessage() {
+ const deprecationMesssageContainer = document.querySelector('.js-milestone-deprecation-message');
+
+ if (!deprecationMesssageContainer) return;
+
+ const deprecationMessage = deprecationMesssageContainer.querySelector('.js-milestone-deprecation-message-template').innerHTML;
+ const $popover = $('.js-popover-link', deprecationMesssageContainer);
+ const hideOnScroll = togglePopover.bind($popover, false);
+
+ $popover.popover({
+ content: deprecationMessage,
+ html: true,
+ placement: 'bottom',
+ })
+ .on('mouseenter', mouseenter)
+ .on('mouseleave', debouncedMouseleave())
+ .on('show.bs.popover', () => {
+ window.addEventListener('scroll', hideOnScroll, { once: true });
+ });
+ }
}
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index d0a2b27b0e6..7e9a50a885d 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -4,6 +4,7 @@
import $ from 'jquery';
import _ from 'underscore';
+import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store';
@@ -25,7 +26,7 @@ export default class MilestoneSelect {
}
$els.each((i, dropdown) => {
- let collapsedSidebarLabelTemplate, milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
+ let milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
const $dropdown = $(dropdown);
const projectId = $dropdown.data('projectId');
const milestonesUrl = $dropdown.data('milestones');
@@ -52,7 +53,6 @@ export default class MilestoneSelect {
if (issueUpdateURL) {
milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
- collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>');
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
@@ -214,10 +214,16 @@ export default class MilestoneSelect {
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
- return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
+ return $sidebarCollapsedValue
+ .attr('data-original-title', `${data.milestone.name}<br />${data.milestone.remaining}`)
+ .find('span')
+ .text(data.milestone.title);
} else {
$value.html(milestoneLinkNoneTemplate);
- return $sidebarCollapsedValue.find('span').text('No');
+ return $sidebarCollapsedValue
+ .attr('data-original-title', __('Milestone'))
+ .find('span')
+ .text(__('None'));
}
})
.catch(() => {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index ac70ddb3ff4..96f2b3eac98 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -19,7 +19,6 @@ import AjaxCache from '~/lib/utils/ajax_cache';
import Vue from 'vue';
import syntaxHighlight from '~/syntax_highlight';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
@@ -198,6 +197,8 @@ export default class Notes {
);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff);
+ this.$wrapperEl.on('click', '.js-toggle-lazy-diff-retry-button', this.onClickRetryLazyLoad.bind(this));
+
// fetch notes when tab becomes visible
this.$wrapperEl.on('visibilitychange', this.visibilityChange);
// when issue status changes, we need to refresh data
@@ -244,6 +245,7 @@ export default class Notes {
this.$wrapperEl.off('click', '.js-comment-resolve-button');
this.$wrapperEl.off('click', '.system-note-commit-list-toggler');
this.$wrapperEl.off('click', '.js-toggle-lazy-diff');
+ this.$wrapperEl.off('click', '.js-toggle-lazy-diff-retry-button');
this.$wrapperEl.off('ajax:success', '.js-main-target-form');
this.$wrapperEl.off('ajax:success', '.js-discussion-note-form');
this.$wrapperEl.off('ajax:complete', '.js-main-target-form');
@@ -1190,12 +1192,12 @@ export default class Notes {
addForm = false;
let lineTypeSelector = '';
rowCssToAdd =
- '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content discussion-notes"></div></td></tr>';
+ '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>';
// In parallel view, look inside the correct left/right pane
if (this.isParallelView()) {
lineTypeSelector = `.${lineType}`;
rowCssToAdd =
- '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content discussion-notes"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content discussion-notes"></div></td></tr>';
+ '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>';
}
const notesContentSelector = `.notes_content${lineTypeSelector} .content`;
let notesContent = targetRow.find(notesContentSelector);
@@ -1425,22 +1427,21 @@ export default class Notes {
const { discussion_html } = data;
const lines = $(discussion_html).find('.line_holder');
lines.addClass('fade-in');
- $container.find('tbody').prepend(lines);
+ $container.find('.diff-content > table > tbody').prepend(lines);
const fileHolder = $container.find('.file-holder');
$container.find('.line-holder-placeholder').remove();
syntaxHighlight(fileHolder);
}
- static renderDiffError($container) {
- $container.find('.line_content').html(
- $(`
- <div class="nothing-here-block">
- ${__(
- 'Unable to load the diff.',
- )} <a class="js-toggle-lazy-diff" href="javascript:void(0)">Try again</a>?
- </div>
- `),
- );
+ onClickRetryLazyLoad(e) {
+ const $retryButton = $(e.currentTarget);
+
+ $retryButton.prop('disabled', true);
+
+ return this.loadLazyDiff(e)
+ .then(() => {
+ $retryButton.prop('disabled', false);
+ });
}
loadLazyDiff(e) {
@@ -1449,20 +1450,35 @@ export default class Notes {
$container.find('.js-toggle-lazy-diff').removeClass('js-toggle-lazy-diff');
- const tableEl = $container.find('tbody');
- if (tableEl.length === 0) return;
+ const $tableEl = $container.find('tbody');
+ if ($tableEl.length === 0) return;
const fileHolder = $container.find('.file-holder');
const url = fileHolder.data('linesPath');
- axios
+ const $errorContainer = $container.find('.js-error-lazy-load-diff');
+ const $successContainer = $container.find('.js-success-lazy-load');
+
+ /**
+ * We only fetch resolved discussions.
+ * Unresolved discussions don't have an endpoint being provided.
+ */
+ if (url) {
+ return axios
.get(url)
.then(({ data }) => {
+ // Reset state in case last request returned error
+ $successContainer.removeClass('hidden');
+ $errorContainer.addClass('hidden');
+
Notes.renderDiffContent($container, data);
})
.catch(() => {
- Notes.renderDiffError($container);
+ $successContainer.addClass('hidden');
+ $errorContainer.removeClass('hidden');
});
+ }
+ return Promise.resolve();
}
toggleCommitList(e) {
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 648fa6ff804..396a675b4ac 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -317,10 +317,10 @@ Please check your network connection and try again.`;
<note-signed-out-widget v-if="!isLoggedIn" />
<discussion-locked-widget
issuable-type="issue"
- v-else-if="!canCreateNote"
+ v-else-if="isLocked(getNoteableData) && !canCreateNote"
/>
<ul
- v-else
+ v-else-if="canCreateNote"
class="notes notes-form timeline">
<li class="timeline-entry">
<div class="timeline-entry-inner">
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index a7e2d857013..626b0799581 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -40,6 +40,10 @@ export default {
type: Boolean,
required: true,
},
+ canAwardEmoji: {
+ type: Boolean,
+ required: true,
+ },
canDelete: {
type: Boolean,
required: true,
@@ -74,9 +78,6 @@ export default {
shouldShowActionsDropdown() {
return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
},
- canAddAwardEmoji() {
- return this.currentUserId;
- },
isAuthoredByCurrentUser() {
return this.authorId === this.currentUserId;
},
@@ -149,7 +150,7 @@ export default {
</button>
</div>
<div
- v-if="canAddAwardEmoji"
+ v-if="canAwardEmoji"
class="note-actions-item">
<a
v-tooltip
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index 6cb8229e268..e8fd155a1ee 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -28,6 +28,10 @@ export default {
type: Number,
required: true,
},
+ canAwardEmoji: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
...mapGetters(['getUserData']),
@@ -67,9 +71,6 @@ export default {
isAuthoredByMe() {
return this.noteAuthorId === this.getUserData.id;
},
- isLoggedIn() {
- return this.getUserData.id;
- },
},
created() {
this.emojiSmiling = emojiSmiling;
@@ -156,7 +157,7 @@ export default {
return title;
},
handleAward(awardName) {
- if (!this.isLoggedIn) {
+ if (!this.canAwardEmoji) {
return;
}
@@ -208,7 +209,7 @@ export default {
</span>
</button>
<div
- v-if="isLoggedIn"
+ v-if="canAwardEmoji"
class="award-menu-holder">
<button
v-tooltip
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 069f94c5845..0cb626c14f4 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -112,6 +112,7 @@ export default {
:note-author-id="note.author.id"
:awards="note.award_emoji"
:toggle-award-path="note.toggle_award_path"
+ :can-award-emoji="note.current_user.can_award_emoji"
/>
<note-attachment
v-if="note.attachment"
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 476b15aca4a..e0f883a8e08 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -258,7 +258,9 @@ Please check your network connection and try again.`;
:key="note.id"
/>
</ul>
- <div class="discussion-reply-holder">
+ <div
+ :class="{ 'is-replying': isReplying }"
+ class="discussion-reply-holder">
<template v-if="!isReplying && canReply">
<div
class="btn-group-justified discussion-with-resolve-btn"
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 3554027d2b4..566f5c68e66 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -177,6 +177,7 @@ export default {
:note-id="note.id"
:access-level="note.human_access"
:can-edit="note.current_user.can_edit"
+ :can-award-emoji="note.current_user.can_award_emoji"
:can-delete="note.current_user.can_edit"
:can-report-as-abuse="canReportAsAbuse"
:report-abuse-path="note.report_abuse_path"
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 244a6980b5a..98ce070288e 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -315,3 +315,6 @@ export const scrollToNoteIfNeeded = (context, el) => {
scrollToElement(el);
}
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index f89591a54d6..787be6f4c99 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -68,3 +68,6 @@ export const resolvedDiscussionCount = (state, getters) => {
return Object.keys(resolvedMap).length;
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/pages/dashboard/milestones/show/index.js b/app/assets/javascripts/pages/dashboard/milestones/show/index.js
index 397149aaa9e..8b529585898 100644
--- a/app/assets/javascripts/pages/dashboard/milestones/show/index.js
+++ b/app/assets/javascripts/pages/dashboard/milestones/show/index.js
@@ -6,4 +6,6 @@ document.addEventListener('DOMContentLoaded', () => {
new Milestone(); // eslint-disable-line no-new
new Sidebar(); // eslint-disable-line no-new
new MountMilestoneSidebar(); // eslint-disable-line no-new
+
+ Milestone.initDeprecationMessage();
});
diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js
index 88f40b5278e..74cc4ba42c1 100644
--- a/app/assets/javascripts/pages/groups/milestones/show/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/show/index.js
@@ -1,3 +1,8 @@
import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
+import Milestone from '~/milestone';
-document.addEventListener('DOMContentLoaded', initMilestonesShow);
+document.addEventListener('DOMContentLoaded', () => {
+ initMilestonesShow();
+
+ Milestone.initDeprecationMessage();
+});
diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js
index be37df36be8..628913483c6 100644
--- a/app/assets/javascripts/pages/projects/edit/index.js
+++ b/app/assets/javascripts/pages/projects/edit/index.js
@@ -1,12 +1,12 @@
import initSettingsPanels from '~/settings_panels';
import setupProjectEdit from '~/project_edit';
import initConfirmDangerModal from '~/confirm_danger_modal';
-import ProjectNew from '../shared/project_new';
+import initProjectLoadingSpinner from '../shared/save_project_loader';
import projectAvatar from '../shared/project_avatar';
import initProjectPermissionsSettings from '../shared/permissions';
document.addEventListener('DOMContentLoaded', () => {
- new ProjectNew(); // eslint-disable-line no-new
+ initProjectLoadingSpinner();
setupProjectEdit();
// Initialize expandable settings panels
initSettingsPanels();
diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js
index ea6fd961393..7db644e2477 100644
--- a/app/assets/javascripts/pages/projects/new/index.js
+++ b/app/assets/javascripts/pages/projects/new/index.js
@@ -1,9 +1,9 @@
-import ProjectNew from '../shared/project_new';
+import initProjectLoadingSpinner from '../shared/save_project_loader';
import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new';
document.addEventListener('DOMContentLoaded', () => {
- new ProjectNew(); // eslint-disable-line no-new
+ initProjectLoadingSpinner();
initProjectVisibilitySelector();
initProjectNew.bindEvents();
});
diff --git a/app/assets/javascripts/pages/projects/shared/project_new.js b/app/assets/javascripts/pages/projects/shared/project_new.js
deleted file mode 100644
index 56d5574aa2f..00000000000
--- a/app/assets/javascripts/pages/projects/shared/project_new.js
+++ /dev/null
@@ -1,152 +0,0 @@
-/* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/
-
-import $ from 'jquery';
-import VisibilitySelect from '../../../visibility_select';
-
-function highlightChanges($elm) {
- $elm.addClass('highlight-changes');
- setTimeout(() => $elm.removeClass('highlight-changes'), 10);
-}
-
-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);
- }
- }
- });
-
- projectVisibility = newProjectVisibility;
- }
- });
- }
-
- 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);
- }
-
- $this.find("option[value='" + selectedVal + "']").nextAll().disable();
- });
-
- if (selectedVal) {
- this.$repoSelects.removeClass('disabled');
-
- if ($lfsEnabledOption.length) {
- $lfsEnabledOption.removeClass('disabled');
- highlightChanges($lfsEnabledOption);
- }
- if (containerRegistry) {
- containerRegistry.style.display = '';
- }
- } else {
- this.$repoSelects.addClass('disabled');
-
- if ($lfsEnabledOption.length) {
- $lfsEnabledOption.val('false').addClass('disabled');
- highlightChanges($lfsEnabledOption);
- }
- if (containerRegistry) {
- containerRegistry.style.display = 'none';
- containerRegistryCheckbox.checked = false;
- }
- }
-
- prevSelectedVal = selectedVal;
- }.bind(this));
- }
-}
diff --git a/app/assets/javascripts/pages/projects/shared/save_project_loader.js b/app/assets/javascripts/pages/projects/shared/save_project_loader.js
new file mode 100644
index 00000000000..aa3589ac88d
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/shared/save_project_loader.js
@@ -0,0 +1,12 @@
+import $ from 'jquery';
+
+export default function initProjectLoadingSpinner() {
+ const $formContainer = $('.project-edit-container');
+ const $loadingSpinner = $('.save-project-loader');
+
+ // show loading spinner when saving
+ $formContainer.on('ajax:before', () => {
+ $formContainer.hide();
+ $loadingSpinner.show();
+ });
+}
diff --git a/app/assets/javascripts/pages/projects/snippets/show/index.js b/app/assets/javascripts/pages/projects/snippets/show/index.js
index a134599cb04..c35b9c30058 100644
--- a/app/assets/javascripts/pages/projects/snippets/show/index.js
+++ b/app/assets/javascripts/pages/projects/snippets/show/index.js
@@ -1,11 +1,13 @@
import initNotes from '~/init_notes';
import ZenMode from '~/zen_mode';
-import LineHighlighter from '../../../../line_highlighter';
-import BlobViewer from '../../../../blob/viewer';
+import LineHighlighter from '~/line_highlighter';
+import BlobViewer from '~/blob/viewer';
+import snippetEmbed from '~/snippet/snippet_embed';
document.addEventListener('DOMContentLoaded', () => {
new LineHighlighter(); // eslint-disable-line no-new
new BlobViewer(); // eslint-disable-line no-new
initNotes();
new ZenMode(); // eslint-disable-line no-new
+ snippetEmbed();
});
diff --git a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
index 08f0afdcce3..d321892d2d2 100644
--- a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
+++ b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
@@ -5,7 +5,7 @@ import AccessorUtilities from '~/lib/utils/accessor';
* Does that setting the current selected tab in the localStorage
*/
export default class SigninTabsMemoizer {
- constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) {
+ constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.new-session-tabs' } = {}) {
this.currentTabKey = currentTabKey;
this.tabSelector = tabSelector;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
diff --git a/app/assets/javascripts/pages/snippets/show/index.js b/app/assets/javascripts/pages/snippets/show/index.js
index f548b9fad65..26936110402 100644
--- a/app/assets/javascripts/pages/snippets/show/index.js
+++ b/app/assets/javascripts/pages/snippets/show/index.js
@@ -1,11 +1,13 @@
-import LineHighlighter from '../../../line_highlighter';
-import BlobViewer from '../../../blob/viewer';
-import ZenMode from '../../../zen_mode';
-import initNotes from '../../../init_notes';
+import LineHighlighter from '~/line_highlighter';
+import BlobViewer from '~/blob/viewer';
+import ZenMode from '~/zen_mode';
+import initNotes from '~/init_notes';
+import snippetEmbed from '~/snippet/snippet_embed';
document.addEventListener('DOMContentLoaded', () => {
new LineHighlighter(); // eslint-disable-line no-new
new BlobViewer(); // eslint-disable-line no-new
initNotes();
new ZenMode(); // eslint-disable-line no-new
+ snippetEmbed();
});
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 8ce938c958b..cbc2d80ee18 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -19,7 +19,7 @@ function getSystemDate(systemUtcOffsetSeconds) {
const date = new Date();
const localUtcOffsetMinutes = 0 - date.getTimezoneOffset();
const systemUtcOffsetMinutes = systemUtcOffsetSeconds / 60;
- date.setMinutes((date.getMinutes() - localUtcOffsetMinutes) + systemUtcOffsetMinutes);
+ date.setMinutes(date.getMinutes() - localUtcOffsetMinutes + systemUtcOffsetMinutes);
return date;
}
@@ -35,18 +35,36 @@ function formatTooltipText({ date, count }) {
return `${contribText}<br />${dateDayName} ${dateText}`;
}
-const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]);
+const initColorKey = () =>
+ d3
+ .scaleLinear()
+ .range(['#acd5f2', '#254e77'])
+ .domain([0, 3]);
export default class ActivityCalendar {
- constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0) {
+ constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0, firstDayOfWeek = 0) {
this.calendarActivitiesPath = calendarActivitiesPath;
this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = '';
this.daySpace = 1;
this.daySize = 15;
- this.daySizeWithSpace = this.daySize + (this.daySpace * 2);
- this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+ this.daySizeWithSpace = this.daySize + this.daySpace * 2;
+ this.monthNames = [
+ 'Jan',
+ 'Feb',
+ 'Mar',
+ 'Apr',
+ 'May',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ 'Sep',
+ 'Oct',
+ 'Nov',
+ 'Dec',
+ ];
this.months = [];
+ this.firstDayOfWeek = firstDayOfWeek;
// Loop through the timestamps to create a group of objects
// The group of objects will be grouped based on the day of the week they are
@@ -70,7 +88,7 @@ export default class ActivityCalendar {
// Create a new group array if this is the first day of the week
// or if is first object
- if ((day === 0 && i !== 0) || i === 0) {
+ if ((day === this.firstDayOfWeek && i !== 0) || i === 0) {
this.timestampsTmp.push([]);
group += 1;
}
@@ -109,21 +127,30 @@ export default class ActivityCalendar {
}
renderSvg(container, group) {
- const width = ((group + 1) * this.daySizeWithSpace) + this.getExtraWidthPadding(group);
- return d3.select(container)
+ const width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
+ return d3
+ .select(container)
.append('svg')
- .attr('width', width)
- .attr('height', 167)
- .attr('class', 'contrib-calendar');
+ .attr('width', width)
+ .attr('height', 167)
+ .attr('class', 'contrib-calendar');
+ }
+
+ dayYPos(day) {
+ return this.daySizeWithSpace * ((day + 7 - this.firstDayOfWeek) % 7);
}
renderDays() {
- this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g')
+ this.svg
+ .selectAll('g')
+ .data(this.timestampsTmp)
+ .enter()
+ .append('g')
.attr('transform', (group, i) => {
_.each(group, (stamp, a) => {
if (a === 0 && stamp.day === 0) {
const month = stamp.date.getMonth();
- const x = (this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace;
+ const x = this.daySizeWithSpace * i + 1 + this.daySizeWithSpace;
const lastMonth = _.last(this.months);
if (
lastMonth == null ||
@@ -133,86 +160,113 @@ export default class ActivityCalendar {
}
}
});
- return `translate(${(this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace}, 18)`;
+ return `translate(${this.daySizeWithSpace * i + 1 + this.daySizeWithSpace}, 18)`;
})
.selectAll('rect')
- .data(stamp => stamp)
- .enter()
- .append('rect')
- .attr('x', '0')
- .attr('y', stamp => this.daySizeWithSpace * stamp.day)
- .attr('width', this.daySize)
- .attr('height', this.daySize)
- .attr('fill', stamp => (
- stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'
- ))
- .attr('title', stamp => formatTooltipText(stamp))
- .attr('class', 'user-contrib-cell js-tooltip')
- .attr('data-container', 'body')
- .on('click', this.clickDay);
+ .data(stamp => stamp)
+ .enter()
+ .append('rect')
+ .attr('x', '0')
+ .attr('y', stamp => this.dayYPos(stamp.day))
+ .attr('width', this.daySize)
+ .attr('height', this.daySize)
+ .attr(
+ 'fill',
+ stamp => (stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'),
+ )
+ .attr('title', stamp => formatTooltipText(stamp))
+ .attr('class', 'user-contrib-cell js-tooltip')
+ .attr('data-container', 'body')
+ .on('click', this.clickDay);
}
renderDayTitles() {
const days = [
{
text: 'M',
- y: 29 + (this.daySizeWithSpace * 1),
- }, {
+ y: 29 + this.dayYPos(1),
+ },
+ {
text: 'W',
- y: 29 + (this.daySizeWithSpace * 3),
- }, {
+ y: 29 + this.dayYPos(2),
+ },
+ {
text: 'F',
- y: 29 + (this.daySizeWithSpace * 5),
+ y: 29 + this.dayYPos(3),
},
];
- this.svg.append('g')
+ this.svg
+ .append('g')
.selectAll('text')
- .data(days)
- .enter()
- .append('text')
- .attr('text-anchor', 'middle')
- .attr('x', 8)
- .attr('y', day => day.y)
- .text(day => day.text)
- .attr('class', 'user-contrib-text');
+ .data(days)
+ .enter()
+ .append('text')
+ .attr('text-anchor', 'middle')
+ .attr('x', 8)
+ .attr('y', day => day.y)
+ .text(day => day.text)
+ .attr('class', 'user-contrib-text');
}
renderMonths() {
- this.svg.append('g')
+ this.svg
+ .append('g')
.attr('direction', 'ltr')
.selectAll('text')
- .data(this.months)
- .enter()
- .append('text')
- .attr('x', date => date.x)
- .attr('y', 10)
- .attr('class', 'user-contrib-text')
- .text(date => this.monthNames[date.month]);
+ .data(this.months)
+ .enter()
+ .append('text')
+ .attr('x', date => date.x)
+ .attr('y', 10)
+ .attr('class', 'user-contrib-text')
+ .text(date => this.monthNames[date.month]);
}
renderKey() {
- const keyValues = ['no contributions', '1-9 contributions', '10-19 contributions', '20-29 contributions', '30+ contributions'];
- const keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
+ const keyValues = [
+ 'no contributions',
+ '1-9 contributions',
+ '10-19 contributions',
+ '20-29 contributions',
+ '30+ contributions',
+ ];
+ const keyColors = [
+ '#ededed',
+ this.colorKey(0),
+ this.colorKey(1),
+ this.colorKey(2),
+ this.colorKey(3),
+ ];
- this.svg.append('g')
- .attr('transform', `translate(18, ${(this.daySizeWithSpace * 8) + 16})`)
+ this.svg
+ .append('g')
+ .attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
.selectAll('rect')
- .data(keyColors)
- .enter()
- .append('rect')
- .attr('width', this.daySize)
- .attr('height', this.daySize)
- .attr('x', (color, i) => this.daySizeWithSpace * i)
- .attr('y', 0)
- .attr('fill', color => color)
- .attr('class', 'js-tooltip')
- .attr('title', (color, i) => keyValues[i])
- .attr('data-container', 'body');
+ .data(keyColors)
+ .enter()
+ .append('rect')
+ .attr('width', this.daySize)
+ .attr('height', this.daySize)
+ .attr('x', (color, i) => this.daySizeWithSpace * i)
+ .attr('y', 0)
+ .attr('fill', color => color)
+ .attr('class', 'js-tooltip')
+ .attr('title', (color, i) => keyValues[i])
+ .attr('data-container', 'body');
}
initColor() {
- const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
- return d3.scaleThreshold().domain([0, 10, 20, 30]).range(colorRange);
+ const colorRange = [
+ '#ededed',
+ this.colorKey(0),
+ this.colorKey(1),
+ this.colorKey(2),
+ this.colorKey(3),
+ ];
+ return d3
+ .scaleThreshold()
+ .domain([0, 10, 20, 30])
+ .range(colorRange);
}
clickDay(stamp) {
@@ -227,14 +281,15 @@ export default class ActivityCalendar {
$('.user-calendar-activities').html(LOADING_HTML);
- axios.get(this.calendarActivitiesPath, {
- params: {
- date,
- },
- responseType: 'text',
- })
- .then(({ data }) => $('.user-calendar-activities').html(data))
- .catch(() => flash(__('An error occurred while retrieving calendar activity')));
+ axios
+ .get(this.calendarActivitiesPath, {
+ params: {
+ date,
+ },
+ responseType: 'text',
+ })
+ .then(({ data }) => $('.user-calendar-activities').html(data))
+ .catch(() => flash(__('An error occurred while retrieving calendar activity')));
} else {
this.currentSelectedDate = '';
$('.user-calendar-activities').html('');
diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
index 3ebfaa87a4e..bc71911ae35 100644
--- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js
+++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
@@ -10,29 +10,25 @@ export default class PerformanceBarService {
}
static registerInterceptor(peekUrl, callback) {
- vueResourceInterceptor = (request, next) => {
- next(response => {
- const requestId = response.headers['x-request-id'];
- const requestUrl = response.url;
-
- if (requestUrl !== peekUrl && requestId) {
- callback(requestId, requestUrl);
- }
- });
- };
-
- Vue.http.interceptors.push(vueResourceInterceptor);
-
- return axios.interceptors.response.use(response => {
+ const interceptor = response => {
const requestId = response.headers['x-request-id'];
- const requestUrl = response.config.url;
+ // Get the request URL from response.config for Axios, and response for
+ // Vue Resource.
+ const requestUrl = (response.config || response).url;
+ const cachedResponse = response.headers['x-gitlab-from-cache'] === 'true';
- if (requestUrl !== peekUrl && requestId) {
+ if (requestUrl !== peekUrl && requestId && !cachedResponse) {
callback(requestId, requestUrl);
}
return response;
- });
+ };
+
+ vueResourceInterceptor = (request, next) => next(interceptor);
+
+ Vue.http.interceptors.push(vueResourceInterceptor);
+
+ return axios.interceptors.response.use(interceptor);
}
static removeInterceptor(interceptor) {
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index e99d949801f..29ee73a2a6f 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -32,26 +32,38 @@ export default {
required: true,
},
- buttonDisabled: {
+ requestFinishedFor: {
type: String,
required: false,
- default: null,
+ default: '',
},
},
+ data() {
+ return {
+ isDisabled: false,
+ linkRequested: '',
+ };
+ },
+
computed: {
cssClass() {
const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`;
},
- isDisabled() {
- return this.buttonDisabled === this.link;
+ },
+ watch: {
+ requestFinishedFor() {
+ if (this.requestFinishedFor === this.linkRequested) {
+ this.isDisabled = false;
+ }
},
},
-
methods: {
onClickAction() {
$(this.$el).tooltip('hide');
eventHub.$emit('graphAction', this.link);
+ this.linkRequested = this.link;
+ this.isDisabled = true;
},
},
};
@@ -62,7 +74,8 @@ export default {
@click="onClickAction"
v-tooltip
:title="tooltipText"
- class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper"
+ class="js-ci-action btn btn-blank
+btn-transparent ci-action-icon-container ci-action-icon-wrapper"
:class="cssClass"
data-container="body"
:disabled="isDisabled"
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
deleted file mode 100644
index 7c4fd65e36f..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<script>
- import icon from '../../../vue_shared/components/icon.vue';
- import tooltip from '../../../vue_shared/directives/tooltip';
-
- /**
- * Renders either a cancel, retry or play icon pointing to the given path.
- * TODO: Remove UJS from here and use an async request instead.
- */
- export default {
- components: {
- icon,
- },
-
- directives: {
- tooltip,
- },
- props: {
- tooltipText: {
- type: String,
- required: true,
- },
-
- link: {
- type: String,
- required: true,
- },
-
- actionMethod: {
- type: String,
- required: true,
- },
-
- actionIcon: {
- type: String,
- required: true,
- },
- },
- };
-</script>
-<template>
- <a
- v-tooltip
- :data-method="actionMethod"
- :title="tooltipText"
- :href="link"
- rel="nofollow"
- class="ci-action-icon-wrapper js-ci-status-icon"
- data-container="body"
- 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 be213c2ee78..43121dd38f3 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -1,77 +1,83 @@
<script>
- import $ from 'jquery';
- import jobNameComponent from './job_name_component.vue';
- import jobComponent from './job_component.vue';
- import tooltip from '../../../vue_shared/directives/tooltip';
+import $ from 'jquery';
+import JobNameComponent from './job_name_component.vue';
+import JobComponent from './job_component.vue';
+import tooltip from '../../../vue_shared/directives/tooltip';
- /**
- * Renders the dropdown for the pipeline graph.
- *
- * The following object should be provided as `job`:
- *
- * {
- * "id": 4256,
- * "name": "test",
- * "status": {
- * "icon": "icon_status_success",
- * "text": "passed",
- * "label": "passed",
- * "group": "success",
- * "details_path": "/root/ci-mock/builds/4256",
- * "action": {
- * "icon": "retry",
- * "title": "Retry",
- * "path": "/root/ci-mock/builds/4256/retry",
- * "method": "post"
- * }
- * }
- * }
- */
- export default {
- directives: {
- tooltip,
- },
+/**
+ * Renders the dropdown for the pipeline graph.
+ *
+ * The following object should be provided as `job`:
+ *
+ * {
+ * "id": 4256,
+ * "name": "test",
+ * "status": {
+ * "icon": "icon_status_success",
+ * "text": "passed",
+ * "label": "passed",
+ * "group": "success",
+ * "details_path": "/root/ci-mock/builds/4256",
+ * "action": {
+ * "icon": "retry",
+ * "title": "Retry",
+ * "path": "/root/ci-mock/builds/4256/retry",
+ * "method": "post"
+ * }
+ * }
+ * }
+ */
+export default {
+ directives: {
+ tooltip,
+ },
- components: {
- jobComponent,
- jobNameComponent,
- },
+ components: {
+ JobComponent,
+ JobNameComponent,
+ },
- props: {
- job: {
- type: Object,
- required: true,
- },
+ props: {
+ job: {
+ type: Object,
+ required: true,
},
-
- computed: {
- tooltipText() {
- return `${this.job.name} - ${this.job.status.label}`;
- },
+ requestFinishedFor: {
+ type: String,
+ required: false,
+ default: '',
},
+ },
- mounted() {
- this.stopDropdownClickPropagation();
+ computed: {
+ tooltipText() {
+ return `${this.job.name} - ${this.job.status.label}`;
},
+ },
+
+ mounted() {
+ this.stopDropdownClickPropagation();
+ },
- methods: {
- /**
- * When the user right clicks or cmd/ctrl + click in the job name
- * the dropdown should not be closed and the link should open in another tab,
- * so we stop propagation of the click event inside the dropdown.
+ methods: {
+ /**
+ * When the user right clicks or cmd/ctrl + click in the job name or the action icon
+ * the dropdown should not be closed so we stop propagation
+ * of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
- stopDropdownClickPropagation() {
- $(this.$el
- .querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
- .on('click', (e) => {
- e.stopPropagation();
- });
- },
+ stopDropdownClickPropagation() {
+ $(
+ '.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item',
+ this.$el,
+ ).on('click', e => {
+ e.stopPropagation();
+ });
},
- };
+ },
+};
</script>
<template>
<div class="ci-job-dropdown-container">
@@ -101,8 +107,8 @@
:key="i">
<job-component
:job="item"
- :is-dropdown="true"
css-class-job-name="mini-pipeline-graph-dropdown-item"
+ :request-finished-for="requestFinishedFor"
/>
</li>
</ul>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index ac9ce7e47d6..7b8a5edcbff 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -7,7 +7,6 @@ export default {
StageColumnComponent,
LoadingIcon,
},
-
props: {
isLoading: {
type: Boolean,
@@ -17,10 +16,10 @@ export default {
type: Object,
required: true,
},
- actionDisabled: {
+ requestFinishedFor: {
type: String,
required: false,
- default: null,
+ default: '',
},
},
@@ -75,7 +74,7 @@ export default {
:key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
- :action-disabled="actionDisabled"
+ :request-finished-for="requestFinishedFor"
/>
</ul>
</div>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index c6e5ae6df41..4fcd4b79f4a 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -1,6 +1,5 @@
<script>
import ActionComponent from './action_component.vue';
-import DropdownActionComponent from './dropdown_action_component.vue';
import JobNameComponent from './job_name_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
@@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip';
export default {
components: {
ActionComponent,
- DropdownActionComponent,
JobNameComponent,
},
-
directives: {
tooltip,
},
@@ -44,26 +41,17 @@ export default {
type: Object,
required: true,
},
-
cssClassJobName: {
type: String,
required: false,
default: '',
},
-
- isDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- actionDisabled: {
+ requestFinishedFor: {
type: String,
required: false,
- default: null,
+ default: '',
},
},
-
computed: {
status() {
return this.job && this.job.status ? this.job.status : {};
@@ -134,19 +122,11 @@ export default {
</div>
<action-component
- v-if="hasAction && !isDropdown"
- :tooltip-text="status.action.title"
- :link="status.action.path"
- :action-icon="status.action.icon"
- :button-disabled="actionDisabled"
- />
-
- <dropdown-action-component
- v-if="hasAction && isDropdown"
+ v-if="hasAction"
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
- :action-method="status.action.method"
+ :request-finished-for="requestFinishedFor"
/>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index f6e6569e15b..5461fdbbadd 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -29,10 +29,11 @@ export default {
required: false,
default: '',
},
- actionDisabled: {
+
+ requestFinishedFor: {
type: String,
required: false,
- default: null,
+ default: '',
},
},
@@ -74,12 +75,12 @@ export default {
v-if="job.size === 1"
:job="job"
css-class-job-name="build-content"
- :action-disabled="actionDisabled"
/>
<dropdown-job-component
v-if="job.size > 1"
:job="job"
+ :request-finished-for="requestFinishedFor"
/>
</li>
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index 8bc7a1f20b2..32cf3dba3c3 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -1,5 +1,4 @@
<script>
- import $ from 'jquery';
/**
* Renders each stage of the pipeline mini graph.
@@ -14,15 +13,18 @@
* 4. Commit widget
*/
+ import $ from 'jquery';
import Flash from '../../flash';
- import icon from '../../vue_shared/components/icon.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+ import axios from '../../lib/utils/axios_utils';
+ import eventHub from '../event_hub';
+ import Icon from '../../vue_shared/components/icon.vue';
+ import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
- loadingIcon,
- icon,
+ LoadingIcon,
+ Icon,
},
directives: {
@@ -82,15 +84,15 @@
methods: {
onClickStage() {
if (!this.isDropdownOpen()) {
+ eventHub.$emit('clickedDropdown');
this.isLoading = true;
this.fetchJobs();
}
},
fetchJobs() {
- this.$http.get(this.stage.dropdown_path)
- .then(response => response.json())
- .then((data) => {
+ axios.get(this.stage.dropdown_path)
+ .then(({ data }) => {
this.dropdownContent = data.html;
this.isLoading = false;
})
@@ -98,8 +100,7 @@
this.closeDropdown();
this.isLoading = false;
- const flash = new Flash('Something went wrong on our end.');
- return flash;
+ Flash('Something went wrong on our end.');
});
},
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
new file mode 100644
index 00000000000..b384c7500e7
--- /dev/null
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -0,0 +1,2 @@
+// eslint-disable-next-line import/prefer-default-export
+export const CANCEL_REQUEST = 'CANCEL_REQUEST';
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 522a4277bd7..6d87f75ae8e 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -7,6 +7,7 @@ import SvgBlankState from '../components/blank_state.vue';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import PipelinesTableComponent from '../components/pipelines_table.vue';
import eventHub from '../event_hub';
+import { CANCEL_REQUEST } from '../constants';
export default {
components: {
@@ -52,34 +53,58 @@ export default {
});
eventHub.$on('postAction', this.postAction);
+ eventHub.$on('clickedDropdown', this.updateTable);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
+ eventHub.$off('clickedDropdown', this.updateTable);
},
destroyed() {
this.poll.stop();
},
methods: {
+ updateTable() {
+ // Cancel ongoing request
+ if (this.isMakingRequest) {
+ this.service.cancelationSource.cancel(CANCEL_REQUEST);
+ }
+ // Stop polling
+ this.poll.stop();
+ // Update the table
+ return this.getPipelines()
+ .then(() => this.poll.restart());
+ },
fetchPipelines() {
if (!this.isMakingRequest) {
this.isLoading = true;
- this.service.getPipelines(this.requestData)
- .then(response => this.successCallback(response))
- .catch(() => this.errorCallback());
+ this.getPipelines();
}
},
+ getPipelines() {
+ return this.service.getPipelines(this.requestData)
+ .then(response => this.successCallback(response))
+ .catch((error) => this.errorCallback(error));
+ },
setCommonData(pipelines) {
this.store.storePipelines(pipelines);
this.isLoading = false;
this.updateGraphDropdown = true;
this.hasMadeRequest = true;
+
+ // In case the previous polling request returned an error, we need to reset it
+ if (this.hasError) {
+ this.hasError = false;
+ }
},
- errorCallback() {
- this.hasError = true;
- this.isLoading = false;
- this.updateGraphDropdown = false;
+ errorCallback(error) {
this.hasMadeRequest = true;
+ this.isLoading = false;
+
+ if (error && error.message && error.message !== CANCEL_REQUEST) {
+ this.hasError = true;
+ this.updateGraphDropdown = false;
+ }
},
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 900eb7855f4..6584f96130b 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -25,7 +25,7 @@ export default () => {
data() {
return {
mediator,
- actionDisabled: null,
+ requestFinishedFor: null,
};
},
created() {
@@ -36,15 +36,17 @@ export default () => {
},
methods: {
postAction(action) {
- this.actionDisabled = action;
+ // Click was made, reset this variable
+ this.requestFinishedFor = null;
- this.mediator.service.postAction(action)
+ this.mediator.service
+ .postAction(action)
.then(() => {
this.mediator.refreshPipeline();
- this.actionDisabled = null;
+ this.requestFinishedFor = action;
})
.catch(() => {
- this.actionDisabled = null;
+ this.requestFinishedFor = action;
Flash(__('An error occurred while making the request.'));
});
},
@@ -54,7 +56,7 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
- actionDisabled: this.actionDisabled,
+ requestFinishedFor: this.requestFinishedFor,
},
});
},
@@ -79,7 +81,8 @@ export default () => {
},
methods: {
postAction(action) {
- this.mediator.service.postAction(action.path)
+ this.mediator.service
+ .postAction(action.path)
.then(() => this.mediator.refreshPipeline())
.catch(() => Flash(__('An error occurred while making the request.')));
},
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
index 621969cd622..5633e54b28a 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
@@ -40,10 +40,8 @@ export default class pipelinesMediator {
}
successCallback(response) {
- return response.json().then((data) => {
- this.state.isLoading = false;
- this.store.storePipeline(data);
- });
+ this.state.isLoading = false;
+ this.store.storePipeline(response.data);
}
errorCallback() {
diff --git a/app/assets/javascripts/pipelines/services/pipeline_service.js b/app/assets/javascripts/pipelines/services/pipeline_service.js
index 3e0c52c7726..a53a9cc8365 100644
--- a/app/assets/javascripts/pipelines/services/pipeline_service.js
+++ b/app/assets/javascripts/pipelines/services/pipeline_service.js
@@ -1,19 +1,16 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
+import axios from '../../lib/utils/axios_utils';
export default class PipelineService {
constructor(endpoint) {
- this.pipeline = Vue.resource(endpoint);
+ this.pipeline = endpoint;
}
getPipeline() {
- return this.pipeline.get();
+ return axios.get(this.pipeline);
}
- // eslint-disable-next-line
+ // eslint-disable-next-line class-methods-use-this
postAction(endpoint) {
- return Vue.http.post(`${endpoint}.json`);
+ return axios.post(`${endpoint}.json`);
}
}
diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js
index 001286f5d52..59c8b9c58e5 100644
--- a/app/assets/javascripts/pipelines/services/pipelines_service.js
+++ b/app/assets/javascripts/pipelines/services/pipelines_service.js
@@ -19,8 +19,13 @@ export default class PipelinesService {
getPipelines(data = {}) {
const { scope, page } = data;
+ const CancelToken = axios.CancelToken;
+
+ this.cancelationSource = CancelToken.source();
+
return axios.get(this.endpoint, {
params: { scope, page },
+ cancelToken: this.cancelationSource.token,
});
}
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js
index 795b39bb3dc..593a43c7cc1 100644
--- a/app/assets/javascripts/registry/stores/actions.js
+++ b/app/assets/javascripts/registry/stores/actions.js
@@ -35,3 +35,6 @@ export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destr
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/registry/stores/getters.js b/app/assets/javascripts/registry/stores/getters.js
index 588f479c492..f4923512578 100644
--- a/app/assets/javascripts/registry/stores/getters.js
+++ b/app/assets/javascripts/registry/stores/getters.js
@@ -1,2 +1,5 @@
export const isLoading = state => state.isLoading;
export const repos = state => state.repos;
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 2088a49590a..6eb0b62fa1c 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -5,6 +5,7 @@ import _ from 'underscore';
import Cookies from 'js-cookie';
import flash from './flash';
import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
function Sidebar(currentUser) {
this.toggleTodo = this.toggleTodo.bind(this);
@@ -41,12 +42,14 @@ Sidebar.prototype.addEventListeners = function() {
};
Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
- var $allGutterToggleIcons, $this, $thisIcon;
+ var $allGutterToggleIcons, $this, isExpanded, tooltipLabel;
e.preventDefault();
$this = $(this);
- $thisIcon = $this.find('i');
+ isExpanded = $this.find('i').hasClass('fa-angle-double-right');
+ tooltipLabel = isExpanded ? __('Expand sidebar') : __('Collapse sidebar');
$allGutterToggleIcons = $('.js-sidebar-toggle i');
- if ($thisIcon.hasClass('fa-angle-double-right')) {
+
+ if (isExpanded) {
$allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
$('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
$('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
@@ -57,6 +60,9 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
}
+
+ $this.attr('data-original-title', tooltipLabel);
+
if (!triggered) {
Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
}
diff --git a/app/assets/javascripts/shared/popover.js b/app/assets/javascripts/shared/popover.js
new file mode 100644
index 00000000000..3fc03553bdd
--- /dev/null
+++ b/app/assets/javascripts/shared/popover.js
@@ -0,0 +1,33 @@
+import $ from 'jquery';
+import _ from 'underscore';
+
+export function togglePopover(show) {
+ const isAlreadyShown = this.hasClass('js-popover-show');
+ if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) {
+ return false;
+ }
+ this.popover(show ? 'show' : 'hide');
+ this.toggleClass('disable-animation js-popover-show', show);
+
+ return true;
+}
+
+export function mouseleave() {
+ if (!$('.popover:hover').length > 0) {
+ const $popover = $(this);
+ togglePopover.call($popover, false);
+ }
+}
+
+export function mouseenter() {
+ const $popover = $(this);
+
+ const showedPopover = togglePopover.call($popover, true);
+ if (showedPopover) {
+ $('.popover').on('mouseleave', mouseleave.bind($popover));
+ }
+}
+
+export function debouncedMouseleave(debounceTimeout = 300) {
+ return _.debounce(mouseleave, debounceTimeout);
+}
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js
index 25f39e4fdb6..9f69f110d06 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js
@@ -1,12 +1,15 @@
+import { visitUrl } from './lib/utils/url_utility';
+
/**
* Helper function that finds the href of the fiven selector and updates the location.
*
* @param {String} selector
*/
-export default (selector) => {
- const link = document.querySelector(selector).getAttribute('href');
+export default function findAndFollowLink(selector) {
+ const element = document.querySelector(selector);
+ const link = element && element.getAttribute('href');
if (link) {
- window.location = link;
+ visitUrl(link);
}
-};
+}
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index 1e7f46454bf..2d00e8ac7e0 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -1,6 +1,12 @@
<script>
+import { __ } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+
export default {
name: 'Assignees',
+ directives: {
+ tooltip,
+ },
props: {
rootPath: {
type: String,
@@ -14,6 +20,11 @@ export default {
type: Boolean,
required: true,
},
+ issuableType: {
+ type: String,
+ require: true,
+ default: 'issue',
+ },
},
data() {
return {
@@ -62,6 +73,12 @@ export default {
names.push(`+ ${this.users.length - maxRender} more`);
}
+ if (!this.users.length) {
+ const emptyTooltipLabel = this.issuableType === 'issue' ?
+ __('Assignee(s)') : __('Assignee');
+ names.push(emptyTooltipLabel);
+ }
+
return names.join(', ');
},
sidebarAvatarCounter() {
@@ -109,7 +126,8 @@ export default {
<div>
<div
class="sidebar-collapsed-icon sidebar-collapsed-user"
- :class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }"
+ :class="{ 'multiple-users': hasMoreThanOneAssignee }"
+ v-tooltip
data-container="body"
data-placement="left"
:title="collapsedTooltipTitle"
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 3c6b9c27814..b04a2eff798 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -1,9 +1,9 @@
<script>
-import Flash from '../../../flash';
+import Flash from '~/flash';
+import eventHub from '~/sidebar/event_hub';
+import Store from '~/sidebar/stores/sidebar_store';
import AssigneeTitle from './assignee_title.vue';
import Assignees from './assignees.vue';
-import Store from '../../stores/sidebar_store';
-import eventHub from '../../event_hub';
export default {
name: 'SidebarAssignees',
@@ -25,6 +25,11 @@ export default {
required: false,
default: false,
},
+ issuableType: {
+ type: String,
+ require: true,
+ default: 'issue',
+ },
},
data() {
return {
@@ -90,6 +95,7 @@ export default {
:users="store.assignees"
:editable="store.editable"
@assign-self="assignSelf"
+ :issuable-type="issuableType"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index ceb02309959..7f0de722f61 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,15 +1,19 @@
<script>
-import Flash from '../../../flash';
+import { __ } from '~/locale';
+import Flash from '~/flash';
+import tooltip from '~/vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
-import Icon from '../../../vue_shared/components/icon.vue';
-import { __ } from '../../../locale';
-import eventHub from '../../event_hub';
export default {
components: {
editForm,
Icon,
},
+ directives: {
+ tooltip,
+ },
props: {
isConfidential: {
required: true,
@@ -33,6 +37,9 @@ export default {
confidentialityIcon() {
return this.isConfidential ? 'eye-slash' : 'eye';
},
+ tooltipLabel() {
+ return this.isConfidential ? __('Confidential') : __('Not confidential');
+ },
},
created() {
eventHub.$on('closeConfidentialityForm', this.toggleForm);
@@ -65,6 +72,10 @@ export default {
<div
class="sidebar-collapsed-icon"
@click="toggleForm"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="tooltipLabel"
>
<icon
:name="confidentialityIcon"
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 e4893451af3..1a5e7b67eca 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -1,15 +1,22 @@
<script>
+import { __ } from '~/locale';
import Flash from '~/flash';
+import tooltip from '~/vue_shared/directives/tooltip';
+import issuableMixin from '~/vue_shared/mixins/issuable';
+import Icon from '~/vue_shared/components/icon.vue';
+import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
-import issuableMixin from '../../../vue_shared/mixins/issuable';
-import Icon from '../../../vue_shared/components/icon.vue';
-import eventHub from '../../event_hub';
export default {
components: {
editForm,
Icon,
},
+
+ directives: {
+ tooltip,
+ },
+
mixins: [issuableMixin],
props: {
@@ -44,6 +51,10 @@ export default {
isLockDialogOpen() {
return this.mediator.store.isLockDialogOpen;
},
+
+ tooltipLabel() {
+ return this.isLocked ? __('Locked') : __('Unlocked');
+ },
},
created() {
@@ -85,6 +96,10 @@ export default {
<div
class="sidebar-collapsed-icon"
@click="toggleForm"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="tooltipLabel"
>
<icon
:name="lockIcon"
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 006a6d2905d..6d95153af28 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -1,9 +1,13 @@
<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';
+ import { __, n__, sprintf } from '~/locale';
+ import tooltip from '~/vue_shared/directives/tooltip';
+ import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+ import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
+ directives: {
+ tooltip,
+ },
components: {
loadingIcon,
userAvatarImage,
@@ -72,7 +76,13 @@
<template>
<div>
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="participantLabel"
+ >
<i
class="fa fa-users"
aria-hidden="true"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
index 3b86f1145d1..9d9ee9dea4d 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -1,12 +1,17 @@
<script>
- import icon from '../../../vue_shared/components/icon.vue';
- import { abbreviateTime } from '../../../lib/utils/pretty_time';
+ import { __, sprintf } from '~/locale';
+ import { abbreviateTime } from '~/lib/utils/pretty_time';
+ import icon from '~/vue_shared/components/icon.vue';
+ import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'TimeTrackingCollapsedState',
components: {
icon,
},
+ directives: {
+ tooltip,
+ },
props: {
showComparisonState: {
type: Boolean,
@@ -79,6 +84,21 @@
return '';
},
+ timeTrackedTooltipText() {
+ let title;
+ if (this.showComparisonState) {
+ title = __('Time remaining');
+ } else if (this.showEstimateOnlyState) {
+ title = __('Estimated');
+ } else if (this.showSpentOnlyState) {
+ title = __('Time spent');
+ }
+
+ return sprintf('%{title}: %{text}', ({ title, text: this.text }));
+ },
+ tooltipText() {
+ return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText;
+ },
},
methods: {
abbreviateTime(timeStr) {
@@ -89,7 +109,13 @@
</script>
<template>
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="tooltipText"
+ >
<icon name="timer" />
<div class="time-tracking-collapsed-summary">
<div :class="divClass">
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js
deleted file mode 100644
index 2d324c71379..00000000000
--- a/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js
+++ /dev/null
@@ -1,17 +0,0 @@
-export default {
- name: 'time-tracking-estimate-only-pane',
- props: {
- timeEstimateHumanReadable: {
- type: String,
- required: true,
- },
- },
- template: `
- <div class="time-tracking-estimate-only-pane">
- <span class="bold">
- {{ s__('TimeTracking|Estimated:') }}
- </span>
- {{ timeEstimateHumanReadable }}
- </div>
- `,
-};
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.vue
new file mode 100644
index 00000000000..08fce597e50
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.vue
@@ -0,0 +1,20 @@
+<script>
+export default {
+ name: 'TimeTrackingEstimateOnlyPane',
+ props: {
+ timeEstimateHumanReadable: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="time-tracking-estimate-only-pane">
+ <span class="bold">
+ {{ s__('TimeTracking|Estimated:') }}
+ </span>
+ {{ timeEstimateHumanReadable }}
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.js b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
index 19f74ad3c6d..825063d9ba6 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.js
+++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
@@ -1,7 +1,8 @@
+<script>
import { sprintf, s__ } from '../../../locale';
export default {
- name: 'time-tracking-help-state',
+ name: 'TimeTrackingHelpState',
props: {
rootPath: {
type: String,
@@ -27,26 +28,28 @@ export default {
);
},
},
- template: `
- <div class="time-tracking-help-state">
- <div class="time-tracking-info">
- <h4>
- {{ __('Track time with quick actions') }}
- </h4>
- <p>
- {{ __('Quick actions can be used in the issues description and comment boxes.') }}
- </p>
- <p v-html="estimateText">
- </p>
- <p v-html="spendText">
- </p>
- <a
- class="btn btn-default learn-more-button"
- :href="href"
- >
- {{ __('Learn more') }}
- </a>
- </div>
- </div>
- `,
};
+</script>
+
+<template>
+ <div class="time-tracking-help-state">
+ <div class="time-tracking-info">
+ <h4>
+ {{ __('Track time with quick actions') }}
+ </h4>
+ <p>
+ {{ __('Quick actions can be used in the issues description and comment boxes.') }}
+ </p>
+ <p v-html="estimateText">
+ </p>
+ <p v-html="spendText">
+ </p>
+ <a
+ class="btn btn-default learn-more-button"
+ :href="href"
+ >
+ {{ __('Learn more') }}
+ </a>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index 1c641c73ea3..71dca498b3d 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -1,9 +1,9 @@
<script>
-import timeTrackingHelpState from './help_state';
+import TimeTrackingHelpState from './help_state.vue';
import TimeTrackingCollapsedState from './collapsed_state.vue';
import timeTrackingSpentOnlyPane from './spent_only_pane';
import timeTrackingNoTrackingPane from './no_tracking_pane';
-import timeTrackingEstimateOnlyPane from './estimate_only_pane';
+import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue';
import eventHub from '../../event_hub';
@@ -12,11 +12,11 @@ export default {
name: 'IssuableTimeTracker',
components: {
TimeTrackingCollapsedState,
- 'time-tracking-estimate-only-pane': timeTrackingEstimateOnlyPane,
+ TimeTrackingEstimateOnlyPane,
'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane,
TimeTrackingComparisonPane,
- 'time-tracking-help-state': timeTrackingHelpState,
+ TimeTrackingHelpState,
},
props: {
time_estimate: {
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 9f5d852260e..26eb4cffba3 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -27,6 +27,7 @@ function mountAssigneesComponent(mediator) {
mediator,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
+ issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
},
}),
});
diff --git a/app/assets/javascripts/snippet/snippet_embed.js b/app/assets/javascripts/snippet/snippet_embed.js
new file mode 100644
index 00000000000..81ec483f2d9
--- /dev/null
+++ b/app/assets/javascripts/snippet/snippet_embed.js
@@ -0,0 +1,23 @@
+export default () => {
+ const { protocol, host, pathname } = location;
+ const shareBtn = document.querySelector('.js-share-btn');
+ const embedBtn = document.querySelector('.js-embed-btn');
+ const snippetUrlArea = document.querySelector('.js-snippet-url-area');
+ const embedAction = document.querySelector('.js-embed-action');
+ const url = `${protocol}//${host + pathname}`;
+
+ shareBtn.addEventListener('click', () => {
+ shareBtn.classList.add('is-active');
+ embedBtn.classList.remove('is-active');
+ snippetUrlArea.value = url;
+ embedAction.innerText = 'Share';
+ });
+
+ embedBtn.addEventListener('click', () => {
+ embedBtn.classList.add('is-active');
+ shareBtn.classList.remove('is-active');
+ const scriptTag = `<script src="${url}.js"></script>`;
+ snippetUrlArea.value = scriptTag;
+ embedAction.innerText = 'Embed';
+ });
+};
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 520a0b3f424..8486019897d 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -5,6 +5,7 @@
import $ from 'jquery';
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
import ModalStore from './boards/stores/modal_store';
// TODO: remove eventHub hack after code splitting refactor
@@ -182,7 +183,7 @@ function UsersSelect(currentUser, els, options = {}) {
return axios.put(issueURL, data)
.then(({ data }) => {
- var user;
+ var user, tooltipTitle;
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
if (data.assignee) {
@@ -191,15 +192,17 @@ function UsersSelect(currentUser, els, options = {}) {
username: data.assignee.username,
avatar: data.assignee.avatar_url
};
+ tooltipTitle = _.escape(user.name);
} else {
user = {
name: 'Unassigned',
username: '',
avatar: ''
};
+ tooltipTitle = __('Assignee');
}
$value.html(assigneeTemplate(user));
- $collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle');
+ $collapsedSidebar.attr('title', tooltipTitle).tooltip('fixTitle');
return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
});
};
diff --git a/app/assets/javascripts/visibility_select.js b/app/assets/javascripts/visibility_select.js
deleted file mode 100644
index 0c928d0d5f6..00000000000
--- a/app/assets/javascripts/visibility_select.js
+++ /dev/null
@@ -1,21 +0,0 @@
-export default class VisibilitySelect {
- constructor(container) {
- if (!container) throw new Error('VisibilitySelect requires a container element as argument 1');
- this.container = container;
- this.helpBlock = this.container.querySelector('.help-block');
- this.select = this.container.querySelector('select');
- }
-
- init() {
- if (this.select) {
- this.updateHelpText();
- this.select.addEventListener('change', this.updateHelpText.bind(this));
- } else {
- this.helpBlock.textContent = this.container.querySelector('.js-locked').dataset.helpBlock;
- }
- }
-
- updateHelpText() {
- this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description;
- }
-}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
index 95c8b0a4c55..f012f9c6772 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
@@ -146,8 +146,8 @@ export default {
</p>
<p
v-if="shouldShowMemoryGraph"
- class="usage-info js-usage-info">
- {{ memoryChangeMessage }}
+ class="usage-info js-usage-info"
+ v-html="memoryChangeMessage">
</p>
<p
v-if="shouldShowLoadFailure"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
deleted file mode 100644
index 4d9a2ca530f..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import statusIcon from '../mr_widget_status_icon.vue';
-
-export default {
- name: 'MRWidgetPipelineBlocked',
- components: {
- statusIcon,
- },
- template: `
- <div class="mr-widget-body media">
- <status-icon status="warning" :show-disabled-button="true" />
- <div class="media-body space-children">
- <span class="bold">
- The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
- </span>
- </div>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
new file mode 100644
index 00000000000..8d55477929f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
@@ -0,0 +1,25 @@
+<script>
+import statusIcon from '../mr_widget_status_icon.vue';
+
+export default {
+ name: 'PipelineFailed',
+ components: {
+ statusIcon,
+ },
+};
+</script>
+
+<template>
+ <div class="mr-widget-body media">
+ <status-icon
+ status="warning"
+ :show-disabled-button="true"
+ />
+ <div class="media-body space-children">
+ <span class="bold">
+ {{ s__(`mrWidget|The pipeline for this merge request failed.
+Please retry the job or push a new commit to fix the failure`) }}
+ </span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 3c781ccddc8..0264625a526 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -1,3 +1,4 @@
+<script>
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll';
@@ -7,7 +8,10 @@ import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
export default {
- name: 'MRWidgetReadyToMerge',
+ name: 'ReadyToMerge',
+ components: {
+ statusIcon,
+ },
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
@@ -26,9 +30,6 @@ export default {
warningSvg,
};
},
- components: {
- statusIcon,
- },
computed: {
shouldShowMergeWhenPipelineSucceedsText() {
return this.mr.isPipelineActive;
@@ -217,136 +218,146 @@ export default {
});
},
},
- template: `
- <div class="mr-widget-body media">
- <status-icon :status="iconClass" />
- <div class="media-body">
- <div class="mr-widget-body-controls media space-children">
- <span class="btn-group append-bottom-5">
- <button
- @click="handleMergeButtonClick()"
- :disabled="isMergeButtonDisabled"
- :class="mergeButtonClass"
- type="button"
- class="qa-merge-button">
- <i
- v-if="isMakingRequest"
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- {{mergeButtonText}}
- </button>
+};
+</script>
+
+<template>
+ <div class="mr-widget-body media">
+ <status-icon :status="iconClass" />
+ <div class="media-body">
+ <div class="mr-widget-body-controls media space-children">
+ <span class="btn-group append-bottom-5">
+ <button
+ @click="handleMergeButtonClick()"
+ :disabled="isMergeButtonDisabled"
+ :class="mergeButtonClass"
+ type="button"
+ class="qa-merge-button">
+ <i
+ v-if="isMakingRequest"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true"
+ ></i>
+ {{ mergeButtonText }}
+ </button>
+ <button
+ v-if="shouldShowMergeOptionsDropdown"
+ :disabled="isMergeButtonDisabled"
+ type="button"
+ class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
+ data-toggle="dropdown"
+ aria-label="Select merge moment">
+ <i
+ class="fa fa-chevron-down"
+ aria-hidden="true"
+ ></i>
+ </button>
+ <ul
+ v-if="shouldShowMergeOptionsDropdown"
+ class="dropdown-menu dropdown-menu-right"
+ role="menu">
+ <li>
+ <a
+ @click.prevent="handleMergeButtonClick(true)"
+ class="merge_when_pipeline_succeeds"
+ href="#">
+ <span class="media">
+ <span
+ v-html="successSvg"
+ class="merge-opt-icon"
+ aria-hidden="true"></span>
+ <span class="media-body merge-opt-title">Merge when pipeline succeeds</span>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a
+ @click.prevent="handleMergeButtonClick(false, true)"
+ class="accept-merge-request"
+ href="#">
+ <span class="media">
+ <span
+ v-html="warningSvg"
+ class="merge-opt-icon"
+ aria-hidden="true"></span>
+ <span class="media-body merge-opt-title">Merge immediately</span>
+ </span>
+ </a>
+ </li>
+ </ul>
+ </span>
+ <div class="media-body-wrap space-children">
+ <template v-if="shouldShowMergeControls()">
+ <label v-if="mr.canRemoveSourceBranch">
+ <input
+ id="remove-source-branch-input"
+ v-model="removeSourceBranch"
+ class="js-remove-source-branch-checkbox"
+ :disabled="isRemoveSourceBranchButtonDisabled"
+ type="checkbox"/> Remove source branch
+ </label>
+
+ <!-- Placeholder for EE extension of this component -->
+ <squash-before-merge
+ v-if="shouldShowSquashBeforeMerge"
+ :mr="mr"
+ :is-merge-button-disabled="isMergeButtonDisabled" />
+
+ <span
+ v-if="mr.ffOnlyEnabled"
+ class="js-fast-forward-message">
+ Fast-forward merge without a merge commit
+ </span>
<button
- v-if="shouldShowMergeOptionsDropdown"
+ v-else
+ @click="toggleCommitMessageEditor"
:disabled="isMergeButtonDisabled"
- type="button"
- class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
- data-toggle="dropdown"
- aria-label="Select merge moment">
- <i
- class="fa fa-chevron-down"
- aria-hidden="true" />
+ class="js-modify-commit-message-button btn btn-default btn-xs"
+ type="button">
+ Modify commit message
</button>
- <ul
- v-if="shouldShowMergeOptionsDropdown"
- class="dropdown-menu dropdown-menu-right"
- role="menu">
- <li>
- <a
- @click.prevent="handleMergeButtonClick(true)"
- class="merge_when_pipeline_succeeds"
- href="#">
- <span class="media">
- <span
- v-html="successSvg"
- class="merge-opt-icon"
- aria-hidden="true"></span>
- <span class="media-body merge-opt-title">Merge when pipeline succeeds</span>
- </span>
- </a>
- </li>
- <li>
- <a
- @click.prevent="handleMergeButtonClick(false, true)"
- class="accept-merge-request"
- href="#">
- <span class="media">
- <span
- v-html="warningSvg"
- class="merge-opt-icon"
- aria-hidden="true"></span>
- <span class="media-body merge-opt-title">Merge immediately</span>
- </span>
- </a>
- </li>
- </ul>
- </span>
- <div class="media-body-wrap space-children">
- <template v-if="shouldShowMergeControls()">
- <label v-if="mr.canRemoveSourceBranch">
- <input
- id="remove-source-branch-input"
- v-model="removeSourceBranch"
- class="js-remove-source-branch-checkbox"
- :disabled="isRemoveSourceBranchButtonDisabled"
- type="checkbox"/> Remove source branch
- </label>
-
- <!-- Placeholder for EE extension of this component -->
- <squash-before-merge
- v-if="shouldShowSquashBeforeMerge"
- :mr="mr"
- :is-merge-button-disabled="isMergeButtonDisabled" />
-
- <span
- v-if="mr.ffOnlyEnabled"
- class="js-fast-forward-message">
- Fast-forward merge without a merge commit
- </span>
- <button
- v-else
- @click="toggleCommitMessageEditor"
- :disabled="isMergeButtonDisabled"
- class="js-modify-commit-message-button btn btn-default btn-xs"
- type="button">
- Modify commit message
- </button>
- </template>
- <template v-else>
- <span class="bold js-resolve-mr-widget-items-message">
- You can only merge once the items above are resolved
- </span>
- </template>
- </div>
+ </template>
+ <template v-else>
+ <span class="bold js-resolve-mr-widget-items-message">
+ You can only merge once the items above are resolved
+ </span>
+ </template>
</div>
- <div
- v-if="showCommitMessageEditor"
- class="prepend-top-default commit-message-editor">
- <div class="form-group clearfix">
- <label
- class="control-label"
- for="commit-message">
- Commit message
- </label>
- <div class="col-sm-10">
- <div class="commit-message-container">
- <div class="max-width-marker"></div>
- <textarea
- v-model="commitMessage"
- class="form-control js-commit-message"
- required="required"
- rows="14"
- name="Commit message"></textarea>
- </div>
- <p class="hint">Try to keep the first line under 52 characters and the others under 72</p>
- <div class="hint">
- <a
- @click.prevent="updateCommitMessage"
- href="#">{{commitMessageLinkTitle}}</a>
- </div>
+ </div>
+ <div
+ v-if="showCommitMessageEditor"
+ class="prepend-top-default commit-message-editor">
+ <div class="form-group clearfix">
+ <label
+ class="control-label"
+ for="commit-message">
+ Commit message
+ </label>
+ <div class="col-sm-10">
+ <div class="commit-message-container">
+ <div class="max-width-marker"></div>
+ <textarea
+ id="commit-message"
+ v-model="commitMessage"
+ class="form-control js-commit-message"
+ required="required"
+ rows="14"
+ name="Commit message"></textarea>
+ </div>
+ <p class="hint">
+ Try to keep the first line under 52 characters and the others under 72
+ </p>
+ <div class="hint">
+ <a
+ @click.prevent="updateCommitMessage"
+ href="#"
+ >
+ {{ commitMessageLinkTitle }}
+ </a>
</div>
</div>
</div>
</div>
</div>
- `,
-};
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
index 9ade6a91747..a1f7e696795 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
@@ -7,7 +7,10 @@ export default {
statusIcon,
},
props: {
- mr: { type: Object, required: true },
+ mr: {
+ type: Object,
+ required: true,
+ },
},
};
</script>
@@ -20,13 +23,14 @@ export default {
/>
<div class="media-body space-children">
<span class="bold">
- There are unresolved discussions. Please resolve these discussions
+ {{ s__("mrWidget|There are unresolved discussions. Please resolve these discussions") }}
</span>
<a
v-if="mr.createIssueToResolveDiscussionsPath"
:href="mr.createIssueToResolveDiscussionsPath"
- class="btn btn-default btn-xs js-create-issue">
- Create an issue to resolve them later
+ class="btn btn-default btn-xs js-create-issue"
+ >
+ {{ s__("mrWidget|Create an issue to resolve them later") }}
</a>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index ed15fc6ab0f..3b5c973e4a0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -27,11 +27,11 @@ export { default as ConflictsState } from './components/states/mr_widget_conflic
export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue';
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue';
-export { default as ReadyToMergeState } from './components/states/mr_widget_ready_to_merge';
+export { default as ReadyToMergeState } from './components/states/ready_to_merge.vue';
export { default as ShaMismatchState } from './components/states/sha_mismatch.vue';
export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue';
-export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
+export { default as PipelineFailedState } from './components/states/pipeline_failed.vue';
export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
export { default as RebaseState } from './components/states/mr_widget_rebase.vue';
export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue';
diff --git a/app/assets/javascripts/vue_shared/components/callout.vue b/app/assets/javascripts/vue_shared/components/callout.vue
new file mode 100644
index 00000000000..ccf802c456c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/callout.vue
@@ -0,0 +1,27 @@
+<script>
+const calloutVariants = ['danger', 'success', 'info', 'warning'];
+
+export default {
+ props: {
+ category: {
+ type: String,
+ required: false,
+ default: calloutVariants[0],
+ validator: value => calloutVariants.includes(value),
+ },
+ message: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div
+ :class="`bs-callout bs-callout-${category}`"
+ role="alert"
+ aria-live="assertive"
+ >
+ {{ message }}
+ </div>
+</template>
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 5324d5dc797..0d64efcbf68 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -1,52 +1,52 @@
<script>
- import ciIcon from './ci_icon.vue';
- import tooltip from '../directives/tooltip';
- /**
- * Renders CI Badge link with CI icon and status text based on
- * API response shared between all places where it is used.
- *
- * Receives status object containing:
- * status: {
- * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
- * group:"running" // used for CSS class
- * icon: "icon_status_running" // used to render the icon
- * label:"running" // used for potential tooltip
- * text:"running" // text rendered
- * }
- *
- * Used in:
- * - Pipelines table - first column
- * - Jobs table - first column
- * - Pipeline show view - header
- * - Job show view - header
- * - MR widget
- */
+import CiIcon from './ci_icon.vue';
+import tooltip from '../directives/tooltip';
+/**
+ * Renders CI Badge link with CI icon and status text based on
+ * API response shared between all places where it is used.
+ *
+ * Receives status object containing:
+ * status: {
+ * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
+ * group:"running" // used for CSS class
+ * icon: "icon_status_running" // used to render the icon
+ * label:"running" // used for potential tooltip
+ * text:"running" // text rendered
+ * }
+ *
+ * Used in:
+ * - Pipelines table - first column
+ * - Jobs table - first column
+ * - Pipeline show view - header
+ * - Job show view - header
+ * - MR widget
+ */
- export default {
- components: {
- ciIcon,
+export default {
+ components: {
+ CiIcon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ status: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ showText: {
+ type: Boolean,
+ required: false,
+ default: true,
},
- props: {
- status: {
- type: Object,
- required: true,
- },
- showText: {
- type: Boolean,
- required: false,
- default: true,
- },
+ },
+ computed: {
+ cssClass() {
+ const className = this.status.group;
+ return className ? `ci-status ci-${className}` : 'ci-status';
},
- computed: {
- cssClass() {
- const className = this.status.group;
- return className ? `ci-status ci-${className}` : 'ci-status';
- },
- },
- };
+ },
+};
</script>
<template>
<a
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index 8fea746f4de..fcab8f571dd 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -1,45 +1,44 @@
<script>
- import icon from '../../vue_shared/components/icon.vue';
+import Icon from '../../vue_shared/components/icon.vue';
- /**
- * Renders CI icon based on API response shared between all places where it is used.
- *
- * Receives status object containing:
- * status: {
- * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
- * group:"running" // used for CSS class
- * icon: "icon_status_running" // used to render the icon
- * label:"running" // used for potential tooltip
- * text:"running" // text rendered
- * }
- *
- * Used in:
- * - Pipelines table Badge
- * - Pipelines table mini graph
- * - Pipeline graph
- * - Pipeline show view badge
- * - Jobs table
- * - Jobs show view header
- * - Jobs show view sidebar
- */
- export default {
- components: {
- icon,
+/**
+ * Renders CI icon based on API response shared between all places where it is used.
+ *
+ * Receives status object containing:
+ * status: {
+ * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
+ * group:"running" // used for CSS class
+ * icon: "icon_status_running" // used to render the icon
+ * label:"running" // used for potential tooltip
+ * text:"running" // text rendered
+ * }
+ *
+ * Used in:
+ * - Pipelines table Badge
+ * - Pipelines table mini graph
+ * - Pipeline graph
+ * - Pipeline show view badge
+ * - Jobs table
+ * - Jobs show view header
+ * - Jobs show view sidebar
+ */
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ status: {
+ type: Object,
+ required: true,
},
- props: {
- status: {
- type: Object,
- required: true,
- },
+ },
+ computed: {
+ cssClass() {
+ const status = this.status.group;
+ return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
},
-
- computed: {
- cssClass() {
- const status = this.status.group;
- return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
- },
- },
- };
+ },
+};
</script>
<template>
<span :class="cssClass">
diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
index cab126a7eca..cb2cc3901ad 100644
--- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue
+++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
@@ -1,40 +1,50 @@
<script>
- /**
- * Falls back to the code used in `copy_to_clipboard.js`
- */
- import tooltip from '../directives/tooltip';
+/**
+ * Falls back to the code used in `copy_to_clipboard.js`
+ *
+ * Renders a button with a clipboard icon that copies the content of `data-clipboard-text`
+ * when clicked.
+ *
+ * @example
+ * <clipboard-button
+ * title="Copy to clipbard"
+ * text="Content to be copied"
+ * css-class="btn-transparent"
+ * />
+ */
+import tooltip from '../directives/tooltip';
- export default {
- name: 'ClipboardButton',
- directives: {
- tooltip,
+export default {
+ name: 'ClipboardButton',
+ directives: {
+ tooltip,
+ },
+ props: {
+ text: {
+ type: String,
+ required: true,
},
- props: {
- text: {
- type: String,
- required: true,
- },
- title: {
- type: String,
- required: true,
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'top',
- },
- tooltipContainer: {
- type: [String, Boolean],
- required: false,
- default: false,
- },
- cssClass: {
- type: String,
- required: false,
- default: 'btn-default',
- },
+ title: {
+ type: String,
+ required: true,
},
- };
+ tooltipPlacement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+ tooltipContainer: {
+ type: [String, Boolean],
+ required: false,
+ default: false,
+ },
+ cssClass: {
+ type: String,
+ required: false,
+ default: 'btn-default',
+ },
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 97789636787..8f250a6c989 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -1,119 +1,111 @@
<script>
- import commitIconSvg from 'icons/_icon_commit.svg';
- import userAvatarLink from './user_avatar/user_avatar_link.vue';
- import tooltip from '../directives/tooltip';
- import icon from '../../vue_shared/components/icon.vue';
+import UserAvatarLink from './user_avatar/user_avatar_link.vue';
+import tooltip from '../directives/tooltip';
+import Icon from '../../vue_shared/components/icon.vue';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ UserAvatarLink,
+ Icon,
+ },
+ props: {
+ /**
+ * Indicates the existance of a tag.
+ * Used to render the correct icon, if true will render `fa-tag` icon,
+ * if false will render a svg sprite fork icon
+ */
+ tag: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- components: {
- userAvatarLink,
- icon,
+ /**
+ * If provided is used to render the branch name and url.
+ * Should contain the following properties:
+ * name
+ * ref_url
+ */
+ commitRef: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ /**
+ * Used to link to the commit sha.
+ */
+ commitUrl: {
+ type: String,
+ required: false,
+ default: '',
},
- props: {
- /**
- * Indicates the existance of a tag.
- * Used to render the correct icon, if true will render `fa-tag` icon,
- * if false will render a svg sprite fork icon
- */
- tag: {
- type: Boolean,
- required: false,
- default: false,
- },
- /**
- * If provided is used to render the branch name and url.
- * Should contain the following properties:
- * name
- * ref_url
- */
- commitRef: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- /**
- * Used to link to the commit sha.
- */
- commitUrl: {
- type: String,
- required: false,
- default: '',
- },
- /**
- * Used to show the commit short sha that links to the commit url.
- */
- shortSha: {
- type: String,
- required: false,
- default: '',
- },
- /**
- * If provided shows the commit tile.
- */
- title: {
- type: String,
- required: false,
- default: '',
- },
- /**
- * If provided renders information about the author of the commit.
- * When provided should include:
- * `avatar_url` to render the avatar icon
- * `web_url` to link to user profile
- * `username` to render alt and title tags
- */
- author: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- showBranch: {
- type: Boolean,
- required: false,
- default: true,
- },
+ /**
+ * Used to show the commit short sha that links to the commit url.
+ */
+ shortSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ /**
+ * If provided shows the commit tile.
+ */
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ /**
+ * If provided renders information about the author of the commit.
+ * When provided should include:
+ * `avatar_url` to render the avatar icon
+ * `web_url` to link to user profile
+ * `username` to render alt and title tags
+ */
+ author: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ showBranch: {
+ type: Boolean,
+ required: false,
+ default: true,
},
- computed: {
- /**
- * Used to verify if all the properties needed to render the commit
- * ref section were provided.
- *
- * @returns {Boolean}
- */
- hasCommitRef() {
- return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
- },
- /**
- * Used to verify if all the properties needed to render the commit
- * author section were provided.
- *
- * @returns {Boolean}
- */
- hasAuthor() {
- return this.author &&
- this.author.avatar_url &&
- this.author.path &&
- this.author.username;
- },
- /**
- * If information about the author is provided will return a string
- * to be rendered as the alt attribute of the img tag.
- *
- * @returns {String}
- */
- userImageAltDescription() {
- return this.author &&
- this.author.username ? `${this.author.username}'s avatar` : null;
- },
+ },
+ computed: {
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * ref section were provided.
+ *
+ * @returns {Boolean}
+ */
+ hasCommitRef() {
+ return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
},
- created() {
- this.commitIconSvg = commitIconSvg;
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * author section were provided.
+ *
+ * @returns {Boolean}
+ */
+ hasAuthor() {
+ return this.author && this.author.avatar_url && this.author.path && this.author.username;
},
- };
+ /**
+ * If information about the author is provided will return a string
+ * to be rendered as the alt attribute of the img tag.
+ *
+ * @returns {String}
+ */
+ userImageAltDescription() {
+ return this.author && this.author.username ? `${this.author.username}'s avatar` : null;
+ },
+ },
+};
</script>
<template>
<div class="branch-commit">
@@ -141,11 +133,10 @@
{{ commitRef.name }}
</a>
</template>
- <div
- v-html="commitIconSvg"
+ <icon
+ name="commit"
class="commit-icon js-commit-icon"
- >
- </div>
+ />
<a
class="commit-sha"
@@ -175,7 +166,7 @@
</a>
</span>
<span v-else>
- Cant find HEAD commit for this branch
+ Can't find HEAD commit for this branch
</span>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue
index c943c8d98a4..9295be3e2b2 100644
--- a/app/assets/javascripts/vue_shared/components/expand_button.vue
+++ b/app/assets/javascripts/vue_shared/components/expand_button.vue
@@ -1,33 +1,33 @@
<script>
- import { __ } from '~/locale';
- /**
- * Port of detail_behavior expand button.
- *
- * @example
- * <expand-button>
- * <template slot="expanded">
- * Text goes here.
- * </template>
- * </expand-button>
- */
- export default {
- name: 'ExpandButton',
- data() {
- return {
- isCollapsed: true,
- };
+import { __ } from '~/locale';
+/**
+ * Port of detail_behavior expand button.
+ *
+ * @example
+ * <expand-button>
+ * <template slot="expanded">
+ * Text goes here.
+ * </template>
+ * </expand-button>
+ */
+export default {
+ name: 'ExpandButton',
+ data() {
+ return {
+ isCollapsed: true,
+ };
+ },
+ computed: {
+ ariaLabel() {
+ return __('Click to expand text');
},
- computed: {
- ariaLabel() {
- return __('Click to expand text');
- },
+ },
+ methods: {
+ onClick() {
+ this.isCollapsed = !this.isCollapsed;
},
- methods: {
- onClick() {
- this.isCollapsed = !this.isCollapsed;
- },
- },
- };
+ },
+};
</script>
<template>
<span>
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index a0cd0cbd200..088187ed348 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -1,78 +1,78 @@
<script>
- import ciIconBadge from './ci_badge_link.vue';
- import loadingIcon from './loading_icon.vue';
- import timeagoTooltip from './time_ago_tooltip.vue';
- import tooltip from '../directives/tooltip';
- import userAvatarImage from './user_avatar/user_avatar_image.vue';
+import CiIconBadge from './ci_badge_link.vue';
+import LoadingIcon from './loading_icon.vue';
+import TimeagoTooltip from './time_ago_tooltip.vue';
+import tooltip from '../directives/tooltip';
+import UserAvatarImage from './user_avatar/user_avatar_image.vue';
- /**
- * Renders header component for job and pipeline page based on UI mockups
- *
- * Used in:
- * - job show page
- * - pipeline show page
- */
- export default {
- components: {
- ciIconBadge,
- loadingIcon,
- timeagoTooltip,
- userAvatarImage,
+/**
+ * Renders header component for job and pipeline page based on UI mockups
+ *
+ * Used in:
+ * - job show page
+ * - pipeline show page
+ */
+export default {
+ components: {
+ CiIconBadge,
+ LoadingIcon,
+ TimeagoTooltip,
+ UserAvatarImage,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ status: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ itemName: {
+ type: String,
+ required: true,
},
- props: {
- status: {
- type: Object,
- required: true,
- },
- itemName: {
- type: String,
- required: true,
- },
- itemId: {
- type: Number,
- required: true,
- },
- time: {
- type: String,
- required: true,
- },
- user: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- actions: {
- type: Array,
- required: false,
- default: () => [],
- },
- hasSidebarButton: {
- type: Boolean,
- required: false,
- default: false,
- },
- shouldRenderTriggeredLabel: {
- type: Boolean,
- required: false,
- default: true,
- },
+ itemId: {
+ type: Number,
+ required: true,
},
+ time: {
+ type: String,
+ required: true,
+ },
+ user: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ actions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ hasSidebarButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ shouldRenderTriggeredLabel: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
- computed: {
- userAvatarAltText() {
- return `${this.user.name}'s avatar`;
- },
+ computed: {
+ userAvatarAltText() {
+ return `${this.user.name}'s avatar`;
},
+ },
- methods: {
- onClickAction(action) {
- this.$emit('actionClicked', action);
- },
+ methods: {
+ onClickAction(action) {
+ this.$emit('actionClicked', action);
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
index 6a2e05000e1..1a0df49bc29 100644
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -1,76 +1,75 @@
<script>
+/* This is a re-usable vue component for rendering a svg sprite
+ icon
- /* This is a re-usable vue component for rendering a svg sprite
- icon
+ Sample configuration:
- Sample configuration:
+ <icon
+ name="retry"
+ :size="32"
+ css-classes="top"
+ />
- <icon
- name="retry"
- :size="32"
- css-classes="top"
- />
+*/
+// only allow classes in images.scss e.g. s12
+const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
- */
- // only allow classes in images.scss e.g. s12
- const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
-
- export default {
- props: {
- name: {
- type: String,
- required: true,
- },
+export default {
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
- size: {
- type: Number,
- required: false,
- default: 16,
- validator(value) {
- return validSizes.includes(value);
- },
+ size: {
+ type: Number,
+ required: false,
+ default: 16,
+ validator(value) {
+ return validSizes.includes(value);
},
+ },
- cssClasses: {
- type: String,
- required: false,
- default: '',
- },
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
- width: {
- type: Number,
- required: false,
- default: null,
- },
+ width: {
+ type: Number,
+ required: false,
+ default: null,
+ },
- height: {
- type: Number,
- required: false,
- default: null,
- },
+ height: {
+ type: Number,
+ required: false,
+ default: null,
+ },
- y: {
- type: Number,
- required: false,
- default: null,
- },
+ y: {
+ type: Number,
+ required: false,
+ default: null,
+ },
- x: {
- type: Number,
- required: false,
- default: null,
- },
+ x: {
+ type: Number,
+ required: false,
+ default: null,
},
+ },
- computed: {
- spriteHref() {
- return `${gon.sprite_icons}#${this.name}`;
- },
- iconSizeClass() {
- return this.size ? `s${this.size}` : '';
- },
+ computed: {
+ spriteHref() {
+ return `${gon.sprite_icons}#${this.name}`;
+ },
+ iconSizeClass() {
+ return this.size ? `s${this.size}` : '';
},
- };
+ },
+};
</script>
<template>
@@ -79,7 +78,8 @@
:width="width"
:height="height"
:x="x"
- :y="y">
+ :y="y"
+ >
<use v-bind="{ 'xlink:href':spriteHref }" />
</svg>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index d91fe3cf0c5..db453c30576 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -27,20 +27,22 @@
$(document).off('markdown-preview:hide.vue', this.writeMarkdownTab);
},
methods: {
- isMarkdownForm(form) {
- return form && !form.find('.js-vue-markdown-field').length;
+ isValid(form) {
+ return !form ||
+ form.find('.js-vue-markdown-field').length ||
+ $(this.$el).closest('form') === form[0];
},
previewMarkdownTab(event, form) {
if (event.target.blur) event.target.blur();
- if (this.isMarkdownForm(form)) return;
+ if (!this.isValid(form)) return;
this.$emit('preview-markdown');
},
writeMarkdownTab(event, form) {
if (event.target.blur) event.target.blur();
- if (this.isMarkdownForm(form)) return;
+ if (!this.isValid(form)) return;
this.$emit('write-markdown');
},
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index 5ede53d8d01..70b46a9c2bb 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -1,4 +1,5 @@
<script>
+import $ from 'jquery';
import { __ } from '~/locale';
import LabelsSelect from '~/labels_select';
import LoadingIcon from '../../loading_icon.vue';
@@ -98,11 +99,18 @@ export default {
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
handleClick: this.handleClick,
});
+ $(this.$refs.dropdown).on('hidden.gl.dropdown', this.handleDropdownHidden);
},
methods: {
handleClick(label) {
this.$emit('onLabelClick', label);
},
+ handleCollapsedValueClick() {
+ this.$emit('toggleCollapse');
+ },
+ handleDropdownHidden() {
+ this.$emit('onDropdownClose');
+ },
},
};
</script>
@@ -112,6 +120,7 @@ export default {
<dropdown-value-collapsed
v-if="showCreate"
:labels="context.labels"
+ @onValueClick="handleCollapsedValueClick"
/>
<dropdown-title
:can-edit="canEdit"
@@ -133,7 +142,10 @@ export default {
:name="hiddenInputName"
:label="label"
/>
- <div class="dropdown">
+ <div
+ class="dropdown"
+ ref="dropdown"
+ >
<dropdown-button
:ability-name="abilityName"
:field-name="hiddenInputName"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
index 5cf728fe050..68fa2ab8d01 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
@@ -26,6 +26,11 @@ export default {
return labelsString;
},
},
+ methods: {
+ handleClick() {
+ this.$emit('onValueClick');
+ },
+ },
};
</script>
@@ -36,6 +41,7 @@ export default {
data-placement="left"
data-container="body"
:title="labelsList"
+ @click="handleClick"
>
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
index 8211d425b1f..de6f8c32e74 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
@@ -1,18 +1,29 @@
<script>
- export default {
- name: 'ToggleSidebar',
- props: {
- collapsed: {
- type: Boolean,
- required: true,
- },
+import { __ } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ name: 'ToggleSidebar',
+ directives: {
+ tooltip,
+ },
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ tooltipLabel() {
+ return this.collapsed ? __('Expand sidebar') : __('Collapse sidebar');
},
- methods: {
- toggle() {
- this.$emit('toggle');
- },
+ },
+ methods: {
+ toggle() {
+ this.$emit('toggle');
},
- };
+ },
+};
</script>
<template>
@@ -20,6 +31,10 @@
type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action"
@click="toggle"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="tooltipLabel"
>
<i
aria-label="toggle collapse"
diff --git a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
index b06493e6c66..16304e4815d 100644
--- a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
+++ b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
@@ -9,7 +9,7 @@
lines: {
type: Number,
required: false,
- default: 6,
+ default: 3,
},
},
computed: {
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 0665622fe4a..f2950308019 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -37,7 +37,11 @@
/*
* Code highlight
*/
-@import "highlight/**/*";
+@import "highlight/dark";
+@import "highlight/monokai";
+@import "highlight/solarized_dark";
+@import "highlight/solarized_light";
+@import "highlight/white";
/*
* Styles for JS behaviors.
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 728f9a27aca..14cd32da9eb 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -187,12 +187,9 @@ a {
animation: fadeInFull $fade-in-duration 1;
}
-
.animation-container {
- background: $repo-editor-grey;
height: 40px;
overflow: hidden;
- position: relative;
&.animation-container-small {
height: 12px;
@@ -205,60 +202,43 @@ a {
}
}
- &::before {
- animation-duration: 1s;
- animation-fill-mode: forwards;
- animation-iteration-count: infinite;
- animation-name: blockTextShine;
- animation-timing-function: linear;
- background-image: $repo-editor-linear-gradient;
- background-repeat: no-repeat;
- background-size: 800px 45px;
- content: ' ';
- display: block;
- height: 100%;
+ [class^="skeleton-line-"] {
position: relative;
- }
-
- div {
- background: $white-light;
- height: 6px;
- left: 0;
- position: absolute;
- right: 0;
- }
-
- .skeleton-line-1 {
- left: 0;
- top: 8px;
- }
-
- .skeleton-line-2 {
- left: 150px;
- top: 0;
+ background-color: $theme-gray-100;
height: 10px;
- }
+ overflow: hidden;
- .skeleton-line-3 {
- left: 0;
- top: 23px;
- }
+ &:not(:last-of-type) {
+ margin-bottom: 4px;
+ }
- .skeleton-line-4 {
- left: 0;
- top: 38px;
+ &::after {
+ content: ' ';
+ display: block;
+ animation: blockTextShine 1s linear infinite forwards;
+ background-repeat: no-repeat;
+ background-size: cover;
+ background-image: linear-gradient(
+ to right,
+ $theme-gray-100 0%,
+ $theme-gray-50 20%,
+ $theme-gray-100 40%,
+ $theme-gray-100 100%
+ );
+ height: 10px;
+ }
}
+}
- .skeleton-line-5 {
- left: 200px;
- top: 28px;
- height: 10px;
- }
+$skeleton-line-widths: (
+ 156px,
+ 235px,
+ 200px,
+);
- .skeleton-line-6 {
- top: 14px;
- left: 230px;
- height: 10px;
+@for $count from 1 through length($skeleton-line-widths) {
+ .skeleton-line-#{$count} {
+ width: nth($skeleton-line-widths, $count);
}
}
diff --git a/app/assets/stylesheets/framework/banner.scss b/app/assets/stylesheets/framework/banner.scss
index 6433b0c7855..02f3896d591 100644
--- a/app/assets/stylesheets/framework/banner.scss
+++ b/app/assets/stylesheets/framework/banner.scss
@@ -1,7 +1,7 @@
.banner-callout {
display: flex;
position: relative;
- flex-wrap: wrap;
+ align-items: start;
.banner-close {
position: absolute;
@@ -16,10 +16,25 @@
}
.banner-graphic {
- margin: 20px auto;
+ margin: 0 $gl-padding $gl-padding 0;
}
&.banner-non-empty-state {
border-bottom: 1px solid $border-color;
}
+
+ @media (max-width: $screen-xs-max) {
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
+
+ .banner-title,
+ .banner-buttons {
+ text-align: center;
+ }
+
+ .banner-graphic {
+ margin-left: $gl-padding;
+ }
+ }
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 6b89387ab5f..f4f5926e198 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -422,25 +422,43 @@
}
}
-.btn-link.btn-secondary-hover-link {
- color: $gl-text-color-secondary;
+.btn-link {
+ padding: 0;
+ background-color: transparent;
+ color: $blue-600;
+ font-weight: normal;
+ border-radius: 0;
+ border-color: transparent;
&:hover,
&:active,
&:focus {
- color: $gl-link-color;
- text-decoration: none;
+ color: $blue-800;
+ text-decoration: underline;
+ background-color: transparent;
+ border-color: transparent;
}
-}
-.btn-link.btn-primary-hover-link {
- color: inherit;
+ &.btn-secondary-hover-link {
+ color: $gl-text-color-secondary;
- &:hover,
- &:active,
- &:focus {
- color: $gl-link-color;
- text-decoration: none;
+ &:hover,
+ &:active,
+ &:focus {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
+ }
+
+ &.btn-primary-hover-link {
+ color: inherit;
+
+ &:hover,
+ &:active,
+ &:focus {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
}
}
@@ -485,3 +503,7 @@ fieldset[disabled] .btn,
@extend %disabled;
}
}
+
+.btn-no-padding {
+ padding: 0;
+}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index d0dda50a835..e058a0b35b7 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -472,6 +472,7 @@ img.emoji {
.append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; }
.append-bottom-5 { margin-bottom: 5px; }
+.append-bottom-8 { margin-bottom: $grid-size; }
.append-bottom-10 { margin-bottom: 10px; }
.append-bottom-15 { margin-bottom: 15px; }
.append-bottom-20 { margin-bottom: 20px; }
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index cc74cb72795..664aade7375 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -43,7 +43,7 @@
border-color: $gray-darkest;
}
- [data-toggle="dropdown"] {
+ [data-toggle='dropdown'] {
outline: 0;
}
}
@@ -172,7 +172,11 @@
color: $brand-danger;
}
- &:hover,
+ &.disable-hover {
+ text-decoration: none;
+ }
+
+ &:not(.disable-hover):hover,
&:active,
&:focus,
&.is-focused {
@@ -481,7 +485,8 @@
.dropdown-menu-selectable {
li {
- a {
+ a,
+ button {
padding: 8px 40px;
position: relative;
@@ -507,17 +512,16 @@
}
&.is-indeterminate::before {
- content: "\f068";
+ content: '\f068';
}
&.is-active::before {
- content: "\f00c";
+ content: '\f00c';
}
}
}
}
-
.dropdown-title {
position: relative;
padding: 2px 25px 10px;
@@ -723,7 +727,6 @@
}
}
-
.dropdown-menu-due-date {
.dropdown-content {
max-height: 230px;
@@ -853,9 +856,13 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
.projects-list-frequent-container,
- .projects-list-search-container, {
+ .projects-list-search-container {
padding: 8px 0;
overflow-y: auto;
+
+ li.section-empty.section-failure {
+ color: $callout-danger-color;
+ }
}
.section-header,
@@ -866,13 +873,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
font-size: $gl-font-size;
}
- .projects-list-frequent-container,
- .projects-list-search-container {
- li.section-empty.section-failure {
- color: $callout-danger-color;
- }
- }
-
.search-input-container {
position: relative;
padding: 4px $gl-padding;
@@ -904,8 +904,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
.projects-list-item-container {
- .project-item-avatar-container
- .project-item-metadata-container {
+ .project-item-avatar-container .project-item-metadata-container {
float: left;
}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 8604e753c18..9e03bb98b8e 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -40,10 +40,6 @@
.project-home-panel {
padding-left: 0 !important;
- .project-avatar {
- display: block;
- }
-
.project-repo-buttons,
.git-clone-holder {
display: none;
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index 17c31d6b184..66dbe403385 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -241,8 +241,6 @@
}
.scrolling-tabs-container {
- position: relative;
-
.merge-request-tabs-container & {
overflow: hidden;
}
@@ -272,8 +270,6 @@
}
.inner-page-scroll-tabs {
- position: relative;
-
.fade-right {
@include fade(left, $white-light);
right: 0;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 798f248dad4..64fff7463d2 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -16,7 +16,7 @@
.nav-header-btn {
padding: 10px $gl-sidebar-padding;
color: inherit;
- transition-duration: .3s;
+ transition-duration: 0.3s;
position: absolute;
top: 0;
cursor: pointer;
@@ -137,6 +137,12 @@
}
}
+.issuable-sidebar .labels {
+ .value.dont-hide ~ .selectbox {
+ padding-top: $gl-padding-8;
+ }
+}
+
.pikaday-container {
.pika-single {
margin-top: 2px;
@@ -151,4 +157,3 @@
.sidebar-collapsed-icon .sidebar-collapsed-value {
font-size: 12px;
}
-
diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
index 30c15c231d5..606d4675f19 100644
--- a/app/assets/stylesheets/framework/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -29,8 +29,10 @@
}
.snippet-title {
- font-size: 24px;
+ color: $gl-text-color;
+ font-size: 2em;
font-weight: $gl-font-weight-bold;
+ min-height: $header-height;
}
.snippet-edited-ago {
@@ -46,3 +48,26 @@
.snippet-scope-menu .btn-new {
margin-top: 15px;
}
+
+.snippet-embed-input {
+ height: 35px;
+}
+
+.embed-snippet {
+ padding-right: 0;
+ padding-top: $gl-padding;
+
+ .form-control {
+ cursor: auto;
+ width: 101%;
+ margin-left: -1px;
+ }
+
+ .embed-toggle-list li button {
+ padding: 8px 40px;
+ }
+
+ .embed-toggle {
+ height: 35px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 8ee1bb03d55..8c44ebc85ef 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -247,6 +247,7 @@ $btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
$issue-status-expired: $orange-500;
$issuable-sidebar-color: $gl-text-color-secondary;
+$sidebar-block-hover-color: #ebebeb;
$group-path-color: #999;
$namespace-kind-color: #aaa;
$panel-heading-link-color: #777;
@@ -373,6 +374,8 @@ $dropdown-hover-color: $blue-400;
$link-active-background: rgba(0, 0, 0, 0.04);
$link-hover-background: rgba(0, 0, 0, 0.06);
$inactive-badge-background: rgba(0, 0, 0, 0.08);
+$sidebar-toggle-height: 60px;
+$sidebar-milestone-toggle-bottom-margin: 10px;
/*
* Buttons
@@ -714,20 +717,6 @@ $color-average-score: $orange-400;
$color-low-score: $red-400;
/*
-Repo editor
-*/
-$repo-editor-grey: #f6f7f9;
-$repo-editor-grey-darker: #e9ebee;
-$repo-editor-linear-gradient: linear-gradient(
- to right,
- $repo-editor-grey 0%,
- $repo-editor-grey-darker,
- 20%,
- $repo-editor-grey 40%,
- $repo-editor-grey 100%
-);
-
-/*
Performance Bar
*/
$perf-bar-text: #999;
diff --git a/app/assets/stylesheets/highlight/embedded.scss b/app/assets/stylesheets/highlight/embedded.scss
new file mode 100644
index 00000000000..44c8a1d39ec
--- /dev/null
+++ b/app/assets/stylesheets/highlight/embedded.scss
@@ -0,0 +1,3 @@
+.code {
+ @import "white_base";
+}
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index c3d8f0c61a2..355c8d223f7 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -1,292 +1,3 @@
-/* https://github.com/aahan/pygments-github-style */
-
-/*
-* White Syntax Colors
-*/
-$white-code-color: $gl-text-color;
-$white-highlight: #fafe3d;
-$white-pre-hll-bg: #f8eec7;
-$white-hll-bg: #f8f8f8;
-$white-over-bg: #ded7fc;
-$white-expanded-border: #e0e0e0;
-$white-expanded-bg: #f7f7f7;
-$white-c: #998;
-$white-err: #a61717;
-$white-err-bg: #e3d2d2;
-$white-cm: #998;
-$white-cp: #999;
-$white-c1: #998;
-$white-cs: #999;
-$white-gd: $black;
-$white-gd-bg: #fdd;
-$white-gd-x: $black;
-$white-gd-x-bg: #faa;
-$white-gr: #a00;
-$white-gh: #999;
-$white-gi: $black;
-$white-gi-bg: #dfd;
-$white-gi-x: $black;
-$white-gi-x-bg: #afa;
-$white-go: #888;
-$white-gp: #555;
-$white-gu: #800080;
-$white-gt: #a00;
-$white-kt: #458;
-$white-m: #099;
-$white-s: #d14;
-$white-n: #333;
-$white-na: teal;
-$white-nb: #0086b3;
-$white-nc: #458;
-$white-no: teal;
-$white-ni: purple;
-$white-ne: #900;
-$white-nf: #900;
-$white-nn: #555;
-$white-nt: navy;
-$white-nv: teal;
-$white-w: #bbb;
-$white-mf: #099;
-$white-mh: #099;
-$white-mi: #099;
-$white-mo: #099;
-$white-sb: #d14;
-$white-sc: #d14;
-$white-sd: #d14;
-$white-s2: #d14;
-$white-se: #d14;
-$white-sh: #d14;
-$white-si: #d14;
-$white-sx: #d14;
-$white-sr: #009926;
-$white-s1: #d14;
-$white-ss: #990073;
-$white-bp: #999;
-$white-vc: teal;
-$white-vg: teal;
-$white-vi: teal;
-$white-il: #099;
-$white-gc-color: #999;
-$white-gc-bg: #eaf2f5;
-
-
-@mixin matchLine {
- color: $black-transparent;
- background-color: $gray-light;
-}
-
.code.white {
- // Line numbers
- .line-numbers,
- .diff-line-num {
- background-color: $gray-light;
- }
-
- .diff-line-num,
- .diff-line-num a {
- color: $black-transparent;
- }
-
- // Code itself
- pre.code,
- .diff-line-num {
- border-color: $white-normal;
- }
-
- &,
- pre.code,
- .line_holder .line_content {
- background-color: $white-light;
- color: $white-code-color;
- }
-
- // Diff line
- .line_holder {
-
- &.match .line_content {
- @include matchLine;
- }
-
- .diff-line-num {
- &.old {
- background-color: $line-number-old;
- border-color: $line-removed-dark;
-
- a {
- color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
- }
- }
-
- &.new {
- background-color: $line-number-new;
- border-color: $line-added-dark;
-
- a {
- color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
- }
- }
-
- &.is-over,
- &.hll:not(.empty-cell).is-over {
- background-color: $white-over-bg;
- border-color: darken($white-over-bg, 5%);
-
- a {
- color: darken($white-over-bg, 15%);
- }
- }
-
- &.hll:not(.empty-cell) {
- background-color: $line-number-select;
- border-color: $line-select-yellow-dark;
- }
- }
-
- &:not(.diff-expanded) + .diff-expanded,
- &.diff-expanded + .line_holder:not(.diff-expanded) {
- > .diff-line-num,
- > .line_content {
- border-top: 1px solid $white-expanded-border;
- }
- }
-
- &.diff-expanded {
- > .diff-line-num,
- > .line_content {
- background: $white-expanded-bg;
- border-color: $white-expanded-bg;
- }
- }
-
- .line_content {
- &.old {
- background-color: $line-removed;
-
- &::before {
- color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
- }
-
- span.idiff {
- background-color: $line-removed-dark;
- }
- }
-
- &.new {
- background-color: $line-added;
-
- &::before {
- color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
- }
-
- span.idiff {
- background-color: $line-added-dark;
- }
- }
-
- &.match {
- @include matchLine;
- }
-
- &.hll:not(.empty-cell) {
- background-color: $line-select-yellow;
- }
- }
- }
-
- // highlight line via anchor
- pre .hll {
- background-color: $white-pre-hll-bg !important;
- }
-
- // Search result highlight
- span.highlight_word {
- background-color: $white-highlight !important;
- }
-
- // Links to URLs, emails, or dependencies
- .line a {
- color: $white-nb;
- }
-
- .hll { background-color: $white-hll-bg; }
- .c { color: $white-c; font-style: italic; }
- .err { color: $white-err; background-color: $white-err-bg; }
- .k { font-weight: $gl-font-weight-bold; }
- .o { font-weight: $gl-font-weight-bold; }
- .cm { color: $white-cm; font-style: italic; }
- .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;
-
- .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;
-
- .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; }
- .gu { color: $white-gu; font-weight: $gl-font-weight-bold; }
- .gt { color: $white-gt; }
- .kc { font-weight: $gl-font-weight-bold; }
- .kd { font-weight: $gl-font-weight-bold; }
- .kn { font-weight: $gl-font-weight-bold; }
- .kp { font-weight: $gl-font-weight-bold; }
- .kr { font-weight: $gl-font-weight-bold; }
- .kt { color: $white-kt; font-weight: $gl-font-weight-bold; }
- .m { color: $white-m; }
- .s { color: $white-s; }
- .n { color: $white-n; }
- .na { color: $white-na; }
- .nb { color: $white-nb; }
- .nc { color: $white-nc; font-weight: $gl-font-weight-bold; }
- .no { color: $white-no; }
- .ni { color: $white-ni; }
- .ne { color: $white-ne; font-weight: $gl-font-weight-bold; }
- .nf { color: $white-nf; font-weight: $gl-font-weight-bold; }
- .nn { color: $white-nn; }
- .nt { color: $white-nt; }
- .nv { color: $white-nv; }
- .ow { font-weight: $gl-font-weight-bold; }
- .w { color: $white-w; }
- .mf { color: $white-mf; }
- .mh { color: $white-mh; }
- .mi { color: $white-mi; }
- .mo { color: $white-mo; }
- .sb { color: $white-sb; }
- .sc { color: $white-sc; }
- .sd { color: $white-sd; }
- .s2 { color: $white-s2; }
- .se { color: $white-se; }
- .sh { color: $white-sh; }
- .si { color: $white-si; }
- .sx { color: $white-sx; }
- .sr { color: $white-sr; }
- .s1 { color: $white-s1; }
- .ss { color: $white-ss; }
- .bp { color: $white-bp; }
- .vc { color: $white-vc; }
- .vg { color: $white-vg; }
- .vi { color: $white-vi; }
- .il { color: $white-il; }
- .gc { color: $white-gc-color; background-color: $white-gc-bg; }
+ @import "white_base";
}
diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss
new file mode 100644
index 00000000000..8cc5252648d
--- /dev/null
+++ b/app/assets/stylesheets/highlight/white_base.scss
@@ -0,0 +1,290 @@
+/* https://github.com/aahan/pygments-github-style */
+
+/*
+* White Syntax Colors
+*/
+$white-code-color: $gl-text-color;
+$white-highlight: #fafe3d;
+$white-pre-hll-bg: #f8eec7;
+$white-hll-bg: #f8f8f8;
+$white-over-bg: #ded7fc;
+$white-expanded-border: #e0e0e0;
+$white-expanded-bg: #f7f7f7;
+$white-c: #998;
+$white-err: #a61717;
+$white-err-bg: #e3d2d2;
+$white-cm: #998;
+$white-cp: #999;
+$white-c1: #998;
+$white-cs: #999;
+$white-gd: $black;
+$white-gd-bg: #fdd;
+$white-gd-x: $black;
+$white-gd-x-bg: #faa;
+$white-gr: #a00;
+$white-gh: #999;
+$white-gi: $black;
+$white-gi-bg: #dfd;
+$white-gi-x: $black;
+$white-gi-x-bg: #afa;
+$white-go: #888;
+$white-gp: #555;
+$white-gu: #800080;
+$white-gt: #a00;
+$white-kt: #458;
+$white-m: #099;
+$white-s: #d14;
+$white-n: #333;
+$white-na: teal;
+$white-nb: #0086b3;
+$white-nc: #458;
+$white-no: teal;
+$white-ni: purple;
+$white-ne: #900;
+$white-nf: #900;
+$white-nn: #555;
+$white-nt: navy;
+$white-nv: teal;
+$white-w: #bbb;
+$white-mf: #099;
+$white-mh: #099;
+$white-mi: #099;
+$white-mo: #099;
+$white-sb: #d14;
+$white-sc: #d14;
+$white-sd: #d14;
+$white-s2: #d14;
+$white-se: #d14;
+$white-sh: #d14;
+$white-si: #d14;
+$white-sx: #d14;
+$white-sr: #009926;
+$white-s1: #d14;
+$white-ss: #990073;
+$white-bp: #999;
+$white-vc: teal;
+$white-vg: teal;
+$white-vi: teal;
+$white-il: #099;
+$white-gc-color: #999;
+$white-gc-bg: #eaf2f5;
+
+
+@mixin matchLine {
+ color: $black-transparent;
+ background-color: $gray-light;
+}
+
+ // Line numbers
+.line-numbers,
+.diff-line-num {
+ background-color: $gray-light;
+}
+
+.diff-line-num,
+.diff-line-num a {
+ color: $black-transparent;
+}
+
+// Code itself
+pre.code,
+.diff-line-num {
+ border-color: $white-normal;
+}
+
+&,
+pre.code,
+.line_holder .line_content {
+ background-color: $white-light;
+ color: $white-code-color;
+}
+
+// Diff line
+.line_holder {
+
+ &.match .line_content {
+ @include matchLine;
+ }
+
+ .diff-line-num {
+ &.old {
+ background-color: $line-number-old;
+ border-color: $line-removed-dark;
+
+ a {
+ color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
+ }
+ }
+
+ &.new {
+ background-color: $line-number-new;
+ border-color: $line-added-dark;
+
+ a {
+ color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
+ }
+ }
+
+ &.is-over,
+ &.hll:not(.empty-cell).is-over {
+ background-color: $white-over-bg;
+ border-color: darken($white-over-bg, 5%);
+
+ a {
+ color: darken($white-over-bg, 15%);
+ }
+ }
+
+ &.hll:not(.empty-cell) {
+ background-color: $line-number-select;
+ border-color: $line-select-yellow-dark;
+ }
+ }
+
+ &:not(.diff-expanded) + .diff-expanded,
+ &.diff-expanded + .line_holder:not(.diff-expanded) {
+ > .diff-line-num,
+ > .line_content {
+ border-top: 1px solid $white-expanded-border;
+ }
+ }
+
+ &.diff-expanded {
+ > .diff-line-num,
+ > .line_content {
+ background: $white-expanded-bg;
+ border-color: $white-expanded-bg;
+ }
+ }
+
+ .line_content {
+ &.old {
+ background-color: $line-removed;
+
+ &::before {
+ color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
+ }
+
+ span.idiff {
+ background-color: $line-removed-dark;
+ }
+ }
+
+ &.new {
+ background-color: $line-added;
+
+ &::before {
+ color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
+ }
+
+ span.idiff {
+ background-color: $line-added-dark;
+ }
+ }
+
+ &.match {
+ @include matchLine;
+ }
+
+ &.hll:not(.empty-cell) {
+ background-color: $line-select-yellow;
+ }
+ }
+}
+
+// highlight line via anchor
+pre .hll {
+ background-color: $white-pre-hll-bg !important;
+}
+
+ // Search result highlight
+span.highlight_word {
+ background-color: $white-highlight !important;
+}
+
+ // Links to URLs, emails, or dependencies
+.line a {
+ color: $white-nb;
+}
+
+.hll { background-color: $white-hll-bg; }
+.c { color: $white-c; font-style: italic; }
+.err { color: $white-err; background-color: $white-err-bg; }
+.k { font-weight: $gl-font-weight-bold; }
+.o { font-weight: $gl-font-weight-bold; }
+.cm { color: $white-cm; font-style: italic; }
+.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;
+
+ .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;
+
+ .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; }
+.gu { color: $white-gu; font-weight: $gl-font-weight-bold; }
+.gt { color: $white-gt; }
+.kc { font-weight: $gl-font-weight-bold; }
+.kd { font-weight: $gl-font-weight-bold; }
+.kn { font-weight: $gl-font-weight-bold; }
+.kp { font-weight: $gl-font-weight-bold; }
+.kr { font-weight: $gl-font-weight-bold; }
+.kt { color: $white-kt; font-weight: $gl-font-weight-bold; }
+.m { color: $white-m; }
+.s { color: $white-s; }
+.n { color: $white-n; }
+.na { color: $white-na; }
+.nb { color: $white-nb; }
+.nc { color: $white-nc; font-weight: $gl-font-weight-bold; }
+.no { color: $white-no; }
+.ni { color: $white-ni; }
+.ne { color: $white-ne; font-weight: $gl-font-weight-bold; }
+.nf { color: $white-nf; font-weight: $gl-font-weight-bold; }
+.nn { color: $white-nn; }
+.nt { color: $white-nt; }
+.nv { color: $white-nv; }
+.ow { font-weight: $gl-font-weight-bold; }
+.w { color: $white-w; }
+.mf { color: $white-mf; }
+.mh { color: $white-mh; }
+.mi { color: $white-mi; }
+.mo { color: $white-mo; }
+.sb { color: $white-sb; }
+.sc { color: $white-sc; }
+.sd { color: $white-sd; }
+.s2 { color: $white-s2; }
+.se { color: $white-se; }
+.sh { color: $white-sh; }
+.si { color: $white-si; }
+.sx { color: $white-sx; }
+.sr { color: $white-sr; }
+.s1 { color: $white-s1; }
+.ss { color: $white-ss; }
+.bp { color: $white-bp; }
+.vc { color: $white-vc; }
+.vg { color: $white-vg; }
+.vi { color: $white-vi; }
+.il { color: $white-il; }
+.gc { color: $white-gc-color; background-color: $white-gc-bg; }
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 7a6352e45f1..50f32660445 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -1,39 +1,56 @@
@keyframes fade-out-status {
- 0%, 50% { opacity: 1; }
- 100% { opacity: 0; }
+ 0%,
+ 50% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ }
}
@keyframes blinking-dots {
0% {
background-color: rgba($white-light, 1);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ 24px 0 0 0 rgba($white-light, 0.2);
}
25% {
background-color: rgba($white-light, 0.4);
box-shadow: 12px 0 0 0 rgba($white-light, 2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ 24px 0 0 0 rgba($white-light, 0.2);
}
75% {
background-color: rgba($white-light, 0.4);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 1);
+ 24px 0 0 0 rgba($white-light, 1);
}
100% {
background-color: rgba($white-light, 1);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ 24px 0 0 0 rgba($white-light, 0.2);
}
}
@keyframes blinking-scroll-button {
- 0% { opacity: 0.2; }
- 25% { opacity: 0.5; }
- 50% { opacity: 0.7; }
- 100% { opacity: 1; }
+ 0% {
+ opacity: 0.2;
+ }
+
+ 25% {
+ opacity: 0.5;
+ }
+
+ 50% {
+ opacity: 0.7;
+ }
+
+ 100% {
+ opacity: 1;
+ }
}
.build-page {
@@ -125,12 +142,12 @@
.btn-scroll.animate {
.first-triangle {
animation: blinking-scroll-button 1s ease infinite;
- animation-delay: .3s;
+ animation-delay: 0.3s;
}
.second-triangle {
animation: blinking-scroll-button 1s ease infinite;
- animation-delay: .2s;
+ animation-delay: 0.2s;
}
.third-triangle {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 86cdda0359e..e9384d41e00 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -180,10 +180,6 @@
justify-content: space-between;
align-items: center;
flex-grow: 1;
-
- .merge-request-branches & {
- flex-direction: column;
- }
}
.commit-content {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 679f783b1b6..11052be40a8 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -160,6 +160,11 @@
}
}
}
+
+ .diff-loading-error-block {
+ padding: $gl-padding * 2 $gl-padding;
+ text-align: center;
+ }
}
.image {
@@ -813,7 +818,6 @@
}
.discussion-notes {
- padding: 0 $gl-padding $gl-padding;
min-height: 35px;
&:first-child {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 2c0ed976301..b2dad4a358a 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -187,7 +187,12 @@
padding-left: 10px;
&:hover {
- color: $gray-darkest;
+ color: $gl-text-color;
+ }
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
}
}
@@ -368,6 +373,14 @@
padding: 15px 0 0;
border-bottom: 0;
overflow: hidden;
+
+ &:hover {
+ background-color: $sidebar-block-hover-color;
+ }
+
+ &.issuable-sidebar-header {
+ padding-top: 0;
+ }
}
.participants {
@@ -380,8 +393,17 @@
.gutter-toggle {
width: 100%;
+ height: $sidebar-toggle-height;
margin-left: 0;
- padding-left: 25px;
+ padding-left: 0;
+ border-bottom: 1px solid $border-gray-dark;
+ }
+
+ a.gutter-toggle {
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ text-align: center;
}
.sidebar-collapsed-icon {
@@ -428,10 +450,10 @@
.btn-clipboard {
border: 0;
+ background: transparent;
color: $issuable-sidebar-color;
&:hover {
- background: transparent;
color: $gl-text-color;
}
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index b0852adb459..d81236c5883 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -314,6 +314,10 @@
display: inline-flex;
vertical-align: top;
+ &:hover .color-label {
+ text-decoration: underline;
+ }
+
.label {
vertical-align: inherit;
font-size: $label-font-size;
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index b2250a1ce2f..97303d02666 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -154,26 +154,10 @@
a {
width: 100%;
font-size: 18px;
- margin-right: 0;
-
- &:hover {
- border: 1px solid transparent;
- }
}
- &.active {
- border-bottom: 1px solid $border-color;
-
- a {
- border: 0;
- border-bottom: 2px solid $link-underline-blue;
- margin-right: 0;
- color: $black;
-
- &:hover {
- border-bottom: 2px solid $link-underline-blue;
- }
- }
+ &.active > a {
+ cursor: default;
}
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 4692d0fb873..66db4917178 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -762,3 +762,20 @@
max-width: 100%;
}
}
+
+// Hack alert: we've rewritten `btn` class in a way that
+// we've broken it and it is not possible to use with `btn-link`
+// which causes a blank button when it's disabled and hovering
+// The css in here is the boostrap one
+.btn-link-retry {
+ &[disabled] {
+ cursor: not-allowed;
+ box-shadow: none;
+ opacity: .65;
+
+ &:hover {
+ color: $file-mode-changed;
+ text-decoration: none;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index e5afa8fffcb..bac3b70c734 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -53,10 +53,6 @@
}
.milestone-sidebar {
- .gutter-toggle {
- margin-bottom: 10px;
- }
-
.milestone-progress {
.title {
padding-top: 5px;
@@ -102,7 +98,17 @@
margin-right: 0;
}
+ .right-sidebar-expanded & {
+ .gutter-toggle {
+ margin-bottom: $sidebar-milestone-toggle-bottom-margin;
+ }
+ }
+
.right-sidebar-collapsed & {
+ .milestone-progress {
+ padding-top: 0;
+ }
+
.reference {
border-top: 1px solid $border-gray-normal;
}
@@ -194,3 +200,38 @@
.issuable-row {
background-color: $white-light;
}
+
+.milestone-deprecation-message {
+ .popover {
+ padding: 0;
+ }
+
+ .popover-content {
+ padding: 0;
+ }
+}
+
+.milestone-popover-body {
+ padding: $gl-padding-8;
+ background-color: $gray-light;
+}
+
+.milestone-popover-footer {
+ padding: $gl-padding-8 $gl-padding;
+ border-top: 1px solid $white-dark;
+}
+
+.milestone-popover-instructions-list {
+ padding-left: 2em;
+
+ > li {
+ padding-left: 1em;
+ }
+}
+
+@media (max-width: $screen-xs-max) {
+ .milestone-banner-text,
+ .milestone-banner-link {
+ display: inline;
+ }
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 8720f821ce9..4a528bc2bb1 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -173,7 +173,11 @@
}
.discussion-form {
- padding-top: $gl-padding-top;
+ background-color: $white-light;
+}
+
+.discussion-form-container {
+ padding: $gl-padding-top $gl-padding $gl-padding;
}
.discussion-notes .disabled-comment {
@@ -233,7 +237,12 @@
.discussion-body,
.diff-file {
.discussion-reply-holder {
- padding-top: $gl-padding;
+ background-color: $white-light;
+ padding: 10px 16px;
+
+ &.is-replying {
+ padding-bottom: $gl-padding;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 9d9cbecc958..81e98f358a8 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -47,7 +47,7 @@ ul.notes {
}
.timeline-entry-inner {
- padding: $gl-padding 0;
+ padding: $gl-padding $gl-btn-padding;
border-bottom: 1px solid $white-normal;
}
@@ -94,6 +94,12 @@ ul.notes {
}
}
+ &.note-discussion {
+ .timeline-entry-inner {
+ padding: $gl-padding 10px;
+ }
+ }
+
.editing-spinner {
display: none;
}
@@ -346,8 +352,6 @@ ul.notes {
}
.discussion-notes {
- background-color: $white-light;
-
&:not(:first-child) {
border-top: 1px solid $white-normal;
margin-top: 20px;
@@ -359,6 +363,10 @@ ul.notes {
}
}
+ .notes {
+ background-color: $white-light;
+ }
+
a code {
top: 0;
margin-right: 0;
@@ -639,6 +647,8 @@ ul.notes {
border-bottom: 1px solid $white-normal;
.timeline-entry-inner {
+ padding-left: $gl-padding;
+ padding-right: $gl-padding;
border-bottom: 0;
}
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 8d5eb2e8c5a..3a8ec779c14 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -14,6 +14,11 @@
.commit-title {
margin: 0;
+ white-space: normal;
+
+ @media (max-width: $screen-sm-max) {
+ justify-content: flex-end;
+ }
}
.ci-table {
@@ -344,7 +349,6 @@
svg {
vertical-align: middle;
- margin-right: 3px;
}
.stage-column {
@@ -464,6 +468,14 @@
margin-bottom: 10px;
white-space: normal;
+ .ci-job-dropdown-container {
+ // override dropdown.scss
+ .dropdown-menu li button {
+ padding: 0;
+ text-align: center;
+ }
+ }
+
// ensure .build-content has hover style when action-icon is hovered
.ci-job-dropdown-container:hover .build-content {
@extend .build-content:hover;
@@ -495,17 +507,12 @@
svg {
fill: $gl-text-color-secondary;
position: relative;
- left: 1px;
top: -1px;
- width: 16px;
- height: 16px;
}
&.play {
svg {
- width: 16px;
- height: 16px;
- left: 3px;
+ left: 2px;
}
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 790e91e4431..d7d343b088a 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -935,11 +935,6 @@ pre.light-well {
}
}
- .dropdown-menu-toggle {
- width: 100%;
- max-width: 300px;
- }
-
.flash-container {
padding: 0;
}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index a414deb8921..6d77260b658 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -17,6 +17,7 @@
}
.ide-view {
+ position: relative;
display: flex;
height: calc(100vh - #{$header-height});
margin-top: 0;
@@ -68,6 +69,10 @@
.ide-file-changed-icon {
margin-left: auto;
+
+ > svg {
+ display: block;
+ }
}
.ide-new-btn {
@@ -378,7 +383,11 @@
padding: $gl-bar-padding $gl-padding;
background: $white-light;
display: flex;
- justify-content: space-between;
+ justify-content: flex-end;
+
+ > div + div {
+ padding-left: $gl-padding;
+ }
svg {
vertical-align: middle;
@@ -429,6 +438,7 @@
.projects-sidebar {
display: flex;
flex-direction: column;
+ height: 100%;
.context-header {
width: auto;
@@ -438,8 +448,8 @@
.multi-file-commit-panel-inner {
display: flex;
- flex: 1;
flex-direction: column;
+ height: 100%;
}
.multi-file-commit-panel-inner-scroll {
@@ -520,9 +530,13 @@
overflow: auto;
}
-.multi-file-commit-empty-state-container {
- align-items: center;
- justify-content: center;
+.ide-commit-empty-state {
+ padding: 0 $gl-padding;
+}
+
+.ide-commit-empty-state-container {
+ margin-top: auto;
+ margin-bottom: auto;
}
.multi-file-commit-panel-header {
@@ -531,35 +545,22 @@
margin-bottom: 0;
border-bottom: 1px solid $white-dark;
padding: $gl-btn-padding 0;
-
- &.is-collapsed {
- border-bottom: 1px solid $white-dark;
-
- svg {
- margin-left: auto;
- margin-right: auto;
- }
-
- .multi-file-commit-panel-collapse-btn {
- margin-right: auto;
- margin-left: auto;
- border-left: 0;
- }
- }
}
.multi-file-commit-panel-header-title {
display: flex;
flex: 1;
- padding: 0 $gl-btn-padding;
+ padding-left: $grid-size;
svg {
margin-right: $gl-btn-padding;
+ color: $theme-gray-700;
}
}
.multi-file-commit-panel-collapse-btn {
border-left: 1px solid $white-dark;
+ margin-left: auto;
}
.multi-file-commit-list {
@@ -573,12 +574,14 @@
display: flex;
padding: 0;
align-items: center;
+ border-radius: $border-radius-default;
.multi-file-discard-btn {
display: none;
+ margin-top: -2px;
margin-left: auto;
+ margin-right: $grid-size;
color: $gl-link-color;
- padding: 0 2px;
&:focus,
&:hover {
@@ -590,26 +593,31 @@
background: $white-normal;
.multi-file-discard-btn {
- display: block;
+ display: flex;
}
}
}
-.multi-file-addition {
+.multi-file-additions,
+.multi-file-additions-solid {
fill: $green-500;
}
-.multi-file-modified {
+.multi-file-modified,
+.multi-file-modified-solid {
fill: $orange-500;
}
.multi-file-commit-list-collapsed {
display: flex;
flex-direction: column;
+ padding: $gl-padding 0;
- > svg {
+ svg {
+ display: block;
margin-left: auto;
margin-right: auto;
+ color: $theme-gray-700;
}
.file-status-icon {
@@ -621,7 +629,7 @@
.multi-file-commit-list-path {
padding: $grid-size / 2;
- padding-left: $gl-padding;
+ padding-left: $grid-size;
background: none;
border: 0;
text-align: left;
@@ -661,11 +669,6 @@
}
}
-.multi-file-commit-message.form-control {
- height: 160px;
- resize: none;
-}
-
.dirty-diff {
// !important need to override monaco inline style
width: 4px !important;
@@ -811,6 +814,41 @@
}
}
+.ide-commit-list-container {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ padding: 0 16px;
+
+ &:not(.is-collapsed) {
+ flex: 1;
+ min-height: 140px;
+ }
+
+ &.is-collapsed {
+ .multi-file-commit-panel-header {
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+
+ svg {
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ .multi-file-commit-panel-collapse-btn {
+ margin-right: auto;
+ margin-left: auto;
+ border-left: 0;
+ }
+ }
+ }
+}
+
+.ide-staged-action-btn {
+ margin-left: auto;
+ color: $gl-link-color;
+}
+
.ide-commit-radios {
label {
font-weight: normal;
@@ -838,3 +876,94 @@
align-items: center;
font-weight: $gl-font-weight-bold;
}
+
+.ide-file-finder-overlay {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 100;
+}
+
+.ide-file-finder {
+ top: 10px;
+ left: 50%;
+ transform: translateX(-50%);
+
+ .highlighted {
+ color: $blue-500;
+ font-weight: $gl-font-weight-bold;
+ }
+}
+
+.ide-commit-message-field {
+ height: 200px;
+ background-color: $white-light;
+
+ .md-area {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+
+ .nav-links {
+ height: 30px;
+ }
+
+ .help-block {
+ margin-top: 2px;
+ color: $blue-500;
+ cursor: pointer;
+ }
+}
+
+.ide-commit-message-textarea-container {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+
+ .note-textarea {
+ font-family: $monospace_font;
+ }
+}
+
+.ide-commit-message-highlights-container {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: -100px;
+ bottom: 0;
+ padding-right: 100px;
+ pointer-events: none;
+ z-index: 1;
+
+ .highlights {
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ color: transparent;
+ }
+
+ mark {
+ margin-left: -1px;
+ padding: 0 2px;
+ border-radius: $border-radius-small;
+ background-color: $orange-200;
+ color: transparent;
+ opacity: 0.6;
+ }
+}
+
+.ide-commit-message-textarea {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 2;
+ background: transparent;
+ resize: none;
+}
diff --git a/app/assets/stylesheets/snippets.scss b/app/assets/stylesheets/snippets.scss
new file mode 100644
index 00000000000..0d6b0735f70
--- /dev/null
+++ b/app/assets/stylesheets/snippets.scss
@@ -0,0 +1,156 @@
+@import "framework/variables";
+
+.gitlab-embed-snippets {
+ @import "highlight/embedded";
+ @import "framework/images";
+
+ $border-style: 1px solid $border-color;
+
+ font-family: $regular_font;
+ font-size: $gl-font-size;
+ line-height: $code_line_height;
+ color: $gl-text-color;
+ margin: 20px;
+ font-weight: 200;
+
+ .gl-snippet-icon {
+ display: inline-block;
+ background: url(asset_path('ext_snippet_icons/ext_snippet_icons.png')) no-repeat;
+ overflow: hidden;
+ text-align: left;
+ width: 16px;
+ height: 16px;
+ background-size: cover;
+
+ &.gl-snippet-icon-doc_code { background-position: 0 0; }
+ &.gl-snippet-icon-doc_text { background-position: 0 -16px; }
+ &.gl-snippet-icon-download { background-position: 0 -32px; }
+ }
+
+ .blob-viewer {
+ background-color: $white-light;
+ text-align: left;
+ }
+
+ .file-content.code {
+ border: $border-style;
+ border-radius: 0 0 4px 4px;
+ display: flex;
+ box-shadow: none;
+ margin: 0;
+ padding: 0;
+ table-layout: fixed;
+
+ .blob-content {
+ overflow-x: auto;
+
+ pre {
+ padding: 10px;
+ border: 0;
+ border-radius: 0;
+ font-family: $monospace_font;
+ font-size: $code_font_size;
+ line-height: $code_line_height;
+ margin: 0;
+ overflow: auto;
+ overflow-y: hidden;
+ white-space: pre;
+ word-wrap: normal;
+ border-left: $border-style;
+ }
+ }
+
+ .line-numbers {
+ padding: 10px;
+ text-align: right;
+ float: left;
+
+ .diff-line-num {
+ font-family: $monospace_font;
+ display: block;
+ font-size: $code_font_size;
+ min-height: $code_line_height;
+ white-space: nowrap;
+ color: $black-transparent;
+ min-width: 30px;
+ }
+
+ .diff-line-num:hover {
+ color: $almost-black;
+ cursor: pointer;
+ }
+ }
+ }
+
+ .file-title-flex-parent {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background-color: $gray-light;
+ border: $border-style;
+ border-bottom: 0;
+ padding: $gl-padding-top $gl-padding;
+ margin: 0;
+ border-radius: $border-radius-default $border-radius-default 0 0;
+
+ .file-header-content {
+ .file-title-name {
+ font-weight: $gl-font-weight-bold;
+ }
+
+ .gitlab-embedded-snippets-title {
+ text-decoration: none;
+ color: $gl-text-color;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ .gitlab-logo {
+ display: inline-block;
+ padding-left: 5px;
+ text-decoration: none;
+ color: $gl-text-color-secondary;
+
+ .logo-text {
+ background: image_url('ext_snippet_icons/logo.png') no-repeat left center;
+ background-size: 18px;
+ font-weight: $gl-font-weight-normal;
+ padding-left: 24px;
+ }
+ }
+ }
+
+ img,
+ .gl-snippet-icon {
+ display: inline-block;
+ vertical-align: middle;
+ }
+ }
+
+ .btn-group {
+ a.btn {
+ background-color: $white-light;
+ text-decoration: none;
+ padding: 7px 9px;
+ border: $border-style;
+ border-right: 0;
+
+ &:hover {
+ background-color: $white-normal;
+ border-color: $border-white-normal;
+ text-decoration: none;
+ }
+
+ &:first-child {
+ border-radius: 3px 0 0 3px;
+ }
+
+ &:last-child {
+ border-radius: 0 3px 3px 0;
+ border-right: $border-style;
+ }
+ }
+ }
+}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 4dfb397e82c..8958eab0423 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -56,21 +56,18 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def application_setting_params
- import_sources = params[:application_setting][:import_sources]
- if import_sources.nil?
- params[:application_setting][:import_sources] = []
- else
- import_sources.map! do |source|
- source.to_str
- end
- end
+ params[:application_setting] ||= {}
- enabled_oauth_sign_in_sources = params[:application_setting].delete(:enabled_oauth_sign_in_sources)
+ if params[:application_setting].key?(:enabled_oauth_sign_in_sources)
+ enabled_oauth_sign_in_sources = params[:application_setting].delete(:enabled_oauth_sign_in_sources)
+ enabled_oauth_sign_in_sources&.delete("")
- params[:application_setting][:disabled_oauth_sign_in_sources] =
- AuthHelper.button_based_providers.map(&:to_s) -
- Array(enabled_oauth_sign_in_sources)
+ params[:application_setting][:disabled_oauth_sign_in_sources] =
+ AuthHelper.button_based_providers.map(&:to_s) -
+ Array(enabled_oauth_sign_in_sources)
+ end
+ params[:application_setting][:import_sources]&.delete("")
params[:application_setting][:restricted_visibility_levels]&.delete("")
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 24651dd392c..0fdd4d2cb47 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -5,6 +5,7 @@ class ApplicationController < ActionController::Base
include Gitlab::GonHelper
include GitlabRoutingHelper
include PageLayoutHelper
+ include SafeParamsHelper
include SentryHelper
include WorkhorseHelper
include EnforcesTwoFactorAuthentication
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index 2fdf346ef44..69a053d4246 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -23,6 +23,9 @@ module AuthenticatesWithTwoFactor
#
# Returns nil
def prompt_for_two_factor(user)
+ # Set @user for Devise views
+ @user = user # rubocop:disable Gitlab/ModuleWithInstanceVariables
+
return locked_user_redirect(user) unless user.can?(:log_in)
session[:otp_user_id] = user.id
diff --git a/app/controllers/concerns/checks_collaboration.rb b/app/controllers/concerns/checks_collaboration.rb
new file mode 100644
index 00000000000..81367663a06
--- /dev/null
+++ b/app/controllers/concerns/checks_collaboration.rb
@@ -0,0 +1,21 @@
+module ChecksCollaboration
+ def can_collaborate_with_project?(project, ref: nil)
+ return true if can?(current_user, :push_code, project)
+
+ can_create_merge_request =
+ can?(current_user, :create_merge_request_in, project) &&
+ current_user.already_forked?(project)
+
+ can_create_merge_request ||
+ user_access(project).can_push_to_branch?(ref)
+ end
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ # enabling this so we can easily cache the user access value as it might be
+ # used across multiple calls in the view
+ def user_access(project)
+ @user_access ||= {}
+ @user_access[project] ||= Gitlab::UserAccess.new(current_user, project: project)
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 4114ca6bf7c..34228cf0b82 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -165,8 +165,8 @@ module IssuableCollections
[:project, :author, :assignees, :labels, :milestone, project: :namespace]
when 'MergeRequest'
[
- :source_project, :target_project, :author, :assignee, :labels, :milestone,
- head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
+ :target_project, :author, :assignee, :labels, :milestone,
+ source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
]
end
end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 839cac3687c..0c34e49206a 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -41,7 +41,7 @@ module NotesActions
@note = Notes::CreateService.new(note_project, current_user, create_params).execute
if @note.is_a?(Note)
- Notes::RenderService.new(current_user).execute([@note], @project)
+ Notes::RenderService.new(current_user).execute([@note])
end
respond_to do |format|
@@ -56,7 +56,7 @@ module NotesActions
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note)
- Notes::RenderService.new(current_user).execute([@note], @project)
+ Notes::RenderService.new(current_user).execute([@note])
end
respond_to do |format|
@@ -217,7 +217,7 @@ module NotesActions
def note_project
strong_memoize(:note_project) do
- return nil unless project
+ next nil unless project
note_project_id = params[:note_project_id]
@@ -228,7 +228,7 @@ module NotesActions
project
end
- return access_denied! unless can?(current_user, :create_note, the_project)
+ next access_denied! unless can?(current_user, :create_note, the_project)
the_project
end
diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb
index e7ef297879f..36e3d76ecfe 100644
--- a/app/controllers/concerns/renders_notes.rb
+++ b/app/controllers/concerns/renders_notes.rb
@@ -4,7 +4,7 @@ module RendersNotes
preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project)
preload_first_time_contribution_for_authors(noteable, notes)
- Notes::RenderService.new(current_user).execute(notes, @project)
+ Notes::RenderService.new(current_user).execute(notes)
notes
end
diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb
index 9095cc7f783..120614739aa 100644
--- a/app/controllers/concerns/snippets_actions.rb
+++ b/app/controllers/concerns/snippets_actions.rb
@@ -17,6 +17,10 @@ module SnippetsActions
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
+ def js_request?
+ request.format.js?
+ end
+
private
def convert_line_endings(content)
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index e89eaf7edda..f9e8fe624e8 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -86,7 +86,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
out_of_range = todos.current_page > total_pages
if out_of_range
- redirect_to url_for(params.merge(page: total_pages, only_path: true))
+ redirect_to url_for(safe_params.merge(page: total_pages, only_path: true))
end
out_of_range
diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb
index 6142e75b4c1..4d8a20de017 100644
--- a/app/controllers/groups/variables_controller.rb
+++ b/app/controllers/groups/variables_controller.rb
@@ -15,7 +15,7 @@ module Groups
def update
if @group.update(group_variables_params)
respond_to do |format|
- format.json { return render_group_variables }
+ format.json { render_group_variables }
end
else
respond_to do |format|
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 283c3e5f1e0..79fa5818359 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -173,7 +173,9 @@ class GroupsController < Groups::ApplicationController
.new(@projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
- Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
+ Events::RenderService
+ .new(current_user)
+ .execute(@events, atom_request: request.format.atom?)
end
def user_actions
@@ -187,6 +189,6 @@ class GroupsController < Groups::ApplicationController
params[:id] = group.to_param
- url_for(params)
+ url_for(safe_params)
end
end
diff --git a/app/controllers/ldap/omniauth_callbacks_controller.rb b/app/controllers/ldap/omniauth_callbacks_controller.rb
new file mode 100644
index 00000000000..fb24edb8602
--- /dev/null
+++ b/app/controllers/ldap/omniauth_callbacks_controller.rb
@@ -0,0 +1,31 @@
+class Ldap::OmniauthCallbacksController < OmniauthCallbacksController
+ extend ::Gitlab::Utils::Override
+
+ def self.define_providers!
+ return unless Gitlab::Auth::LDAP::Config.enabled?
+
+ Gitlab::Auth::LDAP::Config.available_servers.each do |server|
+ alias_method server['provider_name'], :ldap
+ end
+ end
+
+ # We only find ourselves here
+ # if the authentication to LDAP was successful.
+ def ldap
+ sign_in_user_flow(Gitlab::Auth::LDAP::User)
+ end
+
+ define_providers!
+
+ override :set_remember_me
+ def set_remember_me(user)
+ user.remember_me = params[:remember_me] if user.persisted?
+ end
+
+ override :fail_login
+ def fail_login(user)
+ flash[:alert] = 'Access denied for your LDAP account.'
+
+ redirect_to new_user_session_path
+ end
+end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 5e6676ea513..9137bc92810 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -4,18 +4,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
protect_from_forgery except: [:kerberos, :saml, :cas3]
- Gitlab.config.omniauth.providers.each do |provider|
- define_method provider['name'] do
- handle_omniauth
- end
+ def handle_omniauth
+ omniauth_flow(Gitlab::Auth::OAuth)
end
- if Gitlab::Auth::LDAP::Config.enabled?
- Gitlab::Auth::LDAP::Config.available_servers.each do |server|
- define_method server['provider_name'] do
- ldap
- end
- end
+ Gitlab.config.omniauth.providers.each do |provider|
+ alias_method provider['name'], :handle_omniauth
end
# Extend the standard implementation to also increment
@@ -37,51 +31,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
error ||= exception.error if exception.respond_to?(:error)
error ||= exception.message if exception.respond_to?(:message)
error ||= env["omniauth.error.type"].to_s
- error.to_s.humanize if error
- end
-
- # We only find ourselves here
- # if the authentication to LDAP was successful.
- def ldap
- ldap_user = Gitlab::Auth::LDAP::User.new(oauth)
- ldap_user.save if ldap_user.changed? # will also save new users
-
- @user = ldap_user.gl_user
- @user.remember_me = params[:remember_me] if ldap_user.persisted?
- # Do additional LDAP checks for the user filter and EE features
- if ldap_user.allowed?
- if @user.two_factor_enabled?
- prompt_for_two_factor(@user)
- else
- log_audit_event(@user, with: oauth['provider'])
- sign_in_and_redirect(@user)
- end
- else
- fail_ldap_login
- end
+ error.to_s.humanize if error
end
def saml
- if current_user
- log_audit_event(current_user, with: :saml)
- # Update SAML identity if data has changed.
- 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'
- else
- redirect_to after_sign_in_path_for(current_user)
- end
- else
- saml_user = Gitlab::Auth::Saml::User.new(oauth)
- saml_user.save if saml_user.changed?
- @user = saml_user.gl_user
-
- continue_login_process
- end
- rescue Gitlab::Auth::OAuth::User::SignupDisabledError
- handle_signup_error
+ omniauth_flow(Gitlab::Auth::Saml)
end
def omniauth_error
@@ -117,25 +72,36 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
private
- def handle_omniauth
+ def omniauth_flow(auth_module, identity_linker: nil)
if current_user
- # Add new authentication method
- 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
- oauth_user = Gitlab::Auth::OAuth::User.new(oauth)
- oauth_user.save
- @user = oauth_user.gl_user
- continue_login_process
+ identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth)
+
+ identity_linker.link
+
+ if identity_linker.changed?
+ redirect_identity_linked
+ elsif identity_linker.error_message.present?
+ redirect_identity_link_failed(identity_linker.error_message)
+ else
+ redirect_identity_exists
+ end
+ else
+ sign_in_user_flow(auth_module::User)
end
- rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
- handle_disabled_provider
- rescue Gitlab::Auth::OAuth::User::SignupDisabledError
- handle_signup_error
+ end
+
+ def redirect_identity_exists
+ redirect_to after_sign_in_path_for(current_user)
+ end
+
+ def redirect_identity_link_failed(error_message)
+ redirect_to profile_account_path, notice: "Authentication failed: #{error_message}"
+ end
+
+ def redirect_identity_linked
+ redirect_to profile_account_path, notice: 'Authentication method updated'
end
def handle_service_ticket(provider, ticket)
@@ -144,21 +110,27 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
session[:service_tickets][provider] = ticket
end
- def continue_login_process
- # Only allow properly saved users to login.
- if @user.persisted? && @user.valid?
- log_audit_event(@user, with: oauth['provider'])
+ def sign_in_user_flow(auth_user_class)
+ auth_user = auth_user_class.new(oauth)
+ user = auth_user.find_and_update!
+
+ if auth_user.valid_sign_in?
+ log_audit_event(user, with: oauth['provider'])
- if @user.two_factor_enabled?
- params[:remember_me] = '1' if remember_me?
- prompt_for_two_factor(@user)
+ set_remember_me(user)
+
+ if user.two_factor_enabled?
+ prompt_for_two_factor(user)
else
- remember_me(@user) if remember_me?
- sign_in_and_redirect(@user)
+ sign_in_and_redirect(user)
end
else
- fail_login
+ fail_login(user)
end
+ rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
+ handle_disabled_provider
+ rescue Gitlab::Auth::OAuth::User::SignupDisabledError
+ handle_signup_error
end
def handle_signup_error
@@ -178,18 +150,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
@oauth ||= request.env['omniauth.auth']
end
- def fail_login
- error_message = @user.errors.full_messages.to_sentence
+ def fail_login(user)
+ error_message = user.errors.full_messages.to_sentence
return redirect_to omniauth_error_path(oauth['provider'], error: error_message)
end
- def fail_ldap_login
- flash[:alert] = 'Access denied for your LDAP account.'
-
- redirect_to new_user_session_path
- end
-
def fail_auth0_login
flash[:alert] = 'Wrong extern UID provided. Make sure Auth0 is configured correctly.'
@@ -208,6 +174,16 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
.for_authentication.security_event
end
+ def set_remember_me(user)
+ return unless remember_me?
+
+ if user.two_factor_enabled?
+ params[:remember_me] = '1'
+ else
+ remember_me(user)
+ end
+ end
+
def remember_me?
request_params = request.env['omniauth.params']
(request_params['remember_me'] == '1') if request_params.present?
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 6d9b42a2c04..032bb2267e7 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -1,5 +1,6 @@
class Projects::ApplicationController < ApplicationController
include RoutableActions
+ include ChecksCollaboration
skip_before_action :authenticate_user!
before_action :project
@@ -31,14 +32,6 @@ class Projects::ApplicationController < ApplicationController
@repository ||= project.repository
end
- def can_collaborate_with_project?(project = nil, ref: nil)
- project ||= @project
-
- can?(current_user, :push_code, project) ||
- (current_user && current_user.already_forked?(project)) ||
- user_access(project).can_push_to_branch?(ref)
- end
-
def authorize_action!(action)
unless can?(current_user, action, project)
return access_denied!
@@ -91,9 +84,4 @@ class Projects::ApplicationController < ApplicationController
def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user)
end
-
- def user_access(project)
- @user_access ||= {}
- @user_access[project] ||= Gitlab::UserAccess.new(current_user, project: project)
- end
end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index effb484ef0f..b7f548e0e63 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -34,6 +34,7 @@ class Projects::CommitController < Projects::ApplicationController
def pipelines
@pipelines = @commit.pipelines.order(id: :desc)
+ @pipelines = @pipelines.where(ref: params[:ref]) if params[:ref]
respond_to do |format|
format.html
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index b14939c4216..d69015c8665 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -20,7 +20,7 @@ class Projects::IssuesController < Projects::ApplicationController
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]
+ before_action :authorize_create_merge_request_from!, only: [:create_merge_request]
respond_to :html
@@ -134,11 +134,11 @@ class Projects::IssuesController < Projects::ApplicationController
def can_create_branch
can_create = current_user &&
can?(current_user, :push_code, @project) &&
- @issue.can_be_worked_on?(current_user)
+ @issue.can_be_worked_on?
respond_to do |format|
format.json do
- render json: { can_create_branch: can_create, has_related_branch: @issue.has_related_branch? }
+ render json: { can_create_branch: can_create, suggested_branch_name: @issue.suggested_branch_name }
end
end
end
@@ -177,7 +177,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def authorize_create_merge_request!
- render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
+ render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?
end
def render_issue_json
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index a90030a8312..4a377fefc62 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -5,7 +5,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
skip_before_action :merge_request
before_action :whitelist_query_limiting, only: [:create]
- before_action :authorize_create_merge_request!
+ before_action :authorize_create_merge_request_from!
before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path]
before_action :build_merge_request, except: [:create]
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 54e7d81de6a..62b739918e6 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -60,13 +60,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
format.patch do
- return render_404 unless @merge_request.diff_refs
+ break render_404 unless @merge_request.diff_refs
send_git_patch @project.repository, @merge_request.diff_refs
end
format.diff do
- return render_404 unless @merge_request.diff_refs
+ break render_404 unless @merge_request.diff_refs
send_git_diff @project.repository, @merge_request.diff_refs
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index dd41b9648e8..86c50d88a2a 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -68,7 +68,7 @@ class Projects::NotesController < Projects::ApplicationController
private
def render_json_with_notes_serializer
- Notes::RenderService.new(current_user).execute([note], project)
+ Notes::RenderService.new(current_user).execute([note])
render json: note_serializer.represent(note)
end
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index 937b0e39cbd..d01f324e6fd 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -28,11 +28,12 @@ class Projects::RepositoriesController < Projects::ApplicationController
end
def assign_archive_vars
- @id = params[:id]
-
- return unless @id
-
- @ref, @filename = extract_ref(@id)
+ if params[:id]
+ @ref, @filename = extract_ref(params[:id])
+ else
+ @ref = params[:ref]
+ @filename = nil
+ end
rescue InvalidPathError
render_404
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 7c19aa7bb23..208a1d19862 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -5,6 +5,8 @@ class Projects::SnippetsController < Projects::ApplicationController
include SnippetsActions
include RendersBlob
+ skip_before_action :verify_authenticity_token, only: [:show], if: :js_request?
+
before_action :check_snippets_available!
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
@@ -71,6 +73,7 @@ class Projects::SnippetsController < Projects::ApplicationController
format.json do
render_blob_json(blob)
end
+ format.js { render 'shared/snippets/show'}
end
end
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index 517d0b026c2..bf09ea7e4d8 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -12,7 +12,7 @@ class Projects::VariablesController < Projects::ApplicationController
def update
if @project.update(variables_params)
respond_to do |format|
- format.json { return render_variables }
+ format.json { render_variables }
end
else
respond_to do |format|
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index c4930d3d18d..1b0751f48c5 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -29,8 +29,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]
+ @page = build_page(title: params[:id])
render 'edit'
end
@@ -54,7 +53,7 @@ class Projects::WikisController < Projects::ApplicationController
else
render 'edit'
end
- rescue WikiPage::PageChangedError, WikiPage::PageRenameError => e
+ rescue WikiPage::PageChangedError, WikiPage::PageRenameError, Gitlab::Git::Wiki::OperationError => e
@error = e
render 'edit'
end
@@ -70,6 +69,11 @@ class Projects::WikisController < Projects::ApplicationController
else
render action: "edit"
end
+ rescue Gitlab::Git::Wiki::OperationError => e
+ @page = build_page(wiki_params)
+ @error = e
+
+ render 'edit'
end
def history
@@ -94,6 +98,9 @@ class Projects::WikisController < Projects::ApplicationController
redirect_to project_wiki_path(@project, :home),
status: 302,
notice: "Page was successfully deleted"
+ rescue Gitlab::Git::Wiki::OperationError => e
+ @error = e
+ render 'edit'
end
def git_access
@@ -116,4 +123,10 @@ class Projects::WikisController < Projects::ApplicationController
def wiki_params
params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha)
end
+
+ def build_page(args)
+ WikiPage.new(@project_wiki).tap do |page|
+ page.update_attributes(args)
+ end
+ end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 37f14230196..a93b116c6fe 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -404,7 +404,7 @@ class ProjectsController < Projects::ApplicationController
params[:namespace_id] = project.namespace.to_param
params[:id] = project.to_param
- url_for(params)
+ url_for(safe_params)
end
def project_export_enabled
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index be2d3f638ff..3d51520ddf4 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -6,6 +6,8 @@ class SnippetsController < ApplicationController
include RendersBlob
include PreviewMarkdown
+ skip_before_action :verify_authenticity_token, only: [:show], if: :js_request?
+
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet
@@ -77,6 +79,8 @@ class SnippetsController < ApplicationController
format.json do
render_blob_json(blob)
end
+
+ format.js { render 'shared/snippets/show' }
end
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 956df4a0a16..31f47a7aa7c 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -146,6 +146,6 @@ class UsersController < ApplicationController
end
def build_canonical_path(user)
- url_for(params.merge(username: user.to_param))
+ url_for(safe_params.merge(username: user.to_param))
end
end
diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb
index e72fd8eb3a5..051ea108e06 100644
--- a/app/finders/group_descendants_finder.rb
+++ b/app/finders/group_descendants_finder.rb
@@ -134,10 +134,8 @@ class GroupDescendantsFinder
end
def direct_child_projects
- GroupProjectsFinder.new(group: parent_group,
- current_user: current_user,
- options: { only_owned: true },
- params: params).execute
+ GroupProjectsFinder.new(group: parent_group, current_user: current_user, params: params)
+ .execute
end
# Finds all projects nested under `parent_group` or any of its descendant
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 61c72aa22a8..7ed9b1fc6d0 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -159,7 +159,10 @@ class IssuableFinder
finder_options = { include_subgroups: params[:include_subgroups], only_owned: true }
GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute
else
- ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids(items)).execute
+ opts = { current_user: current_user }
+ opts[:project_ids_relation] = item_project_ids(items) if items
+
+ ProjectsFinder.new(opts).execute
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
@@ -316,9 +319,9 @@ class IssuableFinder
def by_project(items)
items =
if project?
- items.of_projects(projects(items)).references_project
- elsif projects(items)
- items.merge(projects(items).reorder(nil)).join_project
+ items.of_projects(projects).references_project
+ elsif projects
+ items.merge(projects.reorder(nil)).join_project
else
items.none
end
diff --git a/app/finders/merge_request_target_project_finder.rb b/app/finders/merge_request_target_project_finder.rb
index f358938344e..188ec447a94 100644
--- a/app/finders/merge_request_target_project_finder.rb
+++ b/app/finders/merge_request_target_project_finder.rb
@@ -12,6 +12,7 @@ class MergeRequestTargetProjectFinder
if @source_project.fork_network
@source_project.fork_network.projects
.public_or_visible_to_user(current_user)
+ .non_archived
.with_feature_available_for_user(:merge_requests, current_user)
else
Project.where(id: source_project)
diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb
index edde8022ec9..65824a51919 100644
--- a/app/finders/users_finder.rb
+++ b/app/finders/users_finder.rb
@@ -32,6 +32,7 @@ class UsersFinder
users = by_active(users)
users = by_external_identity(users)
users = by_external(users)
+ users = by_2fa(users)
users = by_created_at(users)
users = by_custom_attributes(users)
@@ -76,4 +77,15 @@ class UsersFinder
users.external
end
+
+ def by_2fa(users)
+ case params[:two_factor]
+ when 'enabled'
+ users.with_two_factor
+ when 'disabled'
+ users.without_two_factor
+ else
+ users
+ end
+ end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index b3b080e6dcf..3fbb32c5229 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -74,10 +74,12 @@ module ApplicationSettingsHelper
css_class = 'btn'
css_class << ' active' unless disabled
checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]'
+ name = Gitlab::Auth::OAuth::Provider.label_for(source)
label_tag(checkbox_name, class: css_class) do
check_box_tag(checkbox_name, source, !disabled,
- autocomplete: 'off') + Gitlab::Auth::OAuth::Provider.label_for(source)
+ autocomplete: 'off',
+ id: name.tr(' ', '_')) + name
end
end
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 2b440e4d584..fef29789832 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -59,7 +59,7 @@ module BlobHelper
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
elsif can_modify_blob?(blob, project, ref)
button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
- elsif can?(current_user, :fork_project, project)
+ elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project)
edit_fork_button_tag(common_classes, project, label, edit_modify_file_fork_params(action), action)
end
end
@@ -259,7 +259,7 @@ module BlobHelper
options = []
if error == :collapsed
- options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, expanded: true, format: nil)))
+ options << link_to('load it anyway', url_for(safe_params.merge(viewer: viewer.type, expanded: true, format: nil)))
end
# If the error is `:server_side_but_stored_externally`, the simple viewer will show the same error,
@@ -280,7 +280,7 @@ module BlobHelper
options << link_to("submit an issue", new_project_issue_path(project))
end
- merge_project = can?(current_user, :create_merge_request, project) ? project : (current_user && current_user.fork_of(project))
+ merge_project = merge_request_source_project_for_project(@project)
if merge_project
options << link_to("create a merge request", project_new_merge_request_path(project))
end
@@ -334,7 +334,7 @@ module BlobHelper
# Web IDE (Beta) requires the user to have this feature enabled
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
edit_link_tag(text, edit_path, common_classes)
- elsif current_user && can?(current_user, :fork_project, project)
+ elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project)
edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path))
end
end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 636316da80a..f0afcac5986 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -94,7 +94,7 @@ module CiStatusHelper
def render_project_pipeline_status(pipeline_status, tooltip_placement: 'auto left')
project = pipeline_status.project
- path = pipelines_project_commit_path(project, pipeline_status.sha)
+ path = pipelines_project_commit_path(project, pipeline_status.sha, ref: pipeline_status.ref)
render_status_with_link(
'commit',
@@ -105,7 +105,7 @@ module CiStatusHelper
def render_commit_status(commit, ref: nil, tooltip_placement: 'auto left')
project = commit.project
- path = pipelines_project_commit_path(project, commit)
+ path = pipelines_project_commit_path(project, commit, ref: ref)
render_status_with_link(
'commit',
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 7cc56de24e4..98894b86551 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -163,7 +163,7 @@ module CommitsHelper
tooltip = "#{action.capitalize} this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip
btn_class = "btn btn-#{btn_class}" unless btn_class.nil?
- if can_collaborate_with_project?
+ if can_collaborate_with_project?(@project)
link_to action.capitalize, "#modal-#{action}-commit", 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
elsif can?(current_user, :fork_project, @project)
continue_params = {
diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb
index 8bf96c0905f..2df5b5d1695 100644
--- a/app/helpers/compare_helper.rb
+++ b/app/helpers/compare_helper.rb
@@ -3,7 +3,7 @@ module CompareHelper
from.present? &&
to.present? &&
from != to &&
- can?(current_user, :create_merge_request, project) &&
+ can?(current_user, :create_merge_request_from, project) &&
project.repository.branch_exists?(from) &&
project.repository.branch_exists?(to)
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index b5ca39711bc..1bb82fd8150 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -180,7 +180,7 @@ module DiffHelper
private
def diff_btn(title, name, selected)
- params_copy = params.dup
+ params_copy = safe_params.dup
params_copy[:view] = name
# Always use HTML to handle case where JSON diff rendered this button
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 7f3c118c7ab..40073f714ee 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -81,6 +81,14 @@ module GitlabRoutingHelper
end
end
+ def edit_milestone_path(entity, *args)
+ if entity.parent.is_a?(Group)
+ edit_group_milestone_path(entity.parent, entity, *args)
+ else
+ edit_project_milestone_path(entity.parent, entity, *args)
+ end
+ end
+
def toggle_subscription_path(entity, *args)
if entity.is_a?(Issue)
toggle_subscription_project_issue_path(entity.project, entity)
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index c5522ff7a69..2f304b040c7 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -43,6 +43,10 @@ module IconsHelper
content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes)
end
+ def external_snippet_icon(name)
+ content_tag(:span, "", class: "gl-snippet-icon gl-snippet-icon-#{name}")
+ end
+
def audit_icon(names, options = {})
case names
when "standard"
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 06c3e569c84..f39a62bccc8 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -9,6 +9,32 @@ module IssuablesHelper
"right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}"
end
+ def sidebar_gutter_tooltip_text
+ sidebar_gutter_collapsed? ? _('Expand sidebar') : _('Collapse sidebar')
+ end
+
+ def sidebar_assignee_tooltip_label(issuable)
+ if issuable.assignee
+ issuable.assignee.name
+ else
+ issuable.allows_multiple_assignees? ? _('Assignee(s)') : _('Assignee')
+ end
+ end
+
+ def sidebar_due_date_tooltip_label(issuable)
+ if issuable.due_date
+ "#{_('Due date')}<br />#{due_date_remaining_days(issuable)}"
+ else
+ _('Due date')
+ end
+ end
+
+ def due_date_remaining_days(issuable)
+ remaining_days_in_words = remaining_days_in_words(issuable)
+
+ "#{issuable.due_date.to_s(:medium)} (#{remaining_days_in_words})"
+ end
+
def multi_label_name(current_labels, default_label)
if current_labels && current_labels.any?
title = current_labels.first.try(:title)
@@ -153,10 +179,14 @@ module IssuablesHelper
def issuable_labels_tooltip(labels, limit: 5)
first, last = labels.partition.with_index { |_, i| i < limit }
- label_names = first.collect(&:name)
- label_names << "and #{last.size} more" unless last.empty?
+ if labels && labels.any?
+ label_names = first.collect(&:name)
+ label_names << "and #{last.size} more" unless last.empty?
- label_names.join(', ')
+ label_names.join(', ')
+ else
+ _("Labels")
+ end
end
def issuables_state_counter_text(issuable_type, state, display_count)
@@ -321,7 +351,7 @@ module IssuablesHelper
def issuable_todo_button_data(issuable, todo, is_collapsed)
{
todo_text: "Add todo",
- mark_text: "Mark done",
+ mark_text: "Mark todo as done",
todo_icon: (is_collapsed ? icon('plus-square') : nil),
mark_icon: (is_collapsed ? icon('check-square', class: 'todo-undone') : nil),
issuable_id: issuable.id,
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 0f25d401406..96dc7ae1185 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -82,8 +82,8 @@ module IssuesHelper
names.to_sentence
end
- def award_state_class(awards, current_user)
- if !current_user
+ def award_state_class(awardable, awards, current_user)
+ if !can?(current_user, :award_emoji, awardable)
"disabled"
elsif current_user && awards.find { |a| a.user_id == current_user.id }
"active"
@@ -126,6 +126,17 @@ module IssuesHelper
link_to link_text, path
end
+ def show_new_issue_link?(project)
+ return false unless project
+ return false if project.archived?
+
+ # We want to show the link to users that are not signed in, that way they
+ # get directed to the sign-in/sign-up flow and afterwards to the new issue page.
+ return true unless current_user
+
+ can?(current_user, :create_issue, project)
+ end
+
# Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue
module_function :url_for_internal_issue
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 2fe1927a189..39e7a7fd396 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -256,7 +256,7 @@ module MarkupHelper
return '' unless html.present?
context.merge!(
- current_user: (current_user if defined?(current_user)),
+ current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter
commit: @commit,
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index fb4fe1c40b7..c19c5b9cc82 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -138,6 +138,18 @@ module MergeRequestsHelper
end
end
+ def merge_request_source_project_for_project(project = @project)
+ unless can?(current_user, :create_merge_request_in, project)
+ return nil
+ end
+
+ if can?(current_user, :create_merge_request_from, project)
+ project
+ else
+ current_user.fork_of(project)
+ end
+ end
+
def merge_params_ee(merge_request)
{}
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index be8cb358de2..e8caab3e50c 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -1,4 +1,6 @@
module MilestonesHelper
+ include EntityDateHelper
+
def milestones_filter_path(opts = {})
if @project
project_milestones_path(@project, opts)
@@ -72,6 +74,19 @@ module MilestonesHelper
end
end
+ def milestone_progress_tooltip_text(milestone)
+ has_issues = milestone.total_issues_count(current_user) > 0
+
+ if has_issues
+ [
+ _('Progress'),
+ _("%{percent}%% complete") % { percent: milestone.percent_complete(current_user) }
+ ].join('<br />')
+ else
+ _('Progress')
+ end
+ end
+
def milestone_progress_bar(milestone)
options = {
class: 'progress-bar progress-bar-success',
@@ -95,27 +110,69 @@ module MilestonesHelper
end
def milestone_tooltip_title(milestone)
- if milestone.due_date
- [milestone.due_date.to_s(:medium), "(#{milestone_remaining_days(milestone)})"].join(' ')
+ if milestone
+ "#{milestone.title}<br />#{milestone_tooltip_due_date(milestone)}"
+ else
+ _('Milestone')
end
end
- def milestone_remaining_days(milestone)
- if milestone.expired?
- content_tag(:strong, 'Past due')
- elsif milestone.upcoming?
- content_tag(:strong, 'Upcoming')
- elsif milestone.due_date
- time_ago = time_ago_in_words(milestone.due_date)
- content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" }
- content.slice!("about ")
- content << " remaining"
- content.html_safe
- elsif milestone.start_date && milestone.start_date.past?
- days = milestone.elapsed_days
- content = content_tag(:strong, days)
- content << " #{'day'.pluralize(days)} elapsed"
+ def milestone_time_for(date, date_type)
+ title = date_type == :start ? "Start date" : "End date"
+
+ if date
+ time_ago = time_ago_in_words(date)
+ time_ago.slice!("about ")
+
+ time_ago << if date.past?
+ " ago"
+ else
+ " remaining"
+ end
+
+ content = [
+ title,
+ "<br />",
+ date.to_s(:medium),
+ "(#{time_ago})"
+ ].join(" ")
+
content.html_safe
+ else
+ title
+ end
+ end
+
+ def milestone_issues_tooltip_text(milestone)
+ issues = milestone.count_issues_by_state(current_user)
+
+ return _("Issues") if issues.empty?
+
+ content = []
+
+ content << n_("1 open issue", "%d open issues", issues["opened"]) % issues["opened"] if issues["opened"]
+ content << n_("1 closed issue", "%d closed issues", issues["closed"]) % issues["closed"] if issues["closed"]
+
+ content.join('<br />').html_safe
+ end
+
+ def milestone_merge_requests_tooltip_text(milestone)
+ merge_requests = milestone.merge_requests
+
+ return _("Merge requests") if merge_requests.empty?
+
+ content = []
+
+ content << n_("1 open merge request", "%d open merge requests", merge_requests.opened.count) % merge_requests.opened.count if merge_requests.opened.any?
+ content << n_("1 closed merge request", "%d closed merge requests", merge_requests.closed.count) % merge_requests.closed.count if merge_requests.closed.any?
+ content << n_("1 merged merge request", "%d merged merge requests", merge_requests.merged.count) % merge_requests.merged.count if merge_requests.merged.any?
+
+ content.join('<br />').html_safe
+ end
+
+ def milestone_tooltip_due_date(milestone)
+ if milestone.due_date
+ "#{milestone.due_date.to_s(:medium)} (#{remaining_days_in_words(milestone)})"
end
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 56c88e6eab0..7754c34d6f0 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -28,7 +28,7 @@ module NavHelper
end
elsif current_path?('jobs#show')
%w[page-gutter build-sidebar right-sidebar-expanded]
- elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access')
+ elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access', 'destroy')
%w[page-gutter wiki-sidebar right-sidebar-expanded]
else
[]
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 27ed48fdbc7..7f67574a428 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -6,10 +6,6 @@ module NotesHelper
end
end
- def note_editable?(note)
- Ability.can_edit_note?(current_user, note)
- end
-
def note_supports_quick_actions?(note)
Notes::QuickActionsService.supported?(note)
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 15f48e43a28..801e624e1de 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -157,40 +157,6 @@ module ProjectsHelper
current_user&.recent_push(@project)
end
- def project_feature_access_select(field)
- # Don't show option "everyone with access" if project is private
- options = project_feature_options
-
- level = @project.project_feature.public_send(field) # rubocop:disable GitlabSecurity/PublicSend
-
- if @project.private?
- disabled_option = ProjectFeature::ENABLED
- highest_available_option = ProjectFeature::PRIVATE if level == disabled_option
- end
-
- options = options_for_select(
- options.invert,
- selected: highest_available_option || level,
- disabled: disabled_option
- )
-
- content_tag :div, class: "select-wrapper" do
- concat(
- content_tag(
- :select,
- options,
- name: "project[project_feature_attributes][#{field}]",
- id: "project_project_feature_attributes_#{field}",
- class: "pull-right form-control select-control #{repo_children_classes(field)} ",
- data: { field: field }
- )
- )
- concat(
- icon('chevron-down')
- )
- end.html_safe
- end
-
def link_to_autodeploy_doc
link_to _('About auto deploy'), help_page_path('ci/autodeploy/index'), target: '_blank'
end
@@ -274,16 +240,6 @@ module ProjectsHelper
private
- def repo_children_classes(field)
- needs_repo_check = [:merge_requests_access_level, :builds_access_level]
- return unless needs_repo_check.include?(field)
-
- classes = "project-repo-select js-repo-select"
- classes << " disabled" unless @project.feature_available?(:repository, current_user)
-
- classes
- end
-
def get_project_nav_tabs(project, current_user)
nav_tabs = [:home]
@@ -444,15 +400,8 @@ module ProjectsHelper
exports_path = File.join(Settings.shared['path'], 'tmp/project_exports')
filtered_message = message.strip.gsub(exports_path, "[REPO EXPORT PATH]")
- filtered_message.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
- end
-
- def project_feature_options
- {
- ProjectFeature::DISABLED => s_('ProjectFeature|Disabled'),
- ProjectFeature::PRIVATE => s_('ProjectFeature|Only team members'),
- ProjectFeature::ENABLED => s_('ProjectFeature|Everyone with access')
- }
+ disk_path = Gitlab.config.repositories.storages[project.repository_storage].legacy_disk_path
+ filtered_message.gsub(disk_path.chomp('/'), "[REPOS PATH]")
end
def project_child_container_class(view_path)
@@ -463,20 +412,6 @@ module ProjectsHelper
IssuesFinder.new(current_user, project_id: project.id).execute
end
- def visibility_select_options(project, selected_level)
- level_options = Gitlab::VisibilityLevel.values.each_with_object([]) do |level, level_options|
- next if restricted_levels.include?(level)
-
- level_options << [
- visibility_level_label(level),
- { data: { description: visibility_level_description(level, project) } },
- level
- ]
- end
-
- options_for_select(level_options, selected_level)
- end
-
def restricted_levels
return [] if current_user.admin?
diff --git a/app/helpers/safe_params_helper.rb b/app/helpers/safe_params_helper.rb
new file mode 100644
index 00000000000..b568e8810cc
--- /dev/null
+++ b/app/helpers/safe_params_helper.rb
@@ -0,0 +1,11 @@
+module SafeParamsHelper
+ # Rails 5.0 requires to permit `params` if they're used in url helpers.
+ # Use this helper when generating links with `params.merge(...)`
+ def safe_params
+ if params.respond_to?(:permit!)
+ params.except(:host, :port, :protocol).permit!
+ else
+ params
+ end
+ end
+end
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 00e7e4230b9..733832c1bbb 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -101,4 +101,39 @@ module SnippetsHelper
# Return snippet with chunk array
{ snippet_object: snippet, snippet_chunks: snippet_chunks }
end
+
+ def snippet_embed
+ "<script src=\"#{url_for(only_path: false, overwrite_params: nil)}.js\"></script>"
+ end
+
+ def embedded_snippet_raw_button
+ blob = @snippet.blob
+ return if blob.empty? || blob.raw_binary? || blob.stored_externally?
+
+ snippet_raw_url = if @snippet.is_a?(PersonalSnippet)
+ raw_snippet_url(@snippet)
+ else
+ raw_project_snippet_url(@snippet.project, @snippet)
+ end
+
+ link_to external_snippet_icon('doc_code'), snippet_raw_url, class: 'btn', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw'
+ end
+
+ def embedded_snippet_download_button
+ download_url = if @snippet.is_a?(PersonalSnippet)
+ raw_snippet_url(@snippet, inline: false)
+ else
+ raw_project_snippet_url(@snippet.project, @snippet, inline: false)
+ end
+
+ link_to external_snippet_icon('download'), download_url, class: 'btn', target: '_blank', title: 'Download', rel: 'noopener noreferrer'
+ end
+
+ def public_snippet?
+ if @snippet.project_id?
+ can?(nil, :read_project_snippet, @snippet)
+ else
+ can?(nil, :read_personal_snippet, @snippet)
+ end
+ end
end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 5e7c20ef51e..dc42caa70e5 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -90,7 +90,7 @@ module TreeHelper
end
def commit_in_single_accessible_branch
- branch_name = html_escape(selected_branch)
+ branch_name = ERB::Util.html_escape(selected_branch)
message = _("Your changes can be committed to %{branch_name} because a merge "\
"request is open.") % { branch_name: "<strong>#{branch_name}</strong>" }
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index b33131becd3..392cc0bee03 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -6,6 +6,12 @@ module Emails
mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason))
end
+ def issue_due_email(recipient_id, issue_id, reason = nil)
+ setup_issue_mail(issue_id, recipient_id)
+
+ mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason))
+ end
+
def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 6dae49f38dc..618d4af4272 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -46,10 +46,6 @@ class Ability
end
end
- def can_edit_note?(user, note)
- allowed?(user, :edit_note, note)
- end
-
def allowed?(user, action, subject = :global, opts = {})
if subject.is_a?(Hash)
opts, subject = subject, :global
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index 0b561203914..4aa236555cb 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -19,7 +19,7 @@ class BroadcastMessage < ActiveRecord::Base
after_commit :flush_redis_cache
def self.current
- messages = Rails.cache.fetch(CACHE_KEY) { current_and_future_messages.to_a }
+ messages = Rails.cache.fetch(CACHE_KEY, expires_in: cache_expires_in) { current_and_future_messages.to_a }
return messages if messages.empty?
@@ -36,6 +36,10 @@ class BroadcastMessage < ActiveRecord::Base
where('ends_at > :now', now: Time.zone.now).order_id_asc
end
+ def self.cache_expires_in
+ nil
+ end
+
def active?
started? && !ended?
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 4aa65bf4273..9000ad860e9 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -20,13 +20,14 @@ module Ci
has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment'
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
- has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent
has_one :job_artifacts_archive, -> { where(file_type: Ci::JobArtifact.file_types[:archive]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :metadata, class_name: 'Ci::BuildMetadata'
delegate :timeout, to: :metadata, prefix: true, allow_nil: true
+ delegate :gitlab_deploy_token, to: :project
##
# The "environment" field for builds is a String, and is the unexpanded name!
@@ -95,8 +96,8 @@ module Ci
run_after_commit { BuildHooksWorker.perform_async(build.id) }
end
- after_commit :update_project_statistics_after_save, on: [:create, :update]
- after_commit :update_project_statistics, on: :destroy
+ after_save :update_project_statistics_after_save, if: :artifacts_size_changed?
+ after_destroy :update_project_statistics_after_destroy, unless: :project_destroyed?
class << self
# This is needed for url_for to work,
@@ -162,7 +163,7 @@ module Ci
build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies')
end
- before_transition pending: :running do |build|
+ after_transition pending: :running do |build|
build.ensure_metadata.update_timeout_state
end
end
@@ -479,7 +480,7 @@ module Ci
def user_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- return variables if user.blank?
+ break variables if user.blank?
variables.append(key: 'GITLAB_USER_ID', value: user.id.to_s)
variables.append(key: 'GITLAB_USER_EMAIL', value: user.email)
@@ -594,7 +595,7 @@ module Ci
def persisted_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- return variables unless persisted?
+ break variables unless persisted?
variables
.append(key: 'CI_JOB_ID', value: id.to_s)
@@ -604,6 +605,7 @@ module Ci
.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
+ .concat(deploy_token_variables)
end
end
@@ -611,7 +613,7 @@ module Ci
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI', value: 'true')
variables.append(key: 'GITLAB_CI', value: 'true')
- variables.append(key: 'GITLAB_FEATURES', value: project.namespace.features.join(','))
+ variables.append(key: 'GITLAB_FEATURES', value: project.licensed_features.join(','))
variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION)
@@ -643,7 +645,7 @@ module Ci
def persisted_environment_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- return variables unless persisted? && persisted_environment.present?
+ break variables unless persisted? && persisted_environment.present?
variables.concat(persisted_environment.predefined_variables)
@@ -654,6 +656,15 @@ module Ci
end
end
+ def deploy_token_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ break variables unless gitlab_deploy_token
+
+ variables.append(key: 'CI_DEPLOY_USER', value: gitlab_deploy_token.name)
+ variables.append(key: 'CI_DEPLOY_PASSWORD', value: gitlab_deploy_token.token, public: false)
+ end
+ end
+
def environment_url
options&.dig(:environment, :url) || persisted_environment&.external_url
end
@@ -664,16 +675,20 @@ module Ci
pipeline.config_processor.build_attributes(name)
end
- def update_project_statistics
- return unless project
+ def update_project_statistics_after_save
+ update_project_statistics(read_attribute(:artifacts_size).to_i - artifacts_size_was.to_i)
+ end
- ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size])
+ def update_project_statistics_after_destroy
+ update_project_statistics(-artifacts_size)
end
- def update_project_statistics_after_save
- if previous_changes.include?('artifacts_size')
- update_project_statistics
- end
+ def update_project_statistics(difference)
+ ProjectStatistics.increment_statistic(project_id, :build_artifacts_size, difference)
+ end
+
+ def project_destroyed?
+ project.pending_delete?
end
end
end
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
index 62d768cc6cf..44cb583e1bd 100644
--- a/app/models/ci/group_variable.rb
+++ b/app/models/ci/group_variable.rb
@@ -4,7 +4,7 @@ module Ci
include HasVariable
include Presentable
- belongs_to :group
+ belongs_to :group, class_name: "::Group"
alias_attribute :secret_value, :value
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index fbb95fe16df..39676efa08c 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -7,12 +7,15 @@ module Ci
belongs_to :project
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
- before_save :update_file_store
+ mount_uploader :file, JobArtifactUploader
+
before_save :set_size, if: :file_changed?
+ after_save :update_project_statistics_after_save, if: :size_changed?
+ after_destroy :update_project_statistics_after_destroy, unless: :project_destroyed?
- scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
+ after_save :update_file_store
- mount_uploader :file, JobArtifactUploader
+ scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
delegate :exists?, :open, to: :file
@@ -23,7 +26,9 @@ module Ci
}
def update_file_store
- self.file_store = file.object_store
+ # The file.object_store is set during `uploader.store!`
+ # which happens after object is inserted/updated
+ self.update_column(:file_store, file.object_store)
end
def self.artifacts_size_for(project)
@@ -34,10 +39,6 @@ module Ci
[nil, ::JobArtifactUploader::Store::LOCAL].include?(self.file_store)
end
- def set_size
- self.size = file.size
- end
-
def expire_in
expire_at - Time.now if expire_at
end
@@ -48,5 +49,28 @@ module Ci
ChronicDuration.parse(value)&.seconds&.from_now
end
end
+
+ private
+
+ def set_size
+ self.size = file.size
+ end
+
+ def update_project_statistics_after_save
+ update_project_statistics(size.to_i - size_was.to_i)
+ end
+
+ def update_project_statistics_after_destroy
+ update_project_statistics(-self.size)
+ end
+
+ def update_project_statistics(difference)
+ ProjectStatistics.increment_statistic(project_id, :build_artifacts_size, difference)
+ end
+
+ def project_destroyed?
+ # Use job.project to avoid extra DB query for project
+ job.project.pending_delete?
+ end
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index ee0d8df8eb7..5a4c56ec0dc 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -13,7 +13,7 @@ module Ci
has_many :builds
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :projects, -> { auto_include(false) }, through: :runner_projects
+ has_many :projects, through: :runner_projects
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index e4a06f3f976..77947d515c1 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -15,7 +15,7 @@ module Clusters
belongs_to :user
has_many :cluster_projects, class_name: 'Clusters::Project'
- has_many :projects, -> { auto_include(false) }, through: :cluster_projects, class_name: '::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
diff --git a/app/models/commit.rb b/app/models/commit.rb
index de860df4b9c..9750e9298ec 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -248,7 +248,7 @@ class Commit
end
def notes_with_associations
- notes.includes(:author)
+ notes.includes(:author, :award_emoji)
end
def merge_requests
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 3469d5d795c..b6276c2fb50 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -87,7 +87,7 @@ class CommitStatus < ActiveRecord::Base
transition [:created, :pending, :running, :manual] => :canceled
end
- before_transition created: [:pending, :running] do |commit_status|
+ before_transition [:created, :skipped, :manual] => :pending do |commit_status|
commit_status.queued_at = Time.now
end
diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb
index 4b66725a3e6..22f516a172f 100644
--- a/app/models/concerns/atomic_internal_id.rb
+++ b/app/models/concerns/atomic_internal_id.rb
@@ -27,8 +27,9 @@ module AtomicInternalId
module ClassMethods
def has_internal_id(column, scope:, init:) # rubocop:disable Naming/PredicateName
before_validation(on: :create) do
- if read_attribute(column).blank?
- scope_attrs = { scope => association(scope).reader }
+ scope_value = association(scope).reader
+ if read_attribute(column).blank? && scope_value
+ scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value }
usage = self.class.table_name.to_sym
new_iid = InternalId.generate_next(self, scope_attrs, usage, init)
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 7677891b9ce..13246a774e3 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -31,12 +31,13 @@ module Avatarable
asset_host = ActionController::Base.asset_host
use_asset_host = asset_host.present?
+ use_authentication = respond_to?(:public?) && !public?
# 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?
+ if use_asset_host && use_authentication
use_asset_host = false
only_path = false
end
@@ -49,6 +50,6 @@ module Avatarable
url_base << gitlab_config.relative_url_root
end
- url_base + avatar.url
+ url_base + avatar.local_url
end
end
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index d8394415362..fce37e7f78e 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -79,11 +79,7 @@ module Awardable
end
def user_can_award?(current_user, name)
- if user_authored?(current_user)
- !awardable_votes?(normalize_name(name))
- else
- true
- end
+ awardable_by_user?(current_user, name) && Ability.allowed?(current_user, :award_emoji, self)
end
def user_authored?(current_user)
@@ -119,4 +115,12 @@ module Awardable
def normalize_name(name)
Gitlab::Emoji.normalize_emoji_name(name)
end
+
+ def awardable_by_user?(current_user, name)
+ if user_authored?(current_user)
+ !awardable_votes?(normalize_name(name))
+ else
+ true
+ end
+ end
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 4ae5dd8c677..db8cf322ef7 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -11,7 +11,9 @@ module CacheMarkdownField
extend ActiveSupport::Concern
# Increment this number every time the renderer changes its output
- CACHE_VERSION = 3
+ CACHE_REDCARPET_VERSION = 3
+ CACHE_COMMONMARK_VERSION_START = 10
+ CACHE_COMMONMARK_VERSION = 11
# changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze
@@ -49,12 +51,14 @@ module CacheMarkdownField
# Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project)
- group = self.group if self.respond_to?(:group)
+ 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)
+ context[:markdown_engine] = markdown_engine
+
context
end
@@ -69,7 +73,7 @@ module CacheMarkdownField
Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
]
end.to_h
- updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION
+ updates['cached_markdown_version'] = latest_cached_markdown_version
updates.each {|html_field, data| write_attribute(html_field, data) }
end
@@ -90,7 +94,7 @@ module CacheMarkdownField
markdown_changed = attribute_changed?(markdown_field) || false
html_changed = attribute_changed?(html_field) || false
- CacheMarkdownField::CACHE_VERSION == cached_markdown_version &&
+ latest_cached_markdown_version == cached_markdown_version &&
(html_changed || markdown_changed == html_changed)
end
@@ -109,6 +113,24 @@ module CacheMarkdownField
__send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend
end
+ def latest_cached_markdown_version
+ return CacheMarkdownField::CACHE_REDCARPET_VERSION unless cached_markdown_version
+
+ if cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
+ CacheMarkdownField::CACHE_REDCARPET_VERSION
+ else
+ CacheMarkdownField::CACHE_COMMONMARK_VERSION
+ end
+ end
+
+ def markdown_engine
+ if latest_cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
+ :redcarpet
+ else
+ :common_mark
+ end
+ end
+
included do
cattr_reader :cached_markdown_fields do
FieldData.new
diff --git a/app/models/concerns/group_descendant.rb b/app/models/concerns/group_descendant.rb
index 01957da0bf3..261ace57a17 100644
--- a/app/models/concerns/group_descendant.rb
+++ b/app/models/concerns/group_descendant.rb
@@ -37,7 +37,20 @@ module GroupDescendant
parent ||= preloaded.detect { |possible_parent| possible_parent.is_a?(Group) && possible_parent.id == child.parent_id }
if parent.nil? && !child.parent_id.nil?
- raise ArgumentError.new('parent was not preloaded')
+ parent = child.parent
+
+ exception = ArgumentError.new <<~MSG
+ parent: [GroupDescendant: #{parent.inspect}] was not preloaded for [#{child.inspect}]")
+ This error is not user facing, but causes a +1 query.
+ MSG
+ extras = {
+ parent: parent,
+ child: child,
+ preloaded: preloaded.map(&:full_path)
+ }
+ issue_url = 'https://gitlab.com/gitlab-org/gitlab-ce/issues/40785'
+
+ Gitlab::Sentry.track_exception(exception, issue_url: issue_url, extra: extras)
end
if parent.nil? && hierarchy_top.present?
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index d9416352f9c..b45395343cc 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -48,7 +48,7 @@ module Issuable
end
has_many :label_links, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :labels, -> { auto_include(false) }, through: :label_links
+ has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :metrics
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 5130ecec472..967fd9c5eea 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -102,14 +102,14 @@ module Milestoneish
Gitlab::TimeTrackingFormatter.output(total_issue_time_estimate)
end
- private
-
def count_issues_by_state(user)
memoize_per_user(user, :count_issues_by_state) do
issues_visible_to_user(user).reorder(nil).group(:state).count
end
end
+ private
+
def memoize_per_user(user, method_name)
memoized_users[method_name][user&.id] ||= yield
end
diff --git a/app/models/concerns/nonatomic_internal_id.rb b/app/models/concerns/nonatomic_internal_id.rb
deleted file mode 100644
index 9d0c9b8512f..00000000000
--- a/app/models/concerns/nonatomic_internal_id.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-module NonatomicInternalId
- extend ActiveSupport::Concern
-
- included do
- validate :set_iid, on: :create
- validates :iid, presence: true, numericality: true
- end
-
- def set_iid
- if iid.blank?
- parent = project || group
- records = parent.public_send(self.class.name.tableize) # rubocop:disable GitlabSecurity/PublicSend
- max_iid = records.maximum(:iid)
-
- self.iid = max_iid.to_i + 1
- end
- end
-
- def to_param
- iid.to_s
- end
-end
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index 454374121f3..94eef4ff7cd 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -31,7 +31,7 @@ module ProtectedRef
end
end
- def protected_ref_accessible_to?(ref, user, action:, protected_refs: nil)
+ def protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level|
access_level.check_access(user)
end
diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb
index 399abb67c9d..7c236369793 100644
--- a/app/models/concerns/resolvable_discussion.rb
+++ b/app/models/concerns/resolvable_discussion.rb
@@ -102,7 +102,7 @@ module ResolvableDiscussion
yield(notes_relation)
# Set the notes array to the updated notes
- @notes = notes_relation.fresh.auto_include(false).to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @notes = notes_relation.fresh.to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables
self.class.memoized_values.each do |name|
clear_memoization(name)
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index dfd7d94450b..915ad6959be 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -102,7 +102,7 @@ module Routable
# the route. Caching this per request ensures that even if we have multiple instances,
# we will not have to duplicate work, avoiding N+1 queries in some cases.
def full_path
- return uncached_full_path unless RequestStore.active?
+ return uncached_full_path unless RequestStore.active? && persisted?
RequestStore[full_path_key] ||= uncached_full_path
end
@@ -124,6 +124,11 @@ module Routable
end
end
+ # Group would override this to check from association
+ def owned_by?(user)
+ owner == user
+ end
+
private
def set_path_errors
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index f05e606995d..f66bdd529f1 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -45,25 +45,25 @@ module Storage
# Hooks
- # Save the storage paths before the projects are destroyed to use them on after destroy
+ # Save the storages before the projects are destroyed to use them on after destroy
def prepare_for_destroy
- old_repository_storage_paths
+ old_repository_storages
end
private
def move_repositories
- # Move the namespace directory in all storage paths used by member projects
- repository_storage_paths.each do |repository_storage_path|
+ # Move the namespace directory in all storages used by member projects
+ repository_storages.each do |repository_storage|
# Ensure old directory exists before moving it
- gitlab_shell.add_namespace(repository_storage_path, full_path_was)
+ gitlab_shell.add_namespace(repository_storage, full_path_was)
# Ensure new directory exists before moving it (if there's a parent)
- gitlab_shell.add_namespace(repository_storage_path, parent.full_path) if parent
+ gitlab_shell.add_namespace(repository_storage, parent.full_path) if parent
- unless gitlab_shell.mv_namespace(repository_storage_path, full_path_was, full_path)
+ unless gitlab_shell.mv_namespace(repository_storage, full_path_was, full_path)
- Rails.logger.error "Exception moving path #{repository_storage_path} from #{full_path_was} to #{full_path}"
+ Rails.logger.error "Exception moving path #{repository_storage} from #{full_path_was} to #{full_path}"
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
@@ -72,33 +72,33 @@ module Storage
end
end
- def old_repository_storage_paths
- @old_repository_storage_paths ||= repository_storage_paths
+ def old_repository_storages
+ @old_repository_storage_paths ||= repository_storages
end
- def repository_storage_paths
+ def repository_storages
# We need to get the storage paths for all the projects, even the ones that are
# pending delete. Unscoping also get rids of the default order, which causes
# problems with SELECT DISTINCT.
Project.unscoped do
- all_projects.select('distinct(repository_storage)').to_a.map(&:repository_storage_path)
+ all_projects.select('distinct(repository_storage)').to_a.map(&:repository_storage)
end
end
def rm_dir
# Remove the namespace directory in all storages paths used by member projects
- old_repository_storage_paths.each do |repository_storage_path|
+ old_repository_storages.each do |repository_storage|
# Move namespace directory into trash.
# We will remove it later async
new_path = "#{full_path}+#{id}+deleted"
- if gitlab_shell.mv_namespace(repository_storage_path, full_path, new_path)
+ if gitlab_shell.mv_namespace(repository_storage, full_path, new_path)
Gitlab::AppLogger.info %Q(Namespace directory "#{full_path}" moved to "#{new_path}")
# Remove namespace directroy async with delay so
# GitLab has time to remove all projects first
run_after_commit do
- GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path)
+ GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage, new_path)
end
end
end
diff --git a/app/models/concerns/uniquify.rb b/app/models/concerns/uniquify.rb
index a7fe5951b6e..549a76da20e 100644
--- a/app/models/concerns/uniquify.rb
+++ b/app/models/concerns/uniquify.rb
@@ -1,13 +1,21 @@
+# Uniquify
+#
+# Return a version of the given 'base' string that is unique
+# by appending a counter to it. Uniqueness is determined by
+# repeated calls to the passed block.
+#
+# You can pass an initial value for the counter, if not given
+# counting starts from 1.
+#
+# If `base` is a function/proc, we expect that calling it with a
+# candidate counter returns a string to test/return.
class Uniquify
- # Return a version of the given 'base' string that is unique
- # by appending a counter to it. Uniqueness is determined by
- # repeated calls to the passed block.
- #
- # If `base` is a function/proc, we expect that calling it with a
- # candidate counter returns a string to test/return.
+ def initialize(counter = nil)
+ @counter = counter
+ end
+
def string(base)
@base = base
- @counter = nil
increment_counter! while yield(base_string)
base_string
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index 858b7ef533e..89a74b7dcb1 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -2,7 +2,7 @@ class DeployKey < Key
include IgnorableColumn
has_many :deploy_keys_projects, inverse_of: :deploy_key, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :projects, -> { auto_include(false) }, through: :deploy_keys_projects
+ has_many :projects, through: :deploy_keys_projects
scope :in_projects, ->(projects) { joins(:deploy_keys_projects).where('deploy_keys_projects.project_id in (?)', projects) }
scope :are_public, -> { where(public: true) }
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index 8dae821a10e..5082dc45368 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -4,11 +4,12 @@ class DeployToken < ActiveRecord::Base
add_authentication_token_field :token
AVAILABLE_SCOPES = %i(read_repository read_registry).freeze
+ GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'.freeze
default_value_for(:expires_at) { Forever.date }
has_many :project_deploy_tokens, inverse_of: :deploy_token
- has_many :projects, -> { auto_include(false) }, through: :project_deploy_tokens
+ has_many :projects, through: :project_deploy_tokens
validate :ensure_at_least_one_scope
before_save :ensure_token
@@ -17,6 +18,10 @@ class DeployToken < ActiveRecord::Base
scope :active, -> { where("revoked = false AND expires_at >= NOW()") }
+ def self.gitlab_deploy_token
+ active.find_by(name: GITLAB_DEPLOY_TOKEN_NAME)
+ end
+
def revoke!
update!(revoked: true)
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index e18ea8bfea4..254764eefde 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -1,11 +1,13 @@
class Deployment < ActiveRecord::Base
- include NonatomicInternalId
+ include AtomicInternalId
belongs_to :project, required: true
belongs_to :environment, required: true
belongs_to :user
belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
+ has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.deployments&.maximum(:iid) }
+
validates :sha, presence: true
validates :ref, presence: true
diff --git a/app/models/event.rb b/app/models/event.rb
index 3805f6cf857..741a84194e2 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -110,7 +110,10 @@ class Event < ActiveRecord::Base
end
end
+ # Remove this method when removing Gitlab.rails5? code.
def subclass_from_attributes(attrs)
+ return super if Gitlab.rails5?
+
# Without this Rails will keep calling this method on the returned class,
# resulting in an infinite loop.
return unless self == Event
diff --git a/app/models/fork_network.rb b/app/models/fork_network.rb
index aad3509b895..7f1728e8c77 100644
--- a/app/models/fork_network.rb
+++ b/app/models/fork_network.rb
@@ -1,7 +1,7 @@
class ForkNetwork < ActiveRecord::Base
belongs_to :root_project, class_name: 'Project'
has_many :fork_network_members
- has_many :projects, -> { auto_include(false) }, through: :fork_network_members
+ has_many :projects, through: :fork_network_members
after_create :add_root_as_member, if: :root_project
diff --git a/app/models/group.rb b/app/models/group.rb
index 202988d743d..9b42bbf99be 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -12,9 +12,9 @@ class Group < Namespace
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
- has_many :users, -> { auto_include(false) }, through: :group_members
+ has_many :users, through: :group_members
has_many :owners,
- -> { where(members: { access_level: Gitlab::Access::OWNER }).auto_include(false) },
+ -> { where(members: { access_level: Gitlab::Access::OWNER }) },
through: :group_members,
source: :user
@@ -23,7 +23,7 @@ class Group < Namespace
has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :shared_projects, -> { auto_include(false) }, through: :project_group_links, source: :project
+ has_many :shared_projects, through: :project_group_links, source: :project
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'
@@ -125,6 +125,10 @@ class Group < Namespace
self[:lfs_enabled]
end
+ def owned_by?(user)
+ owners.include?(user)
+ end
+
def add_users(users, access_level, current_user: nil, expires_at: nil)
GroupMember.add_users(
self,
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index cbec735c2dd..189942c5ad8 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -12,8 +12,9 @@
# * (Optionally) add columns to `internal_ids` if needed for scope.
class InternalId < ActiveRecord::Base
belongs_to :project
+ belongs_to :namespace
- enum usage: { issues: 0 }
+ enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4 }
validates :usage, presence: true
@@ -23,9 +24,12 @@ class InternalId < ActiveRecord::Base
#
# The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
# As such, the increment is atomic and safe to be called concurrently.
- def increment_and_save!
+ #
+ # If a `maximum_iid` is passed in, this overrides the incremented value if it's
+ # greater than that. This can be used to correct the increment value if necessary.
+ def increment_and_save!(maximum_iid)
lock!
- self.last_value = (last_value || 0) + 1
+ self.last_value = [(last_value || 0) + 1, (maximum_iid || 0) + 1].max
save!
last_value
end
@@ -89,7 +93,16 @@ class InternalId < ActiveRecord::Base
# and increment its last value
#
# Note this will acquire a ROW SHARE lock on the InternalId record
- (lookup || create_record).increment_and_save!
+
+ # Note we always calculate the maximum iid present here and
+ # pass it in to correct the InternalId entry if it's last_value is off.
+ #
+ # This can happen in a transition phase where both `AtomicInternalId` and
+ # `NonatomicInternalId` code runs (e.g. during a deploy).
+ #
+ # This is subject to be cleaned up with the 10.8 release:
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/45389.
+ (lookup || create_record).increment_and_save!(maximum_iid)
end
end
@@ -115,11 +128,15 @@ class InternalId < ActiveRecord::Base
InternalId.create!(
**scope,
usage: usage_value,
- last_value: init.call(subject) || 0
+ last_value: maximum_iid
)
end
rescue ActiveRecord::RecordNotUnique
lookup
end
+
+ def maximum_iid
+ @maximum_iid ||= init.call(subject) || 0
+ end
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index c1ffe6512ea..0332bfa9371 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -34,7 +34,7 @@ class Issue < ActiveRecord::Base
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
- has_many :assignees, -> { auto_include(false) }, class_name: "User", through: :issue_assignees
+ has_many :assignees, class_name: "User", through: :issue_assignees
validates :project, presence: true
@@ -49,6 +49,7 @@ class Issue < ActiveRecord::Base
scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }
+ scope :due_tomorrow, -> { where(due_date: Date.tomorrow) }
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
@@ -193,6 +194,15 @@ class Issue < ActiveRecord::Base
branches_with_iid - branches_with_merge_request
end
+ def suggested_branch_name
+ return to_branch_name unless project.repository.branch_exists?(to_branch_name)
+
+ start_counting_from = 2
+ Uniquify.new(start_counting_from).string(-> (counter) { "#{to_branch_name}-#{counter}" }) do |suggested_branch_name|
+ project.repository.branch_exists?(suggested_branch_name)
+ end
+ end
+
# Returns boolean if a related branch exists for the current issue
# ignores merge requests branchs
def has_related_branch?
@@ -247,11 +257,8 @@ class Issue < ActiveRecord::Base
end
end
- def can_be_worked_on?(current_user)
- !self.closed? &&
- !self.project.forked? &&
- self.related_branches(current_user).empty? &&
- self.closed_by_merge_requests(current_user).empty?
+ def can_be_worked_on?
+ !self.closed? && !self.project.forked?
end
# Returns `true` if the current issue can be viewed by either a logged in User
diff --git a/app/models/label.rb b/app/models/label.rb
index f3496884cff..de7f1d56c64 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -18,8 +18,8 @@ class Label < ActiveRecord::Base
has_many :lists, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :priorities, class_name: 'LabelPriority'
has_many :label_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :issues, -> { auto_include(false) }, through: :label_links, source: :target, source_type: 'Issue'
- has_many :merge_requests, -> { auto_include(false) }, through: :label_links, source: :target, source_type: 'MergeRequest'
+ has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
+ has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
before_validation :strip_whitespace_from_title_and_color
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index ed95613ee59..6b7f280fb70 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -3,7 +3,7 @@ class LfsObject < ActiveRecord::Base
include ObjectStorage::BackgroundMove
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :projects, -> { auto_include(false) }, through: :lfs_objects_projects
+ has_many :projects, through: :lfs_objects_projects
scope :with_files_stored_locally, -> { where(file_store: [nil, LfsObjectUploader::Store::LOCAL]) }
@@ -11,10 +11,12 @@ class LfsObject < ActiveRecord::Base
mount_uploader :file, LfsObjectUploader
- before_save :update_file_store
+ after_save :update_file_store
def update_file_store
- self.file_store = file.object_store
+ # The file.object_store is set during `uploader.store!`
+ # which happens after object is inserted/updated
+ self.update_column(:file_store, file.object_store)
end
def project_allowed_access?(project)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 91d8be5559b..8f964a488aa 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1,5 +1,5 @@
class MergeRequest < ActiveRecord::Base
- include NonatomicInternalId
+ include AtomicInternalId
include Issuable
include Noteable
include Referable
@@ -18,6 +18,8 @@ class MergeRequest < ActiveRecord::Base
belongs_to :source_project, class_name: "Project"
belongs_to :merge_user, class_name: "User"
+ has_internal_id :iid, scope: :target_project, init: ->(s) { s&.target_project&.merge_requests&.maximum(:iid) }
+
has_many :merge_request_diffs
has_one :merge_request_diff,
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index c1c27ccf3e5..06aa67c600f 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -197,10 +197,6 @@ class MergeRequestDiff < ActiveRecord::Base
CompareService.new(project, head_commit_sha).execute(project, sha, straight: true)
end
- def commits_count
- super || merge_request_diff_commits.size
- end
-
private
def create_merge_request_diff_files(diffs)
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 8e33bab81c1..d14e3a4ded5 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -8,7 +8,7 @@ class Milestone < ActiveRecord::Base
Started = MilestoneStruct.new('Started', '#started', -3)
include CacheMarkdownField
- include NonatomicInternalId
+ include AtomicInternalId
include Sortable
include Referable
include StripAttribute
@@ -21,8 +21,11 @@ class Milestone < ActiveRecord::Base
belongs_to :project
belongs_to :group
+ has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.milestones&.maximum(:iid) }
+ has_internal_id :iid, scope: :group, init: ->(s) { s&.group&.milestones&.maximum(:iid) }
+
has_many :issues
- has_many :labels, -> { distinct.reorder('labels.title').auto_include(false) }, through: :issues
+ has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 2b63aa33222..c29a53e5ce7 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -248,10 +248,6 @@ class Namespace < ActiveRecord::Base
all_projects.with_storage_feature(:repository).find_each(&:remove_exports)
end
- def features
- []
- end
-
def refresh_project_authorizations
owner.refresh_authorized_projects
end
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index b3ffad00a07..2c3580bbdc6 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -83,14 +83,14 @@ class NotificationRecipient
def has_access?
DeclarativePolicy.subject_scope do
- return false unless user.can?(:receive_notifications)
- return true if @skip_read_ability
+ break false unless user.can?(:receive_notifications)
+ break true if @skip_read_ability
- return false if @target && !user.can?(:read_cross_project)
- return false if @project && !user.can?(:read_project, @project)
+ break false if @target && !user.can?(:read_cross_project)
+ break false if @project && !user.can?(:read_project, @project)
- return true unless read_ability
- return true unless DeclarativePolicy.has_policy?(@target)
+ break true unless read_ability
+ break true unless DeclarativePolicy.has_policy?(@target)
user.can?(read_ability, @target)
end
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index f6d9b0215fc..9195408551f 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -47,7 +47,8 @@ class NotificationSetting < ActiveRecord::Base
].freeze
EXCLUDED_WATCHER_EVENTS = [
- :push_to_merge_request
+ :push_to_merge_request,
+ :issue_due
].push(*EXCLUDED_PARTICIPATING_EVENTS).freeze
def self.find_or_create_for(source)
diff --git a/app/models/project.rb b/app/models/project.rb
index ffd78d3ab70..d4e9e51c7be 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -68,6 +68,11 @@ class Project < ActiveRecord::Base
after_save :update_project_statistics, if: :namespace_id_changed?
after_create :create_project_feature, unless: :project_feature
+
+ after_create :create_ci_cd_settings,
+ unless: :ci_cd_settings,
+ if: proc { ProjectCiCdSetting.available? }
+
after_create :set_last_activity_at
after_create :set_last_repository_updated_at
after_update :update_forks_visibility_level
@@ -138,11 +143,11 @@ class Project < ActiveRecord::Base
has_one :packagist_service
# TODO: replace these relations with the fork network versions
- has_one :forked_project_link, foreign_key: "forked_to_project_id"
- has_one :forked_from_project, -> { auto_include(false) }, through: :forked_project_link
+ has_one :forked_project_link, foreign_key: "forked_to_project_id"
+ has_one :forked_from_project, through: :forked_project_link
has_many :forked_project_links, foreign_key: "forked_from_project_id"
- has_many :forks, -> { auto_include(false) }, through: :forked_project_links, source: :forked_to_project
+ has_many :forks, through: :forked_project_links, source: :forked_to_project
# TODO: replace these relations with the fork network versions
has_one :root_of_fork_network,
@@ -150,7 +155,7 @@ class Project < ActiveRecord::Base
inverse_of: :root_project,
class_name: 'ForkNetwork'
has_one :fork_network_member
- has_one :fork_network, -> { auto_include(false) }, through: :fork_network_member
+ has_one :fork_network, through: :fork_network_member
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id'
@@ -167,27 +172,27 @@ class Project < ActiveRecord::Base
has_many :protected_tags
has_many :project_authorizations
- has_many :authorized_users, -> { auto_include(false) }, through: :project_authorizations, source: :user, class_name: 'User'
+ has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
has_many :project_members, -> { where(requested_at: nil) },
as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :project_members
- has_many :users, -> { auto_include(false) }, through: :project_members
+ has_many :users, through: :project_members
has_many :requesters, -> { where.not(requested_at: nil) },
as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
has_many :deploy_keys_projects
- has_many :deploy_keys, -> { auto_include(false) }, through: :deploy_keys_projects
+ has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects
- has_many :starrers, -> { auto_include(false) }, through: :users_star_projects, source: :user
+ has_many :starrers, through: :users_star_projects, source: :user
has_many :releases
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :lfs_objects, -> { auto_include(false) }, through: :lfs_objects_projects
+ has_many :lfs_objects, through: :lfs_objects_projects
has_many :lfs_file_locks
has_many :project_group_links
- has_many :invited_groups, -> { auto_include(false) }, through: :project_group_links, source: :group
+ has_many :invited_groups, through: :project_group_links, source: :group
has_many :pages_domains
has_many :todos
has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
@@ -199,7 +204,7 @@ class Project < ActiveRecord::Base
has_one :statistics, class_name: 'ProjectStatistics'
has_one :cluster_project, class_name: 'Clusters::Project'
- has_many :clusters, -> { auto_include(false) }, 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
@@ -216,21 +221,22 @@ class Project < ActiveRecord::Base
has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
has_many :runner_projects, class_name: 'Ci::RunnerProject'
- has_many :runners, -> { auto_include(false) }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
+ has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, class_name: 'Ci::Variable'
has_many :triggers, class_name: 'Ci::Trigger'
has_many :environments
has_many :deployments
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
has_many :project_deploy_tokens
- has_many :deploy_tokens, -> { auto_include(false) }, through: :project_deploy_tokens
+ has_many :deploy_tokens, through: :project_deploy_tokens
- has_many :active_runners, -> { active.auto_include(false) }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
+ 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'
has_many :project_badges, class_name: 'ProjectBadge'
+ has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting'
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
@@ -512,10 +518,6 @@ class Project < ActiveRecord::Base
repository.empty?
end
- def repository_storage_path
- Gitlab.config.repositories.storages[repository_storage]&.legacy_disk_path
- end
-
def team
@team ||= ProjectTeam.new(self)
end
@@ -1041,13 +1043,6 @@ class Project < ActiveRecord::Base
"#{web_url}.git"
end
- def user_can_push_to_empty_repo?(user)
- return false unless empty_repo?
- return false unless Ability.allowed?(user, :push_code, self)
-
- !ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
- end
-
def forked?
return true if fork_network && fork_network.root_project != self
@@ -1106,7 +1101,7 @@ class Project < ActiveRecord::Base
# Check if repository already exists on disk
def check_repository_path_availability
return true if skip_disk_validation
- return false unless repository_storage_path
+ return false unless repository_storage
expires_full_path_cache # we need to clear cache to validate renames correctly
@@ -1637,7 +1632,7 @@ class Project < ActiveRecord::Base
def container_registry_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- return variables unless Gitlab.config.registry.enabled
+ break variables unless Gitlab.config.registry.enabled
variables.append(key: 'CI_REGISTRY', value: Gitlab.config.registry.host_port)
@@ -1875,6 +1870,14 @@ class Project < ActiveRecord::Base
memoized_results[cache_key]
end
+ def licensed_features
+ []
+ end
+
+ def gitlab_deploy_token
+ @gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
+ end
+
private
def storage
@@ -1903,14 +1906,14 @@ class Project < ActiveRecord::Base
def check_repository_absence!
return if skip_disk_validation
- if repository_storage_path.blank? || repository_with_same_path_already_exists?
+ if repository_storage.blank? || repository_with_same_path_already_exists?
errors.add(:base, 'There is already a repository with that name on disk')
throw :abort
end
end
def repository_with_same_path_already_exists?
- gitlab_shell.exists?(repository_storage_path, "#{disk_path}.git")
+ gitlab_shell.exists?(repository_storage, "#{disk_path}.git")
end
# set last_activity_at to the same as created_at
@@ -2000,10 +2003,11 @@ class Project < ActiveRecord::Base
def fetch_branch_allows_maintainer_push?(user, branch_name)
check_access = -> do
+ next false if empty_repo?
+
merge_request = source_of_merge_requests.opened
.where(allow_maintainer_to_push: true)
.find_by(source_branch: branch_name)
-
merge_request&.can_be_merged_by?(user)
end
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
new file mode 100644
index 00000000000..9f10a93148c
--- /dev/null
+++ b/app/models/project_ci_cd_setting.rb
@@ -0,0 +1,16 @@
+class ProjectCiCdSetting < ActiveRecord::Base
+ belongs_to :project
+
+ # The version of the schema that first introduced this model/table.
+ MINIMUM_SCHEMA_VERSION = 20180403035759
+
+ def self.available?
+ @available ||=
+ ActiveRecord::Migrator.current_version >= MINIMUM_SCHEMA_VERSION
+ end
+
+ def self.reset_column_information
+ @available = nil
+ super
+ end
+end
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
index 4d23a17a545..da01ac1b7cf 100644
--- a/app/models/project_services/flowdock_service.rb
+++ b/app/models/project_services/flowdock_service.rb
@@ -1,5 +1,51 @@
require "flowdock-git-hook"
+# Flow dock depends on Grit to compute the number of commits between two given
+# commits. To make this depend on Gitaly, a monkey patch is applied
+module Flowdock
+ class Git
+ # pass down a Repository all the way down
+ def repo
+ @options[:repo]
+ end
+
+ def config
+ {}
+ end
+
+ def messages
+ Git::Builder.new(repo: repo,
+ ref: @ref,
+ before: @from,
+ after: @to,
+ commit_url: @commit_url,
+ branch_url: @branch_url,
+ diff_url: @diff_url,
+ repo_url: @repo_url,
+ repo_name: @repo_name,
+ permanent_refs: @permanent_refs,
+ tags: tags
+ ).to_hashes
+ end
+
+ class Builder
+ def commits
+ @repo.commits_between(@before, @after).map do |commit|
+ {
+ url: @opts[:commit_url] ? @opts[:commit_url] % [commit.sha] : nil,
+ id: commit.sha,
+ message: commit.message,
+ author: {
+ name: commit.author_name,
+ email: commit.author_email
+ }
+ }
+ end
+ end
+ end
+ end
+end
+
class FlowdockService < Service
prop_accessor :token
validates :token, presence: true, if: :activated?
@@ -34,7 +80,7 @@ class FlowdockService < Service
data[:before],
data[:after],
token: token,
- repo: project.repository.path_to_repo,
+ repo: project.repository,
repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}",
commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/%s",
diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s"
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 87a4350f022..5d4e3c34b39 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -4,15 +4,15 @@ class ProjectStatistics < ActiveRecord::Base
before_save :update_storage_size
- STORAGE_COLUMNS = [:repository_size, :lfs_objects_size, :build_artifacts_size].freeze
- STATISTICS_COLUMNS = [:commit_count] + STORAGE_COLUMNS
+ COLUMNS_TO_REFRESH = [:repository_size, :lfs_objects_size, :commit_count].freeze
+ INCREMENTABLE_COLUMNS = [:build_artifacts_size].freeze
def total_repository_size
repository_size + lfs_objects_size
end
def refresh!(only: nil)
- STATISTICS_COLUMNS.each do |column, generator|
+ COLUMNS_TO_REFRESH.each do |column, generator|
if only.blank? || only.include?(column)
public_send("update_#{column}") # rubocop:disable GitlabSecurity/PublicSend
end
@@ -34,13 +34,15 @@ class ProjectStatistics < ActiveRecord::Base
self.lfs_objects_size = project.lfs_objects.sum(:size)
end
- def update_build_artifacts_size
- self.build_artifacts_size =
- project.builds.sum(:artifacts_size) +
- Ci::JobArtifact.artifacts_size_for(self.project)
+ def update_storage_size
+ self.storage_size = repository_size + lfs_objects_size + build_artifacts_size
end
- def update_storage_size
- self.storage_size = STORAGE_COLUMNS.sum(&method(:read_attribute))
+ def self.increment_statistic(project_id, key, amount)
+ raise ArgumentError, "Cannot increment attribute: #{key}" unless key.in?(INCREMENTABLE_COLUMNS)
+ return if amount == 0
+
+ where(project_id: project_id)
+ .update_all(["#{key} = COALESCE(#{key}, 0) + (?)", amount])
end
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 52e067cb44c..f799a0b4227 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, :hashed_storage?, to: :project
+ delegate :repository_storage, :hashed_storage?, to: :project
def path
@project.path + '.wiki'
@@ -179,7 +179,11 @@ class ProjectWiki
def commit_details(action, message = nil, title = nil)
commit_message = message || default_message(action, title)
- Gitlab::Git::Wiki::CommitDetails.new(@user.name, @user.email, commit_message)
+ Gitlab::Git::Wiki::CommitDetails.new(@user.id,
+ @user.username,
+ @user.name,
+ @user.email,
+ commit_message)
end
def default_message(action, title)
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 609780c5587..cb361a66591 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -4,6 +4,15 @@ class ProtectedBranch < ActiveRecord::Base
protected_ref_access_levels :merge, :push
+ def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
+ # Masters, owners and admins are allowed to create the default branch
+ if default_branch_protected? && project.empty_repo?
+ return true if user.admin? || project.team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
+ end
+
+ super
+ end
+
# Check if branch name is marked as protected in the system
def self.protected?(project, ref_name)
return true if project.empty_repo? && default_branch_protected?
diff --git a/app/models/repository.rb b/app/models/repository.rb
index fd1afafe4df..6831305fb93 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -84,9 +84,14 @@ class Repository
# Return absolute path to repository
def path_to_repo
- @path_to_repo ||= File.expand_path(
- File.join(repository_storage_path, disk_path + '.git')
- )
+ @path_to_repo ||=
+ begin
+ storage = Gitlab.config.repositories.storages[@project.repository_storage]
+
+ File.expand_path(
+ File.join(storage.legacy_disk_path, disk_path + '.git')
+ )
+ end
end
def inspect
@@ -331,6 +336,7 @@ class Repository
return unless empty?
expire_method_caches(%i(has_visible_content?))
+ raw_repository.expire_has_local_branches_cache
end
def lookup_cache
@@ -914,10 +920,6 @@ class Repository
raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref)
end
- def repository_storage_path
- @project.repository_storage_path
- end
-
def rebase(user, merge_request)
raw.rebase(user, merge_request.id, branch: merge_request.source_branch,
branch_sha: merge_request.source_branch_sha,
diff --git a/app/models/storage/hashed_project.rb b/app/models/storage/hashed_project.rb
index fae1b64961a..26b4b78ac64 100644
--- a/app/models/storage/hashed_project.rb
+++ b/app/models/storage/hashed_project.rb
@@ -1,7 +1,7 @@
module Storage
class HashedProject
attr_accessor :project
- delegate :gitlab_shell, :repository_storage_path, to: :project
+ delegate :gitlab_shell, :repository_storage, to: :project
ROOT_PATH_PREFIX = '@hashed'.freeze
@@ -24,7 +24,7 @@ module Storage
end
def ensure_storage_path_exists
- gitlab_shell.add_namespace(repository_storage_path, base_dir)
+ gitlab_shell.add_namespace(repository_storage, base_dir)
end
def rename_repo
diff --git a/app/models/storage/legacy_project.rb b/app/models/storage/legacy_project.rb
index 9d9e5e1d352..27cb388c702 100644
--- a/app/models/storage/legacy_project.rb
+++ b/app/models/storage/legacy_project.rb
@@ -1,7 +1,7 @@
module Storage
class LegacyProject
attr_accessor :project
- delegate :namespace, :gitlab_shell, :repository_storage_path, to: :project
+ delegate :namespace, :gitlab_shell, :repository_storage, to: :project
def initialize(project)
@project = project
@@ -24,18 +24,18 @@ module Storage
def ensure_storage_path_exists
return unless namespace
- gitlab_shell.add_namespace(repository_storage_path, base_dir)
+ gitlab_shell.add_namespace(repository_storage, base_dir)
end
def rename_repo
new_full_path = project.build_full_path
- if gitlab_shell.mv_repository(repository_storage_path, project.full_path_was, new_full_path)
+ if gitlab_shell.mv_repository(repository_storage, project.full_path_was, new_full_path)
# If repository moved successfully we need to send update instructions to users.
# However we cannot allow rollback since we moved repository
# So we basically we mute exceptions in next actions
begin
- gitlab_shell.mv_repository(repository_storage_path, "#{project.full_path_was}.wiki", "#{new_full_path}.wiki")
+ gitlab_shell.mv_repository(repository_storage, "#{project.full_path_was}.wiki", "#{new_full_path}.wiki")
return true
rescue => e
Rails.logger.error "Exception renaming #{project.full_path_was} -> #{new_full_path}: #{e}"
diff --git a/app/models/todo.rb b/app/models/todo.rb
index aad2c1dac4e..a2ab405fdbe 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -22,7 +22,7 @@ class Todo < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :note
belongs_to :project
- belongs_to :target, -> { auto_include(false) }, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
+ belongs_to :target, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user
delegate :name, :email, to: :author, prefix: true, allow_nil: true
diff --git a/app/models/user.rb b/app/models/user.rb
index d5c5c0964c5..b0668148972 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -96,23 +96,23 @@ class User < ActiveRecord::Base
# Groups
has_many :members
has_many :group_members, -> { where(requested_at: nil) }, source: 'GroupMember'
- has_many :groups, -> { auto_include(false) }, through: :group_members
- has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }).auto_include(false) }, through: :group_members, source: :group
- has_many :masters_groups, -> { where(members: { access_level: Gitlab::Access::MASTER }).auto_include(false) }, through: :group_members, source: :group
+ has_many :groups, through: :group_members
+ has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group
+ has_many :masters_groups, -> { where(members: { access_level: Gitlab::Access::MASTER }) }, through: :group_members, source: :group
# Projects
- has_many :groups_projects, -> { auto_include(false) }, through: :groups, source: :projects
- has_many :personal_projects, -> { auto_include(false) }, through: :namespace, source: :projects
+ has_many :groups_projects, through: :groups, source: :projects
+ has_many :personal_projects, through: :namespace, source: :projects
has_many :project_members, -> { where(requested_at: nil) }
- has_many :projects, -> { auto_include(false) }, through: :project_members
- has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
+ has_many :projects, through: :project_members
+ has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :starred_projects, -> { auto_include(false) }, through: :users_star_projects, source: :project
+ has_many :starred_projects, through: :users_star_projects, source: :project
has_many :project_authorizations
- has_many :authorized_projects, -> { auto_include(false) }, through: :project_authorizations, source: :project
+ has_many :authorized_projects, through: :project_authorizations, source: :project
has_many :user_interacted_projects
- has_many :project_interactions, -> { auto_include(false) }, through: :user_interacted_projects, source: :project, class_name: 'Project'
+ has_many :project_interactions, through: :user_interacted_projects, source: :project, class_name: 'Project'
has_many :snippets, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :notes, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
@@ -132,7 +132,7 @@ class User < ActiveRecord::Base
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
- has_many :assigned_issues, -> { auto_include(false) }, class_name: "Issue", through: :issue_assignees, source: :issue
+ has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
has_many :custom_attributes, class_name: 'UserCustomAttribute'
@@ -947,10 +947,13 @@ class User < ActiveRecord::Base
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))
+ union_sql = Gitlab::SQL::Union.new([owned_groups.select(:id), masters_groups.select(:id)]).to_sql
+
+ # Update this line to not use raw SQL when migrated to Rails 5.2.
+ # Either ActiveRecord or Arel constructions are fine.
+ # This was replaced with the raw SQL construction because of bugs in the arel gem.
+ # Bugs were fixed in arel 9.0.0 (Rails 5.2).
+ owned_and_master_groups = Group.where("namespaces.id IN (#{union_sql})") # rubocop:disable GitlabSecurity/SqlInjection
Gitlab::GroupHierarchy.new(owned_and_master_groups).base_and_descendants
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 0f5536415f7..cde79b95062 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -265,6 +265,15 @@ class WikiPage
title.present? && self.class.unhyphenize(@page.url_path) != title
end
+ # Updates the current @attributes hash by merging a hash of params
+ def update_attributes(attrs)
+ attrs[:title] = process_title(attrs[:title]) if attrs[:title].present?
+
+ attrs.slice!(:content, :format, :message, :title)
+
+ @attributes.merge!(attrs)
+ end
+
private
# Process and format the title based on the user input.
@@ -290,15 +299,6 @@ class WikiPage
File.join(components)
end
- # Updates the current @attributes hash by merging a hash of params
- def update_attributes(attrs)
- attrs[:title] = process_title(attrs[:title]) if attrs[:title].present?
-
- attrs.slice!(:content, :format, :message, :title)
-
- @attributes.merge!(attrs)
- end
-
def set_attributes
attributes[:slug] = @page.url_path
attributes[:title] = @page.title
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 1ab391a5a9d..808a81cbbf9 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -11,7 +11,7 @@ module Ci
end
condition(:owner_of_job) do
- can?(:developer_access) && @subject.triggered_by?(@user)
+ @subject.triggered_by?(@user)
end
rule { protected_ref }.policy do
@@ -19,6 +19,6 @@ module Ci
prevent :erase_build
end
- rule { can?(:master_access) | owner_of_job }.enable :erase_build
+ rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build
end
end
diff --git a/app/policies/ci/pipeline_schedule_policy.rb b/app/policies/ci/pipeline_schedule_policy.rb
index dc7a4aed577..ecba0488d3c 100644
--- a/app/policies/ci/pipeline_schedule_policy.rb
+++ b/app/policies/ci/pipeline_schedule_policy.rb
@@ -7,23 +7,17 @@ module Ci
end
condition(:owner_of_schedule) do
- can?(:developer_access) && pipeline_schedule.owned_by?(@user)
+ pipeline_schedule.owned_by?(@user)
end
- condition(:non_owner_of_schedule) do
- !pipeline_schedule.owned_by?(@user)
- end
-
- rule { can?(:developer_access) }.policy do
- enable :play_pipeline_schedule
- end
+ rule { can?(:create_pipeline) }.enable :play_pipeline_schedule
- rule { can?(:master_access) | owner_of_schedule }.policy do
+ rule { can?(:admin_pipeline) | (can?(:update_build) & owner_of_schedule) }.policy do
enable :update_pipeline_schedule
enable :admin_pipeline_schedule
end
- rule { can?(:master_access) & non_owner_of_schedule }.policy do
+ rule { can?(:admin_pipeline_schedule) & ~owner_of_schedule }.policy do
enable :take_ownership_pipeline_schedule
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index c9cb730c4e9..520710b757d 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -22,7 +22,7 @@ class GroupPolicy < BasePolicy
condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) }
condition(:has_projects) do
- GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
+ GroupProjectsFinder.new(group: @subject, current_user: @user, options: { include_subgroups: true }).execute.any?
end
with_options scope: :subject, score: 0
@@ -43,7 +43,11 @@ class GroupPolicy < BasePolicy
end
rule { admin } .enable :read_group
- rule { has_projects } .enable :read_group
+
+ rule { has_projects }.policy do
+ enable :read_group
+ enable :read_label
+ end
rule { has_access }.enable :read_namespace
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index e86d1c8f98e..b431d376e3d 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -18,9 +18,7 @@ class IssuablePolicy < BasePolicy
rule { locked & ~is_project_member }.policy do
prevent :create_note
- prevent :update_note
prevent :admin_note
prevent :resolve_note
- prevent :edit_note
end
end
diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb
index d4cb5a77e63..077a6761ee6 100644
--- a/app/policies/note_policy.rb
+++ b/app/policies/note_policy.rb
@@ -1,26 +1,21 @@
class NotePolicy < BasePolicy
delegate { @subject.project }
- delegate { @subject.noteable if @subject.noteable.lockable? }
+ delegate { @subject.noteable if DeclarativePolicy.has_policy?(@subject.noteable) }
condition(:is_author) { @user && @subject.author == @user }
- condition(:for_merge_request, scope: :subject) { @subject.for_merge_request? }
condition(:is_noteable_author) { @user && @subject.noteable.author_id == @user.id }
condition(:editable, scope: :subject) { @subject.editable? }
- rule { ~editable | anonymous }.prevent :edit_note
-
- rule { is_author | admin }.enable :edit_note
- rule { can?(:master_access) }.enable :edit_note
+ rule { ~editable }.prevent :admin_note
rule { is_author }.policy do
enable :read_note
- enable :update_note
enable :admin_note
enable :resolve_note
end
- rule { for_merge_request & is_noteable_author }.policy do
+ rule { is_noteable_author }.policy do
enable :resolve_note
end
end
diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb
index cac0530b9f7..c1a84727cfa 100644
--- a/app/policies/personal_snippet_policy.rb
+++ b/app/policies/personal_snippet_policy.rb
@@ -25,4 +25,6 @@ class PersonalSnippetPolicy < BasePolicy
end
rule { anonymous }.prevent :comment_personal_snippet
+
+ rule { can?(:comment_personal_snippet) }.enable :award_emoji
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 21bb0934dee..3529d0aa60c 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -1,12 +1,26 @@
class ProjectPolicy < BasePolicy
- def self.create_read_update_admin(name)
- [
- :"create_#{name}",
- :"read_#{name}",
- :"update_#{name}",
- :"admin_#{name}"
- ]
- end
+ extend ClassMethods
+
+ READONLY_FEATURES_WHEN_ARCHIVED = %i[
+ issue
+ list
+ merge_request
+ label
+ milestone
+ project_snippet
+ wiki
+ note
+ pipeline
+ pipeline_schedule
+ build
+ trigger
+ environment
+ deployment
+ commit_status
+ container_image
+ pages
+ cluster
+ ].freeze
desc "User is a project owner"
condition :owner do
@@ -15,7 +29,7 @@ class ProjectPolicy < BasePolicy
end
desc "Project has public builds enabled"
- condition(:public_builds, scope: :subject) { project.public_builds? }
+ condition(:public_builds, scope: :subject, score: 0) { project.public_builds? }
# For guest access we use #team_member? so we can use
# project.members, which gets cached in subject scope.
@@ -35,7 +49,7 @@ class ProjectPolicy < BasePolicy
condition(:master) { team_access_level >= Gitlab::Access::MASTER }
desc "Project is public"
- condition(:public_project, scope: :subject) { project.public? }
+ condition(:public_project, scope: :subject, score: 0) { project.public? }
desc "Project is visible to internal users"
condition(:internal_access) do
@@ -46,7 +60,7 @@ class ProjectPolicy < BasePolicy
condition(:group_member, scope: :subject) { project_group_member? }
desc "Project is archived"
- condition(:archived, scope: :subject) { project.archived? }
+ condition(:archived, scope: :subject, score: 0) { project.archived? }
condition(:default_issues_tracker, scope: :subject) { project.default_issues_tracker? }
@@ -56,10 +70,10 @@ class ProjectPolicy < BasePolicy
end
desc "Project has an external wiki"
- condition(:has_external_wiki, scope: :subject) { project.has_external_wiki? }
+ condition(:has_external_wiki, scope: :subject, score: 0) { project.has_external_wiki? }
desc "Project has request access enabled"
- condition(:request_access_enabled, scope: :subject) { project.request_access_enabled }
+ condition(:request_access_enabled, scope: :subject, score: 0) { project.request_access_enabled }
desc "Has merge requests allowing pushes to user"
condition(:has_merge_requests_allowing_pushes, scope: :subject) do
@@ -126,6 +140,7 @@ class ProjectPolicy < BasePolicy
rule { can?(:guest_access) }.policy do
enable :read_project
+ enable :create_merge_request_in
enable :read_board
enable :read_list
enable :read_wiki
@@ -140,6 +155,7 @@ class ProjectPolicy < BasePolicy
enable :create_note
enable :upload_file
enable :read_cycle_analytics
+ enable :award_emoji
end
# These abilities are not allowed to admins that are not members of the project,
@@ -197,7 +213,7 @@ class ProjectPolicy < BasePolicy
enable :create_pipeline
enable :update_pipeline
enable :create_pipeline_schedule
- enable :create_merge_request
+ enable :create_merge_request_from
enable :create_wiki
enable :push_code
enable :resolve_note
@@ -208,7 +224,7 @@ class ProjectPolicy < BasePolicy
end
rule { can?(:master_access) }.policy do
- enable :delete_protected_branch
+ enable :push_to_delete_protected_branch
enable :update_project_snippet
enable :update_environment
enable :update_deployment
@@ -231,37 +247,50 @@ class ProjectPolicy < BasePolicy
end
rule { archived }.policy do
- prevent :create_merge_request
prevent :push_code
- prevent :delete_protected_branch
- prevent :update_merge_request
- prevent :admin_merge_request
+ prevent :push_to_delete_protected_branch
+ prevent :request_access
+ prevent :upload_file
+ prevent :resolve_note
+ prevent :create_merge_request_from
+ prevent :create_merge_request_in
+ prevent :award_emoji
+
+ READONLY_FEATURES_WHEN_ARCHIVED.each do |feature|
+ prevent(*create_update_admin_destroy(feature))
+ end
+ end
+
+ rule { issues_disabled }.policy do
+ prevent(*create_read_update_admin_destroy(:issue))
end
rule { merge_requests_disabled | repository_disabled }.policy do
- prevent(*create_read_update_admin(:merge_request))
+ prevent :create_merge_request_in
+ prevent :create_merge_request_from
+ prevent(*create_read_update_admin_destroy(:merge_request))
end
rule { issues_disabled & merge_requests_disabled }.policy do
- prevent(*create_read_update_admin(:label))
- prevent(*create_read_update_admin(:milestone))
+ prevent(*create_read_update_admin_destroy(:label))
+ prevent(*create_read_update_admin_destroy(:milestone))
end
rule { snippets_disabled }.policy do
- prevent(*create_read_update_admin(:project_snippet))
+ prevent(*create_read_update_admin_destroy(:project_snippet))
end
rule { wiki_disabled & ~has_external_wiki }.policy do
- prevent(*create_read_update_admin(:wiki))
+ prevent(*create_read_update_admin_destroy(:wiki))
prevent(:download_wiki_code)
end
rule { builds_disabled | repository_disabled }.policy do
- prevent(*create_read_update_admin(:build))
- prevent(*(create_read_update_admin(:pipeline) - [:read_pipeline]))
- prevent(*create_read_update_admin(:pipeline_schedule))
- prevent(*create_read_update_admin(:environment))
- prevent(*create_read_update_admin(:deployment))
+ prevent(*create_update_admin_destroy(:pipeline))
+ prevent(*create_read_update_admin_destroy(:build))
+ prevent(*create_read_update_admin_destroy(:pipeline_schedule))
+ prevent(*create_read_update_admin_destroy(:environment))
+ prevent(*create_read_update_admin_destroy(:deployment))
end
rule { repository_disabled }.policy do
@@ -272,7 +301,7 @@ class ProjectPolicy < BasePolicy
end
rule { container_registry_disabled }.policy do
- prevent(*create_read_update_admin(:container_image))
+ prevent(*create_read_update_admin_destroy(:container_image))
end
rule { anonymous & ~public_project }.prevent_all
@@ -314,13 +343,6 @@ class ProjectPolicy < BasePolicy
enable :read_pipeline_schedule
end
- rule { issues_disabled }.policy do
- prevent :create_issue
- prevent :update_issue
- prevent :admin_issue
- prevent :read_issue
- end
-
# These rules are included to allow maintainers of projects to push to certain
# to run pipelines for the branches they have access to.
rule { can?(:public_access) & has_merge_requests_allowing_pushes }.policy do
diff --git a/app/policies/project_policy/class_methods.rb b/app/policies/project_policy/class_methods.rb
new file mode 100644
index 00000000000..60e5aba00ba
--- /dev/null
+++ b/app/policies/project_policy/class_methods.rb
@@ -0,0 +1,19 @@
+class ProjectPolicy
+ module ClassMethods
+ def create_read_update_admin_destroy(name)
+ [
+ :"read_#{name}",
+ *create_update_admin_destroy(name)
+ ]
+ end
+
+ def create_update_admin_destroy(name)
+ [
+ :"create_#{name}",
+ :"update_#{name}",
+ :"admin_#{name}",
+ :"destroy_#{name}"
+ ]
+ end
+ end
+end
diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb
index 9afebda19be..4873d7ce662 100644
--- a/app/presenters/ci/build_presenter.rb
+++ b/app/presenters/ci/build_presenter.rb
@@ -1,5 +1,14 @@
module Ci
class BuildPresenter < Gitlab::View::Presenter::Delegated
+ CALLOUT_FAILURE_MESSAGES = {
+ unknown_failure: 'There is an unknown failure, please try again',
+ script_failure: 'There has been a script failure. Check the job log for more information',
+ api_failure: 'There has been an API failure, please try again',
+ stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again',
+ runner_system_failure: 'There has been a runner system failure, please try again',
+ missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information'
+ }.freeze
+
presents :build
def erased_by_user?
@@ -35,6 +44,14 @@ module Ci
"#{subject.name} - #{detailed_status.status_tooltip}"
end
+ def callout_failure_message
+ CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym]
+ end
+
+ def recoverable?
+ failed? && !unrecoverable?
+ end
+
private
def tooltip_for_badge
@@ -44,5 +61,9 @@ module Ci
def detailed_status
@detailed_status ||= subject.detailed_status(user)
end
+
+ def unrecoverable?
+ script_failure? || missing_dependency_failure?
+ end
end
end
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 9f3f2637183..4b4132af2d0 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -3,6 +3,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
include GitlabRoutingHelper
include MarkupHelper
include TreeHelper
+ include ChecksCollaboration
include Gitlab::Utils::StrongMemoize
presents :merge_request
@@ -152,11 +153,11 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def can_revert_on_current_merge_request?
- user_can_collaborate_with_project? && cached_can_be_reverted?
+ can_collaborate_with_project?(project) && cached_can_be_reverted?
end
def can_cherry_pick_on_current_merge_request?
- user_can_collaborate_with_project? && can_be_cherry_picked?
+ can_collaborate_with_project?(project) && can_be_cherry_picked?
end
def can_push_to_source_branch?
@@ -195,12 +196,6 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end.sort.to_sentence
end
- def user_can_collaborate_with_project?
- can?(current_user, :push_code, project) ||
- (current_user && current_user.already_forked?(project)) ||
- can_push_to_source_branch?
- end
-
def user_can_fork_project?
can?(current_user, :fork_project, project)
end
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 484ac64580d..ad655a7b3f4 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -4,6 +4,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
include GitlabRoutingHelper
include StorageHelper
include TreeHelper
+ include ChecksCollaboration
include Gitlab::Utils::StrongMemoize
presents :project
@@ -170,9 +171,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def can_current_user_push_to_branch?(branch)
- return false unless repository.branch_exists?(branch)
+ user_access(project).can_push_to_branch?(branch)
+ end
- ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch)
+ def can_current_user_push_to_default_branch?
+ can_current_user_push_to_branch?(default_branch)
end
def files_anchor_data
@@ -200,7 +203,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def new_file_anchor_data
- if current_user && can_current_user_push_code?
+ if current_user && can_current_user_push_to_default_branch?
OpenStruct.new(enabled: false,
label: _('New file'),
link: project_new_blob_path(project, default_branch || 'master'),
@@ -209,7 +212,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def readme_anchor_data
- if current_user && can_current_user_push_code? && repository.readme.blank?
+ if current_user && can_current_user_push_to_default_branch? && repository.readme.blank?
OpenStruct.new(enabled: false,
label: _('Add Readme'),
link: add_readme_path)
@@ -221,7 +224,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def changelog_anchor_data
- if current_user && can_current_user_push_code? && repository.changelog.blank?
+ if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank?
OpenStruct.new(enabled: false,
label: _('Add Changelog'),
link: add_changelog_path)
@@ -233,7 +236,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def license_anchor_data
- if current_user && can_current_user_push_code? && repository.license_blob.blank?
+ if current_user && can_current_user_push_to_default_branch? && repository.license_blob.blank?
OpenStruct.new(enabled: false,
label: _('Add License'),
link: add_license_path)
@@ -245,7 +248,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def contribution_guide_anchor_data
- if current_user && can_current_user_push_code? && repository.contribution_guide.blank?
+ if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank?
OpenStruct.new(enabled: false,
label: _('Add Contribution guide'),
link: add_contribution_guide_path)
@@ -260,7 +263,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout
OpenStruct.new(enabled: auto_devops_enabled?,
label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'),
- link: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings'))
+ link: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
elsif auto_devops_enabled?
OpenStruct.new(enabled: true,
label: _('Auto DevOps enabled'),
diff --git a/app/serializers/entity_date_helper.rb b/app/serializers/entity_date_helper.rb
index 71d9a65fb58..464217123b4 100644
--- a/app/serializers/entity_date_helper.rb
+++ b/app/serializers/entity_date_helper.rb
@@ -1,5 +1,6 @@
module EntityDateHelper
include ActionView::Helpers::DateHelper
+ include ActionView::Helpers::TagHelper
def interval_in_words(diff)
return 'Not started' unless diff
@@ -34,4 +35,30 @@ module EntityDateHelper
duration_hash
end
+
+ # Generates an HTML-formatted string for remaining dates based on start_date and due_date
+ #
+ # It returns "Past due" for expired entities
+ # It returns "Upcoming" for upcoming entities
+ # If due date is provided, it returns "# days|weeks|months remaining|ago"
+ # If start date is provided and elapsed, with no due date, it returns "# days elapsed"
+ def remaining_days_in_words(entity)
+ if entity.try(:expired?)
+ content_tag(:strong, 'Past due')
+ elsif entity.try(:upcoming?)
+ content_tag(:strong, 'Upcoming')
+ elsif entity.due_date
+ is_upcoming = (entity.due_date - Date.today).to_i > 0
+ time_ago = time_ago_in_words(entity.due_date)
+ content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" }
+ content.slice!("about ")
+ content << " " + (is_upcoming ? _("remaining") : _("ago"))
+ content.html_safe
+ elsif entity.start_date && entity.start_date.past?
+ days = entity.elapsed_days
+ content = content_tag(:strong, days)
+ content << " #{'day'.pluralize(days)} elapsed"
+ content.html_safe
+ end
+ end
end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index b5e2334b6e3..840fdbcbf14 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -29,6 +29,10 @@ class IssueEntity < IssuableEntity
expose :can_update do |issue|
can?(request.current_user, :update_issue, issue)
end
+
+ expose :can_award_emoji do |issue|
+ can?(request.current_user, :award_emoji, issue)
+ end
end
expose :create_note_path do |issue|
diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb
index 523b522d449..3076fed1674 100644
--- a/app/serializers/job_entity.rb
+++ b/app/serializers/job_entity.rb
@@ -26,6 +26,8 @@ class JobEntity < Grape::Entity
expose :created_at
expose :updated_at
expose :detailed_status, as: :status, with: StatusEntity
+ expose :callout_message, if: -> (*) { failed? }
+ expose :recoverable, if: -> (*) { failed? }
private
@@ -50,4 +52,20 @@ class JobEntity < Grape::Entity
def path_to(route, build)
send("#{route}_path", build.project.namespace, build.project, build) # rubocop:disable GitlabSecurity/PublicSend
end
+
+ def failed?
+ build.failed?
+ end
+
+ def callout_message
+ build_presenter.callout_failure_message
+ end
+
+ def recoverable
+ build_presenter.recoverable?
+ end
+
+ def build_presenter
+ @build_presenter ||= build.present
+ end
end
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index c964aa9c99b..06d603b277e 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -15,7 +15,11 @@ class NoteEntity < API::Entities::Note
expose :current_user do
expose :can_edit do |note|
- Ability.can_edit_note?(request.current_user, note)
+ Ability.allowed?(request.current_user, :admin_note, note)
+ end
+
+ expose :can_award_emoji do |note|
+ Ability.allowed?(request.current_user, :award_emoji, note)
end
end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index e09b445636f..0b087ad73da 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -4,6 +4,9 @@ module Ci
class RegisterJobService
attr_reader :runner
+ JOB_QUEUE_DURATION_SECONDS_BUCKETS = [1, 3, 10, 30].freeze
+ JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET = 5.freeze
+
Result = Struct.new(:build, :valid?)
def initialize(runner)
@@ -41,7 +44,7 @@ module Ci
build.run!
register_success(build)
- return Result.new(build, true)
+ return Result.new(build, true) # rubocop:disable Cop/AvoidReturnFromBlocks
rescue Ci::Build::MissingDependenciesError
build.drop!(:missing_dependency_failure)
end
@@ -104,10 +107,22 @@ module Ci
end
def register_success(job)
- job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at)
+ labels = { shared_runner: runner.shared?,
+ jobs_running_for_project: jobs_running_for_project(job) }
+
+ job_queue_duration_seconds.observe(labels, Time.now - job.queued_at) unless job.queued_at.nil?
attempt_counter.increment
end
+ def jobs_running_for_project(job)
+ return '+Inf' unless runner.shared?
+
+ # excluding currently started job
+ running_jobs_count = job.project.builds.running.where(runner: Ci::Runner.shared)
+ .limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1
+ running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+"
+ end
+
def failed_attempt_counter
@failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job")
end
@@ -117,7 +132,7 @@ module Ci
end
def job_queue_duration_seconds
- @job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time')
+ @job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time', {}, JOB_QUEUE_DURATION_SECONDS_BUCKETS)
end
end
end
diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb
index 15ab2d54404..84944e95542 100644
--- a/app/services/clusters/gcp/finalize_creation_service.rb
+++ b/app/services/clusters/gcp/finalize_creation_service.rb
@@ -13,7 +13,7 @@ module Clusters
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}")
+ provider.make_errored!("Failed to configure Google Kubernetes Engine Cluster: #{e.message}")
end
private
diff --git a/app/services/clusters/gcp/verify_provision_status_service.rb b/app/services/clusters/gcp/verify_provision_status_service.rb
index f994aacd086..7cc4324677e 100644
--- a/app/services/clusters/gcp/verify_provision_status_service.rb
+++ b/app/services/clusters/gcp/verify_provision_status_service.rb
@@ -17,7 +17,7 @@ module Clusters
when 'DONE'
finalize_creation
else
- return provider.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
+ provider.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
end
end
end
diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb
index 88dfb7a4a90..7e5a77fb056 100644
--- a/app/services/create_deployment_service.rb
+++ b/app/services/create_deployment_service.rb
@@ -19,8 +19,8 @@ class CreateDeploymentService
environment.fire_state_event(action)
- return unless environment.save
- return if environment.stopped?
+ break unless environment.save
+ break if environment.stopped?
deploy.tap(&:update_merge_request_metrics!)
end
diff --git a/app/services/events/render_service.rb b/app/services/events/render_service.rb
index 0b62d8aedf1..bb72d7685dd 100644
--- a/app/services/events/render_service.rb
+++ b/app/services/events/render_service.rb
@@ -1,15 +1,17 @@
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
+ notes = events.map(&:note).compact
+
+ render_notes(notes, atom_request)
end
private
- def render_notes(notes, project, atom_request)
- Notes::RenderService.new(current_user).execute(notes, project, render_options(atom_request))
+ def render_notes(notes, atom_request)
+ Notes::RenderService
+ .new(current_user)
+ .execute(notes, render_options(atom_request))
end
def render_options(atom_request)
diff --git a/app/services/import_export_clean_up_service.rb b/app/services/import_export_clean_up_service.rb
index 6442406d77e..74088b970c9 100644
--- a/app/services/import_export_clean_up_service.rb
+++ b/app/services/import_export_clean_up_service.rb
@@ -10,7 +10,7 @@ class ImportExportCleanUpService
def execute
Gitlab::Metrics.measure(:import_export_clean_up) do
- return unless File.directory?(path)
+ next unless File.directory?(path)
clean_up_export_files
end
diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb
index 775efed48eb..9b7486cf53b 100644
--- a/app/services/labels/transfer_service.rb
+++ b/app/services/labels/transfer_service.rb
@@ -64,9 +64,14 @@ module Labels
end
def update_label_links(labels, old_label_id:, new_label_id:)
- LabelLink.joins(:label)
- .merge(labels)
- .where(label_id: old_label_id)
+ # use 'labels' relation to get label_link ids only of issues/MRs
+ # in the project being transferred.
+ # IDs are fetched in a separate query because MySQL doesn't
+ # allow referring of 'label_links' table in UPDATE query:
+ # https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/62435068
+ link_ids = labels.pluck('label_links.id')
+
+ LabelLink.where(id: link_ids, label_id: old_label_id)
.update_all(label_id: new_label_id)
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index c57a2445341..fe1ac70781e 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -71,8 +71,8 @@ module MergeRequests
params.delete(:source_project_id)
params.delete(:target_project_id)
- unless can?(current_user, :read_project, @source_project) &&
- can?(current_user, :read_project, @project)
+ unless can?(current_user, :create_merge_request_from, @source_project) &&
+ can?(current_user, :create_merge_request_in, @project)
raise Gitlab::Access::AccessDeniedError
end
diff --git a/app/services/notes/render_service.rb b/app/services/notes/render_service.rb
index a77e98c2b07..efc9d6da2aa 100644
--- a/app/services/notes/render_service.rb
+++ b/app/services/notes/render_service.rb
@@ -3,19 +3,18 @@ module Notes
# 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)
+ def execute(notes, options = {})
+ Banzai::ObjectRenderer
+ .new(user: current_user, redaction_context: options)
+ .render(notes, :note)
end
end
end
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index b82d9c64296..83e59a649b6 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -203,10 +203,11 @@ module NotificationRecipientService
attr_reader :action
attr_reader :previous_assignee
attr_reader :skip_current_user
- def initialize(target, current_user, action:, previous_assignee: nil, skip_current_user: true)
+ def initialize(target, current_user, action:, custom_action: nil, previous_assignee: nil, skip_current_user: true)
@target = target
@current_user = current_user
@action = action
+ @custom_action = custom_action
@previous_assignee = previous_assignee
@skip_current_user = skip_current_user
end
@@ -236,7 +237,13 @@ module NotificationRecipientService
add_mentions(current_user, target: target)
# Add the assigned users, if any
- assignees = custom_action == :new_issue ? target.assignees : target.assignee
+ assignees = case custom_action
+ when :new_issue
+ target.assignees
+ else
+ target.assignee
+ end
+
# We use the `:participating` notification level in order to match existing legacy behavior as captured
# in existing specs (notification_service_spec.rb ~ line 507)
add_recipients(assignees, :participating, NotificationReason::ASSIGNED) if assignees
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index f94c76cf3ac..274161df946 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -373,6 +373,20 @@ class NotificationService
end
end
+ def issue_due(issue)
+ recipients = NotificationRecipientService.build_recipients(
+ issue,
+ issue.author,
+ action: 'due',
+ custom_action: :issue_due,
+ skip_current_user: false
+ )
+
+ recipients.each do |recipient|
+ mailer.send(:issue_due_email, recipient.user.id, issue.id, recipient.reason).deliver_later
+ end
+ end
+
protected
def new_resource_email(target, method)
diff --git a/app/services/projects/create_from_template_service.rb b/app/services/projects/create_from_template_service.rb
index a549cfbabea..29b133cc466 100644
--- a/app/services/projects/create_from_template_service.rb
+++ b/app/services/projects/create_from_template_service.rb
@@ -8,9 +8,10 @@ module Projects
template_name = params.delete(:template_name)
file = Gitlab::ProjectTemplate.find(template_name).file
+ override_params = params.dup
params[:file] = file
- GitlabProjectsImportService.new(current_user, params).execute
+ GitlabProjectsImportService.new(current_user, params, override_params).execute
ensure
file&.close
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index aa14206db3b..71c93660b4b 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -91,7 +91,7 @@ module Projects
project.run_after_commit do
# self is now project
- GitlabShellWorker.perform_in(5.minutes, :remove_repository, self.repository_storage_path, new_path)
+ GitlabShellWorker.perform_in(5.minutes, :remove_repository, self.repository_storage, new_path)
end
else
false
@@ -100,9 +100,9 @@ module Projects
def mv_repository(from_path, to_path)
# There is a possibility project does not have repository or wiki
- return true unless gitlab_shell.exists?(project.repository_storage_path, from_path + '.git')
+ return true unless gitlab_shell.exists?(project.repository_storage, from_path + '.git')
- gitlab_shell.mv_repository(project.repository_storage_path, from_path, to_path)
+ gitlab_shell.mv_repository(project.repository_storage, from_path, to_path)
end
def attempt_rollback(project, message)
@@ -137,7 +137,7 @@ module Projects
return true unless Gitlab.config.registry.enabled
ContainerRepository.build_root_repository(project).tap do |repository|
- return repository.has_tags? ? repository.delete_tags! : true
+ break repository.has_tags? ? repository.delete_tags! : true
end
end
diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb
index 67178de75de..68c1af2396b 100644
--- a/app/services/projects/hashed_storage/migrate_repository_service.rb
+++ b/app/services/projects/hashed_storage/migrate_repository_service.rb
@@ -47,8 +47,8 @@ module Projects
private
def move_repository(from_name, to_name)
- from_exists = gitlab_shell.exists?(project.repository_storage_path, "#{from_name}.git")
- to_exists = gitlab_shell.exists?(project.repository_storage_path, "#{to_name}.git")
+ from_exists = gitlab_shell.exists?(project.repository_storage, "#{from_name}.git")
+ to_exists = gitlab_shell.exists?(project.repository_storage, "#{to_name}.git")
# If we don't find the repository on either original or target we should log that as it could be an issue if the
# project was not originally empty.
@@ -60,7 +60,7 @@ module Projects
return true
end
- gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
+ gitlab_shell.mv_repository(project.repository_storage, from_name, to_name)
end
def rollback_folder_move
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 5a23f0f0a62..61acdd58021 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -127,7 +127,7 @@ module Projects
end
def move_repo_folder(from_name, to_name)
- gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
+ gitlab_shell.mv_repository(project.repository_storage, from_name, to_name)
end
def execute_system_hooks
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 6cc51b6ee1b..0215994b1a7 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -138,8 +138,10 @@ module QuickActions
'Remove assignee'
end
end
- explanation do
- "Removes #{'assignee'.pluralize(issuable.assignees.size)} #{issuable.assignees.map(&:to_reference).to_sentence}."
+ explanation do |users = nil|
+ assignees = issuable.assignees
+ assignees &= users if users.present? && issuable.allows_multiple_assignees?
+ "Removes #{'assignee'.pluralize(assignees.size)} #{assignees.map(&:to_reference).to_sentence}."
end
params do
issuable.allows_multiple_assignees? ? '@user1 @user2' : ''
@@ -268,6 +270,26 @@ module QuickActions
end
end
+ desc 'Copy labels and milestone from other issue or merge request'
+ explanation do |source_issuable|
+ "Copy labels and milestone from #{source_issuable.to_reference}."
+ end
+ params '#issue | !merge_request'
+ condition do
+ issuable.persisted? &&
+ current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ end
+ parse_params do |issuable_param|
+ extract_references(issuable_param, :issue).first ||
+ extract_references(issuable_param, :merge_request).first
+ end
+ command :copy_metadata do |source_issuable|
+ if source_issuable.present? && source_issuable.project.id == issuable.project.id
+ @updates[:add_label_ids] = source_issuable.labels.map(&:id)
+ @updates[:milestone_id] = source_issuable.milestone.id if source_issuable.milestone
+ end
+ end
+
desc 'Add a todo'
explanation 'Adds a todo.'
condition do
diff --git a/app/services/repository_archive_clean_up_service.rb b/app/services/repository_archive_clean_up_service.rb
index aa84d36a206..9a88459b511 100644
--- a/app/services/repository_archive_clean_up_service.rb
+++ b/app/services/repository_archive_clean_up_service.rb
@@ -10,7 +10,7 @@ class RepositoryArchiveCleanUpService
def execute
Gitlab::Metrics.measure(:repository_archive_clean_up) do
- return unless File.directory?(path)
+ next unless File.directory?(path)
clean_up_old_archives
clean_up_empty_directories
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 00bf5434b7f..958ef065012 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -159,7 +159,7 @@ module SystemNoteService
body = if noteable.time_estimate == 0
"removed time estimate"
else
- "changed time estimate to #{parsed_time}"
+ "changed time estimate to #{parsed_time},"
end
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
diff --git a/app/services/test_hooks/base_service.rb b/app/services/test_hooks/base_service.rb
index e9aefb1fb75..aadc1ea644b 100644
--- a/app/services/test_hooks/base_service.rb
+++ b/app/services/test_hooks/base_service.rb
@@ -19,7 +19,7 @@ module TestHooks
error_message = catch(:validation_error) do
sample_data = self.__send__(trigger_data_method) # rubocop:disable GitlabSecurity/PublicSend
- return hook.execute(sample_data, trigger_key)
+ return hook.execute(sample_data, trigger_key) # rubocop:disable Cop/AvoidReturnFromBlocks
end
error(error_message)
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index f12f0466a1d..f8a237178d9 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -65,6 +65,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
!!model
end
+ def local_url
+ File.join('/', self.class.base_dir, dynamic_segment, filename)
+ end
+
private
# Designed to be overridden by child uploaders that have a dynamic path
diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb
index dd86753479d..2a5a830ce4f 100644
--- a/app/uploaders/job_artifact_uploader.rb
+++ b/app/uploaders/job_artifact_uploader.rb
@@ -6,10 +6,10 @@ class JobArtifactUploader < GitlabUploader
storage_options Gitlab.config.artifacts
- def size
- return super if model.size.nil?
+ def cached_size
+ return model.size if model.size.present? && !model.file_changed?
- model.size
+ size
end
def store_dir
@@ -20,7 +20,7 @@ class JobArtifactUploader < GitlabUploader
if file_storage?
File.open(path, "rb") if path
else
- ::Gitlab::Ci::Trace::HttpIO.new(url, size) if url
+ ::Gitlab::Ci::Trace::HttpIO.new(url, cached_size) if url
end
end
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index bd258e04d3f..a3549cada95 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -183,14 +183,6 @@ module ObjectStorage
StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options)
}
end
-
- def default_object_store
- if self.object_store_enabled? && self.direct_upload_enabled?
- Store::REMOTE
- else
- Store::LOCAL
- end
- end
end
# allow to configure and overwrite the filename
@@ -211,12 +203,13 @@ module ObjectStorage
end
def object_store
- @object_store ||= model.try(store_serialization_column) || self.class.default_object_store
+ # We use Store::LOCAL as null value indicates the local storage
+ @object_store ||= model.try(store_serialization_column) || Store::LOCAL
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def object_store=(value)
- @object_store = value || self.class.default_object_store
+ @object_store = value || Store::LOCAL
@storage = storage_for(object_store)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
@@ -302,6 +295,15 @@ module ObjectStorage
super
end
+ def store!(new_file = nil)
+ # when direct upload is enabled, always store on remote storage
+ if self.class.object_store_enabled? && self.class.direct_upload_enabled?
+ self.object_store = Store::REMOTE
+ end
+
+ super
+ end
+
private
def schedule_background_upload?
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
index ac31977e1a9..4eebb59110a 100644
--- a/app/views/admin/application_settings/_repository_storage.html.haml
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -21,7 +21,7 @@
.help-block
Manage repository storage paths. Learn more in the
= succeed "." do
- = link_to "repository storages documentation", help_page_path("administration/repository_storages")
+ = link_to "repository storages documentation", help_page_path("administration/repository_storage_paths")
.sub-section
%h4 Circuit breaker
.form-group
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
index 864e64b5fa9..48331c40bca 100644
--- a/app/views/admin/application_settings/_signin.html.haml
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -24,6 +24,7 @@
- if omniauth_enabled? && button_based_providers.any?
.form-group
= f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2'
+ = hidden_field_tag 'application_setting[enabled_oauth_sign_in_sources][]'
.col-sm-10
.btn-group{ data: { toggle: 'buttons' } }
- oauth_providers_checkboxes.each do |source|
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index cbc779548f6..a75dd90fe6b 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -32,6 +32,7 @@
.form-group
= f.label :import_sources, class: 'control-label col-sm-2'
.col-sm-10
+ = hidden_field_tag 'application_setting[import_sources][]'
- import_sources_checkboxes('import-sources-help').each do |source|
.checkbox= source
%span.help-block#import-sources-help
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 05c41082882..bbf0e0fb95c 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -126,6 +126,7 @@
GitLab
%span.pull-right
= Gitlab::VERSION
+ = "(#{Gitlab::REVISION})"
%p
GitLab Shell
%span.pull-right
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index c47b8a88f56..aeba9788fda 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -101,7 +101,7 @@
- if @project.archived?
%li
%span.light archived:
- %strong repository is read-only
+ %strong project is read-only
%li
%span.light access:
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index bbfeceff5b9..2ff4221efbd 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -33,7 +33,7 @@
= link_to 'Block', block_admin_user_path(user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put
- if user.access_locked?
%li
- = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
+ = link_to _('Unlock'), unlock_admin_user_path(user), method: :put, data: { confirm: _('Are you sure?') }
- if can?(current_user, :destroy_user, user)
%li.divider
- if user.can_be_removed?
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index 5f07d2720c2..4b3c52af16a 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -3,13 +3,13 @@
.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } }
- awards_sort(grouped_emojis).each do |emoji, awards|
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button",
- class: [(award_state_class(awards, current_user)), (award_user_authored_class(emoji) if user_authored)],
+ class: [(award_state_class(awardable, awards, current_user)), (award_user_authored_class(emoji) if user_authored)],
data: { placement: "bottom", title: award_user_list(awards, current_user) } }
= emoji_icon(emoji)
%span.award-control-text.js-counter
= awards.count
- - if current_user
+ - if can?(current_user, :award_emoji, awardable)
.award-menu-holder.js-award-holder
%button.btn.award-control.has-tooltip.js-add-award{ type: 'button',
'aria-label': 'Add reaction',
diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder
index 70ec6bc6257..d7b6fb9a4a1 100644
--- a/app/views/dashboard/issues.atom.builder
+++ b/app/views/dashboard/issues.atom.builder
@@ -1,5 +1,5 @@
xml.title "#{current_user.name} issues"
-xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
+xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
xml.id issues_dashboard_url
xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index bb472b4c900..4bf04dadf01 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -2,12 +2,12 @@
- page_title _("Issues")
- @breadcrumb_link = issues_dashboard_path(assignee_id: current_user.id)
= content_for :meta_tags do
- = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues")
+ = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{current_user.name} issues")
.top-area
= render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set
.nav-controls
- = link_to params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do
+ = link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do
= icon('rss')
= render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues
diff --git a/app/views/devise/shared/_tab_single.html.haml b/app/views/devise/shared/_tab_single.html.haml
index f943d25e41a..7bd414d64c3 100644
--- a/app/views/devise/shared/_tab_single.html.haml
+++ b/app/views/devise/shared/_tab_single.html.haml
@@ -1,3 +1,3 @@
-%ul.nav-links.nav-tabs.new-session-tabs.single-tab
+%ul.nav-links.new-session-tabs.single-tab
%li.active
%a= tab_title
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index 270191f9452..f50e0724e09 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -1,4 +1,4 @@
-%ul.new-session-tabs.nav-links.nav-tabs{ class: ('custom-provider-tabs' if form_based_providers.any?) }
+%ul.nav-links.new-session-tabs{ class: ('custom-provider-tabs' if form_based_providers.any?) }
- if crowd_enabled?
%li.active
= link_to "Crowd", "#crowd", 'data-toggle' => 'tab'
diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml
index 1ba6d390875..fa3c3df7f60 100644
--- a/app/views/devise/shared/_tabs_normal.html.haml
+++ b/app/views/devise/shared/_tabs_normal.html.haml
@@ -1,4 +1,4 @@
-%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist' }
+%ul.nav-links.new-session-tabs{ role: 'tablist' }
%li.active{ role: 'presentation' }
%a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in
- if allow_signup?
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index 8680ec2e298..646e89e9bd1 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -7,7 +7,7 @@
- unless expanded
- diff_data = { lines_path: project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion) }
-.diff-file.file-holder{ class: diff_file_class, data: diff_data }
+.diff-file.file-holder.js-lazy-load-discussion{ class: diff_file_class, data: diff_data }
.js-file-title.file-title.file-title-flex-parent
.file-header-content
= render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false
@@ -28,8 +28,11 @@
%tr.line_holder.line-holder-placeholder
%td.old_line.diff-line-num
%td.new_line.diff-line-num
- %td.line_content
+ %td.line_content.js-success-lazy-load
.js-code-placeholder
+ %td.js-error-lazy-load-diff.hidden.diff-loading-error-block
+ - button = button_tag(_("Try again"), class: "btn-link btn-link-retry btn-no-padding js-toggle-lazy-diff-retry-button")
+ = _("Unable to load the diff. %{button_try_again}").html_safe % { button_try_again: button}
= render "discussions/diff_discussion", discussions: [discussion], expanded: true
- else
- partial = (diff_file.new_file? || diff_file.deleted_file?) ? 'single_image_diff' : 'replaced_image_diff'
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 86cd0759a2c..3375e01b3a1 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,4 +1,6 @@
- breadcrumb_title "General Settings"
+- @content_class = "limit-container-width" unless fluid_layout
+
.panel.panel-default.prepend-top-default
.panel-heading
Group settings
diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder
index a239ea8caf0..2a385b661e5 100644
--- a/app/views/groups/issues.atom.builder
+++ b/app/views/groups/issues.atom.builder
@@ -1,5 +1,5 @@
xml.title "#{@group.name} issues"
-xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
+xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
xml.link href: issues_group_url, rel: "alternate", type: "text/html"
xml.id issues_group_url
xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 36df03302e8..bbfbea4ac7a 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -1,6 +1,6 @@
- page_title "Issues"
= content_for :meta_tags do
- = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues")
+ = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@group.name} issues")
- if group_issues_count(state: 'all').zero?
= render 'shared/empty_states/issues', project_select_button: true
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index eb32f393310..6f53f5ac1ae 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -19,8 +19,8 @@
%li.dropdown-bold-header GitLab
- if @project&.persisted?
- - create_project_issue = can?(current_user, :create_issue, @project)
- - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+ - create_project_issue = show_new_issue_link?(@project)
+ - merge_project = merge_request_source_project_for_project(@project)
- create_project_snippet = can?(current_user, :create_project_snippet, @project)
- if create_project_issue || merge_project || create_project_snippet
%li.dropdown-bold-header This project
diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb
index 198f30a1dc4..8e20c4a4b2a 100644
--- a/app/views/layouts/mailer.text.erb
+++ b/app/views/layouts/mailer.text.erb
@@ -1,4 +1,4 @@
<%= yield -%>
----
+-- <%# signature marker %>
You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 93f674b9d3c..196db08cebd 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -13,7 +13,7 @@
.nav-icon-container
= sprite_icon('project')
%span.nav-item-name
- Overview
+ Project
%ul.sidebar-sub-level-items
= nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
diff --git a/app/views/layouts/notify.text.erb b/app/views/layouts/notify.text.erb
index de48f548a1b..9dc490efa9a 100644
--- a/app/views/layouts/notify.text.erb
+++ b/app/views/layouts/notify.text.erb
@@ -1,6 +1,6 @@
<%= yield -%>
----
+-- <%# signature marker %>
<% if @target_url -%>
<% if @reply_by_email -%>
<%= "Reply to this email directly or view it on GitLab: #{@target_url}" -%>
diff --git a/app/views/notify/issue_due_email.html.haml b/app/views/notify/issue_due_email.html.haml
new file mode 100644
index 00000000000..e81144b8fcb
--- /dev/null
+++ b/app/views/notify/issue_due_email.html.haml
@@ -0,0 +1,12 @@
+%p.details
+ #{link_to @issue.author_name, user_url(@issue.author)}'s issue is due soon.
+
+- if @issue.assignees.any?
+ %p
+ Assignee: #{@issue.assignee_list}
+%p
+ This issue is due on: #{@issue.due_date.to_s(:medium)}
+
+- if @issue.description
+ %div
+ = markdown(@issue.description, pipeline: :email, author: @issue.author)
diff --git a/app/views/notify/issue_due_email.text.erb b/app/views/notify/issue_due_email.text.erb
new file mode 100644
index 00000000000..3c7a57a8a2e
--- /dev/null
+++ b/app/views/notify/issue_due_email.text.erb
@@ -0,0 +1,7 @@
+The following issue is due on <%= @issue.due_date %>:
+
+Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
+Author: <%= @issue.author_name %>
+Assignee: <%= @issue.assignee_list %>
+
+<%= @issue.description %>
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 1bd10018b40..d1eae05c46c 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -20,7 +20,7 @@
- else
%p
Download the Google Authenticator application from App Store or Google Play Store and scan this code.
- More information is available in the #{link_to('documentation', help_page_path('profile/two_factor_authentication'))}.
+ More information is available in the #{link_to('documentation', help_page_path('user/profile/account/two_factor_authentication'))}.
.row.append-bottom-10
.col-md-4
= raw @qr_code
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 6a1035d2dc7..f6d396c8127 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -13,6 +13,7 @@
#{time_ago_with_tooltip(event.created_at)}
- .flex-right
- = link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm qa-create-merge-request" do
- #{ _('Create merge request') }
+ - if can?(current_user, :create_merge_request_in, event.project.default_merge_request_target)
+ .flex-right
+ = link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm qa-create-merge-request" do
+ #{ _('Create merge request') }
diff --git a/app/views/projects/_visibility_select.html.haml b/app/views/projects/_visibility_select.html.haml
deleted file mode 100644
index 4026b9e3c46..00000000000
--- a/app/views/projects/_visibility_select.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- if can_change_visibility_level?(@project, current_user)
- .select-wrapper
- = form.select(model_method, visibility_select_options(@project, selected_level), {}, class: 'form-control visibility-select select-control')
- = icon('chevron-down')
-- else
- .info.js-locked{ data: { help_block: visibility_level_description(@project.visibility_level, @project) } }
- = visibility_level_icon(@project.visibility_level)
- %strong
- = visibility_level_label(@project.visibility_level)
diff --git a/app/views/projects/blob/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml
index 3124443b4e4..b9663bbba15 100644
--- a/app/views/projects/blob/_viewer.html.haml
+++ b/app/views/projects/blob/_viewer.html.haml
@@ -2,13 +2,16 @@
- render_error = viewer.render_error
- rich_type = viewer.type == :rich ? viewer.partial_name : nil
- load_async = local_assigns.fetch(:load_async, viewer.load_async? && render_error.nil?)
+- external_embed = local_assigns.fetch(:external_embed, false)
-- viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_async
+- viewer_url = local_assigns.fetch(:viewer_url) { url_for(safe_params.merge(viewer: viewer.type, format: :json)) } if load_async
.blob-viewer{ data: { type: viewer.type, rich_type: rich_type, url: viewer_url }, class: ('hidden' if hidden) }
- if render_error
= render 'projects/blob/render_error', viewer: viewer
- elsif load_async
= render viewer.loading_partial_path, viewer: viewer
+ - elsif external_embed
+ = render 'projects/blob/viewers/highlight_embed', blob: viewer.blob
- else
- viewer.prepare!
diff --git a/app/views/projects/blob/viewers/_highlight_embed.html.haml b/app/views/projects/blob/viewers/_highlight_embed.html.haml
new file mode 100644
index 00000000000..9bd4ef6ad0b
--- /dev/null
+++ b/app/views/projects/blob/viewers/_highlight_embed.html.haml
@@ -0,0 +1,7 @@
+.file-content.code.js-syntax-highlight
+ .line-numbers
+ - if blob.data.present?
+ - blob.data.each_line.each_with_index do |_, index|
+ %span.diff-line-num= index + 1
+ .blob-content{ data: { blob_id: blob.id } }
+ = highlight(blob.path, blob.data, repository: nil, plain: blob.no_highlighting?)
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 883dfb3e6c8..d0c01f95cb7 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -4,7 +4,7 @@
- diverging_commit_counts = @repository.diverging_commit_counts(branch)
- number_commits_behind = diverging_commit_counts[:behind]
- number_commits_ahead = diverging_commit_counts[:ahead]
-- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+- merge_project = merge_request_source_project_for_project(@project)
%li{ class: "branch-item js-branch-#{branch.name}" }
.branch-info
.branch-title
@@ -29,7 +29,7 @@
= s_('Branches|Cant find HEAD commit for this branch')
- if branch.name != @repository.root_ref
- .divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
+ .divergence-graph.hidden-xs{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
default_branch: @repository.root_ref,
number_commits_ahead: diverging_count_label(number_commits_ahead) } }
.graph-side
@@ -61,7 +61,7 @@
title: s_('Branches|The default branch cannot be deleted') }
= icon("trash-o")
- elsif protected_branch?(@project, branch)
- - if can?(current_user, :delete_protected_branch, @project)
+ - if can?(current_user, :push_to_delete_protected_branch, @project)
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip",
title: s_('Branches|Delete protected branch'),
data: { toggle: "modal",
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 18e948ce35a..2e86a7d36d7 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -1,13 +1,17 @@
-- if current_user
+- can_create_issue = show_new_issue_link?(@project)
+- can_create_project_snippet = can?(current_user, :create_project_snippet, @project)
+- can_push_code = can?(current_user, :push_code, @project)
+- create_mr_from_new_fork = can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project)
+- merge_project = merge_request_source_project_for_project(@project)
+
+- show_menu = can_create_issue || can_create_project_snippet || can_push_code || create_mr_from_new_fork || merge_project
+
+- if show_menu
.project-action-button.dropdown.inline
%a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...') }
= icon('plus')
= icon("caret-down")
%ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown
- - can_create_issue = can?(current_user, :create_issue, @project)
- - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- - can_create_project_snippet = can?(current_user, :create_project_snippet, @project)
-
- if can_create_issue || merge_project || can_create_project_snippet
%li.dropdown-header= _('This project')
@@ -20,17 +24,17 @@
- if can_create_project_snippet
%li= link_to _('New snippet'), new_project_snippet_path(@project)
- - if can?(current_user, :push_code, @project)
+ - if can_push_code
%li.dropdown-header= _('This repository')
- - if can?(current_user, :push_code, @project)
+ - if can_push_code
%li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master')
- unless @project.empty_repo?
%li= link_to _('New branch'), new_project_branch_path(@project)
%li= link_to _('New tag'), new_project_tag_path(@project)
- - elsif current_user && current_user.already_forked?(@project)
+ - elsif can_collaborate_with_project?(@project)
%li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master')
- - elsif can?(current_user, :fork_project, @project)
+ - elsif create_mr_from_new_fork
- continue_params = { to: project_new_blob_path(@project, @project.default_branch || 'master'),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now }
diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml
index 112dde66ff7..5f49d03b1bb 100644
--- a/app/views/projects/clusters/_empty_state.html.haml
+++ b/app/views/projects/clusters/_empty_state.html.haml
@@ -7,5 +7,6 @@
- link_to_help_page = link_to(_('Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
%p= s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
- .text-center
- = link_to s_('ClusterIntegration|Add Kubernetes cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
+ - if can?(current_user, :create_cluster, @project)
+ .text-center
+ = link_to s_('ClusterIntegration|Add Kubernetes cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml
index ebb7d247125..e004966bdcc 100644
--- a/app/views/projects/clusters/new.html.haml
+++ b/app/views/projects/clusters/new.html.haml
@@ -8,6 +8,6 @@
%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up Kubernetes cluster integration')
%p= s_('ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab')
- = link_to s_('ClusterIntegration|Create on GKE'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
+ = link_to s_('ClusterIntegration|Create on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
%p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster')
= link_to s_('ClusterIntegration|Add an existing Kubernetes cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 74c5317428c..213c4c90a0e 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -1,3 +1,5 @@
+- can_collaborate = can_collaborate_with_project?(@project)
+
.page-content-header.js-commit-box{ 'data-commit-path' => branches_project_commit_path(@project, @commit.id) }
.header-main-content
= render partial: 'signature', object: @commit.signature
@@ -32,12 +34,13 @@
%li.visible-xs-block.visible-sm-block
= link_to project_tree_path(@project, @commit) do
#{ _('Browse Files') }
- - unless @commit.has_been_reverted?(current_user)
+ - if can_collaborate && !@commit.has_been_reverted?(current_user)
%li.clearfix
= revert_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false)
- %li.clearfix
- = cherry_pick_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false)
- - if can_collaborate_with_project?
+ - if can_collaborate
+ %li.clearfix
+ = cherry_pick_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false)
+ - if can?(current_user, :push_code, @project)
%li.clearfix
= link_to s_("CreateTag|Tag"), new_project_tag_path(@project, ref: @commit)
%li.divider
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index abb292f8f27..541ae905246 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -17,6 +17,6 @@
.limited-width-notes
= render "shared/notes/notes_with_form", :autocomplete => true
- - if can_collaborate_with_project?
+ - if can_collaborate_with_project?(@project)
- %w(revert cherry-pick).each do |type|
= render "projects/commit/change", type: type, commit: @commit, title: @commit.title
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 163432c9263..3fd0fa348b3 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -5,6 +5,7 @@
- link = commit_path(project, commit, merge_request: merge_request)
- cache_key = [project.full_path,
+ ref,
commit.id,
Gitlab::CurrentSettings.current_application_settings,
@path.presence,
@@ -21,7 +22,7 @@
= author_avatar(commit, size: 36)
.commit-detail.flex-list
- .commit-content
+ .commit-content.qa-commit-content
- if view_details && merge_request
= link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title"
- else
@@ -54,7 +55,7 @@
- if commit.status(ref)
= render_commit_status(commit, ref: ref)
- .js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id) } }
+ .js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } }
.commit-sha-group
.label.label-monospace
diff --git a/app/views/projects/diffs/_collapsed.html.haml b/app/views/projects/diffs/_collapsed.html.haml
index 8772bd4705f..5762f4d86d7 100644
--- a/app/views/projects/diffs/_collapsed.html.haml
+++ b/app/views/projects/diffs/_collapsed.html.haml
@@ -1,5 +1,5 @@
- diff_file = viewer.diff_file
-- url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
+- url = url_for(safe_params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
.nothing-here-block.diff-collapsed{ data: { diff_for_path: url } }
This diff is collapsed.
%a.click-to-expand Click to expand it.
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 99eeb9551e3..0994498c6be 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -114,17 +114,18 @@
Archive project
- if @project.archived?
%p
- Unarchiving the project will mark its repository as active. The project can be committed to.
+ Unarchiving the project will restore people's ability to make changes to it.
+ The repository can be committed to, and issues, comments and other entities can be created.
%strong Once active this project shows up in the search and on the dashboard.
= link_to 'Unarchive project', unarchive_project_path(@project),
- data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." },
+ data: { confirm: "Are you sure that you want to unarchive this project?" },
method: :post, class: "btn btn-success"
- else
%p
- Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches.
- %strong Archived projects cannot be committed to!
+ Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches.
+ %strong The repository cannot be committed to, and no issues, comments or other entities can be created.
= link_to 'Archive project', archive_project_path(@project),
- data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." },
+ data: { confirm: "Are you sure that you want to archive this project?" },
method: :post, class: "btn btn-warning"
.sub-section.rename-respository
%h4.warning-title
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index b15fe514a08..a066f9f4cca 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -22,7 +22,7 @@
%hr
%p
- - link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'))
+ - link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'))
- link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), new_project_cluster_path(@project))
= s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster }
@@ -58,7 +58,9 @@
touch README.md
git add README.md
git commit -m "add README"
- git push -u origin master
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin master
%fieldset
%h5 Existing folder
@@ -69,7 +71,9 @@
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
git add .
git commit -m "Initial commit"
- git push -u origin master
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin master
%fieldset
%h5 Existing Git repository
@@ -78,8 +82,10 @@
cd existing_repo
git remote rename origin old-origin
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- git push -u origin --all
- git push -u origin --tags
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin --all
+ git push -u origin --tags
- if can? current_user, :remove_project, @project
.prepend-top-20
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index 475c6ba4d3d..a603b1024eb 100644..100755
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -12,7 +12,7 @@
- if @namespaces.present?
.fork-thumbnail-container.js-fork-content
%h5.prepend-top-0.append-bottom-0.prepend-left-default.append-right-default
- Click to fork the project
+ = _("Select a namespace to fork the project")
- @namespaces.each do |namespace|
= render 'fork_button', namespace: namespace
- else
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 0c58dd60e2c..e27f5658e87 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -26,7 +26,7 @@
- if issue.milestone
%span.issuable-milestone.hidden-xs
&nbsp;
- = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: issuable_milestone_tooltip_title(issue) } do
+ = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_due_date(issue.milestone) } do
= icon('clock-o')
= issue.milestone.title
- if issue.due_date
diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml
index 0d39edb7bfd..297b928f020 100644
--- a/app/views/projects/issues/_nav_btns.html.haml
+++ b/app/views/projects/issues/_nav_btns.html.haml
@@ -1,10 +1,11 @@
-= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
+= link_to safe_params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
= icon('rss')
- 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: finder.assignee.try(:id),
- milestone_id: finder.milestones.first.try(:id) }),
- class: "btn btn-new",
- title: "New issue",
- id: "new_issue_link"
+- if show_new_issue_link?(@project)
+ = link_to "New issue", new_project_issue_path(@project,
+ 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 36e24037214..4b8bf578b28 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -1,8 +1,8 @@
-- can_create_merge_request = can?(current_user, :create_merge_request, @project)
-- data_action = can_create_merge_request ? 'create-mr' : 'create-branch'
-- value = can_create_merge_request ? 'Create merge request' : 'Create branch'
-
- if can?(current_user, :push_code, @project)
+ - can_create_merge_request = can?(current_user, :create_merge_request_in, @project)
+ - data_action = can_create_merge_request ? 'create-mr' : 'create-branch'
+ - value = can_create_merge_request ? 'Create merge request' : 'Create branch'
+
- can_create_path = can_create_branch_project_issue_path(@project, @issue)
- create_mr_path = create_merge_request_project_issue_path(@project, @issue, branch_name: @issue.to_branch_name, ref: @project.default_branch)
- create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid)
diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder
index 4029926f373..6330245954e 100644
--- a/app/views/projects/issues/index.atom.builder
+++ b/app/views/projects/issues/index.atom.builder
@@ -1,5 +1,5 @@
xml.title "#{@project.name} issues"
-xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
+xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
xml.link href: project_issues_url(@project), rel: "alternate", type: "text/html"
xml.id project_issues_url(@project)
xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index c427a9eedc2..1e7737aeb97 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -5,7 +5,7 @@
- new_issue_email = @project.new_issuable_address(current_user, 'issue')
= content_for :meta_tags do
- = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues")
+ = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@project.name} issues")
- if project_issues(@project).exists?
%div{ class: (container_class) }
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index ec7e87219f5..f1fc1c2316d 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -7,6 +7,7 @@
- can_update_issue = can?(current_user, :update_issue, @issue)
- can_report_spam = @issue.submittable_as_spam_by?(current_user)
+- can_create_issue = show_new_issue_link?(@project)
.detail-page-header
.detail-page-header-body
@@ -42,16 +43,18 @@
%li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- if can_report_spam
%li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
- - if can_update_issue || can_report_spam
- %li.divider
- %li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
+ - if can_create_issue
+ - if can_update_issue || can_report_spam
+ %li.divider
+ %li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
= render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue
- if can_report_spam
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
- = link_to new_project_issue_path(@project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
- New issue
+ - if can_create_issue
+ = link_to new_project_issue_path(@project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
+ New issue
.issue-details.issuable-details
.detail-page-description.content-block
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index 0b57ebedebd..7f0bef5ede0 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -1,15 +1,8 @@
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.sidebar-container
.blocks-container
- .block
- %strong.inline.prepend-top-8
- = @build.name
- - if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'js-retry-button pull-right btn btn-inverted-secondary btn-retry visible-md-block visible-lg-block', method: :post
- %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-build-toggle{ href: "#", 'aria-label': 'Toggle Sidebar', role: 'button' }
- = icon('angle-double-right')
- #js-details-block-vue
+ #js-details-block-vue{ data: { can_user_retry: can?(current_user, :update_build, @build) && @build.retryable? } }
- if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
.block
diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml
index 9963cc93633..fe1c338b634 100644
--- a/app/views/projects/jobs/index.html.haml
+++ b/app/views/projects/jobs/index.html.haml
@@ -15,7 +15,7 @@
- unless @repository.gitlab_ci_yml
= link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
- = link_to ci_lint_path, class: 'btn btn-default' do
+ = link_to project_ci_lint_path(@project), class: 'btn btn-default' do
%span CI lint
.content-list.builds-content-list
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index 8beb4ffef45..cbbcc8f1db5 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -55,7 +55,7 @@
- else
Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
- - if @build.has_trace?
+ - if @build.running? || @build.has_trace?
.build-trace-container.prepend-top-default
.top-bar.js-top-bar
.js-truncated-info.truncated-info.hidden-xs.pull-left.hidden<
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index a94267deeb2..027a9ff1416 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -23,7 +23,7 @@
- if merge_request.milestone
%span.issuable-milestone.hidden-xs
&nbsp;
- = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: issuable_milestone_tooltip_title(merge_request) } do
+ = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_due_date(merge_request.milestone) } do
= icon('clock-o')
= merge_request.milestone.title
- if merge_request.target_project.default_branch != merge_request.target_branch
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index 9d5cebdda53..f81db9b4e28 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -3,7 +3,7 @@
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f|
.hide.alert.alert-danger.mr-compare-errors
- .merge-request-branches.js-merge-request-new-compare.row{ 'data-target-project-url': project_new_merge_request_update_branches_path(@source_project), 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
+ .js-merge-request-new-compare.row{ 'data-target-project-url': project_new_merge_request_update_branches_path(@source_project), 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
.col-md-6
.panel.panel-default.panel-new-merge-request
.panel-heading
diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml
index 376ac377562..68780cedeb1 100644
--- a/app/views/projects/merge_requests/creations/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml
@@ -26,16 +26,16 @@
- else
%ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.commits-tab.active
- = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
+ = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
Commits
%span.badge= @commits.size
- if @pipelines.any?
%li.builds-tab
- = link_to url_for(params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
+ = link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
Pipelines
%span.badge= @pipelines.size
%li.diffs-tab
- = link_to url_for(params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
+ = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
Changes
%span.badge= @merge_request.diff_size
@@ -46,7 +46,7 @@
-# This tab is always loaded via AJAX
- if @pipelines.any?
#pipelines.pipelines.tab-pane
- = render 'projects/merge_requests/pipelines', endpoint: url_for(params.merge(action: 'pipelines', format: :json)), disable_initialization: true
+ = render 'projects/merge_requests/pipelines', endpoint: url_for(safe_params.merge(action: 'pipelines', format: :json)), disable_initialization: true
.mr-loading-status
= spinner
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index b2c0d9e1cfa..623380c9c61 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -1,6 +1,6 @@
- @no_container = true
- @can_bulk_update = can?(current_user, :admin_merge_request, @project)
-- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+- merge_project = merge_request_source_project_for_project(@project)
- new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project
- page_title "Merge Requests"
diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml
index 5ea653ccad5..b4fe1cabdfd 100644
--- a/app/views/projects/notes/_actions.html.haml
+++ b/app/views/projects/notes/_actions.html.haml
@@ -36,7 +36,7 @@
%template{ 'v-else' => '' }
= render 'shared/icons/icon_resolve_discussion.svg'
-- if current_user
+- if can?(current_user, :award_emoji, note)
- if note.emoji_awardable?
- user_authored = note.user_authored?(current_user)
.note-actions-item
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 852143ecb2a..218e7338c83 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -26,7 +26,7 @@
%ul
- pipeline.yaml_errors.split(",").each do |error|
%li= error
- You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path}
+ You can also test your .gitlab-ci.yml in the #{link_to "Lint", project_ci_lint_path(@project)}
- if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
.bs-callout.bs-callout-warning
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 877101b05ca..8f2142af2ce 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -1,24 +1,25 @@
- breadcrumb_title "Pipelines"
-- page_title "New Pipeline"
+- page_title = s_("Pipeline|Run Pipeline")
%h3.page-title
- New Pipeline
+ = s_("Pipeline|Run Pipeline")
%hr
= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f|
= form_errors(@pipeline)
.form-group
- = f.label :ref, 'Create for', class: 'control-label'
+ = f.label :ref, s_('Pipeline|Run on'), class: 'control-label'
.col-sm-10
= hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
= dropdown_tag(params[:ref] || @project.default_branch,
options: { toggle_class: 'js-branch-select wide git-revision-dropdown-toggle',
- filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: "Search branches",
+ filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: s_("Pipeline|Search branches"),
data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
- .help-block Existing branch name, tag
+ .help-block
+ = s_("Pipeline|Existing branch name, tag")
.form-actions
- = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
- = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-cancel'
+ = f.submit s_('Pipeline|Run pipeline'), class: 'btn btn-success', tabindex: 3
+ = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-default pull-right'
-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml
index 98d56a3e5c5..12ccae10260 100644
--- a/app/views/projects/protected_branches/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml
@@ -7,8 +7,8 @@
- content_for :push_access_levels do
.push_access_levels-container
= dropdown_tag('Select',
- options: { toggle_class: 'js-allowed-to-push wide',
- dropdown_class: 'dropdown-menu-selectable capitalize-header',
+ options: { toggle_class: 'js-allowed-to-push qa-allowed-to-push-select wide',
+ dropdown_class: 'dropdown-menu-selectable qa-allowed-to-push-dropdown capitalize-header',
data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})
= render 'projects/protected_branches/shared/create_protected_branch'
diff --git a/app/views/projects/protected_branches/_update_protected_branch.html.haml b/app/views/projects/protected_branches/_update_protected_branch.html.haml
index c61b2951e1e..98363f2018a 100644
--- a/app/views/projects/protected_branches/_update_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_update_protected_branch.html.haml
@@ -6,5 +6,5 @@
%td
= hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level
= dropdown_tag( (protected_branch.push_access_levels.first.humanize || 'Select') ,
- options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
+ options: { toggle_class: 'js-allowed-to-push qa-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
data: { field_name: "allowed_to_push_#{protected_branch.id}", access_level_id: protected_branch.push_access_levels.first.id }})
diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml
index a09c13176c3..d1ed438eb21 100644
--- a/app/views/projects/protected_branches/shared/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml
@@ -1,4 +1,4 @@
-.panel.panel-default.protected-branches-list.js-protected-branches-list
+.protected-branches-list.js-protected-branches-list.qa-protected-branches-list
- if @protected_branches.empty?
.panel-heading
%h3.panel-title
diff --git a/app/views/projects/protected_branches/shared/_dropdown.html.haml b/app/views/projects/protected_branches/shared/_dropdown.html.haml
index 74435236808..b3d6068039a 100644
--- a/app/views/projects/protected_branches/shared/_dropdown.html.haml
+++ b/app/views/projects/protected_branches/shared/_dropdown.html.haml
@@ -1,8 +1,8 @@
= f.hidden_field(:name)
= dropdown_tag('Select branch or create wildcard',
- options: { toggle_class: 'js-protected-branch-select js-filter-submit wide git-revision-dropdown-toggle',
- filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: "Search protected branches",
+ options: { toggle_class: 'js-protected-branch-select js-filter-submit wide git-revision-dropdown-toggle qa-protected-branch-select',
+ filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown qa-protected-branch-dropdown", placeholder: "Search protected branches",
footer_content: true,
data: { show_no: true, show_any: true, show_upcoming: true,
selected: params[:protected_branch_name],
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml
index 55d87c35a80..fd5c1aa342a 100644
--- a/app/views/projects/protected_branches/shared/_index.html.haml
+++ b/app/views/projects/protected_branches/shared/_index.html.haml
@@ -4,7 +4,7 @@
.settings-header
%h4
Protected Branches
- %button.btn.js-settings-toggle{ type: 'button' }
+ %button.btn.js-settings-toggle.qa-expand-protected-branches{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Keep stable branches secure and force developers to use merge requests.
diff --git a/app/views/projects/protected_branches/shared/_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_protected_branch.html.haml
index 10b81e42572..f5b21f0e887 100644
--- a/app/views/projects/protected_branches/shared/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/shared/_protected_branch.html.haml
@@ -2,7 +2,7 @@
%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) } }
%td
- %span.ref-name= protected_branch.name
+ %span.ref-name.qa-protected-branch-name= protected_branch.name
- if @project.root_ref?(protected_branch.name)
%span.label.label-info.prepend-left-5 default
diff --git a/app/views/projects/protected_tags/shared/_tags_list.html.haml b/app/views/projects/protected_tags/shared/_tags_list.html.haml
index 02908e16dc5..3ed82e51dbe 100644
--- a/app/views/projects/protected_tags/shared/_tags_list.html.haml
+++ b/app/views/projects/protected_tags/shared/_tags_list.html.haml
@@ -1,4 +1,4 @@
-.panel.panel-default.protected-tags-list.js-protected-tags-list
+.protected-tags-list.js-protected-tags-list
- if @protected_tags.empty?
.panel-heading
%h3.panel-title
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 2c80f7c3fa3..76f57320f99 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -29,7 +29,7 @@
docker login #{Gitlab.config.registry.host_port}
%br
%p
- - deploy_token = link_to(_('deploy token'), help_page_path('user/projects/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank')
+ - deploy_token = link_to(_('deploy token'), help_page_path('user/project/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank')
= s_('ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token }
%br
%p
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
new file mode 100644
index 00000000000..7b410101c05
--- /dev/null
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -0,0 +1,40 @@
+.row.prepend-top-default
+ .col-lg-12
+ = form_for @project, url: project_settings_ci_cd_path(@project) do |f|
+ = form_errors(@project)
+ %fieldset.builds-feature
+ .form-group
+ - message = auto_devops_warning_message(@project)
+ - ci_file_formatted = '<code>.gitlab-ci.yml</code>'.html_safe
+ - if message
+ %p.settings-message.text-center
+ = message.html_safe
+ = f.fields_for :auto_devops_attributes, @auto_devops do |form|
+ .radio
+ = form.label :enabled_true do
+ = form.radio_button :enabled, 'true'
+ %strong= s_('CICD|Enable Auto DevOps')
+ %br
+ = s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted }
+
+ .radio
+ = form.label :enabled_false do
+ = form.radio_button :enabled, 'false'
+ %strong= s_('CICD|Disable Auto DevOps')
+ %br
+ = s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted }
+
+ .radio
+ = form.label :enabled_ do
+ = form.radio_button :enabled, ''
+ %strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" }
+ %br
+ = s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted }
+
+ = form.label :domain, class:"prepend-top-10" do
+ = _('Domain')
+ = form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
+ .help-block
+ = s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.')
+
+ = f.submit 'Save changes', class: "btn btn-success prepend-top-15"
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index 20868f9ba5d..80c226ad273 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -3,44 +3,6 @@
= form_for @project, url: project_settings_ci_cd_path(@project) do |f|
= form_errors(@project)
%fieldset.builds-feature
- .form-group
- %h5 Auto DevOps (Beta)
- %p
- Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.
- = link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md')
- - message = auto_devops_warning_message(@project)
- - if message
- %p.settings-message.text-center
- = message.html_safe
- = f.fields_for :auto_devops_attributes, @auto_devops do |form|
- .radio
- = form.label :enabled_true do
- = form.radio_button :enabled, 'true'
- %strong Enable Auto DevOps
- %br
- %span.descr
- The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project.
-
- .radio
- = form.label :enabled_false do
- = form.radio_button :enabled, 'false'
- %strong Disable Auto DevOps
- %br
- %span.descr
- An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery.
-
- .radio
- = form.label :enabled_ do
- = form.radio_button :enabled, ''
- %strong Instance default (#{Gitlab::CurrentSettings.auto_devops_enabled? ? 'enabled' : 'disabled'})
- %br
- %span.descr
- Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>.
- %p
- You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.
- = form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
-
- %hr
.form-group.append-bottom-default.js-secret-runner-token
= f.label :runners_token, "Runner token", class: 'label-light'
.form-control.js-secret-value-placeholder
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 09268c9943b..5f596a019f7 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -12,10 +12,22 @@
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
- Update your CI/CD configuration, like job timeout or Auto DevOps.
+ Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.
.settings-content
= render 'form'
+%section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = s_('CICD|Auto DevOps (Beta)')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = s_('CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.')
+ = link_to s_('CICD|Learn more about Auto DevOps'), help_page_path('topics/autodevops/index.md')
+ .settings-content
+ = render 'autodevops_form'
+
%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 94331a16abd..e28accd5b43 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -24,7 +24,7 @@
.text-warning.center.prepend-top-20
%p
= icon("exclamation-triangle fw")
- #{ _('Archived project! Repository is read-only') }
+ #{ _('Archived project! Repository and other project resources are read-only') }
- view_path = @project.default_view
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 3d5f92f9aaa..98b4d6339da 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -31,6 +31,6 @@
= 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 prepend-left-10 #{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")
+ - if can?(current_user, :admin_project, @project)
+ = link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip prepend-left-10 #{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/show.html.haml b/app/views/projects/tags/show.html.haml
index dfe2c37ed8e..7a3469cdd26 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -28,7 +28,7 @@
= icon('history')
.btn-container.controls-item
= render 'projects/buttons/download', project: @project, ref: @tag.name
- - if can?(current_user, :admin_project, @project)
+ - if can?(current_user, :push_code, @project) && 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: 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
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 5ef5e9c09a2..8587d3b0c0d 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -1,3 +1,6 @@
+- can_collaborate = can_collaborate_with_project?(@project)
+- can_create_mr_from_fork = can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project)
+
.tree-ref-container
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
@@ -15,7 +18,7 @@
%li
= link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path))
- - if current_user
+ - if can_collaborate || can_create_mr_from_fork
%li
%a.btn.add-to-tree{ addtotree_toggle_attributes }
= sprite_icon('plus', size: 16, css_class: 'pull-left')
@@ -35,7 +38,7 @@
%li
= link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
#{ _('New directory') }
- - elsif can?(current_user, :fork_project, @project)
+ - elsif can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project)
%li
- continue_params = { to: project_new_blob_path(@project, @id),
notice: edit_in_new_fork_notice,
@@ -61,23 +64,25 @@
= link_to fork_path, method: :post do
#{ _('New directory') }
- %li.divider
- %li.dropdown-header
- #{ _('This repository') }
- %li
- = link_to new_project_branch_path(@project) do
- #{ _('New branch') }
- %li
- = link_to new_project_tag_path(@project) do
- #{ _('New tag') }
+ - if can?(current_user, :push_code, @project)
+ %li.divider
+ %li.dropdown-header
+ #{ _('This repository') }
+ %li
+ = link_to new_project_branch_path(@project) do
+ #{ _('New branch') }
+ %li
+ = link_to new_project_tag_path(@project) do
+ #{ _('New tag') }
.tree-controls
= link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
= render 'projects/find_file_link'
- = succeed " " do
- = link_to ide_edit_path(@project, @id, ""), class: 'btn btn-default' do
- = _('Web IDE')
+ - if can_collaborate
+ = succeed " " do
+ = link_to ide_edit_path(@project, @id, ""), class: 'btn btn-default' do
+ = _('Web IDE')
= render 'projects/buttons/download', project: @project, ref: @ref
diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml
index 934d65e8b42..d3fa324e460 100644
--- a/app/views/shared/_auto_devops_callout.html.haml
+++ b/app/views/shared/_auto_devops_callout.html.haml
@@ -1,15 +1,15 @@
-.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }
+.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20.prepend-top-10{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }
.banner-graphic
= custom_icon('icon_autodevops')
- .prepend-top-10.prepend-left-10.append-bottom-10
- %h5= s_('AutoDevOps|Auto DevOps (Beta)')
+ .banner-body.prepend-left-10.append-bottom-10
+ %h5.banner-title= s_('AutoDevOps|Auto DevOps (Beta)')
%p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
%p
- link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
= s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
- .prepend-top-10
- = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout'
+ .banner-buttons
+ = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), class: 'btn js-close-callout'
%button.btn-transparent.banner-close.close.js-close-callout{ type: 'button',
'aria-label' => 'Dismiss Auto DevOps box' }
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index cb21f90696f..403d22c79f8 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -32,6 +32,13 @@
required: true,
title: 'You can choose a descriptive name different from the path.'
+- if @group.persisted?
+ .form-group.group-name-holder
+ = f.label :id, class: 'control-label' do
+ = _("Group ID")
+ .col-sm-10
+ = f.text_field :id, class: 'form-control', readonly: true
+
.form-group.group-description-holder
= f.label :description, class: 'control-label'
.col-sm-10
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 56403907844..836df57a3a2 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -47,20 +47,20 @@
class: 'text-danger'
.pull-right.hidden-xs.hidden-sm
- - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
- %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'),
- disabled: true,
- type: 'button',
- data: { url: promote_project_label_path(label.project, label),
- label_title: label.title,
- label_color: label.color,
- label_text_color: label.text_color,
- group_name: label.project.group.name,
- target: '#promote-label-modal',
- container: 'body',
- toggle: 'modal' } }
- = sprite_icon('level-up')
- if can?(current_user, :admin_label, label)
+ - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
+ %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'),
+ disabled: true,
+ type: 'button',
+ data: { url: promote_project_label_path(label.project, label),
+ label_title: label.title,
+ label_color: label.color,
+ label_text_color: label.text_color,
+ group_name: label.project.group.name,
+ target: '#promote-label-modal',
+ container: 'body',
+ toggle: 'modal' } }
+ = sprite_icon('level-up')
= link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
%span.sr-only Edit
= sprite_icon('pencil')
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index 4c8c92d722a..f1c39b9e923 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -8,8 +8,8 @@
- @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, 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_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown qa-branches-select" }
+ .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging.qa-branches-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
.dropdown-page-one
= dropdown_title _("Switch branch/tag")
= dropdown_filter _("Search branches and tags")
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index 87e6b52f46e..1c73534c642 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -4,7 +4,7 @@
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
- .value.issuable-show-labels
+ .value.issuable-show-labels.dont-hide
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
None
%a{ href: "#",
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 975b9cb4729..093033775a9 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -7,7 +7,7 @@
- if current_user
%span.issuable-header-text.hide-collapsed.pull-left
= _('Todo')
- %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
+ %a.gutter-toggle.pull-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left' } }
= sidebar_gutter_toggle_icon
- if current_user
= render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable
@@ -19,12 +19,11 @@
.block.assignee
= render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present?
.block.milestone
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } }
= icon('clock-o', 'aria-hidden': 'true')
%span.milestone-title
- if issuable.milestone
- %span.has-tooltip{ title: "#{issuable.milestone.title}<br>#{milestone_tooltip_title(issuable.milestone)}", data: { container: 'body', html: 1, placement: 'left' } }
- = issuable.milestone.title
+ = issuable.milestone.title
- else
= _('None')
.title.hide-collapsed
@@ -34,7 +33,7 @@
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed
- if issuable.milestone
- = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_title(issuable.milestone), data: { container: "body", html: 1 }
+ = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_due_date(issuable.milestone), data: { container: "body", html: 1 }
- else
%span.no-value
= _('None')
@@ -50,7 +49,7 @@
= icon('spinner spin', 'aria-hidden': 'true')
- if issuable.has_attribute?(:due_date)
.block.due_date
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 1 }, title: sidebar_due_date_tooltip_label(issuable) }
= icon('calendar', 'aria-hidden': 'true')
%span.js-due-date-sidebar-value
= issuable.due_date.try(:to_s, :medium) || 'None'
@@ -96,7 +95,7 @@
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
- .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
+ .value.issuable-show-labels.dont-hide.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- selected_labels.each do |label|
= link_to_label(label, subject: issuable.project, type: issuable.to_ability_name)
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 304df38a096..21006a76b28 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -4,7 +4,7 @@
= _('Assignee')
= icon('spinner spin')
- else
- .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee.name if issuable.assignee) }
+ .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: sidebar_assignee_tooltip_label(issuable) }
- if issuable.assignee
= link_to_member(@project, issuable.assignee, size: 24)
- else
diff --git a/app/views/shared/issuable/_sidebar_todo.html.haml b/app/views/shared/issuable/_sidebar_todo.html.haml
index b77e104c072..74327fb1ba8 100644
--- a/app/views/shared/issuable/_sidebar_todo.html.haml
+++ b/app/views/shared/issuable/_sidebar_todo.html.haml
@@ -1,11 +1,11 @@
- is_collapsed = local_assigns.fetch(:is_collapsed, false)
-- mark_content = is_collapsed ? icon('check-square', class: 'todo-undone') : _('Mark done')
+- mark_content = is_collapsed ? icon('check-square', class: 'todo-undone') : _('Mark todo as done')
- todo_content = is_collapsed ? icon('plus-square') : _('Add todo')
%button.issuable-todo-btn.js-issuable-todo{ type: 'button',
class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn pull-right'),
- title: (todo.nil? ? _('Add todo') : _('Mark done')),
- 'aria-label' => (todo.nil? ? _('Add todo') : _('Mark done')),
+ title: (todo.nil? ? _('Add todo') : _('Mark todo as done')),
+ 'aria-label' => (todo.nil? ? _('Add todo') : _('Mark todo as done')),
data: issuable_todo_button_data(issuable, todo, is_collapsed) }
%span.issuable-todo-inner.js-issuable-todo-inner<
- if todo
diff --git a/app/views/shared/issuable/form/_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_merge_request_assignee.html.haml
index bf8613b0f0d..d7740eddcca 100644
--- a/app/views/shared/issuable/form/_merge_request_assignee.html.haml
+++ b/app/views/shared/issuable/form/_merge_request_assignee.html.haml
@@ -1,6 +1,6 @@
- merge_request = issuable
.block.assignee
- .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (merge_request.assignee.name if merge_request.assignee) }
+ .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: sidebar_assignee_tooltip_label(issuable) }
- if merge_request.assignee
= link_to_member(@project, merge_request.assignee, size: 24)
- else
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index 5868c52566d..fc634856061 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -8,7 +8,7 @@
%strong
= link_to group.full_name, group_path(group)
.cgray
- Joined #{time_ago_with_tooltip(group.created_at)}
+ Given access #{time_ago_with_tooltip(group_link.created_at)}
- if group_link.expires?
·
%span{ class: ('text-warning' if group_link.expires_soon?) }
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index ba57d922c6d..1c139827acf 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -29,7 +29,7 @@
Requested
= time_ago_with_tooltip(member.requested_at)
- else
- Joined #{time_ago_with_tooltip(member.created_at)}
+ Given access #{time_ago_with_tooltip(member.created_at)}
- if member.expires?
·
%span{ class: "#{"text-warning" if member.expires_soon?} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) }
diff --git a/app/views/shared/milestones/_deprecation_message.html.haml b/app/views/shared/milestones/_deprecation_message.html.haml
new file mode 100644
index 00000000000..4a8f90937ea
--- /dev/null
+++ b/app/views/shared/milestones/_deprecation_message.html.haml
@@ -0,0 +1,14 @@
+.banner-callout.compact.milestone-deprecation-message.js-milestone-deprecation-message.prepend-top-20
+ .banner-graphic= image_tag 'illustrations/milestone_removing-page.svg'
+ .banner-body.prepend-left-10.append-right-10
+ %h5.banner-title.prepend-top-0= _('This page will be removed in a future release.')
+ %p.milestone-banner-text= _('Use group milestones to manage issues from multiple projects in the same milestone.')
+ = button_tag _('Promote these project milestones into a group milestone.'), class: 'btn btn-link js-popover-link text-align-left milestone-banner-link'
+ .milestone-banner-buttons.prepend-top-20= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-default', target: '_blank'
+
+ %template.js-milestone-deprecation-message-template
+ .milestone-popover-body
+ %ol.milestone-popover-instructions-list.append-bottom-0
+ %li= _('Click any <strong>project name</strong> in the project list below to navigate to the project milestone.').html_safe
+ %li= _('Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone.').html_safe
+ .milestone-popover-footer= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-link prepend-left-0', target: '_blank'
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index a942ebc328b..8e9a1b56bb8 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -4,12 +4,8 @@
%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar.milestone-sidebar
.block.milestone-progress.issuable-sidebar-header
- %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
+ %a.gutter-toggle.pull-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left' } }
= sidebar_gutter_toggle_icon
-
- .sidebar-collapsed-icon
- %span== #{milestone.percent_complete(current_user)}%
- = milestone_progress_bar(milestone)
.title.hide-collapsed
%strong.bold== #{milestone.percent_complete(current_user)}%
%span.hide-collapsed
@@ -17,6 +13,11 @@
.value.hide-collapsed
= milestone_progress_bar(milestone)
+ .block.milestone-progress.hide-expanded
+ .sidebar-collapsed-icon.has-tooltip{ title: milestone_progress_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } }
+ %span== #{milestone.percent_complete(current_user)}%
+ = milestone_progress_bar(milestone)
+
.block.start_date.hide-collapsed
.title
Start date
@@ -35,19 +36,25 @@
%span.collapsed-milestone-date
- if milestone.start_date && milestone.due_date
- if milestone.start_date.year == milestone.due_date.year
- .milestone-date= milestone.start_date.strftime('%b %-d')
+ .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
+ = milestone.start_date.strftime('%b %-d')
- else
- .milestone-date= milestone.start_date.strftime('%b %-d %Y')
+ .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
+ = milestone.start_date.strftime('%b %-d %Y')
.date-separator -
- .due_date= milestone.due_date.strftime('%b %-d %Y')
+ .due_date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 1, placement: 'left' } }
+ = milestone.due_date.strftime('%b %-d %Y')
- elsif milestone.start_date
From
- .milestone-date= milestone.start_date.strftime('%b %-d %Y')
+ .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
+ = milestone.start_date.strftime('%b %-d %Y')
- elsif milestone.due_date
Until
- .milestone-date= milestone.due_date.strftime('%b %-d %Y')
+ .milestone-date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 1, placement: 'left' } }
+ = milestone.due_date.strftime('%b %-d %Y')
- else
- None
+ .has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
+ None
.title.hide-collapsed
Due date
- if @project && can?(current_user, :admin_milestone, @project)
@@ -58,21 +65,21 @@
%span.bold= milestone.due_date.to_s(:medium)
- else
%span.no-value No due date
- - remaining_days = milestone_remaining_days(milestone)
+ - remaining_days = remaining_days_in_words(milestone)
- if remaining_days.present?
= surround '(', ')' do
%span.remaining-days= remaining_days
- if !project || can?(current_user, :read_issue, project)
.block.issues
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.has-tooltip{ title: milestone_issues_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } }
%strong
= custom_icon('issues')
%span= milestone.issues_visible_to_user(current_user).count
.title.hide-collapsed
Issues
%span.badge= milestone.issues_visible_to_user(current_user).count
- - if project && can?(current_user, :create_issue, project)
+ - if show_new_issue_link?(project)
= link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "pull-right", title: "New Issue" do
New issue
.value.hide-collapsed.bold
@@ -93,7 +100,7 @@
= icon('spinner spin')
.block.merge-requests
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } }
%strong
= custom_icon('mr_bold')
%span= milestone.merge_requests.count
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index f302299eb24..797ff034bb2 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -1,7 +1,8 @@
-- page_title @milestone.title
+- page_title milestone.title
- @breadcrumb_link = dashboard_milestone_path(milestone.safe_title, title: milestone.title)
- group = local_assigns[:group]
+- is_dynamic_milestone = milestone.legacy_group_milestone? || milestone.dashboard_milestone?
.detail-page-header
%a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" }
@@ -31,21 +32,23 @@
- else
= link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
+= render 'shared/milestones/deprecation_message' if is_dynamic_milestone
+
.detail-page-description.milestone-detail
%h2.title
= markdown_field(milestone, :title)
- - if @milestone.group_milestone? && @milestone.description.present?
+ - if milestone.group_milestone? && milestone.description.present?
%div
.description
.wiki
- = markdown_field(@milestone, :description)
+ = markdown_field(milestone, :description)
- if milestone.complete?(current_user) && milestone.active?
.alert.alert-success.prepend-top-default
- close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
%span All issues for this milestone are closed. #{close_msg}
-- if @milestone.legacy_group_milestone? || @milestone.dashboard_milestone?
+- if is_dynamic_milestone
.table-holder
%table.table
%thead
@@ -68,7 +71,7 @@
Open
%td
= ms.expires_at
-- elsif @milestone.group_milestone?
+- elsif milestone.group_milestone?
%br
View
= link_to 'Issues', issues_group_path(@group, milestone_title: milestone.title)
diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml
index 725bf916592..71c0d740bc8 100644
--- a/app/views/shared/notes/_form.html.haml
+++ b/app/views/shared/notes/_form.html.haml
@@ -24,20 +24,21 @@
-# DiffNote
= f.hidden_field :position
- = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
- = render 'projects/zen', f: f,
- attr: :note,
- classes: 'note-textarea js-note-text',
- placeholder: "Write a comment or drag your files here...",
- supports_quick_actions: supports_quick_actions,
- supports_autocomplete: supports_autocomplete
- = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
- .error-alert
-
- .note-form-actions.clearfix
- = render partial: 'shared/notes/comment_button'
-
- = yield(:note_actions)
-
- %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } }
- Discard draft
+ .discussion-form-container
+ = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
+ = render 'projects/zen', f: f,
+ attr: :note,
+ classes: 'note-textarea js-note-text',
+ placeholder: "Write a comment or drag your files here...",
+ supports_quick_actions: supports_quick_actions,
+ supports_autocomplete: supports_autocomplete
+ = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
+ .error-alert
+
+ .note-form-actions.clearfix
+ = render partial: 'shared/notes/comment_button'
+
+ = yield(:note_actions)
+
+ %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } }
+ Discard draft
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index bf359774ead..893a7f26ebd 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -2,7 +2,7 @@
- return if note.cross_reference_not_visible_for?(current_user)
- show_image_comment_badge = local_assigns.fetch(:show_image_comment_badge, false)
-- note_editable = note_editable?(note)
+- note_editable = can?(current_user, :admin_note, note)
- note_counter = local_assigns.fetch(:note_counter, 0)
%li.timeline-entry{ id: dom_id(note),
diff --git a/app/views/shared/snippets/_embed.html.haml b/app/views/shared/snippets/_embed.html.haml
new file mode 100644
index 00000000000..2d93e51a2d9
--- /dev/null
+++ b/app/views/shared/snippets/_embed.html.haml
@@ -0,0 +1,24 @@
+- blob = @snippet.blob
+.gitlab-embed-snippets
+ .js-file-title.file-title-flex-parent
+ .file-header-content
+ = external_snippet_icon('doc_text')
+
+ %strong.file-title-name
+ %a.gitlab-embedded-snippets-title{ href: url_for(only_path: false, overwrite_params: nil) }
+ = blob.name
+
+ %small
+ = number_to_human_size(blob.raw_size)
+ %a.gitlab-logo{ href: url_for(only_path: false, overwrite_params: nil), title: 'view on gitlab' }
+ on &nbsp;
+ %span.logo-text
+ GitLab
+
+ .file-actions.hidden-xs
+ .btn-group{ role: "group" }<
+ = embedded_snippet_raw_button
+
+ = embedded_snippet_download_button
+ %article.file-holder.snippet-file-content
+ = render 'projects/blob/viewer', viewer: @snippet.blob.simple_viewer, load_async: false, external_embed: true
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index 12df79a28c7..836230ae8ee 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -19,11 +19,32 @@
%h2.snippet-title.prepend-top-0.append-bottom-0
= markdown_field(@snippet, :title)
- - if @snippet.updated_at != @snippet.created_at
- = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago', exclude_author: true)
- if @snippet.description.present?
.description
.wiki
= markdown_field(@snippet, :description)
%textarea.hidden.js-task-list-field
= @snippet.description
+
+ - if @snippet.updated_at != @snippet.created_at
+ = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago', exclude_author: true)
+
+ - if public_snippet?
+ .embed-snippet
+ .input-group
+ .input-group-btn
+ %button.btn.embed-toggle{ 'data-toggle': 'dropdown', type: 'button' }
+ %span.js-embed-action= _("Embed")
+ = sprite_icon('angle-down', size: 12)
+ %ul.dropdown-menu.dropdown-menu-selectable.embed-toggle-list
+ %li
+ %button.js-embed-btn.btn.btn-transparent.is-active{ type: 'button' }
+ %strong.embed-toggle-list-item= _("Embed")
+ %li
+ %button.js-share-btn.btn.btn-transparent{ type: 'button' }
+ %strong.embed-toggle-list-item= _("Share")
+ %input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed }
+ .input-group-btn
+ %button.js-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '#snippet-url-area' }
+ = sprite_icon('duplicate', size: 16)
+ .clearfix
diff --git a/app/views/shared/snippets/show.js.haml b/app/views/shared/snippets/show.js.haml
new file mode 100644
index 00000000000..a9af732bbb5
--- /dev/null
+++ b/app/views/shared/snippets/show.js.haml
@@ -0,0 +1,2 @@
+document.write('#{escape_javascript(stylesheet_link_tag "#{stylesheet_url 'snippets'}")}');
+document.write('#{escape_javascript(render 'shared/snippets/embed')}');
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 9a11cdb121e..9aea3bad27b 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -18,6 +18,7 @@
- cronjob:stuck_import_jobs
- cronjob:stuck_merge_jobs
- cronjob:trending_projects
+- cronjob:issue_due_scheduler
- gcp_cluster:cluster_install_app
- gcp_cluster:cluster_provision
@@ -39,6 +40,8 @@
- github_importer:github_import_stage_import_pull_requests
- github_importer:github_import_stage_import_repository
+- mail_scheduler:mail_scheduler_issue_due
+
- object_storage_upload
- object_storage:object_storage_background_move
- object_storage:object_storage_migrate_uploads
diff --git a/app/workers/concerns/mail_scheduler_queue.rb b/app/workers/concerns/mail_scheduler_queue.rb
new file mode 100644
index 00000000000..9df55ad9522
--- /dev/null
+++ b/app/workers/concerns/mail_scheduler_queue.rb
@@ -0,0 +1,7 @@
+module MailSchedulerQueue
+ extend ActiveSupport::Concern
+
+ included do
+ queue_namespace :mail_scheduler
+ end
+end
diff --git a/app/workers/issue_due_scheduler_worker.rb b/app/workers/issue_due_scheduler_worker.rb
new file mode 100644
index 00000000000..16ab5d069e0
--- /dev/null
+++ b/app/workers/issue_due_scheduler_worker.rb
@@ -0,0 +1,10 @@
+class IssueDueSchedulerWorker
+ include ApplicationWorker
+ include CronjobQueue
+
+ def perform
+ project_ids = Issue.opened.due_tomorrow.group(:project_id).pluck(:project_id).map { |id| [id] }
+
+ MailScheduler::IssueDueWorker.bulk_perform_async(project_ids)
+ end
+end
diff --git a/app/workers/mail_scheduler/issue_due_worker.rb b/app/workers/mail_scheduler/issue_due_worker.rb
new file mode 100644
index 00000000000..b06079d68ca
--- /dev/null
+++ b/app/workers/mail_scheduler/issue_due_worker.rb
@@ -0,0 +1,14 @@
+module MailScheduler
+ class IssueDueWorker
+ include ApplicationWorker
+ include MailSchedulerQueue
+
+ def perform(project_id)
+ notification_service = NotificationService.new
+
+ Issue.opened.due_tomorrow.in_projects(project_id).preload(:project).find_each do |issue|
+ notification_service.issue_due(issue)
+ end
+ end
+ end
+end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 3909dbf7d7f..f88b3fdbfb1 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -33,7 +33,7 @@ class PostReceive
unless @user
log("Triggered hook for non-existing user \"#{post_received.identifier}\"")
- return false
+ return false # rubocop:disable Cop/AvoidReturnFromBlocks
end
if Gitlab::Git.tag_ref?(ref)
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 51fad4faf36..08b1c3a7d7a 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -13,7 +13,9 @@ class RepositoryForkWorker
# See https://gitlab.com/gitlab-org/gitaly/issues/1110
if args.empty?
source_project = target_project.forked_from_project
- return target_project.mark_import_as_failed('Source project cannot be found.') unless source_project
+ unless source_project
+ return target_project.mark_import_as_failed('Source project cannot be found.')
+ end
fork_repository(target_project, source_project.repository_storage, source_project.disk_path)
else
diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb
index fb26fa4c515..7ebf69bdc39 100644
--- a/app/workers/stuck_ci_jobs_worker.rb
+++ b/app/workers/stuck_ci_jobs_worker.rb
@@ -38,7 +38,7 @@ class StuckCiJobsWorker
def drop_stuck(status, timeout)
search(status, timeout) do |build|
- return unless build.stuck?
+ break unless build.stuck?
drop_build :stuck, build, status, timeout
end
diff --git a/bin/secpick b/bin/secpick
new file mode 100755
index 00000000000..76ae231e913
--- /dev/null
+++ b/bin/secpick
@@ -0,0 +1,47 @@
+#!/usr/bin/env ruby
+require 'optparse'
+require 'open3'
+require 'rainbow/refinement'
+using Rainbow
+
+BRANCH_PREFIX = 'security'.freeze
+STABLE_BRANCH_SUFFIX = 'stable'.freeze
+REMOTE = 'dev'.freeze
+
+options = { version: nil, branch: nil, sha: nil }
+
+parser = OptionParser.new do |opts|
+ opts.banner = "Usage: #{$0} [options]"
+ opts.on('-v', '--version 10.0', 'Version') do |version|
+ options[:version] = version&.tr('.', '-')
+ end
+
+ opts.on('-b', '--branch security-fix-branch', 'Original branch name') do |branch|
+ options[:branch] = branch
+ end
+
+ opts.on('-s', '--sha abcd', 'SHA to cherry pick') do |sha|
+ options[:sha] = sha
+ end
+
+ opts.on('-h', '--help', 'Displays Help') do
+ puts opts
+
+ exit
+ end
+end
+
+parser.parse!
+
+abort("Missing options. Use #{$0} --help to see the list of options available".red) if options.values.include?(nil)
+abort("Wrong version format #{options[:version].bold}".red) unless options[:version] =~ /\A\d*\-\d*\Z/
+
+branch = [BRANCH_PREFIX, options[:branch], options[:version]].join('-').freeze
+stable_branch = "#{options[:version]}-#{STABLE_BRANCH_SUFFIX}".freeze
+
+command = "git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch}"
+
+_stdin, stdout, stderr = Open3.popen3(command)
+
+puts stdout.read&.green
+puts stderr.read&.red
diff --git a/bin/spinach b/bin/spinach
index 474050e29d1..eda81c9ed8a 100755
--- a/bin/spinach
+++ b/bin/spinach
@@ -1,4 +1,9 @@
#!/usr/bin/env ruby
+
+# Remove this block when removing rails5? code.
+gemfile = %w[1 true].include?(ENV["RAILS5"]) ? "Gemfile.rails5" : "Gemfile"
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../#{gemfile}", __dir__)
+
begin
load File.expand_path('../spring', __FILE__)
rescue LoadError => e
diff --git a/changelogs/no-rm-rf-gitlab-basics.yml b/changelogs/no-rm-rf-gitlab-basics.yml
new file mode 100644
index 00000000000..d5aa1091b45
--- /dev/null
+++ b/changelogs/no-rm-rf-gitlab-basics.yml
@@ -0,0 +1,5 @@
+---
+ title: Do not use '-f' with 'rm' in gitlab-basics docs
+ merge_request: 18027
+ author: Elias Werberich
+ type: changed
diff --git a/changelogs/unreleased/10244-add-project-ci-cd-settings.yml b/changelogs/unreleased/10244-add-project-ci-cd-settings.yml
new file mode 100644
index 00000000000..89f9a0fe03c
--- /dev/null
+++ b/changelogs/unreleased/10244-add-project-ci-cd-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Introduce new ProjectCiCdSetting model with group_runners_enabled
+merge_request: 18144
+author:
+type: performance
diff --git a/changelogs/unreleased/16957-issue-due-email.yml b/changelogs/unreleased/16957-issue-due-email.yml
new file mode 100644
index 00000000000..83944ca4f73
--- /dev/null
+++ b/changelogs/unreleased/16957-issue-due-email.yml
@@ -0,0 +1,5 @@
+---
+title: Add cron job to email users on issue due date
+merge_request: 17985
+author: Stuart Nelson
+type: added
diff --git a/changelogs/unreleased/17516-nested-restore-changelog.yml b/changelogs/unreleased/17516-nested-restore-changelog.yml
deleted file mode 100644
index 89753f45457..00000000000
--- a/changelogs/unreleased/17516-nested-restore-changelog.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Enable restore rake task to handle nested storage directories
-merge_request: 17516
-author: Balasankar C
-type: fixed
diff --git a/changelogs/unreleased/17939-osw-patch-support-gfm.yml b/changelogs/unreleased/17939-osw-patch-support-gfm.yml
deleted file mode 100644
index 576581e25d6..00000000000
--- a/changelogs/unreleased/17939-osw-patch-support-gfm.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for patch link extension for commit links on GitLab Flavored Markdown
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/20394-protected-branches-wildcard.yml b/changelogs/unreleased/20394-protected-branches-wildcard.yml
deleted file mode 100644
index 3fa8ee4f69f..00000000000
--- a/changelogs/unreleased/20394-protected-branches-wildcard.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Include matching branches and tags in protected branches / tags count
-merge_request:
-author: Jan Beckmann
-type: fixed
diff --git a/changelogs/unreleased/21677-run-pipeline-word.yml b/changelogs/unreleased/21677-run-pipeline-word.yml
new file mode 100644
index 00000000000..9cc280244e4
--- /dev/null
+++ b/changelogs/unreleased/21677-run-pipeline-word.yml
@@ -0,0 +1,5 @@
+---
+title: Improves wording in new pipeline page
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/23460-send-email-when-pushing-more-commits-to-the-merge-request.yml b/changelogs/unreleased/23460-send-email-when-pushing-more-commits-to-the-merge-request.yml
deleted file mode 100644
index a62137ea2c9..00000000000
--- a/changelogs/unreleased/23460-send-email-when-pushing-more-commits-to-the-merge-request.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Send notification emails when push to a merge request
-merge_request: 7610
-author: YarNayar
-type: feature
diff --git a/changelogs/unreleased/25010-collapsed-sidebar-tooltips.yml b/changelogs/unreleased/25010-collapsed-sidebar-tooltips.yml
new file mode 100644
index 00000000000..1226fb4eefd
--- /dev/null
+++ b/changelogs/unreleased/25010-collapsed-sidebar-tooltips.yml
@@ -0,0 +1,5 @@
+---
+title: Improve tooltips in collapsed right sidebar
+merge_request: 17714
+author:
+type: changed
diff --git a/changelogs/unreleased/27210-add-cancel-btn-to-new-page-domain.yml b/changelogs/unreleased/27210-add-cancel-btn-to-new-page-domain.yml
deleted file mode 100644
index d96f7e54c8d..00000000000
--- a/changelogs/unreleased/27210-add-cancel-btn-to-new-page-domain.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds cancel btn to new pages domain page
-merge_request: 18026
-author: Jacopo Beschi @jacopo-beschi
-type: added
diff --git a/changelogs/unreleased/30739-fix-joined-information-on-project-members-page.yml b/changelogs/unreleased/30739-fix-joined-information-on-project-members-page.yml
new file mode 100644
index 00000000000..f2d5b503661
--- /dev/null
+++ b/changelogs/unreleased/30739-fix-joined-information-on-project-members-page.yml
@@ -0,0 +1,5 @@
+---
+title: Fix `joined` information on project members page
+merge_request: 18290
+author: Fabian Schneider
+type: fixed
diff --git a/changelogs/unreleased/31114-internal-ids-are-not-atomic.yml b/changelogs/unreleased/31114-internal-ids-are-not-atomic.yml
deleted file mode 100644
index bc1955bc66f..00000000000
--- a/changelogs/unreleased/31114-internal-ids-are-not-atomic.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Atomic generation of internal ids for issues.
-merge_request: 17580
-author:
-type: other
diff --git a/changelogs/unreleased/31591-project-deploy-tokens-to-allow-permanent-access.yml b/changelogs/unreleased/31591-project-deploy-tokens-to-allow-permanent-access.yml
deleted file mode 100644
index 5546d26d0fb..00000000000
--- a/changelogs/unreleased/31591-project-deploy-tokens-to-allow-permanent-access.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Create Deploy Tokens to allow permanent access to repository and registry
-merge_request: 17894
-author:
-type: added
diff --git a/changelogs/unreleased/33697-remove-ujs-action-big-graph.yml b/changelogs/unreleased/33697-remove-ujs-action-big-graph.yml
new file mode 100644
index 00000000000..43ce52c1209
--- /dev/null
+++ b/changelogs/unreleased/33697-remove-ujs-action-big-graph.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent pipeline actions in dropdown to redirct to a new page
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/33803-drop-json-support-in-project-milestone.yml b/changelogs/unreleased/33803-drop-json-support-in-project-milestone.yml
deleted file mode 100644
index 0382ede4565..00000000000
--- a/changelogs/unreleased/33803-drop-json-support-in-project-milestone.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Drop JSON response in Project Milestone along with avoiding error
-merge_request: 17977
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/34262-show-current-labels-when-editing.yml b/changelogs/unreleased/34262-show-current-labels-when-editing.yml
new file mode 100644
index 00000000000..d3b15b9ddd1
--- /dev/null
+++ b/changelogs/unreleased/34262-show-current-labels-when-editing.yml
@@ -0,0 +1,5 @@
+---
+title: Keep current labels visible when editing them in the sidebar
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/34604-fix-generated-url-for-external-repository.yml b/changelogs/unreleased/34604-fix-generated-url-for-external-repository.yml
deleted file mode 100644
index c4b5f59b724..00000000000
--- a/changelogs/unreleased/34604-fix-generated-url-for-external-repository.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix generated URL when listing repoitories for import
-merge_request: 17692
-author:
-type: fixed
diff --git a/changelogs/unreleased/35475-lazy-diff.yml b/changelogs/unreleased/35475-lazy-diff.yml
deleted file mode 100644
index bafa66ebe39..00000000000
--- a/changelogs/unreleased/35475-lazy-diff.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: lazy load diffs on merge request discussions
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml b/changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml
deleted file mode 100644
index cec06bf2dfe..00000000000
--- a/changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed bug in dropdown selector when selecting the same selection again
-merge_request: 14631
-author: bitsapien
-type: fixed
diff --git a/changelogs/unreleased/39584-nesting-depth-5-framework-dropdowns.yml b/changelogs/unreleased/39584-nesting-depth-5-framework-dropdowns.yml
deleted file mode 100644
index 30a8dc63983..00000000000
--- a/changelogs/unreleased/39584-nesting-depth-5-framework-dropdowns.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Apply NestingDepth (level 5) (framework/dropdowns.scss)
-merge_request: 17820
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/39880-merge-method-api.yml b/changelogs/unreleased/39880-merge-method-api.yml
deleted file mode 100644
index dd44a752c4f..00000000000
--- a/changelogs/unreleased/39880-merge-method-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'API: Add parameter merge_method to projects'
-merge_request: 18031
-author: Jan Beckmann
-type: added
diff --git a/changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml b/changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml
new file mode 100644
index 00000000000..e47577f9058
--- /dev/null
+++ b/changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml
@@ -0,0 +1,5 @@
+---
+title: Add a comma to the time estimate system notes
+merge_request: 18326
+author:
+type: changed
diff --git a/changelogs/unreleased/40781-os-to-ce.yml b/changelogs/unreleased/40781-os-to-ce.yml
deleted file mode 100644
index 4a364292c60..00000000000
--- a/changelogs/unreleased/40781-os-to-ce.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add object storage support for LFS objects, CI artifacts, and uploads.
-merge_request: 17358
-author:
-type: added
diff --git a/changelogs/unreleased/41059-calculate-artifact-size-more-efficiently.yml b/changelogs/unreleased/41059-calculate-artifact-size-more-efficiently.yml
new file mode 100644
index 00000000000..e3f94bbf081
--- /dev/null
+++ b/changelogs/unreleased/41059-calculate-artifact-size-more-efficiently.yml
@@ -0,0 +1,5 @@
+---
+title: Improve DB performance of calculating total artifacts size
+merge_request: 17839
+author:
+type: performance
diff --git a/changelogs/unreleased/41224-pipeline-icons.yml b/changelogs/unreleased/41224-pipeline-icons.yml
deleted file mode 100644
index 3fe05448d1c..00000000000
--- a/changelogs/unreleased/41224-pipeline-icons.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Increase dropdown width in pipeline graph & center action icon
-merge_request: 18089
-author:
-type: fixed
diff --git a/changelogs/unreleased/41748-vertical-misalignment-login-box.yml b/changelogs/unreleased/41748-vertical-misalignment-login-box.yml
new file mode 100644
index 00000000000..77a97400323
--- /dev/null
+++ b/changelogs/unreleased/41748-vertical-misalignment-login-box.yml
@@ -0,0 +1,5 @@
+---
+title: Refactor CSS to eliminate vertical misalignment of login nav
+merge_request: 16275
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/41758-after-changing-username-url-still-redirects-to-old-route.yml b/changelogs/unreleased/41758-after-changing-username-url-still-redirects-to-old-route.yml
deleted file mode 100644
index 36e79ea1ed4..00000000000
--- a/changelogs/unreleased/41758-after-changing-username-url-still-redirects-to-old-route.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added confirmation modal for changing username
-merge_request: 17405
-author:
-type: added
diff --git a/changelogs/unreleased/41902-add-api-option-to-overwrite-project-description-on-project-export.yml b/changelogs/unreleased/41902-add-api-option-to-overwrite-project-description-on-project-export.yml
deleted file mode 100644
index 60a649f22c9..00000000000
--- a/changelogs/unreleased/41902-add-api-option-to-overwrite-project-description-on-project-export.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds the option to the project export API to override the project description and display GitLab export description once imported
-merge_request: 17744
-author:
-type: added
diff --git a/changelogs/unreleased/41967_issue_api_closed_by_info.yml b/changelogs/unreleased/41967_issue_api_closed_by_info.yml
deleted file mode 100644
index 436574c3638..00000000000
--- a/changelogs/unreleased/41967_issue_api_closed_by_info.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: adds closed by informations in issue api
-merge_request: 17042
-author: haseebeqx
-type: added
diff --git a/changelogs/unreleased/42028-xss-diffs.yml b/changelogs/unreleased/42028-xss-diffs.yml
deleted file mode 100644
index a05f9d3c78d..00000000000
--- a/changelogs/unreleased/42028-xss-diffs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix XSS on diff view stored on filenames
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/42037-long-instance-names-group-names-covers-namespace-dropdown.yml b/changelogs/unreleased/42037-long-instance-names-group-names-covers-namespace-dropdown.yml
deleted file mode 100644
index f7758734a6f..00000000000
--- a/changelogs/unreleased/42037-long-instance-names-group-names-covers-namespace-dropdown.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Long instance urls do not overflow anymore during project creation
-merge_request: 17717
-author:
-type: fixed
diff --git a/changelogs/unreleased/42448-change-commit-row-actions-and-sha-design-for-project-commit-list.yml b/changelogs/unreleased/42448-change-commit-row-actions-and-sha-design-for-project-commit-list.yml
deleted file mode 100644
index 77d1ebf69df..00000000000
--- a/changelogs/unreleased/42448-change-commit-row-actions-and-sha-design-for-project-commit-list.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Improved visual styles and consistency for commit hash and possible actions
- across commit lists
-merge_request: 17406
-author:
-type: changed
diff --git a/changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml b/changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml
new file mode 100644
index 00000000000..7452a264bfd
--- /dev/null
+++ b/changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml
@@ -0,0 +1,5 @@
+---
+title: Remove ahead/behind graphs on project branches on mobile
+merge_request: 18415
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/42568-pipeline-empty-state.yml b/changelogs/unreleased/42568-pipeline-empty-state.yml
deleted file mode 100644
index d36edcf1b37..00000000000
--- a/changelogs/unreleased/42568-pipeline-empty-state.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve empty state for canceled job
-merge_request: 17646
-author:
-type: fixed
diff --git a/changelogs/unreleased/42579-fix-sidebar-dropdown-hover-style.yml b/changelogs/unreleased/42579-fix-sidebar-dropdown-hover-style.yml
deleted file mode 100644
index c0a247dc895..00000000000
--- a/changelogs/unreleased/42579-fix-sidebar-dropdown-hover-style.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix hover style of dropdown items in the right sidebar
-merge_request: 17519
-author:
-type: fixed
diff --git a/changelogs/unreleased/42803-show-new-branch-mr-button.yml b/changelogs/unreleased/42803-show-new-branch-mr-button.yml
new file mode 100644
index 00000000000..d689ff7f001
--- /dev/null
+++ b/changelogs/unreleased/42803-show-new-branch-mr-button.yml
@@ -0,0 +1,5 @@
+---
+title: Show new branch/mr button even when branch exists
+merge_request: 17712
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/42880-loss-of-input-text-on-comments-after-preview.yml b/changelogs/unreleased/42880-loss-of-input-text-on-comments-after-preview.yml
deleted file mode 100644
index 0e892a51bc5..00000000000
--- a/changelogs/unreleased/42880-loss-of-input-text-on-comments-after-preview.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix Firefox stealing formatting characters on issue notes
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/42889-avoid-return-inside-block.yml b/changelogs/unreleased/42889-avoid-return-inside-block.yml
new file mode 100644
index 00000000000..e3e1341ddcc
--- /dev/null
+++ b/changelogs/unreleased/42889-avoid-return-inside-block.yml
@@ -0,0 +1,5 @@
+---
+title: Rubocop rule to avoid returning from a block
+merge_request: 18000
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/42936-improve-ns-factory-avoid-duplicates.yml b/changelogs/unreleased/42936-improve-ns-factory-avoid-duplicates.yml
new file mode 100644
index 00000000000..54b523b65a1
--- /dev/null
+++ b/changelogs/unreleased/42936-improve-ns-factory-avoid-duplicates.yml
@@ -0,0 +1,6 @@
+---
+title: Fix discussions API setting created_at for notable in a group or notable in
+ a project in a group with owners
+merge_request: 18464
+author:
+type: fixed
diff --git a/changelogs/unreleased/43098-controller-projects-issuescontroller-show-executes-more-than-100-sql-queries.yml b/changelogs/unreleased/43098-controller-projects-issuescontroller-show-executes-more-than-100-sql-queries.yml
deleted file mode 100644
index 686258460e0..00000000000
--- a/changelogs/unreleased/43098-controller-projects-issuescontroller-show-executes-more-than-100-sql-queries.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve performance of loading issues with lots of references to merge requests
-merge_request: 17986
-author:
-type: performance
diff --git a/changelogs/unreleased/43111-controller-projects-mergerequestscontroller-index-executes-more-than-100-sql-queries.yml b/changelogs/unreleased/43111-controller-projects-mergerequestscontroller-index-executes-more-than-100-sql-queries.yml
new file mode 100644
index 00000000000..120e319acfb
--- /dev/null
+++ b/changelogs/unreleased/43111-controller-projects-mergerequestscontroller-index-executes-more-than-100-sql-queries.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce queries on merge requests list page for merge requests from forks
+merge_request: 18561
+author:
+type: performance
diff --git a/changelogs/unreleased/43215-update-design-for-verifying-domains.yml b/changelogs/unreleased/43215-update-design-for-verifying-domains.yml
deleted file mode 100644
index 8326540f7b2..00000000000
--- a/changelogs/unreleased/43215-update-design-for-verifying-domains.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Polish design for verifying domains
-merge_request: 17767
-author:
-type: changed
diff --git a/changelogs/unreleased/43246-checkfilter.yml b/changelogs/unreleased/43246-checkfilter.yml
deleted file mode 100644
index e6c0e716213..00000000000
--- a/changelogs/unreleased/43246-checkfilter.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Require at least one filter when listing issues or merge requests on dashboard
- page
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/43316-controller-parameters-handling-sensitive-information-should-use-a-more-specific-name.yml b/changelogs/unreleased/43316-controller-parameters-handling-sensitive-information-should-use-a-more-specific-name.yml
deleted file mode 100644
index de1cee6e436..00000000000
--- a/changelogs/unreleased/43316-controller-parameters-handling-sensitive-information-should-use-a-more-specific-name.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use specific names for filtered CI variable controller parameters
-merge_request: 17796
-author:
-type: other
diff --git a/changelogs/unreleased/43404-pipelines-commit.yml b/changelogs/unreleased/43404-pipelines-commit.yml
new file mode 100644
index 00000000000..0b9a4a6451f
--- /dev/null
+++ b/changelogs/unreleased/43404-pipelines-commit.yml
@@ -0,0 +1,5 @@
+---
+title: Breaks commit not found message in pipelines table
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/43466-make-auto-devops-settings-first-class.yml b/changelogs/unreleased/43466-make-auto-devops-settings-first-class.yml
new file mode 100644
index 00000000000..f5c5415159c
--- /dev/null
+++ b/changelogs/unreleased/43466-make-auto-devops-settings-first-class.yml
@@ -0,0 +1,5 @@
+---
+title: Create settings section for autodevops
+merge_request: 18321
+author:
+type: changed
diff --git a/changelogs/unreleased/43482-enabling-auto-devops-on-an-empty-project-gives-you-wrong-information.yml b/changelogs/unreleased/43482-enabling-auto-devops-on-an-empty-project-gives-you-wrong-information.yml
deleted file mode 100644
index 889fd008bad..00000000000
--- a/changelogs/unreleased/43482-enabling-auto-devops-on-an-empty-project-gives-you-wrong-information.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add empty repo check before running AutoDevOps pipeline
-merge_request: 17605
-author:
-type: changed
diff --git a/changelogs/unreleased/43512-add-support-for-omniauth-jwt-provider.yml b/changelogs/unreleased/43512-add-support-for-omniauth-jwt-provider.yml
deleted file mode 100644
index 039d3de7168..00000000000
--- a/changelogs/unreleased/43512-add-support-for-omniauth-jwt-provider.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds support for OmniAuth JWT provider
-merge_request: 17774
-author:
-type: added
diff --git a/changelogs/unreleased/43525-limit-number-of-failed-logins-using-ldap.yml b/changelogs/unreleased/43525-limit-number-of-failed-logins-using-ldap.yml
deleted file mode 100644
index f30fea3c4a7..00000000000
--- a/changelogs/unreleased/43525-limit-number-of-failed-logins-using-ldap.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Limit the number of failed logins when using LDAP for authentication
-merge_request: 43525
-author:
-type: added
diff --git a/changelogs/unreleased/43552-user-owned-projects-query-performance-improvement.yml b/changelogs/unreleased/43552-user-owned-projects-query-performance-improvement.yml
deleted file mode 100644
index 39f92c281ad..00000000000
--- a/changelogs/unreleased/43552-user-owned-projects-query-performance-improvement.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improves the performance of projects list page
-merge_request: 17934
-author:
-type: performance
diff --git a/changelogs/unreleased/43567-replace-gke.yml b/changelogs/unreleased/43567-replace-gke.yml
new file mode 100644
index 00000000000..8ec79fc3d4d
--- /dev/null
+++ b/changelogs/unreleased/43567-replace-gke.yml
@@ -0,0 +1,5 @@
+---
+title: Replace GKE acronym with Google Kubernetes Engine
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/43603-ci-lint-support.yml b/changelogs/unreleased/43603-ci-lint-support.yml
deleted file mode 100644
index 8e4a92c0287..00000000000
--- a/changelogs/unreleased/43603-ci-lint-support.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move ci/lint under project's namespace
-merge_request: 17729
-author:
-type: added
diff --git a/changelogs/unreleased/43617-mailsig.yml b/changelogs/unreleased/43617-mailsig.yml
new file mode 100644
index 00000000000..2c7568e32ca
--- /dev/null
+++ b/changelogs/unreleased/43617-mailsig.yml
@@ -0,0 +1,5 @@
+---
+title: Use RFC 3676 mail signature delimiters
+merge_request: 17979
+author: Enrico Scholz
+type: changed
diff --git a/changelogs/unreleased/43702-update-label-dropdown-wording.yml b/changelogs/unreleased/43702-update-label-dropdown-wording.yml
deleted file mode 100644
index 97100ec89de..00000000000
--- a/changelogs/unreleased/43702-update-label-dropdown-wording.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update wording to specify create/manage project vs group labels in labels dropdown
-merge_request: 17640
-author:
-type: changed
diff --git a/changelogs/unreleased/43717-breadcrumb-on-admin-runner-page.yml b/changelogs/unreleased/43717-breadcrumb-on-admin-runner-page.yml
deleted file mode 100644
index 3aec71d5ac4..00000000000
--- a/changelogs/unreleased/43717-breadcrumb-on-admin-runner-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Set breadcrumb for admin/runners/show
-merge_request: 17431
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/43720-update-fe-webpack-docs.yml b/changelogs/unreleased/43720-update-fe-webpack-docs.yml
deleted file mode 100644
index 9e461eaaec8..00000000000
--- a/changelogs/unreleased/43720-update-fe-webpack-docs.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Update documentation to reflect current minimum required versions of node and
- yarn
-merge_request: 17706
-author:
-type: other
diff --git a/changelogs/unreleased/43745-store-metadata-checksum-for-artifacts.yml b/changelogs/unreleased/43745-store-metadata-checksum-for-artifacts.yml
deleted file mode 100644
index 6283e797930..00000000000
--- a/changelogs/unreleased/43745-store-metadata-checksum-for-artifacts.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Store sha256 checksum of artifact metadata
-merge_request: 18149
-author:
-type: added
diff --git a/changelogs/unreleased/43771-improve-avatar-error-message.yml b/changelogs/unreleased/43771-improve-avatar-error-message.yml
deleted file mode 100644
index 1fae10f4d1f..00000000000
--- a/changelogs/unreleased/43771-improve-avatar-error-message.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Change avatar error message to include allowed file formats
-merge_request: 17747
-author: Fabian Schneider
-type: changed
diff --git a/changelogs/unreleased/43786-on-the-issuable-list-add-tooltips-to-icons.yml b/changelogs/unreleased/43786-on-the-issuable-list-add-tooltips-to-icons.yml
deleted file mode 100644
index 19b633daace..00000000000
--- a/changelogs/unreleased/43786-on-the-issuable-list-add-tooltips-to-icons.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add tooltips to icons in lists of issues and merge requests
-merge_request: 17700
-author:
-type: changed
diff --git a/changelogs/unreleased/43805-list-gitaly-calls-and-arguments-in-the-performance-bar.yml b/changelogs/unreleased/43805-list-gitaly-calls-and-arguments-in-the-performance-bar.yml
deleted file mode 100644
index 4c63e69f0bb..00000000000
--- a/changelogs/unreleased/43805-list-gitaly-calls-and-arguments-in-the-performance-bar.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Gitaly call details to performance bar
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/43806-update-ruby-saml-to-1-7-2.yml b/changelogs/unreleased/43806-update-ruby-saml-to-1-7-2.yml
deleted file mode 100644
index 7335d313510..00000000000
--- a/changelogs/unreleased/43806-update-ruby-saml-to-1-7-2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update ruby-saml to 1.7.2 and omniauth-saml to 1.10.0
-merge_request: 17734
-author: Takuya Noguchi
-type: security
diff --git a/changelogs/unreleased/43933-always-notify-mentions.yml b/changelogs/unreleased/43933-always-notify-mentions.yml
deleted file mode 100644
index 7b494d38541..00000000000
--- a/changelogs/unreleased/43933-always-notify-mentions.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Send @mention notifications even if a user has explicitly unsubscribed from
- item
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/43949-verify-job-artifacts.yml b/changelogs/unreleased/43949-verify-job-artifacts.yml
deleted file mode 100644
index 45e1916ae17..00000000000
--- a/changelogs/unreleased/43949-verify-job-artifacts.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Implement foreground verification of CI artifacts
-merge_request: 17578
-author:
-type: added
diff --git a/changelogs/unreleased/43976-fix-access-token-clipboard-button-style.yml b/changelogs/unreleased/43976-fix-access-token-clipboard-button-style.yml
deleted file mode 100644
index b341d5dfa7f..00000000000
--- a/changelogs/unreleased/43976-fix-access-token-clipboard-button-style.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix personal access token clipboard button style
-merge_request: 17978
-author: Fabian Schneider
-type: fixed
diff --git a/changelogs/unreleased/44022-singular-1-diff.yml b/changelogs/unreleased/44022-singular-1-diff.yml
deleted file mode 100644
index f4942925a73..00000000000
--- a/changelogs/unreleased/44022-singular-1-diff.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use singular in the diff stats if only one line has been changed
-merge_request: 17697
-author: Jan Beckmann
-type: fixed
diff --git a/changelogs/unreleased/44139-fix-issue-boards-dup-keys.yml b/changelogs/unreleased/44139-fix-issue-boards-dup-keys.yml
deleted file mode 100644
index dd5f2f06d6c..00000000000
--- a/changelogs/unreleased/44139-fix-issue-boards-dup-keys.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Use object ID to prevent duplicate keys Vue warning on Issue Boards page during
- development
-merge_request: 17682
-author:
-type: other
diff --git a/changelogs/unreleased/44160-update-foreman-to-0-84-0.yml b/changelogs/unreleased/44160-update-foreman-to-0-84-0.yml
deleted file mode 100644
index 990d188eb78..00000000000
--- a/changelogs/unreleased/44160-update-foreman-to-0-84-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update foreman from 0.78.0 to 0.84.0
-merge_request: 17690
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/44191-reduce-redis-usage-from-merge-request-diffs-caching.yml b/changelogs/unreleased/44191-reduce-redis-usage-from-merge-request-diffs-caching.yml
deleted file mode 100644
index 8fdca6eec83..00000000000
--- a/changelogs/unreleased/44191-reduce-redis-usage-from-merge-request-diffs-caching.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Stop caching highlighted diffs in Redis unnecessarily
-merge_request: 17746
-author:
-type: performance
diff --git a/changelogs/unreleased/44218-add-internationalization-support-for-the-prometheus-merge-request-widget.yml b/changelogs/unreleased/44218-add-internationalization-support-for-the-prometheus-merge-request-widget.yml
deleted file mode 100644
index 12c73281998..00000000000
--- a/changelogs/unreleased/44218-add-internationalization-support-for-the-prometheus-merge-request-widget.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added i18n support for the prometheus memory widget
-merge_request: 17753
-author:
-type: other
diff --git a/changelogs/unreleased/44235-update-knapsack-to-1-16-0.yml b/changelogs/unreleased/44235-update-knapsack-to-1-16-0.yml
deleted file mode 100644
index 265d36b763f..00000000000
--- a/changelogs/unreleased/44235-update-knapsack-to-1-16-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update knapsack to 1.16.0
-merge_request: 17735
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/44257-viewing-a-particular-commit-gives-500-error-error-undefined-method-binary.yml b/changelogs/unreleased/44257-viewing-a-particular-commit-gives-500-error-error-undefined-method-binary.yml
deleted file mode 100644
index 934860b95fe..00000000000
--- a/changelogs/unreleased/44257-viewing-a-particular-commit-gives-500-error-error-undefined-method-binary.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix viewing diffs on old merge requests
-merge_request: 17805
-author:
-type: fixed
diff --git a/changelogs/unreleased/44269-show-failure-reason-on-upgrade-tooltip-of-jobs.yml b/changelogs/unreleased/44269-show-failure-reason-on-upgrade-tooltip-of-jobs.yml
deleted file mode 100644
index b3ae8ca7340..00000000000
--- a/changelogs/unreleased/44269-show-failure-reason-on-upgrade-tooltip-of-jobs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Display error message on job's tooltip if this one fails
-merge_request: 17782
-author:
-type: added
diff --git a/changelogs/unreleased/44280-fix-code-search.yml b/changelogs/unreleased/44280-fix-code-search.yml
deleted file mode 100644
index 07f3abb224c..00000000000
--- a/changelogs/unreleased/44280-fix-code-search.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix search results stripping last endline when parsing the results
-merge_request: 17777
-author: Jasper Maes
-type: fixed
diff --git a/changelogs/unreleased/44291-usage-ping-for-kubernetes-integration.yml b/changelogs/unreleased/44291-usage-ping-for-kubernetes-integration.yml
deleted file mode 100644
index b5c12d8f40e..00000000000
--- a/changelogs/unreleased/44291-usage-ping-for-kubernetes-integration.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add additional cluster usage metrics to usage ping.
-merge_request: 17922
-author:
-type: changed
diff --git a/changelogs/unreleased/44382-ui-breakdown-for-create-merge-request.yml b/changelogs/unreleased/44382-ui-breakdown-for-create-merge-request.yml
deleted file mode 100644
index dd8c0b19d5f..00000000000
--- a/changelogs/unreleased/44382-ui-breakdown-for-create-merge-request.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix UI breakdown for Create merge request button
-merge_request: 17821
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/44383-cleanup-framework-header.yml b/changelogs/unreleased/44383-cleanup-framework-header.yml
deleted file mode 100644
index ef9be9f48de..00000000000
--- a/changelogs/unreleased/44383-cleanup-framework-header.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Clean up selectors in framework/header.scss
-merge_request: 17822
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/44384-cleanup-css-for-nested-lists.yml b/changelogs/unreleased/44384-cleanup-css-for-nested-lists.yml
deleted file mode 100644
index 79c470ea4e1..00000000000
--- a/changelogs/unreleased/44384-cleanup-css-for-nested-lists.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Unify format for nested non-task lists
-merge_request: 17823
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/44386-better-ux-for-long-name-branches.yml b/changelogs/unreleased/44386-better-ux-for-long-name-branches.yml
deleted file mode 100644
index 16712486f0f..00000000000
--- a/changelogs/unreleased/44386-better-ux-for-long-name-branches.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: UX re-design branch items with flexbox
-merge_request: 17832
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/44388-update-rack-protection-to-2-0-1.yml b/changelogs/unreleased/44388-update-rack-protection-to-2-0-1.yml
deleted file mode 100644
index c21d02d4d87..00000000000
--- a/changelogs/unreleased/44388-update-rack-protection-to-2-0-1.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update rack-protection to 2.0.1
-merge_request: 17835
-author: Takuya Noguchi
-type: security
diff --git a/changelogs/unreleased/44389-always-allow-http-for-ci-git-operations.yml b/changelogs/unreleased/44389-always-allow-http-for-ci-git-operations.yml
deleted file mode 100644
index 2e5a0302ee6..00000000000
--- a/changelogs/unreleased/44389-always-allow-http-for-ci-git-operations.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow HTTP(s) when git request is made by GitLab CI
-merge_request: 18021
-author:
-type: changed
diff --git a/changelogs/unreleased/44392-resolve-projects-creation-silently-failing-on-after-create-error.yml b/changelogs/unreleased/44392-resolve-projects-creation-silently-failing-on-after-create-error.yml
deleted file mode 100644
index 3bbd5a05b98..00000000000
--- a/changelogs/unreleased/44392-resolve-projects-creation-silently-failing-on-after-create-error.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Project creation will now raise an error if a service template is invalid
-merge_request: 18013
-author:
-type: fixed
diff --git a/changelogs/unreleased/44425-use-gitlab_environment.yml b/changelogs/unreleased/44425-use-gitlab_environment.yml
deleted file mode 100644
index a774143d5f5..00000000000
--- a/changelogs/unreleased/44425-use-gitlab_environment.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix `gitlab-rake gitlab:two_factor:disable_for_all_users`
-merge_request: 18154
-author:
-type: fixed
diff --git a/changelogs/unreleased/44447-expose-deploy-token-to-ci-cd.yml b/changelogs/unreleased/44447-expose-deploy-token-to-ci-cd.yml
new file mode 100644
index 00000000000..d01b797b1ff
--- /dev/null
+++ b/changelogs/unreleased/44447-expose-deploy-token-to-ci-cd.yml
@@ -0,0 +1,5 @@
+---
+title: Expose Deploy Token data as environment varialbes on CI/CD jobs
+merge_request: 18414
+author:
+type: added
diff --git a/changelogs/unreleased/44508-fix-fork-namespace-images.yml b/changelogs/unreleased/44508-fix-fork-namespace-images.yml
deleted file mode 100644
index 63b4b9a5e56..00000000000
--- a/changelogs/unreleased/44508-fix-fork-namespace-images.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix bug rendering group icons when forking
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/44541-fix-file-tree-commit-status-cache.yml b/changelogs/unreleased/44541-fix-file-tree-commit-status-cache.yml
new file mode 100644
index 00000000000..ff734fe0c05
--- /dev/null
+++ b/changelogs/unreleased/44541-fix-file-tree-commit-status-cache.yml
@@ -0,0 +1,5 @@
+---
+title: Fix pipeline status in branch/tag tree page
+merge_request: 17995
+author:
+type: fixed
diff --git a/changelogs/unreleased/44582-clear-pipeline-status-cache.yml b/changelogs/unreleased/44582-clear-pipeline-status-cache.yml
new file mode 100644
index 00000000000..1777f2ffaab
--- /dev/null
+++ b/changelogs/unreleased/44582-clear-pipeline-status-cache.yml
@@ -0,0 +1,5 @@
+---
+title: Now `rake cache:clear` will also clear pipeline status cache
+merge_request: 18257
+author:
+type: fixed
diff --git a/changelogs/unreleased/44657-reuse-root_ref_hash-on-branches.yml b/changelogs/unreleased/44657-reuse-root_ref_hash-on-branches.yml
deleted file mode 100644
index 4f21aadd86b..00000000000
--- a/changelogs/unreleased/44657-reuse-root_ref_hash-on-branches.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Reuse root_ref_hash for performance on Branches
-merge_request: 17998
-author: Takuya Noguchi
-type: performance
diff --git a/changelogs/unreleased/44665-fix-db-trace-stream-by-raw-access.yml b/changelogs/unreleased/44665-fix-db-trace-stream-by-raw-access.yml
deleted file mode 100644
index 4166d4fe320..00000000000
--- a/changelogs/unreleased/44665-fix-db-trace-stream-by-raw-access.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix `JobsController#raw` endpoint can not read traces in database
-merge_request: 18101
-author:
-type: fixed
diff --git a/changelogs/unreleased/44697-prevue.yml b/changelogs/unreleased/44697-prevue.yml
new file mode 100644
index 00000000000..9fdce5869ae
--- /dev/null
+++ b/changelogs/unreleased/44697-prevue.yml
@@ -0,0 +1,5 @@
+---
+title: Make toggle markdown preview shortcut only toggle selected field
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml b/changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml
deleted file mode 100644
index bdfed89d2ea..00000000000
--- a/changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update asciidoctor-plantuml to 0.0.8
-merge_request: 18022
-author: Takuya Noguchi
-type: performance
diff --git a/changelogs/unreleased/44774-migrate-upload-task-fails-for-upload-with-store-nil.yml b/changelogs/unreleased/44774-migrate-upload-task-fails-for-upload-with-store-nil.yml
deleted file mode 100644
index 372f4293964..00000000000
--- a/changelogs/unreleased/44774-migrate-upload-task-fails-for-upload-with-store-nil.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed gitlab:uploads:migrate task ignoring some uploads.
-merge_request: 18082
-author:
-type: fixed
diff --git a/changelogs/unreleased/44775-avatar-on-os-fails-with-cdn.yml b/changelogs/unreleased/44775-avatar-on-os-fails-with-cdn.yml
new file mode 100644
index 00000000000..80b5b4a8abe
--- /dev/null
+++ b/changelogs/unreleased/44775-avatar-on-os-fails-with-cdn.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed wrong avatar URL when the avatar is on object storage.
+merge_request: 18092
+author:
+type: fixed
diff --git a/changelogs/unreleased/44776-fix-upload-migrate-fails-for-group.yml b/changelogs/unreleased/44776-fix-upload-migrate-fails-for-group.yml
deleted file mode 100644
index 6094fcd0b3e..00000000000
--- a/changelogs/unreleased/44776-fix-upload-migrate-fails-for-group.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed gitlab:uploads:migrate task failing for Groups' avatar.
-merge_request: 18088
-author:
-type: fixed
diff --git a/changelogs/unreleased/44834-ide-remove-branch-from-bottom-bar.yml b/changelogs/unreleased/44834-ide-remove-branch-from-bottom-bar.yml
new file mode 100644
index 00000000000..d3f838ad362
--- /dev/null
+++ b/changelogs/unreleased/44834-ide-remove-branch-from-bottom-bar.yml
@@ -0,0 +1,5 @@
+---
+title: Remove branch name from the status bar of WebIDE
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/44870-remove-extra-space-around-comment-form-on-merge-requests.yml b/changelogs/unreleased/44870-remove-extra-space-around-comment-form-on-merge-requests.yml
deleted file mode 100644
index 53e4ebdb996..00000000000
--- a/changelogs/unreleased/44870-remove-extra-space-around-comment-form-on-merge-requests.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Refactor and tweak margin for note forms on Issuable
-merge_request: 18120
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/44878-update-brakeman-3-6-1-to-4-2-1.yml b/changelogs/unreleased/44878-update-brakeman-3-6-1-to-4-2-1.yml
deleted file mode 100644
index f5710cf4f7f..00000000000
--- a/changelogs/unreleased/44878-update-brakeman-3-6-1-to-4-2-1.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update brakeman 3.6.1 to 4.2.1
-merge_request: 18122
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/44902-remove-rake-test-ci.yml b/changelogs/unreleased/44902-remove-rake-test-ci.yml
deleted file mode 100644
index 459de1c2ca3..00000000000
--- a/changelogs/unreleased/44902-remove-rake-test-ci.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove test_ci rake task
-merge_request: 18139
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml b/changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml
new file mode 100644
index 00000000000..4af2af2a561
--- /dev/null
+++ b/changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml
@@ -0,0 +1,5 @@
+---
+title: Fix confirmation modal for deleting a protected branch
+merge_request: 18176
+author: Paul Bonaud @PaulRbR
+type: fixed
diff --git a/changelogs/unreleased/45159-fix-illustration.yml b/changelogs/unreleased/45159-fix-illustration.yml
new file mode 100644
index 00000000000..3b9cb45b916
--- /dev/null
+++ b/changelogs/unreleased/45159-fix-illustration.yml
@@ -0,0 +1,5 @@
+---
+title: Adds illustration for when job log was erased
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/45397-update-faraday_middleware-to-0-12-2.yml b/changelogs/unreleased/45397-update-faraday_middleware-to-0-12-2.yml
new file mode 100644
index 00000000000..3370ec3feba
--- /dev/null
+++ b/changelogs/unreleased/45397-update-faraday_middleware-to-0-12-2.yml
@@ -0,0 +1,5 @@
+---
+title: Update faraday_middlewar to 0.12.2
+merge_request: 18397
+author: Takuya Noguchi
+type: security
diff --git a/changelogs/unreleased/45398-fix-rss-button.yml b/changelogs/unreleased/45398-fix-rss-button.yml
new file mode 100644
index 00000000000..2c8ee6f0564
--- /dev/null
+++ b/changelogs/unreleased/45398-fix-rss-button.yml
@@ -0,0 +1,5 @@
+---
+title: Fix tabs container styles to make RSS button clickable
+merge_request: 18559
+author:
+type: fixed
diff --git a/changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml b/changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml
new file mode 100644
index 00000000000..0f1d111ca58
--- /dev/null
+++ b/changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml
@@ -0,0 +1,5 @@
+---
+title: Fix undefined `html_escape` method during markdown rendering
+merge_request: 18418
+author:
+type: fixed
diff --git a/changelogs/unreleased/45576-fix-create-project-for-user-endpoint.yml b/changelogs/unreleased/45576-fix-create-project-for-user-endpoint.yml
new file mode 100644
index 00000000000..12631c75b44
--- /dev/null
+++ b/changelogs/unreleased/45576-fix-create-project-for-user-endpoint.yml
@@ -0,0 +1,5 @@
+---
+title: Fix project creation for user endpoint when jobs_enabled parameter supplied
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/45666-project-ci-lint-links.yml b/changelogs/unreleased/45666-project-ci-lint-links.yml
new file mode 100644
index 00000000000..dbf803c0921
--- /dev/null
+++ b/changelogs/unreleased/45666-project-ci-lint-links.yml
@@ -0,0 +1,5 @@
+---
+title: Update links to /ci/lint with ones to project ci/lint
+merge_request: 18539
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/4950-unassign-slash-command-preview-fix.yml b/changelogs/unreleased/4950-unassign-slash-command-preview-fix.yml
new file mode 100644
index 00000000000..0b8c14ae699
--- /dev/null
+++ b/changelogs/unreleased/4950-unassign-slash-command-preview-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Fix unassign slash command preview
+merge_request: 18447
+author:
+type: fixed
diff --git a/changelogs/unreleased/8088_embedded_snippets_support.yml b/changelogs/unreleased/8088_embedded_snippets_support.yml
new file mode 100644
index 00000000000..7bd77a69dbd
--- /dev/null
+++ b/changelogs/unreleased/8088_embedded_snippets_support.yml
@@ -0,0 +1,5 @@
+---
+title: Adds Embedded Snippets Support
+merge_request: 15695
+author: haseebeqx
+type: added
diff --git a/changelogs/unreleased/Link_to_project_labels_page.yml b/changelogs/unreleased/Link_to_project_labels_page.yml
deleted file mode 100644
index 7bdeec423fc..00000000000
--- a/changelogs/unreleased/Link_to_project_labels_page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Always display Labels section in issuable sidebar, even when the project has no labels
-merge_request: 18081
-author: Branka Martinovic
-type: fixed
diff --git a/changelogs/unreleased/ab-37462-cache-personal-projects-count.yml b/changelogs/unreleased/ab-37462-cache-personal-projects-count.yml
deleted file mode 100644
index 55069b1f2d2..00000000000
--- a/changelogs/unreleased/ab-37462-cache-personal-projects-count.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Cache personal projects count.
-merge_request: 18197
-author:
-type: performance
diff --git a/changelogs/unreleased/ab-43150-users-controller-show-query-limit.yml b/changelogs/unreleased/ab-43150-users-controller-show-query-limit.yml
deleted file mode 100644
index 502c1176d2d..00000000000
--- a/changelogs/unreleased/ab-43150-users-controller-show-query-limit.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove N+1 query for Noteable association.
-merge_request: 17956
-author:
-type: performance
diff --git a/changelogs/unreleased/ab-44259-atomic-internal-ids-for-all-models.yml b/changelogs/unreleased/ab-44259-atomic-internal-ids-for-all-models.yml
new file mode 100644
index 00000000000..e154f7dbc85
--- /dev/null
+++ b/changelogs/unreleased/ab-44259-atomic-internal-ids-for-all-models.yml
@@ -0,0 +1,5 @@
+---
+title: Transition to atomic internal ids for all models.
+merge_request: 44259
+author:
+type: other
diff --git a/changelogs/unreleased/ab-44467-remove-index.yml b/changelogs/unreleased/ab-44467-remove-index.yml
deleted file mode 100644
index fb772ce85d5..00000000000
--- a/changelogs/unreleased/ab-44467-remove-index.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove unused index from events table.
-merge_request: 18014
-author:
-type: other
diff --git a/changelogs/unreleased/ac-fix-use_file-race.yml b/changelogs/unreleased/ac-fix-use_file-race.yml
deleted file mode 100644
index f1315d5d50e..00000000000
--- a/changelogs/unreleased/ac-fix-use_file-race.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix data race between ObjectStorage background_upload and Pages publishing
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml b/changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml
deleted file mode 100644
index 4db7f76e0af..00000000000
--- a/changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Port direct upload of LFS artifacts from EE
-merge_request: 17752
-author:
-type: added
diff --git a/changelogs/unreleased/ac-pages-port.yml b/changelogs/unreleased/ac-pages-port.yml
deleted file mode 100644
index 4f7257b4798..00000000000
--- a/changelogs/unreleased/ac-pages-port.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add missing port to artifact links
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/accessible-text.yml b/changelogs/unreleased/accessible-text.yml
new file mode 100755
index 00000000000..d39d5a9eb2c
--- /dev/null
+++ b/changelogs/unreleased/accessible-text.yml
@@ -0,0 +1,6 @@
+---
+title: Replace "Click" with "Select" to be more inclusive of people with accessibility
+ requirements
+merge_request: 18386
+author: Mark Lapierre
+type: other
diff --git a/changelogs/unreleased/adamco-gitlab-ce-move-issue-command.yml b/changelogs/unreleased/adamco-gitlab-ce-move-issue-command.yml
deleted file mode 100644
index 3b057373e7d..00000000000
--- a/changelogs/unreleased/adamco-gitlab-ce-move-issue-command.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add slash command for moving issues
-merge_request:
-author: Adam Pahlevi
-type: added
diff --git a/changelogs/unreleased/add-canary-favicon.yml b/changelogs/unreleased/add-canary-favicon.yml
deleted file mode 100644
index 1af6572588d..00000000000
--- a/changelogs/unreleased/add-canary-favicon.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add yellow favicon when `CANARY=true` to differientate canary environment
-merge_request: 12477
-author:
-type: changed
diff --git a/changelogs/unreleased/add-copy-metadata-command.yml b/changelogs/unreleased/add-copy-metadata-command.yml
new file mode 100644
index 00000000000..3bf25ae6ce0
--- /dev/null
+++ b/changelogs/unreleased/add-copy-metadata-command.yml
@@ -0,0 +1,5 @@
+---
+title: Add Copy metadata quick action
+merge_request: 16473
+author: Mateusz Bajorski
+type: added
diff --git a/changelogs/unreleased/add-cpu-mem-totals.yml b/changelogs/unreleased/add-cpu-mem-totals.yml
deleted file mode 100644
index bc8babab731..00000000000
--- a/changelogs/unreleased/add-cpu-mem-totals.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Total CPU/Memory consumption metrics for Kubernetes
-merge_request: 17731
-author:
-type: added
diff --git a/changelogs/unreleased/add-milestone-path-to-dashboard-milestones-breadcrumb-link.yml b/changelogs/unreleased/add-milestone-path-to-dashboard-milestones-breadcrumb-link.yml
deleted file mode 100644
index 015bee99170..00000000000
--- a/changelogs/unreleased/add-milestone-path-to-dashboard-milestones-breadcrumb-link.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update dashboard milestones breadcrumb link
-merge_request: 17933
-author: George Tsiolis
-type: fixed
diff --git a/changelogs/unreleased/add-per-runner-job-timeout.yml b/changelogs/unreleased/add-per-runner-job-timeout.yml
deleted file mode 100644
index 336b4d15ddf..00000000000
--- a/changelogs/unreleased/add-per-runner-job-timeout.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add per-runner configured job timeout
-merge_request: 17221
-author:
-type: added
diff --git a/changelogs/unreleased/add-query-counts-to-profiler-output.yml b/changelogs/unreleased/add-query-counts-to-profiler-output.yml
deleted file mode 100644
index 8a90b1cbeb0..00000000000
--- a/changelogs/unreleased/add-query-counts-to-profiler-output.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add query counts to profiler output
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/ajax-requests-in-performance-bar.yml b/changelogs/unreleased/ajax-requests-in-performance-bar.yml
deleted file mode 100644
index 88cc3678c2b..00000000000
--- a/changelogs/unreleased/ajax-requests-in-performance-bar.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow viewing timings for AJAX requests in the performance bar
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/align-project-avatar-on-small-viewports.yml b/changelogs/unreleased/align-project-avatar-on-small-viewports.yml
new file mode 100644
index 00000000000..320e7fbc294
--- /dev/null
+++ b/changelogs/unreleased/align-project-avatar-on-small-viewports.yml
@@ -0,0 +1,5 @@
+---
+title: Align project avatar on small viewports
+merge_request: 18513
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/ash-mckenzie-include-sha-with-version.yml b/changelogs/unreleased/ash-mckenzie-include-sha-with-version.yml
new file mode 100644
index 00000000000..b49c48e0fe1
--- /dev/null
+++ b/changelogs/unreleased/ash-mckenzie-include-sha-with-version.yml
@@ -0,0 +1,5 @@
+---
+title: git SHA is now displayed alongside the GitLab version on the Admin Dashboard
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/blackst0ne-add-missing-changelog-type-to-docs.yml b/changelogs/unreleased/blackst0ne-add-missing-changelog-type-to-docs.yml
new file mode 100644
index 00000000000..f8790fa45aa
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-add-missing-changelog-type-to-docs.yml
@@ -0,0 +1,5 @@
+---
+title: Add missing changelog type to docs
+merge_request: 18526
+author: "@blackst0ne"
+type: other
diff --git a/changelogs/unreleased/blackst0ne-bump-html-pipeline-gem.yml b/changelogs/unreleased/blackst0ne-bump-html-pipeline-gem.yml
deleted file mode 100644
index 9885c8853cc..00000000000
--- a/changelogs/unreleased/blackst0ne-bump-html-pipeline-gem.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Bump html-pipeline to 2.7.1
-merge_request: 18132
-author: "@blackst0ne"
-type: other
diff --git a/changelogs/unreleased/blackst0ne-rails5-update-state_machines-activerecord-gem.yml b/changelogs/unreleased/blackst0ne-rails5-update-state_machines-activerecord-gem.yml
deleted file mode 100644
index a9c6fcbf428..00000000000
--- a/changelogs/unreleased/blackst0ne-rails5-update-state_machines-activerecord-gem.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Bump `state_machines-activerecord` to 0.5.1
-merge_request: 17924
-author: blackst0ne
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-branches-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-branches-feature.yml
new file mode 100644
index 00000000000..bcfba4ae70d
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-branches-feature.yml
@@ -0,0 +1,5 @@
+---
+title: "Replace the `project/commits/branches.feature` spinach test with an rspec analog"
+merge_request: 18302
+author: "@blackst0ne"
+type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-comments-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-comments-feature.yml
new file mode 100644
index 00000000000..e7077f27555
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-comments-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the `project/commits/comments.feature` spinach test with an rspec analog
+merge_request: 18356
+author: "@blackst0ne"
+type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-issues-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-issues-feature.yml
deleted file mode 100644
index 7defdc0a28f..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-issues-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the spinach test with an rspec analog
-merge_request: 17950
-author: blackst0ne
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-labels-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-labels-feature.yml
deleted file mode 100644
index 4e1bb15f150..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-labels-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the `project/issues/labels.feature` spinach test with an rspec analog
-merge_request: 18126
-author: blackst0ne
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-milestones-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-milestones-feature.yml
new file mode 100644
index 00000000000..0dcac0a80eb
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-milestones-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the `project/issues/milestones.feature` spinach test with an rspec analog
+merge_request: 18300
+author: "@blackst0ne"
+type: other
diff --git a/changelogs/unreleased/bvl-export-import-lfs.yml b/changelogs/unreleased/bvl-export-import-lfs.yml
deleted file mode 100644
index dd1f499c3a3..00000000000
--- a/changelogs/unreleased/bvl-export-import-lfs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Support LFS objects when importing/exporting GitLab project archives
-merge_request: 18115
-author:
-type: added
diff --git a/changelogs/unreleased/bvl-fix-maintainer-push-error.yml b/changelogs/unreleased/bvl-fix-maintainer-push-error.yml
new file mode 100644
index 00000000000..66ab8fbf884
--- /dev/null
+++ b/changelogs/unreleased/bvl-fix-maintainer-push-error.yml
@@ -0,0 +1,5 @@
+---
+title: Fix errors on pushing to an empty repository
+merge_request: 18462
+author:
+type: fixed
diff --git a/changelogs/unreleased/bvl-import-zip-multiple-assignees.yml b/changelogs/unreleased/bvl-import-zip-multiple-assignees.yml
deleted file mode 100644
index 86bd5faf8ed..00000000000
--- a/changelogs/unreleased/bvl-import-zip-multiple-assignees.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix importing multiple assignees from GitLab export
-merge_request: 17718
-author:
-type: fixed
diff --git a/changelogs/unreleased/bvl-no-permanent-redirect.yml b/changelogs/unreleased/bvl-no-permanent-redirect.yml
deleted file mode 100644
index c34a3789b58..00000000000
--- a/changelogs/unreleased/bvl-no-permanent-redirect.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Don't create permanent redirect routes
-merge_request: 17521
-author:
-type: changed
diff --git a/changelogs/unreleased/bvl-override-import-params.yml b/changelogs/unreleased/bvl-override-import-params.yml
deleted file mode 100644
index 18cfef873df..00000000000
--- a/changelogs/unreleased/bvl-override-import-params.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow overriding params on project import through API
-merge_request: 18086
-author:
-type: added
diff --git a/changelogs/unreleased/bvl-shared-groups-on-group-page.yml b/changelogs/unreleased/bvl-shared-groups-on-group-page.yml
new file mode 100644
index 00000000000..6c0703fd138
--- /dev/null
+++ b/changelogs/unreleased/bvl-shared-groups-on-group-page.yml
@@ -0,0 +1,5 @@
+---
+title: Show shared projects on group page
+merge_request: 18390
+author:
+type: fixed
diff --git a/changelogs/unreleased/ci-pipeline-commit-lookup.yml b/changelogs/unreleased/ci-pipeline-commit-lookup.yml
deleted file mode 100644
index b2a1e4c2163..00000000000
--- a/changelogs/unreleased/ci-pipeline-commit-lookup.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use porcelain commit lookup method on CI::CreatePipelineService
-merge_request: 17911
-author:
-type: fixed
diff --git a/changelogs/unreleased/da-gitaly-calculate-repository-checksum.yml b/changelogs/unreleased/da-gitaly-calculate-repository-checksum.yml
deleted file mode 100644
index de09f87a7c9..00000000000
--- a/changelogs/unreleased/da-gitaly-calculate-repository-checksum.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Repository checksum calculation is handled by Gitaly when feature is enabled
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml b/changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml
deleted file mode 100644
index 92a03070d78..00000000000
--- a/changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add 'Assigned Issues' and 'Assigned Merge Requests' as dashboard view choices for users
-merge_request: 17860
-author: Elias Werberich
-type: added
diff --git a/changelogs/unreleased/deploy-tokens-container-registry-specs.yml b/changelogs/unreleased/deploy-tokens-container-registry-specs.yml
deleted file mode 100644
index d86f955c966..00000000000
--- a/changelogs/unreleased/deploy-tokens-container-registry-specs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Verify that deploy token has valid access when pulling container registry image
-merge_request: 18260
-author:
-type: fixed
diff --git a/changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml b/changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml
new file mode 100644
index 00000000000..3e1ac7b795d
--- /dev/null
+++ b/changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml
@@ -0,0 +1,5 @@
+---
+title: Add deprecation message to dynamic milestone pages
+merge_request: 17505
+author:
+type: added
diff --git a/changelogs/unreleased/direct-upload-of-uploads.yml b/changelogs/unreleased/direct-upload-of-uploads.yml
deleted file mode 100644
index 7900fa5f58d..00000000000
--- a/changelogs/unreleased/direct-upload-of-uploads.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow to store uploads by default on Object Storage
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/dm-deploy-keys-default-user.yml b/changelogs/unreleased/dm-deploy-keys-default-user.yml
deleted file mode 100644
index b82d67d028c..00000000000
--- a/changelogs/unreleased/dm-deploy-keys-default-user.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Ensure hooks run when a deploy key without a user pushes
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/dm-flatten-tree-plus-chars.yml b/changelogs/unreleased/dm-flatten-tree-plus-chars.yml
deleted file mode 100644
index 23f1b30d8fa..00000000000
--- a/changelogs/unreleased/dm-flatten-tree-plus-chars.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix links to subdirectories of a directory with a plus character in its path
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/dm-internal-user-namespace.yml b/changelogs/unreleased/dm-internal-user-namespace.yml
deleted file mode 100644
index 8517c116795..00000000000
--- a/changelogs/unreleased/dm-internal-user-namespace.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Ensure internal users (ghost, support bot) get assigned a namespace
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/dz-add-2fa-filter-admin-api.yml b/changelogs/unreleased/dz-add-2fa-filter-admin-api.yml
new file mode 100644
index 00000000000..df479e69380
--- /dev/null
+++ b/changelogs/unreleased/dz-add-2fa-filter-admin-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add 2FA filter to users API for admins only
+merge_request: 18503
+author:
+type: changed
diff --git a/changelogs/unreleased/dz-improve-app-settings-2.yml b/changelogs/unreleased/dz-improve-app-settings-2.yml
deleted file mode 100644
index ebe571decb8..00000000000
--- a/changelogs/unreleased/dz-improve-app-settings-2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Redesign application settings to match project settings
-merge_request: 18019
-author:
-type: changed
diff --git a/changelogs/unreleased/escape-autocomplete-values-for-markdown.yml b/changelogs/unreleased/escape-autocomplete-values-for-markdown.yml
deleted file mode 100644
index eea9da4c579..00000000000
--- a/changelogs/unreleased/escape-autocomplete-values-for-markdown.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Escape Markdown characters properly when using autocomplete
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/expose-commits-mr-api.yml b/changelogs/unreleased/expose-commits-mr-api.yml
deleted file mode 100644
index 77ea2f27431..00000000000
--- a/changelogs/unreleased/expose-commits-mr-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow merge requests related to a commit to be found via API
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/feature-add-language-in-repository-to-api.yml b/changelogs/unreleased/feature-add-language-in-repository-to-api.yml
new file mode 100644
index 00000000000..bd9bd377212
--- /dev/null
+++ b/changelogs/unreleased/feature-add-language-in-repository-to-api.yml
@@ -0,0 +1,5 @@
+---
+title: 'API: add languages of project GET /projects/:id/languages'
+merge_request: 17770
+author: Roger Rüttimann
+type: added
diff --git a/changelogs/unreleased/feature-gb-variables-expressions-in-only-except.yml b/changelogs/unreleased/feature-gb-variables-expressions-in-only-except.yml
deleted file mode 100644
index 84977ce11c8..00000000000
--- a/changelogs/unreleased/feature-gb-variables-expressions-in-only-except.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for pipeline variables expressions in only/except
-merge_request: 17316
-author:
-type: added
diff --git a/changelogs/unreleased/feature_detect_co_authored_commits.yml b/changelogs/unreleased/feature_detect_co_authored_commits.yml
deleted file mode 100644
index 7b1269ed982..00000000000
--- a/changelogs/unreleased/feature_detect_co_authored_commits.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Detect commit message trailers and link users properly to their accounts
- on Gitlab
-merge_request: 17919
-author: cousine
-type: added
diff --git a/changelogs/unreleased/fix-40798-namespace-forking.yml b/changelogs/unreleased/fix-40798-namespace-forking.yml
deleted file mode 100644
index 095235725f8..00000000000
--- a/changelogs/unreleased/fix-40798-namespace-forking.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix forking to subgroup via API when namespace is given by name
-merge_request: 17815
-author: Jan Beckmann
-type: fixed
diff --git a/changelogs/unreleased/fix-42459---in-branch.yml b/changelogs/unreleased/fix-42459---in-branch.yml
deleted file mode 100644
index 26cc2046206..00000000000
--- a/changelogs/unreleased/fix-42459---in-branch.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix relative uri when "#" is in branch name
-merge_request:
-author: Jan
-type: fixed
diff --git a/changelogs/unreleased/fix-500-error-when-mr-ref-is-not-yet-fetched.yml b/changelogs/unreleased/fix-500-error-when-mr-ref-is-not-yet-fetched.yml
deleted file mode 100644
index e21554f091a..00000000000
--- a/changelogs/unreleased/fix-500-error-when-mr-ref-is-not-yet-fetched.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fix 500 error when a merge request from a fork has conflicts and has not yet
- been updated
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-auth0-unsafe-login.yml b/changelogs/unreleased/fix-auth0-unsafe-login.yml
deleted file mode 100644
index 01c6ea69dcc..00000000000
--- a/changelogs/unreleased/fix-auth0-unsafe-login.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix GitLab Auth0 integration signing in the wrong user
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/fix-dashboard-sorting.yml b/changelogs/unreleased/fix-dashboard-sorting.yml
deleted file mode 100644
index 2ba13a93fa9..00000000000
--- a/changelogs/unreleased/fix-dashboard-sorting.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prioritize weight over title when sorting charts
-merge_request: 18233
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-emoji-popup.yml b/changelogs/unreleased/fix-emoji-popup.yml
deleted file mode 100644
index c81d061a5d7..00000000000
--- a/changelogs/unreleased/fix-emoji-popup.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Hide emoji popup after multiple spaces
-merge_request:
-author: Jan Beckmann
-type: fixed
diff --git a/changelogs/unreleased/fix-mattermost-delete-team.yml b/changelogs/unreleased/fix-mattermost-delete-team.yml
deleted file mode 100644
index d14ae023114..00000000000
--- a/changelogs/unreleased/fix-mattermost-delete-team.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed group deletion linked to Mattermost
-merge_request: 16209
-author: Julien Millau
-type: fixed
diff --git a/changelogs/unreleased/fix-n-plus-one-when-getting-notification-settings-for-recipients.yml b/changelogs/unreleased/fix-n-plus-one-when-getting-notification-settings-for-recipients.yml
deleted file mode 100644
index 837bfed19b7..00000000000
--- a/changelogs/unreleased/fix-n-plus-one-when-getting-notification-settings-for-recipients.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Goldiloader to fix N+1 issues when calculating email recipients
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/fix-projects-no-repository-placeholder.yml b/changelogs/unreleased/fix-projects-no-repository-placeholder.yml
deleted file mode 100644
index 3d11c897020..00000000000
--- a/changelogs/unreleased/fix-projects-no-repository-placeholder.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update no repository placeholder
-merge_request: 17964
-author: George Tsiolis
-type: fixed
diff --git a/changelogs/unreleased/fj-15329-services-callbacks-ssrf.yml b/changelogs/unreleased/fj-15329-services-callbacks-ssrf.yml
deleted file mode 100644
index 7fa6f6a5874..00000000000
--- a/changelogs/unreleased/fj-15329-services-callbacks-ssrf.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed some SSRF vulnerabilities in services, hooks and integrations
-merge_request: 2337
-author:
-type: security
diff --git a/changelogs/unreleased/fj-174-better-ldap-connection-handling.yml b/changelogs/unreleased/fj-174-better-ldap-connection-handling.yml
deleted file mode 100644
index be0b83505fb..00000000000
--- a/changelogs/unreleased/fj-174-better-ldap-connection-handling.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add better LDAP connection handling
-merge_request: 18039
-author:
-type: fixed
diff --git a/changelogs/unreleased/fj-41900-import-endpoint-with-overwrite-support.yml b/changelogs/unreleased/fj-41900-import-endpoint-with-overwrite-support.yml
deleted file mode 100644
index 0553cc684ce..00000000000
--- a/changelogs/unreleased/fj-41900-import-endpoint-with-overwrite-support.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Extend API for importing a project export with overwrite support
-merge_request: 17883
-author:
-type: added
diff --git a/changelogs/unreleased/fj-42354-custom-hooks-not-triggered-by-UI-wiki-edit.yml b/changelogs/unreleased/fj-42354-custom-hooks-not-triggered-by-UI-wiki-edit.yml
new file mode 100644
index 00000000000..9fe458aba4a
--- /dev/null
+++ b/changelogs/unreleased/fj-42354-custom-hooks-not-triggered-by-UI-wiki-edit.yml
@@ -0,0 +1,5 @@
+---
+title: Triggering custom hooks by Wiki UI edit
+merge_request: 18251
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-42685-extend-project-export-endpoint.yml b/changelogs/unreleased/fj-42685-extend-project-export-endpoint.yml
deleted file mode 100644
index a06499d821a..00000000000
--- a/changelogs/unreleased/fj-42685-extend-project-export-endpoint.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Extend API for exporting a project with direct upload URL
-merge_request: 17686
-author:
-type: added
diff --git a/changelogs/unreleased/fj-45057-improve-ssrf-documentation.yml b/changelogs/unreleased/fj-45057-improve-ssrf-documentation.yml
new file mode 100644
index 00000000000..b923f442b26
--- /dev/null
+++ b/changelogs/unreleased/fj-45057-improve-ssrf-documentation.yml
@@ -0,0 +1,5 @@
+---
+title: Added Webhook SSRF prevention to documentation
+merge_request: 18532
+author:
+type: other
diff --git a/changelogs/unreleased/fj-change-gollum-gems-to-custom-ones.yml b/changelogs/unreleased/fj-change-gollum-gems-to-custom-ones.yml
new file mode 100644
index 00000000000..53883e8d907
--- /dev/null
+++ b/changelogs/unreleased/fj-change-gollum-gems-to-custom-ones.yml
@@ -0,0 +1,5 @@
+---
+title: Replacing gollum libraries for gitlab custom libs
+merge_request: 18343
+author:
+type: other
diff --git a/changelogs/unreleased/fl-fix-annoying-actions.yml b/changelogs/unreleased/fl-fix-annoying-actions.yml
deleted file mode 100644
index fe17f9a8978..00000000000
--- a/changelogs/unreleased/fl-fix-annoying-actions.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Stop redirecting the page in pipeline main actions
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fl-pipelines-details-axios.yml b/changelogs/unreleased/fl-pipelines-details-axios.yml
new file mode 100644
index 00000000000..0b72e54cba3
--- /dev/null
+++ b/changelogs/unreleased/fl-pipelines-details-axios.yml
@@ -0,0 +1,5 @@
+---
+title: Replace vue resource with axios for pipelines details page
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/ide-file-finder.yml b/changelogs/unreleased/ide-file-finder.yml
new file mode 100644
index 00000000000..252dd3a30c4
--- /dev/null
+++ b/changelogs/unreleased/ide-file-finder.yml
@@ -0,0 +1,5 @@
+---
+title: Added fuzzy file finder to web IDE
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/ide-file-row-hover-style.yml b/changelogs/unreleased/ide-file-row-hover-style.yml
deleted file mode 100644
index 158379a5aef..00000000000
--- a/changelogs/unreleased/ide-file-row-hover-style.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added hover background color to IDE file list rows
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/ide-folder-button-path.yml b/changelogs/unreleased/ide-folder-button-path.yml
deleted file mode 100644
index 84a122fab75..00000000000
--- a/changelogs/unreleased/ide-folder-button-path.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed IDE button opening the wrong URL in tree list
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/ide-project-avatar-identicon.yml b/changelogs/unreleased/ide-project-avatar-identicon.yml
deleted file mode 100644
index 2b8b00018a8..00000000000
--- a/changelogs/unreleased/ide-project-avatar-identicon.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make project avatar in IDE consistent with the rest of GitLab
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/improve-jobs-queuing-time-metric.yml b/changelogs/unreleased/improve-jobs-queuing-time-metric.yml
new file mode 100644
index 00000000000..cee8b8523fd
--- /dev/null
+++ b/changelogs/unreleased/improve-jobs-queuing-time-metric.yml
@@ -0,0 +1,5 @@
+---
+title: Partition job_queue_duration_seconds with jobs_running_for_project
+merge_request: 17730
+author:
+type: changed
diff --git a/changelogs/unreleased/increase-unicorn-memory-killer-limits.yml b/changelogs/unreleased/increase-unicorn-memory-killer-limits.yml
deleted file mode 100644
index 6d7d2df4f4a..00000000000
--- a/changelogs/unreleased/increase-unicorn-memory-killer-limits.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Increase the memory limits used in the unicorn killer
-merge_request: 17948
-author:
-type: other
diff --git a/changelogs/unreleased/issue_25542.yml b/changelogs/unreleased/issue_25542.yml
deleted file mode 100644
index eba491f7e2a..00000000000
--- a/changelogs/unreleased/issue_25542.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve JIRA event descriptions
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/issue_40915.yml b/changelogs/unreleased/issue_40915.yml
deleted file mode 100644
index 2b6d98e69a6..00000000000
--- a/changelogs/unreleased/issue_40915.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow assigning and filtering issuables by ancestor group labels
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/issue_42443.yml b/changelogs/unreleased/issue_42443.yml
deleted file mode 100644
index 954fbd98a4b..00000000000
--- a/changelogs/unreleased/issue_42443.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Include subgroup issues when searching for group issues using the API
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/issue_44270.yml b/changelogs/unreleased/issue_44270.yml
deleted file mode 100644
index 6234162be30..00000000000
--- a/changelogs/unreleased/issue_44270.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show issues of subgroups in group-level issue board
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/issue_45463.yml b/changelogs/unreleased/issue_45463.yml
new file mode 100644
index 00000000000..a350568d04b
--- /dev/null
+++ b/changelogs/unreleased/issue_45463.yml
@@ -0,0 +1,5 @@
+---
+title: Fix users not seeing labels from private groups when being a member of a child project
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jej-commit-api-tracks-lfs.yml b/changelogs/unreleased/jej-commit-api-tracks-lfs.yml
deleted file mode 100644
index 8284abf9f28..00000000000
--- a/changelogs/unreleased/jej-commit-api-tracks-lfs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Create commit API and Web IDE obey LFS filters
-merge_request: 16718
-author:
-type: fixed
diff --git a/changelogs/unreleased/jej-mattermost-notification-confidentiality.yml b/changelogs/unreleased/jej-mattermost-notification-confidentiality.yml
deleted file mode 100644
index d5219b5d019..00000000000
--- a/changelogs/unreleased/jej-mattermost-notification-confidentiality.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds confidential notes channel for Slack/Mattermost
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/jivl-realtime-update-adding-file.yml b/changelogs/unreleased/jivl-realtime-update-adding-file.yml
deleted file mode 100644
index df1bdb1648d..00000000000
--- a/changelogs/unreleased/jivl-realtime-update-adding-file.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add realtime pipeline status for adding/viewing files
-merge_request: 17705
-author:
-type: other
diff --git a/changelogs/unreleased/jivl-refactor-activity-calendar.yml b/changelogs/unreleased/jivl-refactor-activity-calendar.yml
new file mode 100644
index 00000000000..0702ede4af9
--- /dev/null
+++ b/changelogs/unreleased/jivl-refactor-activity-calendar.yml
@@ -0,0 +1,5 @@
+---
+title: Refactored activity calendar
+merge_request: 18469
+author: Enrico Scholz
+type: changed
diff --git a/changelogs/unreleased/jivl-summary-statistics-prometheus-dashboard.yml b/changelogs/unreleased/jivl-summary-statistics-prometheus-dashboard.yml
deleted file mode 100644
index c5cdbcf7b40..00000000000
--- a/changelogs/unreleased/jivl-summary-statistics-prometheus-dashboard.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add average and maximum summary statistics to the prometheus dashboard
-merge_request: 17921
-author:
-type: changed
diff --git a/changelogs/unreleased/jprovazn-issueref.yml b/changelogs/unreleased/jprovazn-issueref.yml
deleted file mode 100644
index ee19cac7b19..00000000000
--- a/changelogs/unreleased/jprovazn-issueref.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Display state indicator for issuable references in non-project scope (e.g.
- when referencing issuables from group scope).
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/jramsay-38830-tarball.yml b/changelogs/unreleased/jramsay-38830-tarball.yml
deleted file mode 100644
index 6d40c305614..00000000000
--- a/changelogs/unreleased/jramsay-38830-tarball.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add alternate archive route for simplified packaging
-merge_request: 17225
-author:
-type: added
diff --git a/changelogs/unreleased/label-links-on-project-transfer.yml b/changelogs/unreleased/label-links-on-project-transfer.yml
new file mode 100644
index 00000000000..fabedb77cb3
--- /dev/null
+++ b/changelogs/unreleased/label-links-on-project-transfer.yml
@@ -0,0 +1,5 @@
+---
+title: Fix label links update on project transfer
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/merge-request-widget-source-branch-improvements.yml b/changelogs/unreleased/merge-request-widget-source-branch-improvements.yml
deleted file mode 100644
index 942eb6062fd..00000000000
--- a/changelogs/unreleased/merge-request-widget-source-branch-improvements.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fixes remove source branch checkbox being visible when user cannot remove the
- branch
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/move-email-footer-info-to-single-line.yml b/changelogs/unreleased/move-email-footer-info-to-single-line.yml
deleted file mode 100644
index 87ed5638056..00000000000
--- a/changelogs/unreleased/move-email-footer-info-to-single-line.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move email footer info to a single line
-merge_request: 17916
-author:
-type: changed
diff --git a/changelogs/unreleased/move-estimate-only-pane-vue-component.yml b/changelogs/unreleased/move-estimate-only-pane-vue-component.yml
new file mode 100644
index 00000000000..b6c538f70b3
--- /dev/null
+++ b/changelogs/unreleased/move-estimate-only-pane-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Move TimeTrackingEstimateOnlyPane vue component
+merge_request: 18318
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/move-help-state-vue-component.yml b/changelogs/unreleased/move-help-state-vue-component.yml
new file mode 100644
index 00000000000..6108368cde0
--- /dev/null
+++ b/changelogs/unreleased/move-help-state-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Move TimeTrackingHelpState vue component
+merge_request: 18319
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/move-pipeline-failed-vue-component.yml b/changelogs/unreleased/move-pipeline-failed-vue-component.yml
new file mode 100644
index 00000000000..38d42134876
--- /dev/null
+++ b/changelogs/unreleased/move-pipeline-failed-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Move PipelineFailed vue component
+merge_request: 18277
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/move-registry-after-cicd-project-nav-sidebar.yml b/changelogs/unreleased/move-registry-after-cicd-project-nav-sidebar.yml
deleted file mode 100644
index 03a6fd42228..00000000000
--- a/changelogs/unreleased/move-registry-after-cicd-project-nav-sidebar.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
- title: Move 'Registry' after 'CI/CD' in project navigation sidebar
- merge_request: 18018
- author: Elias Werberich
- type: changed
diff --git a/changelogs/unreleased/optional-api-delimiter.yml b/changelogs/unreleased/optional-api-delimiter.yml
deleted file mode 100644
index 0bcd0787306..00000000000
--- a/changelogs/unreleased/optional-api-delimiter.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make /-/ delimiter optional for search endpoints
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/osw-41401-render-mr-commit-sha-instead-diffs.yml b/changelogs/unreleased/osw-41401-render-mr-commit-sha-instead-diffs.yml
deleted file mode 100644
index 44973641325..00000000000
--- a/changelogs/unreleased/osw-41401-render-mr-commit-sha-instead-diffs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Render MR commit SHA instead "diffs" when viable
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/osw-44295-adjust-authorization-for-discussions-show.yml b/changelogs/unreleased/osw-44295-adjust-authorization-for-discussions-show.yml
deleted file mode 100644
index 978c5468bb1..00000000000
--- a/changelogs/unreleased/osw-44295-adjust-authorization-for-discussions-show.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adjust 404's for LegacyDiffNote discussion rendering
-merge_request: 18201
-author:
-type: fixed
diff --git a/changelogs/unreleased/pages_force_https.yml b/changelogs/unreleased/pages_force_https.yml
deleted file mode 100644
index da7e29087f3..00000000000
--- a/changelogs/unreleased/pages_force_https.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add HTTPS-only pages
-merge_request: 16273
-author: rfwatson
-type: added
diff --git a/changelogs/unreleased/poc-upload-hashing-path.yml b/changelogs/unreleased/poc-upload-hashing-path.yml
deleted file mode 100644
index 7970405bea1..00000000000
--- a/changelogs/unreleased/poc-upload-hashing-path.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: File uploads in remote storage now support project renaming.
-merge_request: 4597
-author:
-type: fixed
diff --git a/changelogs/unreleased/rd-44635-error-500-when-clicking-ghost-user-or-gitlab-support-bot-in-ui.yml b/changelogs/unreleased/rd-44635-error-500-when-clicking-ghost-user-or-gitlab-support-bot-in-ui.yml
new file mode 100644
index 00000000000..a08a75ceda6
--- /dev/null
+++ b/changelogs/unreleased/rd-44635-error-500-when-clicking-ghost-user-or-gitlab-support-bot-in-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Fix missing namespace for some internal users
+merge_request: 18357
+author:
+type: fixed
diff --git a/changelogs/unreleased/rd-45502-uploading-project-export-with-lfs-file-locks-fails.yml b/changelogs/unreleased/rd-45502-uploading-project-export-with-lfs-file-locks-fails.yml
new file mode 100644
index 00000000000..e3266dda629
--- /dev/null
+++ b/changelogs/unreleased/rd-45502-uploading-project-export-with-lfs-file-locks-fails.yml
@@ -0,0 +1,5 @@
+---
+title: Don't include lfs_file_locks data in export bundle
+merge_request: 18495
+author:
+type: fixed
diff --git a/changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml b/changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml
deleted file mode 100644
index 1f793fe5e7c..00000000000
--- a/changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Reduce number of queries when viewing a merge request
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/refactor-move-assignee-title-vue-component.yml b/changelogs/unreleased/refactor-move-assignee-title-vue-component.yml
deleted file mode 100644
index f6521339c39..00000000000
--- a/changelogs/unreleased/refactor-move-assignee-title-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move AssigneeTitle vue component
-merge_request: 17397
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-mr-widget-memory-usage-and-graph-components.yml b/changelogs/unreleased/refactor-move-mr-widget-memory-usage-and-graph-components.yml
deleted file mode 100644
index 96e63343963..00000000000
--- a/changelogs/unreleased/refactor-move-mr-widget-memory-usage-and-graph-components.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move MemoryGraph and MemoryUsage vue components
-merge_request: 17533
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-mr-widget-nothing-to-merge-vue-component.yml b/changelogs/unreleased/refactor-move-mr-widget-nothing-to-merge-vue-component.yml
deleted file mode 100644
index dc8ff95dc27..00000000000
--- a/changelogs/unreleased/refactor-move-mr-widget-nothing-to-merge-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move NothingToMerge vue component
-merge_request: 17544
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-mr-widget-ready-to-merge-vue-component.yml b/changelogs/unreleased/refactor-move-mr-widget-ready-to-merge-vue-component.yml
new file mode 100644
index 00000000000..90192fae030
--- /dev/null
+++ b/changelogs/unreleased/refactor-move-mr-widget-ready-to-merge-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Move ReadyToMerge vue component
+merge_request: 17545
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/refactor-move-mr-widget-sha-mismatch-vue-component.yml b/changelogs/unreleased/refactor-move-mr-widget-sha-mismatch-vue-component.yml
deleted file mode 100644
index ac41fe23d3d..00000000000
--- a/changelogs/unreleased/refactor-move-mr-widget-sha-mismatch-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move ShaMismatch vue component
-merge_request: 17546
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-mr-widget-unresolved-discussions-vue-component.yml b/changelogs/unreleased/refactor-move-mr-widget-unresolved-discussions-vue-component.yml
deleted file mode 100644
index a31f1f372a8..00000000000
--- a/changelogs/unreleased/refactor-move-mr-widget-unresolved-discussions-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move UnresolvedDiscussions vue component
-merge_request: 17538
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-time-tracking-comparison-pane-vue-component.yml b/changelogs/unreleased/refactor-move-time-tracking-comparison-pane-vue-component.yml
deleted file mode 100644
index 88a4b8ec8c1..00000000000
--- a/changelogs/unreleased/refactor-move-time-tracking-comparison-pane-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move TimeTrackingComparisonPane vue component
-merge_request: 17931
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-time-tracking-vue-components.yml b/changelogs/unreleased/refactor-move-time-tracking-vue-components.yml
deleted file mode 100644
index 8151655250a..00000000000
--- a/changelogs/unreleased/refactor-move-time-tracking-vue-components.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move TimeTrackingCollapsedState vue component
-merge_request: 17399
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/remove-pages-tar-support.yml b/changelogs/unreleased/remove-pages-tar-support.yml
deleted file mode 100644
index 73448687912..00000000000
--- a/changelogs/unreleased/remove-pages-tar-support.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove support for legacy tar.gz pages artifacts
-merge_request: 18090
-author:
-type: deprecated
diff --git a/changelogs/unreleased/rename-overview-project-sidenav.yml b/changelogs/unreleased/rename-overview-project-sidenav.yml
new file mode 100644
index 00000000000..3632ef25c00
--- /dev/null
+++ b/changelogs/unreleased/rename-overview-project-sidenav.yml
@@ -0,0 +1,5 @@
+---
+title: Renamed Overview to Project in the contextual navigation at a project level
+merge_request: 18295
+author: Constance Okoghenun
+type: changed
diff --git a/changelogs/unreleased/restore-label-underline-color.yml b/changelogs/unreleased/restore-label-underline-color.yml
new file mode 100644
index 00000000000..2e24ece5c36
--- /dev/null
+++ b/changelogs/unreleased/restore-label-underline-color.yml
@@ -0,0 +1,5 @@
+---
+title: Restore label underline color
+merge_request: 18407
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/sh-add-cleanup-rpc-gitaly.yml b/changelogs/unreleased/sh-add-cleanup-rpc-gitaly.yml
deleted file mode 100644
index 81b48fc255b..00000000000
--- a/changelogs/unreleased/sh-add-cleanup-rpc-gitaly.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Automatically cleanup stale worktrees and lock files upon a push
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-appearance-cache-key-version.yml b/changelogs/unreleased/sh-appearance-cache-key-version.yml
deleted file mode 100644
index c17a3dc36cd..00000000000
--- a/changelogs/unreleased/sh-appearance-cache-key-version.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use the GitLab version as part of the appearances cache key
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-bump-lograge.yml b/changelogs/unreleased/sh-bump-lograge.yml
new file mode 100644
index 00000000000..65b15153a35
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-lograge.yml
@@ -0,0 +1,5 @@
+---
+title: Bump lograge to 0.10.0 and remove monkey patch
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/sh-gitlab-sidekiq-logger.yml b/changelogs/unreleased/sh-gitlab-sidekiq-logger.yml
deleted file mode 100644
index f68d45d2f38..00000000000
--- a/changelogs/unreleased/sh-gitlab-sidekiq-logger.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for Sidekiq JSON logging
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/sh-move-sidekiq-exporter-logs.yml b/changelogs/unreleased/sh-move-sidekiq-exporter-logs.yml
deleted file mode 100644
index 1990f4f6124..00000000000
--- a/changelogs/unreleased/sh-move-sidekiq-exporter-logs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move Sidekiq exporter logs to log/sidekiq_exporter.log
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/show-group-id-in-group-settings.yml b/changelogs/unreleased/show-group-id-in-group-settings.yml
new file mode 100644
index 00000000000..b975fe8c71d
--- /dev/null
+++ b/changelogs/unreleased/show-group-id-in-group-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Show group id in group settings
+merge_request: 18482
+author: George Tsiolis
+type: added
diff --git a/changelogs/unreleased/show-runners-description-on-jobs-page.yml b/changelogs/unreleased/show-runners-description-on-jobs-page.yml
new file mode 100644
index 00000000000..d9414a3d021
--- /dev/null
+++ b/changelogs/unreleased/show-runners-description-on-jobs-page.yml
@@ -0,0 +1,5 @@
+---
+title: Show Runner's description on job's page
+merge_request: 17321
+author:
+type: added
diff --git a/changelogs/unreleased/tc-re-add-read-only-banner.yml b/changelogs/unreleased/tc-re-add-read-only-banner.yml
deleted file mode 100644
index 35bcd7e184e..00000000000
--- a/changelogs/unreleased/tc-re-add-read-only-banner.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add read-only banner to all pages
-merge_request: 17798
-author:
-type: fixed
diff --git a/changelogs/unreleased/ui-mr-counter-cache.yml b/changelogs/unreleased/ui-mr-counter-cache.yml
deleted file mode 100644
index 5e241bfe93f..00000000000
--- a/changelogs/unreleased/ui-mr-counter-cache.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Deleting a MR you are assigned to should decrements counter
-merge_request: 17951
-author: m b
-type: fixed
diff --git a/changelogs/unreleased/unresolved-discussions-vue-component-i18n-and-tests.yml b/changelogs/unreleased/unresolved-discussions-vue-component-i18n-and-tests.yml
new file mode 100644
index 00000000000..d99a9c93c0b
--- /dev/null
+++ b/changelogs/unreleased/unresolved-discussions-vue-component-i18n-and-tests.yml
@@ -0,0 +1,5 @@
+---
+title: Add i18n and update specs for UnresolvedDiscussions vue component
+merge_request: 17866
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/update-doorkeeper-changelog.yml b/changelogs/unreleased/update-doorkeeper-changelog.yml
new file mode 100644
index 00000000000..b47bdf4a28d
--- /dev/null
+++ b/changelogs/unreleased/update-doorkeeper-changelog.yml
@@ -0,0 +1,5 @@
+---
+title: Update doorkeeper to 4.3.2 to fix GitLab OAuth authentication
+merge_request: 18543
+author:
+type: fixed
diff --git a/changelogs/unreleased/update-gitlab-ci-yml-services-docs.yml b/changelogs/unreleased/update-gitlab-ci-yml-services-docs.yml
deleted file mode 100644
index c76495ec959..00000000000
--- a/changelogs/unreleased/update-gitlab-ci-yml-services-docs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update CI services documnetation
-merge_request: 17749
-author:
-type: other
diff --git a/changelogs/unreleased/update-spec-import-path-for-vue-mount-component-helper.yml b/changelogs/unreleased/update-spec-import-path-for-vue-mount-component-helper.yml
deleted file mode 100644
index 9c13bfbaf6f..00000000000
--- a/changelogs/unreleased/update-spec-import-path-for-vue-mount-component-helper.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update spec import path for vue mount component helper
-merge_request: 17880
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/update-unresolved-discussions-vue-component.yml b/changelogs/unreleased/update-unresolved-discussions-vue-component.yml
deleted file mode 100644
index 246eaaae2bd..00000000000
--- a/changelogs/unreleased/update-unresolved-discussions-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add i18n and update specs for ShaMismatch vue component
-merge_request: 17870
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/use-chronic-duration-attribute-for-project-build-timeout.yml b/changelogs/unreleased/use-chronic-duration-attribute-for-project-build-timeout.yml
deleted file mode 100644
index 675d347b64c..00000000000
--- a/changelogs/unreleased/use-chronic-duration-attribute-for-project-build-timeout.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use human readable value build_timeout in Project
-merge_request: 17386
-author:
-type: changed
diff --git a/changelogs/unreleased/winh-41174-projects-groups-badges-ui.yml b/changelogs/unreleased/winh-41174-projects-groups-badges-ui.yml
deleted file mode 100644
index 14114eca2b2..00000000000
--- a/changelogs/unreleased/winh-41174-projects-groups-badges-ui.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Projects and groups badges settings UI
-merge_request: 17114
-author:
-type: added
diff --git a/changelogs/unreleased/winh-deprecate-old-modal.yml b/changelogs/unreleased/winh-deprecate-old-modal.yml
deleted file mode 100644
index 4fae1fafbea..00000000000
--- a/changelogs/unreleased/winh-deprecate-old-modal.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Rename modal.vue to deprecated_modal.vue
-merge_request: 17438
-author:
-type: other
diff --git a/changelogs/unreleased/winh-dropdown-entry-unlocking.yml b/changelogs/unreleased/winh-dropdown-entry-unlocking.yml
new file mode 100644
index 00000000000..fc669af1f57
--- /dev/null
+++ b/changelogs/unreleased/winh-dropdown-entry-unlocking.yml
@@ -0,0 +1,5 @@
+---
+title: Remove green background from unlock button in admin area
+merge_request: 18288
+author:
+type: changed
diff --git a/changelogs/unreleased/workhorse-gitaly-mandatory.yml b/changelogs/unreleased/workhorse-gitaly-mandatory.yml
deleted file mode 100644
index 77b62302e86..00000000000
--- a/changelogs/unreleased/workhorse-gitaly-mandatory.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make all workhorse gitaly calls opt-out, take 2
-merge_request: 18043
-author:
-type: other
diff --git a/changelogs/unreleased/zj-branch-containing-sha-opt-out.yml b/changelogs/unreleased/zj-branch-containing-sha-opt-out.yml
new file mode 100644
index 00000000000..3d11ee588ae
--- /dev/null
+++ b/changelogs/unreleased/zj-branch-containing-sha-opt-out.yml
@@ -0,0 +1,5 @@
+---
+title: Detecting branchnames containing a commit uses Gitaly by default
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/zj-bump-gitaly.yml b/changelogs/unreleased/zj-bump-gitaly.yml
deleted file mode 100644
index eb28bed70e4..00000000000
--- a/changelogs/unreleased/zj-bump-gitaly.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade Gitaly to upgrade its charlock_holmes
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/zj-feature-gate-remove-http-api.yml b/changelogs/unreleased/zj-feature-gate-remove-http-api.yml
deleted file mode 100644
index 2095f60146c..00000000000
--- a/changelogs/unreleased/zj-feature-gate-remove-http-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow feature gates to be removed through the API
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/zj-opt-out-delete-refs.yml b/changelogs/unreleased/zj-opt-out-delete-refs.yml
deleted file mode 100644
index b02a45eee17..00000000000
--- a/changelogs/unreleased/zj-opt-out-delete-refs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Bulk deleting refs is handled by Gitaly by default
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/zj-opt-out-list-commits-by-oid.yml b/changelogs/unreleased/zj-opt-out-list-commits-by-oid.yml
deleted file mode 100644
index 3871293ee04..00000000000
--- a/changelogs/unreleased/zj-opt-out-list-commits-by-oid.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: ListCommitsByOid is executed by Gitaly by default
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/zj-ref-exists-opt-out.yml b/changelogs/unreleased/zj-ref-exists-opt-out.yml
new file mode 100644
index 00000000000..cdffecb0d0a
--- /dev/null
+++ b/changelogs/unreleased/zj-ref-exists-opt-out.yml
@@ -0,0 +1,5 @@
+---
+title: Check if a ref exists is done by Gitaly by default
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/zj-remote-repo-exists.yml b/changelogs/unreleased/zj-remote-repo-exists.yml
deleted file mode 100644
index f024b83159b..00000000000
--- a/changelogs/unreleased/zj-remote-repo-exists.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Test if remote repository exists when importing wikis
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/zj-tag-containing-sha-opt-out.yml b/changelogs/unreleased/zj-tag-containing-sha-opt-out.yml
new file mode 100644
index 00000000000..4774c7811d1
--- /dev/null
+++ b/changelogs/unreleased/zj-tag-containing-sha-opt-out.yml
@@ -0,0 +1,5 @@
+---
+title: Detecting tags containing a commit uses Gitaly by default
+merge_request:
+author:
+type: performance
diff --git a/config/application.rb b/config/application.rb
index 13501d4bdb5..ad7338763f7 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -113,6 +113,7 @@ module Gitlab
config.assets.precompile << "performance_bar.css"
config.assets.precompile << "lib/ace.js"
config.assets.precompile << "test.css"
+ config.assets.precompile << "snippets.css"
config.assets.precompile << "locale/**/app.js"
# Import gitlab-svgs directly from vendored directory
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index acf7754abe6..575f27d1ea9 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -1,131 +1,4 @@
-# rubocop:disable GitlabSecurity/PublicSend
-
-require_dependency Rails.root.join('lib/gitlab') # Load Gitlab as soon as possible
-
-class Settings < Settingslogic
- source ENV.fetch('GITLAB_CONFIG') { "#{Rails.root}/config/gitlab.yml" }
- namespace Rails.env
-
- class << self
- def gitlab_on_standard_port?
- on_standard_port?(gitlab)
- end
-
- def host_without_www(url)
- host(url).sub('www.', '')
- end
-
- def build_gitlab_ci_url
- custom_port =
- if on_standard_port?(gitlab)
- nil
- else
- ":#{gitlab.port}"
- end
-
- [
- gitlab.protocol,
- "://",
- gitlab.host,
- custom_port,
- gitlab.relative_url_root
- ].join('')
- end
-
- def build_pages_url
- base_url(pages).join('')
- end
-
- def build_gitlab_shell_ssh_path_prefix
- user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}"
-
- if gitlab_shell.ssh_port != 22
- "ssh://#{user_host}:#{gitlab_shell.ssh_port}/"
- else
- if gitlab_shell.ssh_host.include? ':'
- "[#{user_host}]:"
- else
- "#{user_host}:"
- end
- end
- end
-
- def build_base_gitlab_url
- base_url(gitlab).join('')
- end
-
- def build_gitlab_url
- (base_url(gitlab) + [gitlab.relative_url_root]).join('')
- end
-
- # check that values in `current` (string or integer) is a contant in `modul`.
- def verify_constant_array(modul, current, default)
- values = default || []
- unless current.nil?
- values = []
- current.each do |constant|
- values.push(verify_constant(modul, constant, nil))
- end
- values.delete_if { |value| value.nil? }
- end
-
- values
- end
-
- # check that `current` (string or integer) is a contant in `modul`.
- def verify_constant(modul, current, default)
- constant = modul.constants.find { |name| modul.const_get(name) == current }
- value = constant.nil? ? default : modul.const_get(constant)
- if current.is_a? String
- value = modul.const_get(current.upcase) rescue default
- end
-
- value
- end
-
- def absolute(path)
- File.expand_path(path, Rails.root)
- end
-
- private
-
- def base_url(config)
- custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
-
- [
- config.protocol,
- "://",
- config.host,
- custom_port
- ]
- end
-
- def on_standard_port?(config)
- config.port.to_i == (config.https ? 443 : 80)
- end
-
- # Extract the host part of the given +url+.
- def host(url)
- url = url.downcase
- url = "http://#{url}" unless url.start_with?('http')
-
- # Get rid of the path so that we don't even have to encode it
- url_without_path = url.sub(%r{(https?://[^/]+)/?.*}, '\1')
-
- URI.parse(url_without_path).host
- end
-
- # Runs every minute in a random ten-minute period on Sundays, to balance the
- # load on the server receiving these pings. The usage ping is safe to run
- # multiple times because of a 24 hour exclusive lock.
- def cron_for_usage_ping
- hour = rand(24)
- minute = rand(6)
-
- "#{minute}0-#{minute}9 #{hour} * * 0"
- end
- end
-end
+require_relative '../settings'
# Default settings
Settings['ldap'] ||= Settingslogic.new({})
@@ -455,6 +328,10 @@ Settings.cron_jobs['pages_domain_verification_cron_worker'] ||= Settingslogic.ne
Settings.cron_jobs['pages_domain_verification_cron_worker']['cron'] ||= '*/15 * * * *'
Settings.cron_jobs['pages_domain_verification_cron_worker']['job_class'] = 'PagesDomainVerificationCronWorker'
+Settings.cron_jobs['issue_due_scheduler_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['issue_due_scheduler_worker']['cron'] ||= '50 00 * * *'
+Settings.cron_jobs['issue_due_scheduler_worker']['job_class'] = 'IssueDueSchedulerWorker'
+
#
# Sidekiq
#
diff --git a/config/initializers/2_app.rb b/config/initializers/2_app.rb
deleted file mode 100644
index bd74f90e7d2..00000000000
--- a/config/initializers/2_app.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-module Gitlab
- def self.config
- Settings
- end
-
- VERSION = File.read(Rails.root.join("VERSION")).strip.freeze
- REVISION = Gitlab::Popen.popen(%W(#{config.git.bin_path} log --pretty=format:%h -n 1)).first.chomp.freeze
-end
diff --git a/config/initializers/2_gitlab.rb b/config/initializers/2_gitlab.rb
new file mode 100644
index 00000000000..1d2ab606a63
--- /dev/null
+++ b/config/initializers/2_gitlab.rb
@@ -0,0 +1 @@
+require_relative '../../lib/gitlab'
diff --git a/config/initializers/active_record_avoid_type_casting_in_uniqueness_validator.rb b/config/initializers/active_record_avoid_type_casting_in_uniqueness_validator.rb
new file mode 100644
index 00000000000..d9418caf68b
--- /dev/null
+++ b/config/initializers/active_record_avoid_type_casting_in_uniqueness_validator.rb
@@ -0,0 +1,98 @@
+# This is a monkey patch which must be removed when migrating to Rails 5.1 from 5.0.
+#
+# In Rails 5.0 there was introduced a bug which casts types in the uniqueness validator.
+# https://github.com/rails/rails/pull/23523/commits/811a4fa8eb6ceea841e61e8ac05747ffb69595ae
+#
+# That causes to bugs like this:
+#
+# 1) API::Users POST /user/:id/gpg_keys/:key_id/revoke when authenticated revokes existing key
+# Failure/Error: let(:gpg_key) { create(:gpg_key, user: user) }
+#
+# TypeError:
+# can't cast Hash
+# # ./spec/requests/api/users_spec.rb:7:in `block (2 levels) in <top (required)>'
+# # ./spec/requests/api/users_spec.rb:908:in `block (4 levels) in <top (required)>'
+# # ------------------
+# # --- Caused by: ---
+# # TypeError:
+# # TypeError
+# # ./spec/requests/api/users_spec.rb:7:in `block (2 levels) in <top (required)>'
+#
+# This bug was fixed in Rails 5.1 by https://github.com/rails/rails/pull/24745/commits/aa062318c451512035c10898a1af95943b1a3803
+
+if Gitlab.rails5?
+ ActiveSupport::Deprecation.warn("#{__FILE__} is a monkey patch which must be removed when upgrading to Rails 5.1")
+
+ if Rails.version.start_with?("5.1")
+ raise "Remove this monkey patch: #{__FILE__}"
+ end
+
+ # Copy-paste from https://github.com/kamipo/rails/blob/aa062318c451512035c10898a1af95943b1a3803/activerecord/lib/active_record/validations/uniqueness.rb
+ # including local fixes to make Rubocop happy again.
+ module ActiveRecord
+ module Validations
+ class UniquenessValidator < ActiveModel::EachValidator # :nodoc:
+ def validate_each(record, attribute, value)
+ finder_class = find_finder_class_for(record)
+ table = finder_class.arel_table
+ value = map_enum_attribute(finder_class, attribute, value)
+
+ relation = build_relation(finder_class, table, attribute, value)
+
+ if record.persisted?
+ if finder_class.primary_key
+ relation = relation.where.not(finder_class.primary_key => record.id_was || record.id)
+ else
+ raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.")
+ end
+ end
+
+ relation = scope_relation(record, table, relation)
+ relation = relation.merge(options[:conditions]) if options[:conditions]
+
+ if relation.exists?
+ error_options = options.except(:case_sensitive, :scope, :conditions)
+ error_options[:value] = value
+
+ record.errors.add(attribute, :taken, error_options)
+ end
+ rescue RangeError
+ end
+
+ protected
+
+ def build_relation(klass, table, attribute, value) #:nodoc:
+ if reflection = klass._reflect_on_association(attribute)
+ attribute = reflection.foreign_key
+ value = value.attributes[reflection.klass.primary_key] unless value.nil?
+ end
+
+ # the attribute may be an aliased attribute
+ if klass.attribute_alias?(attribute)
+ attribute = klass.attribute_alias(attribute)
+ end
+
+ attribute_name = attribute.to_s
+
+ column = klass.columns_hash[attribute_name]
+ cast_type = klass.type_for_attribute(attribute_name)
+
+ comparison =
+ if !options[:case_sensitive] && !value.nil?
+ # will use SQL LOWER function before comparison, unless it detects a case insensitive collation
+ klass.connection.case_insensitive_comparison(table, attribute, column, value)
+ else
+ klass.connection.case_sensitive_comparison(table, attribute, column, value)
+ end
+
+ if value.nil?
+ klass.unscoped.where(comparison)
+ else
+ bind = Relation::QueryAttribute.new(attribute_name, value, cast_type)
+ klass.unscoped.where(comparison, bind)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/config/initializers/deprecations.rb b/config/initializers/deprecations.rb
new file mode 100644
index 00000000000..2476ea9e38a
--- /dev/null
+++ b/config/initializers/deprecations.rb
@@ -0,0 +1,5 @@
+deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab')
+
+if Gitlab.dev_env_or_com?
+ ActiveSupport::Deprecation.deprecate_methods(Gitlab::GitalyClient::StorageSettings, :legacy_disk_path, deprecator: deprecator)
+end
diff --git a/config/initializers/forbid_sidekiq_in_transactions.rb b/config/initializers/forbid_sidekiq_in_transactions.rb
index 4cf1d455eb4..4603123665d 100644
--- a/config/initializers/forbid_sidekiq_in_transactions.rb
+++ b/config/initializers/forbid_sidekiq_in_transactions.rb
@@ -27,16 +27,8 @@ module Sidekiq
Use an `after_commit` hook, or include `AfterCommitQueue` and use a `run_after_commit` block instead.
MSG
rescue Sidekiq::Worker::EnqueueFromTransactionError => e
- if Rails.env.production?
- Rails.logger.error(e.message)
-
- if Gitlab::Sentry.enabled?
- Gitlab::Sentry.context
- Raven.capture_exception(e)
- end
- else
- raise
- end
+ Rails.logger.error(e.message) if Rails.env.production?
+ Gitlab::Sentry.track_exception(e)
end
end
diff --git a/config/initializers/gollum.rb b/config/initializers/gollum.rb
index 6dfaceb8427..81e0577a7c9 100644
--- a/config/initializers/gollum.rb
+++ b/config/initializers/gollum.rb
@@ -7,139 +7,6 @@ module Gollum
end
require "gollum-lib"
-module Gollum
- class Committer
- # Patch for UTF-8 path
- def method_missing(name, *args)
- 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
-
- # Remove if https://github.com/gollum/gollum-lib/pull/292 has been merged
- def update_page(page, name, format, data, commit = {})
- name = name ? ::File.basename(name) : page.name
- format ||= page.format
- dir = ::File.dirname(page.path)
- dir = '' if dir == '.'
- filename = (rename = page.name != name) ? Gollum::Page.cname(name) : page.filename_stripped
-
- multi_commit = !!commit[:committer]
- committer = multi_commit ? commit[:committer] : Committer.new(self, commit)
-
- if !rename && page.format == format
- committer.add(page.path, normalize(data))
- else
- committer.delete(page.path)
- committer.add_to_index(dir, filename, format, data)
- end
-
- committer.after_commit do |index, _sha|
- @access.refresh
- index.update_working_dir(dir, page.filename_stripped, page.format)
- index.update_working_dir(dir, filename, format)
- end
-
- multi_commit ? committer : committer.commit
- end
-
- # Remove if https://github.com/gollum/gollum-lib/pull/292 has been merged
- def rename_page(page, rename, commit = {})
- return false if page.nil?
- return false if rename.nil? || rename.empty?
-
- (target_dir, target_name) = ::File.split(rename)
- (source_dir, source_name) = ::File.split(page.path)
- source_name = page.filename_stripped
-
- # File.split gives us relative paths with ".", commiter.add_to_index doesn't like that.
- target_dir = '' if target_dir == '.'
- source_dir = '' if source_dir == '.'
- target_dir = target_dir.gsub(/^\//, '') # rubocop:disable Style/RegexpLiteral
-
- # if the rename is a NOOP, abort
- if source_dir == target_dir && source_name == target_name
- return false
- end
-
- multi_commit = !!commit[:committer]
- committer = multi_commit ? commit[:committer] : Committer.new(self, commit)
-
- # This piece only works for multi_commit
- # If we are in a commit batch and one of the previous operations
- # has updated the page, any information we ask to the page can be outdated.
- # Therefore, we should ask first to the current committer tree to see if
- # there is any updated change.
- raw_data = raw_data_in_committer(committer, source_dir, page.filename) ||
- raw_data_in_committer(committer, source_dir, "#{target_name}.#{Page.format_to_ext(page.format)}") ||
- page.raw_data
-
- committer.delete(page.path)
- committer.add_to_index(target_dir, target_name, page.format, raw_data)
-
- committer.after_commit do |index, _sha|
- @access.refresh
- index.update_working_dir(source_dir, source_name, page.format)
- index.update_working_dir(target_dir, target_name, page.format)
- end
-
- multi_commit ? committer : committer.commit
- end
-
- # Remove if https://github.com/gollum/gollum-lib/pull/292 has been merged
- def raw_data_in_committer(committer, dir, filename)
- data = nil
-
- [*dir.split(::File::SEPARATOR), filename].each do |key|
- data = data ? data[key] : committer.tree[key]
- break unless data
- end
-
- data
- end
- end
-
- module Git
- class Git
- def tree_entry(commit, path)
- pathname = Pathname.new(path)
- tmp_entry = nil
-
- pathname.each_filename do |dir|
- tmp_entry = if tmp_entry.nil?
- commit.tree[dir]
- else
- @repo.lookup(tmp_entry[:oid])[dir]
- end
-
- return nil unless tmp_entry
- end
- tmp_entry
- end
- end
- end
-end
-
Rails.application.configure do
config.after_initialize do
Gollum::Page.per_page = Kaminari.config.default_per_page
diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb
index 49fdd23064c..114c1cb512f 100644
--- a/config/initializers/lograge.rb
+++ b/config/initializers/lograge.rb
@@ -1,21 +1,3 @@
-# Monkey patch lograge until https://github.com/roidrage/lograge/pull/241 is released
-module Lograge
- class RequestLogSubscriber < ActiveSupport::LogSubscriber
- def strip_query_string(path)
- index = path.index('?')
- index ? path[0, index] : path
- end
-
- def extract_location
- location = Thread.current[:lograge_location]
- return {} unless location
-
- Thread.current[:lograge_location] = nil
- { location: strip_query_string(location) }
- end
- end
-end
-
# Only use Lograge for Rails
unless Sidekiq.server?
filename = File.join(Rails.root, 'log', "#{Rails.env}_json.log")
diff --git a/config/karma.config.js b/config/karma.config.js
index c378e621953..691cda98861 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -1,5 +1,6 @@
var path = require('path');
var webpack = require('webpack');
+var argumentsParser = require('commander');
var webpackConfig = require('./webpack.config.js');
var ROOT_PATH = path.resolve(__dirname, '..');
@@ -14,7 +15,25 @@ if (webpackConfig.plugins) {
});
}
-webpackConfig.devtool = 'cheap-inline-source-map';
+var testFiles = argumentsParser
+ .option(
+ '-f, --filter-spec [filter]',
+ 'Filter run spec files by path. Multiple filters are like a logical OR.',
+ (val, memo) => {
+ memo.push(val);
+ return memo;
+ },
+ []
+ )
+ .parse(process.argv).filterSpec;
+
+webpackConfig.plugins.push(
+ new webpack.DefinePlugin({
+ 'process.env.TEST_FILES': JSON.stringify(testFiles),
+ })
+);
+
+webpackConfig.devtool = process.env.BABEL_ENV !== 'coverage' && 'cheap-inline-source-map';
// Karma configuration
module.exports = function(config) {
diff --git a/config/routes/user.rb b/config/routes/user.rb
index 57fb37530bb..f8677693fab 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -1,3 +1,21 @@
+# Allows individual providers to be directed to a chosen controller
+# Call from inside devise_scope
+def override_omniauth(provider, controller, path_prefix = '/users/auth')
+ match "#{path_prefix}/#{provider}/callback",
+ to: "#{controller}##{provider}",
+ as: "#{provider}_omniauth_callback",
+ via: [:get, :post]
+end
+
+# Use custom controller for LDAP omniauth callback
+if Gitlab::Auth::LDAP::Config.enabled?
+ devise_scope :user do
+ Gitlab::Auth::LDAP::Config.available_servers.each do |server|
+ override_omniauth(server['provider_name'], 'ldap/omniauth_callbacks')
+ end
+ end
+end
+
devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks,
registrations: :registrations,
passwords: :passwords,
diff --git a/config/settings.rb b/config/settings.rb
new file mode 100644
index 00000000000..69d637761ea
--- /dev/null
+++ b/config/settings.rb
@@ -0,0 +1,126 @@
+require 'settingslogic'
+
+class Settings < Settingslogic
+ source ENV.fetch('GITLAB_CONFIG') { Pathname.new(File.expand_path('..', __dir__)).join('config/gitlab.yml') }
+ namespace ENV.fetch('GITLAB_ENV') { Rails.env }
+
+ class << self
+ def gitlab_on_standard_port?
+ on_standard_port?(gitlab)
+ end
+
+ def host_without_www(url)
+ host(url).sub('www.', '')
+ end
+
+ def build_gitlab_ci_url
+ custom_port =
+ if on_standard_port?(gitlab)
+ nil
+ else
+ ":#{gitlab.port}"
+ end
+
+ [
+ gitlab.protocol,
+ "://",
+ gitlab.host,
+ custom_port,
+ gitlab.relative_url_root
+ ].join('')
+ end
+
+ def build_pages_url
+ base_url(pages).join('')
+ end
+
+ def build_gitlab_shell_ssh_path_prefix
+ user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}"
+
+ if gitlab_shell.ssh_port != 22
+ "ssh://#{user_host}:#{gitlab_shell.ssh_port}/"
+ else
+ if gitlab_shell.ssh_host.include? ':'
+ "[#{user_host}]:"
+ else
+ "#{user_host}:"
+ end
+ end
+ end
+
+ def build_base_gitlab_url
+ base_url(gitlab).join('')
+ end
+
+ def build_gitlab_url
+ (base_url(gitlab) + [gitlab.relative_url_root]).join('')
+ end
+
+ # check that values in `current` (string or integer) is a contant in `modul`.
+ def verify_constant_array(modul, current, default)
+ values = default || []
+ unless current.nil?
+ values = []
+ current.each do |constant|
+ values.push(verify_constant(modul, constant, nil))
+ end
+ values.delete_if { |value| value.nil? }
+ end
+
+ values
+ end
+
+ # check that `current` (string or integer) is a contant in `modul`.
+ def verify_constant(modul, current, default)
+ constant = modul.constants.find { |name| modul.const_get(name) == current }
+ value = constant.nil? ? default : modul.const_get(constant)
+ if current.is_a? String
+ value = modul.const_get(current.upcase) rescue default
+ end
+
+ value
+ end
+
+ def absolute(path)
+ File.expand_path(path, Rails.root)
+ end
+
+ private
+
+ def base_url(config)
+ custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
+
+ [
+ config.protocol,
+ "://",
+ config.host,
+ custom_port
+ ]
+ end
+
+ def on_standard_port?(config)
+ config.port.to_i == (config.https ? 443 : 80)
+ end
+
+ # Extract the host part of the given +url+.
+ def host(url)
+ url = url.downcase
+ url = "http://#{url}" unless url.start_with?('http')
+
+ # Get rid of the path so that we don't even have to encode it
+ url_without_path = url.sub(%r{(https?://[^/]+)/?.*}, '\1')
+
+ URI.parse(url_without_path).host
+ end
+
+ # Runs every minute in a random ten-minute period on Sundays, to balance the
+ # load on the server receiving these pings. The usage ping is safe to run
+ # multiple times because of a 24 hour exclusive lock.
+ def cron_for_usage_ping
+ hour = rand(24)
+ minute = rand(6)
+
+ "#{minute}0-#{minute}9 #{hour} * * 0"
+ end
+ end
+end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index c811034b29d..47fbbed44cf 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -34,6 +34,7 @@
- [email_receiver, 2]
- [emails_on_push, 2]
- [mailers, 2]
+ - [mail_scheduler, 2]
- [invalid_gpg_signature_update, 2]
- [create_gpg_signature, 2]
- [rebase, 2]
diff --git a/db/fixtures/development/01_admin.rb b/db/fixtures/development/01_admin.rb
index dfb50c195c1..1e260236dc5 100644
--- a/db/fixtures/development/01_admin.rb
+++ b/db/fixtures/development/01_admin.rb
@@ -1,14 +1,14 @@
require './spec/support/sidekiq'
Gitlab::Seeder.quiet do
- User.seed do |s|
- s.id = 1
- s.name = 'Administrator'
- s.email = 'admin@example.com'
- s.notification_email = 'admin@example.com'
- s.username = 'root'
- s.password = '5iveL!fe'
- s.admin = true
- s.confirmed_at = DateTime.now
- end
+ User.create!(
+ name: 'Administrator',
+ email: 'admin@example.com',
+ username: 'root',
+ password: '5iveL!fe',
+ admin: true,
+ confirmed_at: DateTime.now
+ )
+
+ print '.'
end
diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb
index 30244ee4431..bcfdd058a1c 100644
--- a/db/fixtures/development/10_merge_requests.rb
+++ b/db/fixtures/development/10_merge_requests.rb
@@ -4,7 +4,7 @@ Gitlab::Seeder.quiet do
# Limit the number of merge requests per project to avoid long seeds
MAX_NUM_MERGE_REQUESTS = 10
- Project.all.reject(&:empty_repo?).each do |project|
+ Project.non_archived.with_merge_requests_enabled.reject(&:empty_repo?).each do |project|
branches = project.repository.branch_names.sample(MAX_NUM_MERGE_REQUESTS * 2)
branches.each do |branch_name|
@@ -21,7 +21,11 @@ Gitlab::Seeder.quiet do
assignee: project.team.users.sample
}
- MergeRequests::CreateService.new(project, project.team.users.sample, params).execute
+ # Only create MRs with users that are allowed to create MRs
+ developer = project.team.developers.sample
+ break unless developer
+
+ MergeRequests::CreateService.new(project, developer, params).execute
print '.'
end
end
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index d7be6f5950f..7b9a4bad449 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -1,5 +1,5 @@
require './spec/support/sidekiq'
-require './spec/support/test_env'
+require './spec/support/helpers/test_env'
class Gitlab::Seeder::CycleAnalytics
def initialize(project, perf: false)
diff --git a/db/fixtures/development/19_environments.rb b/db/fixtures/development/19_environments.rb
index c1bbc9af6d6..00a14f458d1 100644
--- a/db/fixtures/development/19_environments.rb
+++ b/db/fixtures/development/19_environments.rb
@@ -28,7 +28,11 @@ class Gitlab::Seeder::Environments
end
def create_merge_request_review_deployments!
- @project.merge_requests.sample(4).map do |merge_request|
+ @project
+ .merge_requests
+ .select { |mr| mr.source_branch.match(/\p{Alnum}+/) }
+ .sample(4)
+ .each do |merge_request|
next unless merge_request.diff_head_sha
create_deployment!(
diff --git a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
index bddc234db25..17357b67ab7 100644
--- a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
+++ b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
@@ -59,17 +59,17 @@ class RemoveDotGitFromGroupNames < ActiveRecord::Migration
end
def move_namespace(group_id, path_was, path)
- repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row|
- Gitlab.config.repositories.storages[row['repository_storage']].legacy_disk_path
+ repository_storages = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row|
+ row['repository_storage']
end.compact
# Move the namespace directory in all storages paths used by member projects
- repository_storage_paths.each do |repository_storage_path|
+ repository_storages.each do |repository_storage|
# Ensure old directory exists before moving it
- gitlab_shell.add_namespace(repository_storage_path, path_was)
+ gitlab_shell.add_namespace(repository_storage, path_was)
- unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
- Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
+ unless gitlab_shell.mv_namespace(repository_storage, path_was, path)
+ Rails.logger.error "Exception moving on shard #{repository_storage} from #{path_was} to #{path}"
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
diff --git a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
index 7c28d934c29..8986cd8cb4b 100644
--- a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
+++ b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
@@ -53,8 +53,8 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present?
end
- def path_exists?(path, repository_storage_path)
- repository_storage_path && gitlab_shell.exists?(repository_storage_path, path)
+ def path_exists?(shard, repository_storage_path)
+ repository_storage_path && gitlab_shell.exists?(shard, repository_storage_path)
end
# Accepts invalid path like test.git and returns test_git or
@@ -70,8 +70,8 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
def check_routes(base, counter, path)
route_exists = route_exists?(path)
- Gitlab.config.repositories.storages.each_value do |storage|
- if route_exists || path_exists?(path, storage.legacy_disk_path)
+ Gitlab.config.repositories.storages.each do |shard, storage|
+ if route_exists || path_exists?(shard, storage.legacy_disk_path)
counter += 1
path = "#{base}#{counter}"
@@ -83,17 +83,17 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
end
def move_namespace(namespace_id, path_was, path)
- repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{namespace_id}").map do |row|
- Gitlab.config.repositories.storages[row['repository_storage']].legacy_disk_path
+ repository_storages = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{namespace_id}").map do |row|
+ row['repository_storage']
end.compact
- # Move the namespace directory in all storages paths used by member projects
- repository_storage_paths.each do |repository_storage_path|
+ # Move the namespace directory in all storages used by member projects
+ repository_storages.each do |repository_storage|
# Ensure old directory exists before moving it
- gitlab_shell.add_namespace(repository_storage_path, path_was)
+ gitlab_shell.add_namespace(repository_storage, path_was)
- unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
- Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
+ unless gitlab_shell.mv_namespace(repository_storage, path_was, path)
+ Rails.logger.error "Exception moving on shard #{repository_storage} from #{path_was} to #{path}"
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
diff --git a/db/migrate/20180330121048_add_issue_due_to_notification_settings.rb b/db/migrate/20180330121048_add_issue_due_to_notification_settings.rb
new file mode 100644
index 00000000000..c64a481fcf0
--- /dev/null
+++ b/db/migrate/20180330121048_add_issue_due_to_notification_settings.rb
@@ -0,0 +1,9 @@
+class AddIssueDueToNotificationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :notification_settings, :issue_due, :boolean
+ end
+end
diff --git a/db/migrate/20180403035759_create_project_ci_cd_settings.rb b/db/migrate/20180403035759_create_project_ci_cd_settings.rb
new file mode 100644
index 00000000000..06856af6204
--- /dev/null
+++ b/db/migrate/20180403035759_create_project_ci_cd_settings.rb
@@ -0,0 +1,68 @@
+class CreateProjectCiCdSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ unless table_exists?(:project_ci_cd_settings)
+ create_table(:project_ci_cd_settings) do |t|
+ t.integer(:project_id, null: false)
+ t.boolean(:group_runners_enabled, default: true, null: false)
+ end
+ end
+
+ disable_statement_timeout
+
+ # This particular INSERT will take between 10 and 20 seconds.
+ execute 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects'
+
+ # We add the index and foreign key separately so the above INSERT statement
+ # takes as little time as possible.
+ add_concurrent_index(:project_ci_cd_settings, :project_id, unique: true)
+
+ add_foreign_key_with_retry
+ end
+
+ def down
+ drop_table :project_ci_cd_settings
+ end
+
+ def add_foreign_key_with_retry
+ if Gitlab::Database.mysql?
+ # When using MySQL we don't support online upgrades, thus projects can't
+ # be deleted while we are running this migration.
+ return add_project_id_foreign_key
+ end
+
+ # Between the initial INSERT and the addition of the foreign key some
+ # projects may have been removed, leaving orphaned rows in our new settings
+ # table.
+ loop do
+ remove_orphaned_settings
+
+ begin
+ add_project_id_foreign_key
+ break
+ rescue ActiveRecord::InvalidForeignKey
+ say 'project_ci_cd_settings contains some orphaned rows, retrying...'
+ end
+ end
+ end
+
+ def add_project_id_foreign_key
+ add_concurrent_foreign_key(:project_ci_cd_settings, :projects, column: :project_id)
+ end
+
+ def remove_orphaned_settings
+ execute <<~SQL
+ DELETE FROM project_ci_cd_settings
+ WHERE NOT EXISTS (
+ SELECT 1
+ FROM projects
+ WHERE projects.id = project_ci_cd_settings.project_id
+ )
+ SQL
+ end
+end
diff --git a/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb b/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb
new file mode 100644
index 00000000000..8fc558be733
--- /dev/null
+++ b/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb
@@ -0,0 +1,66 @@
+class CreateMissingNamespaceForInternalUsers < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ connection.exec_query(users_query.to_sql).rows.each do |id, username|
+ create_namespace(id, username)
+ # When testing locally I've noticed that these internal users are missing
+ # the notification email, for more details visit the below link:
+ # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18357#note_68327560
+ set_notification_email(id)
+ end
+ end
+
+ def down
+ # no-op
+ end
+
+ private
+
+ def users
+ @users ||= Arel::Table.new(:users)
+ end
+
+ def namespaces
+ @namespaces ||= Arel::Table.new(:namespaces)
+ end
+
+ def users_query
+ condition = users[:ghost].eq(true)
+
+ if column_exists?(:users, :support_bot)
+ condition = condition.or(users[:support_bot].eq(true))
+ end
+
+ users.join(namespaces, Arel::Nodes::OuterJoin)
+ .on(namespaces[:type].eq(nil).and(namespaces[:owner_id].eq(users[:id])))
+ .where(namespaces[:owner_id].eq(nil))
+ .where(condition)
+ .project(users[:id], users[:username])
+ end
+
+ def create_namespace(user_id, username)
+ path = Uniquify.new.string(username) do |str|
+ query = "SELECT id FROM namespaces WHERE parent_id IS NULL AND path='#{str}' LIMIT 1"
+ connection.exec_query(query).present?
+ end
+
+ insert_query = "INSERT INTO namespaces(owner_id, path, name) VALUES(#{user_id}, '#{path}', '#{path}')"
+ namespace_id = connection.insert_sql(insert_query)
+
+ create_route(namespace_id)
+ end
+
+ def create_route(namespace_id)
+ return unless namespace_id
+
+ row = connection.exec_query("SELECT id, path FROM namespaces WHERE id=#{namespace_id}").first
+ id, path = row.values_at('id', 'path')
+
+ execute("INSERT INTO routes(source_id, source_type, path, name) VALUES(#{id}, 'Namespace', '#{path}', '#{path}')")
+ end
+
+ def set_notification_email(user_id)
+ execute "UPDATE users SET notification_email = email WHERE notification_email IS NULL AND id = #{user_id}"
+ end
+end
diff --git a/db/migrate/20180416155103_add_further_scope_columns_to_internal_id_table.rb b/db/migrate/20180416155103_add_further_scope_columns_to_internal_id_table.rb
new file mode 100644
index 00000000000..37e2d19e022
--- /dev/null
+++ b/db/migrate/20180416155103_add_further_scope_columns_to_internal_id_table.rb
@@ -0,0 +1,15 @@
+class AddFurtherScopeColumnsToInternalIdTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ change_column_null :internal_ids, :project_id, true
+ add_column :internal_ids, :namespace_id, :integer, null: true
+ end
+
+ def down
+ change_column_null :internal_ids, :project_id, false
+ remove_column :internal_ids, :namespace_id
+ end
+end
diff --git a/db/migrate/20180417090132_add_index_constraints_to_internal_id_table.rb b/db/migrate/20180417090132_add_index_constraints_to_internal_id_table.rb
new file mode 100644
index 00000000000..582b89a3948
--- /dev/null
+++ b/db/migrate/20180417090132_add_index_constraints_to_internal_id_table.rb
@@ -0,0 +1,40 @@
+class AddIndexConstraintsToInternalIdTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :internal_ids, [:usage, :namespace_id], unique: true, where: 'namespace_id IS NOT NULL'
+
+ replace_index(:internal_ids, [:usage, :project_id], name: 'index_internal_ids_on_usage_and_project_id') do
+ add_concurrent_index :internal_ids, [:usage, :project_id], unique: true, where: 'project_id IS NOT NULL'
+ end
+
+ add_concurrent_foreign_key :internal_ids, :namespaces, column: :namespace_id, on_delete: :cascade
+ end
+
+ def down
+ remove_concurrent_index :internal_ids, [:usage, :namespace_id]
+
+ replace_index(:internal_ids, [:usage, :project_id], name: 'index_internal_ids_on_usage_and_project_id') do
+ add_concurrent_index :internal_ids, [:usage, :project_id], unique: true
+ end
+
+ remove_foreign_key :internal_ids, column: :namespace_id
+ end
+
+ private
+ def replace_index(table, columns, name:)
+ temporary_name = "#{name}_old"
+
+ if index_exists?(table, columns, name: name)
+ rename_index table, name, temporary_name
+ end
+
+ yield
+
+ remove_concurrent_index_by_name table, temporary_name
+ end
+end
diff --git a/db/migrate/20180418053107_add_index_to_ci_job_artifacts_file_store.rb b/db/migrate/20180418053107_add_index_to_ci_job_artifacts_file_store.rb
new file mode 100644
index 00000000000..1084ca14a34
--- /dev/null
+++ b/db/migrate/20180418053107_add_index_to_ci_job_artifacts_file_store.rb
@@ -0,0 +1,15 @@
+class AddIndexToCiJobArtifactsFileStore < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_job_artifacts, :file_store
+ end
+
+ def down
+ remove_index :ci_job_artifacts, :file_store if index_exists?(:ci_job_artifacts, :file_store)
+ end
+end
diff --git a/db/migrate/20180425131009_assure_commits_count_for_merge_request_diff.rb b/db/migrate/20180425131009_assure_commits_count_for_merge_request_diff.rb
new file mode 100644
index 00000000000..0e991c23bfa
--- /dev/null
+++ b/db/migrate/20180425131009_assure_commits_count_for_merge_request_diff.rb
@@ -0,0 +1,27 @@
+class AssureCommitsCountForMergeRequestDiff < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class MergeRequestDiff < ActiveRecord::Base
+ self.table_name = 'merge_request_diffs'
+
+ include ::EachBatch
+ end
+
+ def up
+ Gitlab::BackgroundMigration.steal('AddMergeRequestDiffCommitsCount')
+
+ MergeRequestDiff.where(commits_count: nil).each_batch(of: 50) do |batch|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+
+ Gitlab::BackgroundMigration::AddMergeRequestDiffCommitsCount.new.perform(*range)
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/post_migrate/20180409170809_populate_missing_project_ci_cd_settings.rb b/db/post_migrate/20180409170809_populate_missing_project_ci_cd_settings.rb
new file mode 100644
index 00000000000..3b0fdb3aeea
--- /dev/null
+++ b/db/post_migrate/20180409170809_populate_missing_project_ci_cd_settings.rb
@@ -0,0 +1,34 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class PopulateMissingProjectCiCdSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ # MySQL does not support online upgrades, thus there can't be any missing
+ # rows.
+ return if Gitlab::Database.mysql?
+
+ # Projects created after the initial migration but before the code started
+ # using ProjectCiCdSetting won't have a corresponding row in
+ # project_ci_cd_settings, so let's fix that.
+ execute <<~SQL
+ INSERT INTO project_ci_cd_settings (project_id)
+ SELECT id
+ FROM projects
+ WHERE NOT EXISTS (
+ SELECT 1
+ FROM project_ci_cd_settings
+ WHERE project_ci_cd_settings.project_id = projects.id
+ )
+ SQL
+ end
+
+ def down
+ # There's nothing to revert for this migration.
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index fd75b176318..5853b428430 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: 20180405142733) do
+ActiveRecord::Schema.define(version: 20180425131009) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -367,6 +367,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
end
add_index "ci_job_artifacts", ["expire_at", "job_id"], name: "index_ci_job_artifacts_on_expire_at_and_job_id", using: :btree
+ add_index "ci_job_artifacts", ["file_store"], name: "index_ci_job_artifacts_on_file_store", using: :btree
add_index "ci_job_artifacts", ["job_id", "file_type"], name: "index_ci_job_artifacts_on_job_id_and_file_type", unique: true, using: :btree
add_index "ci_job_artifacts", ["project_id"], name: "index_ci_job_artifacts_on_project_id", using: :btree
@@ -895,12 +896,14 @@ ActiveRecord::Schema.define(version: 20180405142733) do
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
create_table "internal_ids", id: :bigserial, force: :cascade do |t|
- t.integer "project_id", null: false
+ t.integer "project_id"
t.integer "usage", null: false
t.integer "last_value", null: false
+ t.integer "namespace_id"
end
- add_index "internal_ids", ["usage", "project_id"], name: "index_internal_ids_on_usage_and_project_id", unique: true, using: :btree
+ add_index "internal_ids", ["usage", "namespace_id"], name: "index_internal_ids_on_usage_and_namespace_id", unique: true, where: "(namespace_id IS NOT NULL)", using: :btree
+ add_index "internal_ids", ["usage", "project_id"], name: "index_internal_ids_on_usage_and_project_id", unique: true, where: "(project_id IS NOT NULL)", using: :btree
create_table "issue_assignees", id: false, force: :cascade do |t|
t.integer "user_id", null: false
@@ -1325,6 +1328,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
t.boolean "failed_pipeline"
t.boolean "success_pipeline"
t.boolean "push_to_merge_request"
+ t.boolean "issue_due"
end
add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree
@@ -1432,6 +1436,13 @@ ActiveRecord::Schema.define(version: 20180405142733) do
add_index "project_auto_devops", ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree
+ create_table "project_ci_cd_settings", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.boolean "group_runners_enabled", default: true, null: false
+ end
+
+ add_index "project_ci_cd_settings", ["project_id"], name: "index_project_ci_cd_settings_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
@@ -2111,6 +2122,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify
add_foreign_key "gpg_signatures", "projects", on_delete: :cascade
add_foreign_key "group_custom_attributes", "namespaces", column: "group_id", on_delete: :cascade
+ add_foreign_key "internal_ids", "namespaces", name: "fk_162941d509", on_delete: :cascade
add_foreign_key "internal_ids", "projects", on_delete: :cascade
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
@@ -2157,6 +2169,7 @@ ActiveRecord::Schema.define(version: 20180405142733) 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_ci_cd_settings", "projects", name: "fk_24c15d2f2e", on_delete: :cascade
add_foreign_key "project_custom_attributes", "projects", on_delete: :cascade
add_foreign_key "project_deploy_tokens", "deploy_tokens", on_delete: :cascade
add_foreign_key "project_deploy_tokens", "projects", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index 604f7244a34..a841a4cfbf1 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -80,6 +80,7 @@ on projects and code.
- [Search through GitLab](user/search/index.md): Search for issues, merge requests, projects, groups, todos, and issues in Issue Boards.
- [Snippets](user/snippets.md): Snippets allow you to create little bits of code.
- [Wikis](user/project/wiki/index.md): Enhance your repository documentation with built-in wikis.
+- [Web IDE](user/project/web_ide/index.md)
#### Repositories
@@ -159,8 +160,12 @@ applications are always responsive and available. GitLab collects and displays
performance metrics for deployed apps using Prometheus so you can know in an
instant how code changes impact your production environment.
+- [GitLab Prometheus](administration/monitoring/prometheus/index.md): Configure the bundled Prometheus to collect various metrics from your GitLab instance.
+- [Prometheus project integration](user/project/integrations/prometheus.md): Configure the Prometheus integration per project and monitor your CI/CD environments.
+- [Prometheus metrics](user/project/integrations/prometheus_library/metrics.md): Let Prometheus collect metrics from various services, like Kubernetes, NGINX, NGINX ingress controller, HAProxy, and Amazon Cloud Watch.
+- [GitLab Performance Monitoring](administration/monitoring/performance/index.md): Use InfluxDB and Grafana to monitor the performance of your GitLab instance (will be eventually replaced by Prometheus).
+- [Health check](user/admin_area/monitoring/health_check.md): GitLab provides liveness and readiness probes to indicate service health and reachability to required services.
- [GitLab Cycle Analytics](user/project/cycle_analytics.md): Cycle Analytics measures the time it takes to go from an [idea to production](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab) for each project you have.
-- [GitLab Performance Monitoring](administration/monitoring/performance/index.md)
## Getting started with GitLab
@@ -179,7 +184,7 @@ instant how code changes impact your production environment.
### Git and GitLab
- [Git](topics/git/index.md): Getting started with Git, branching strategies, Git LFS, advanced use.
-- [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf): Download a PDF describing the most used Git operations.
+- [Git cheatsheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf): Download a PDF describing the most used Git operations.
- [GitLab Flow](workflow/gitlab_flow.md): explore the best of Git with the GitLab Flow strategy.
## Administrator documentation
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index d8928a7fe4c..ad8ffc46559 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -75,43 +75,33 @@ Notice several options that you should consider using:
| `nobootwait` | Don't halt boot process waiting for this mount to become available
| `lookupcache=positive` | Tells the NFS client to honor `positive` cache results but invalidates any `negative` cache results. Negative cache results cause problems with Git. Specifically, a `git push` can fail to register uniformly across all NFS clients. The negative cache causes the clients to 'remember' that the files did not exist previously.
-## Mount locations
+## A single NFS mount
-When using default Omnibus configuration you will need to share 5 data locations
-between all GitLab cluster nodes. No other locations should be shared. The
-following are the 5 locations you need to mount:
-
-| Location | Description | Default configuration |
-| -------- | ----------- | --------------------- |
-| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data | `git_data_dirs({"default" => "/var/opt/gitlab/git-data"})`
-| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services | `user['home'] = '/var/opt/gitlab/'`
-| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments | `gitlab_rails['uploads_directory'] = '/var/opt/gitlab/gitlab-rails/uploads'`
-| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data | `gitlab_rails['shared_path'] = '/var/opt/gitlab/gitlab-rails/shared'`
-| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces | `gitlab_ci['builds_directory'] = '/var/opt/gitlab/gitlab-ci/builds'`
+It's recommended to nest all gitlab data dirs within a mount, that allows automatic
+restore of backups without manually moving existing data.
-Other GitLab directories should not be shared between nodes. They contain
-node-specific files and GitLab code that does not need to be shared. To ship
-logs to a central location consider using remote syslog. GitLab Omnibus packages
-provide configuration for [UDP log shipping][udp-log-shipping].
-
-### Consolidating mount points
-
-If you don't want to configure 5-6 different NFS mount points, you have a few
-alternative options.
+```
+mountpoint
+└── gitlab-data
+ ├── builds
+ ├── git-data
+ ├── home-git
+ ├── shared
+ └── uploads
+```
-#### Change default file locations
+To do so, we'll need to configure Omnibus with the paths to each directory nested
+in the mount point as follows:
-Omnibus allows you to configure the file locations. With custom configuration
-you can specify just one main mountpoint and have all of these locations
-as subdirectories. Mount `/gitlab-data` then use the following Omnibus
+Mount `/gitlab-nfs` then use the following Omnibus
configuration to move each data location to a subdirectory:
```ruby
-git_data_dirs({"default" => "/gitlab-data/git-data"})
-user['home'] = '/gitlab-data/home'
-gitlab_rails['uploads_directory'] = '/gitlab-data/uploads'
-gitlab_rails['shared_path'] = '/gitlab-data/shared'
-gitlab_ci['builds_directory'] = '/gitlab-data/builds'
+git_data_dirs({"default" => "/gitlab-nfs/gitlab-data/git-data"})
+user['home'] = '/gitlab-nfs/gitlab-data/home'
+gitlab_rails['uploads_directory'] = '/gitlab-nfs/gitlab-data/uploads'
+gitlab_rails['shared_path'] = '/gitlab-nfs/gitlab-data/shared'
+gitlab_ci['builds_directory'] = '/gitlab-nfs/gitlab-data/builds'
```
To move the `git` home directory, all GitLab services must be stopped. Run
@@ -122,22 +112,52 @@ Run `sudo gitlab-ctl reconfigure` to start using the central location. Please
be aware that if you had existing data you will need to manually copy/rsync it
to these new locations and then restart GitLab.
-#### Bind mounts
+## Bind mounts
+
+Alternatively to changing the configuration in Omnibus, bind mounts can be used
+to store the data on an NFS mount.
Bind mounts provide a way to specify just one NFS mount and then
bind the default GitLab data locations to the NFS mount. Start by defining your
single NFS mount point as you normally would in `/etc/fstab`. Let's assume your
-NFS mount point is `/gitlab-data`. Then, add the following bind mounts in
+NFS mount point is `/gitlab-nfs`. Then, add the following bind mounts in
`/etc/fstab`:
```bash
-/gitlab-data/git-data /var/opt/gitlab/git-data none bind 0 0
-/gitlab-data/.ssh /var/opt/gitlab/.ssh none bind 0 0
-/gitlab-data/uploads /var/opt/gitlab/gitlab-rails/uploads none bind 0 0
-/gitlab-data/shared /var/opt/gitlab/gitlab-rails/shared none bind 0 0
-/gitlab-data/builds /var/opt/gitlab/gitlab-ci/builds none bind 0 0
+/gitlab-nfs/gitlab-data/git-data /var/opt/gitlab/git-data none bind 0 0
+/gitlab-nfs/gitlab-data/.ssh /var/opt/gitlab/.ssh none bind 0 0
+/gitlab-nfs/gitlab-data/uploads /var/opt/gitlab/gitlab-rails/uploads none bind 0 0
+/gitlab-nfs/gitlab-data/shared /var/opt/gitlab/gitlab-rails/shared none bind 0 0
+/gitlab-nfs/gitlab-data/builds /var/opt/gitlab/gitlab-ci/builds none bind 0 0
```
+Using bind mounts will require manually making sure the data directories
+are empty before attempting a restore. Read more about the
+[restore prerequisites](../../raketasks/backup_restore.md).
+
+## Multiple NFS mounts
+
+When using default Omnibus configuration you will need to share 5 data locations
+between all GitLab cluster nodes. No other locations should be shared. The
+following are the 5 locations need to be shared:
+
+| Location | Description | Default configuration |
+| -------- | ----------- | --------------------- |
+| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data | `git_data_dirs({"default" => "/var/opt/gitlab/git-data"})`
+| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services | `user['home'] = '/var/opt/gitlab/'`
+| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments | `gitlab_rails['uploads_directory'] = '/var/opt/gitlab/gitlab-rails/uploads'`
+| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data | `gitlab_rails['shared_path'] = '/var/opt/gitlab/gitlab-rails/shared'`
+| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces | `gitlab_ci['builds_directory'] = '/var/opt/gitlab/gitlab-ci/builds'`
+
+Other GitLab directories should not be shared between nodes. They contain
+node-specific files and GitLab code that does not need to be shared. To ship
+logs to a central location consider using remote syslog. GitLab Omnibus packages
+provide configuration for [UDP log shipping][udp-log-shipping].
+
+Having multiple NFS mounts will require manually making sure the data directories
+are empty before attempting a restore. Read more about the
+[restore prerequisites](../../raketasks/backup_restore.md).
+
---
Read more on high-availability configuration:
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index fd2677996b1..430f865f1e7 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -650,6 +650,47 @@ gitlab_rails['redis_sentinels'] = [
Omnibus GitLab configures some things behind the curtains to make the sysadmins'
lives easier. If you want to know what happens underneath keep reading.
+### Running multiple Redis clusters
+
+GitLab supports running [separate Redis clusters for different persistent
+classes](https://docs.gitlab.com/omnibus/settings/redis.html#running-with-multiple-redis-instances):
+cache, queues, and shared_state. To make this work with Sentinel:
+
+1. Set the appropriate variable in `/etc/gitlab/gitlab.rb` for each instance you are using:
+
+ ```ruby
+ gitlab_rails['redis_cache_instance'] = REDIS_CACHE_URL
+ gitlab_rails['redis_queues_instance'] = REDIS_QUEUES_URL
+ gitlab_rails['redis_shared_state_instance'] = REDIS_SHARED_STATE_URL
+ ```
+ **Note**: Redis URLs should be in the format: `redis://:PASSWORD@SENTINEL_MASTER_NAME`
+
+ 1. PASSWORD is the plaintext password for the Redis instance
+ 2. SENTINEL_MASTER_NAME is the Sentinel master name (e.g. `gitlab-redis-cache`)
+1. Include an array of hashes with host/port combinations, such as the following:
+
+ ```ruby
+ gitlab_rails['redis_cache_sentinels'] = [
+ { host: REDIS_CACHE_SENTINEL_HOST, port: PORT1 },
+ { host: REDIS_CACHE_SENTINEL_HOST2, port: PORT2 }
+ ]
+ gitlab_rails['redis_queues_sentinels'] = [
+ { host: REDIS_QUEUES_SENTINEL_HOST, port: PORT1 },
+ { host: REDIS_QUEUES_SENTINEL_HOST2, port: PORT2 }
+ ]
+ gitlab_rails['redis_shared_state_sentinels'] = [
+ { host: SHARED_STATE_SENTINEL_HOST, port: PORT1 },
+ { host: SHARED_STATE_SENTINEL_HOST2, port: PORT2 }
+ ]
+ ```
+1. Note that for each persistence class, GitLab will default to using the
+ configuration specified in `gitlab_rails['redis_sentinels']` unless
+ overriden by the settings above.
+1. Be sure to include BOTH configuration options for each persistent classes. For example,
+ if you choose to configure a cache instance, you must specify both `gitlab_rails['redis_cache_instance']`
+ and `gitlab_rails['redis_cache_sentinels']` for GitLab to generate the proper configuration files.
+1. Run `gitlab-ctl reconfigure`
+
### Control running services
In the previous example, we've used `redis_sentinel_role` and
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 0906821d6a3..b472ca5b4d8 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -1,4 +1,4 @@
-# Administrator documentation
+# Administrator documentation **[CORE ONLY]**
Learn how to administer your GitLab instance (Community Edition and
Enterprise Edition).
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 00c631fdaae..9b3b1e48efd 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -119,11 +119,17 @@ The Pages daemon doesn't listen to the outside world.
1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`:
- ```ruby
+ ```shell
pages_external_url 'http://example.io'
```
1. [Reconfigure GitLab][reconfigure]
+1. Restart gitlab-pages by running the following command:
+
+ ```shell
+ sudo gitlab-ctl restart gitlab-pages
+ ```
+
Watch the [video tutorial][video-admin] for this configuration.
@@ -143,7 +149,7 @@ outside world.
1. Place the certificate and key inside `/etc/gitlab/ssl`
1. In `/etc/gitlab/gitlab.rb` specify the following configuration:
- ```ruby
+ ```shell
pages_external_url 'https://example.io'
pages_nginx['redirect_http_to_https'] = true
@@ -155,6 +161,11 @@ outside world.
respectively.
1. [Reconfigure GitLab][reconfigure]
+1. Restart gitlab-pages by running the following command:
+
+ ```shell
+ sudo gitlab-ctl restart gitlab-pages
+ ```
## Advanced configuration
@@ -180,7 +191,7 @@ world. Custom domains are supported, but no TLS.
1. Edit `/etc/gitlab/gitlab.rb`:
- ```ruby
+ ```shell
pages_external_url "http://example.io"
nginx['listen_addresses'] = ['1.1.1.1']
pages_nginx['enable'] = false
@@ -192,6 +203,11 @@ world. Custom domains are supported, but no TLS.
listens on. If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab][reconfigure]
+1. Restart gitlab-pages by running the following command:
+
+ ```shell
+ sudo gitlab-ctl restart gitlab-pages
+ ```
### Custom domains with TLS support
@@ -210,7 +226,7 @@ world. Custom domains and TLS are supported.
1. Edit `/etc/gitlab/gitlab.rb`:
- ```ruby
+ ```shell
pages_external_url "https://example.io"
nginx['listen_addresses'] = ['1.1.1.1']
pages_nginx['enable'] = false
@@ -225,6 +241,11 @@ world. Custom domains and TLS are supported.
listens on. If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab][reconfigure]
+1. Restart gitlab-pages by running the following command:
+
+ ```shell
+ sudo gitlab-ctl restart gitlab-pages
+ ```
### Custom domain verification
@@ -247,11 +268,16 @@ are stored.
If you wish to store them in another location you must set it up in
`/etc/gitlab/gitlab.rb`:
- ```ruby
+ ```shell
gitlab_rails['pages_path'] = "/mnt/storage/pages"
```
1. [Reconfigure GitLab][reconfigure]
+1. Restart gitlab-pages by running the following command:
+
+ ```shell
+ sudo gitlab-ctl restart gitlab-pages
+ ```
## Set maximum pages size
diff --git a/doc/administration/plugins.md b/doc/administration/plugins.md
index 3ae41638ac3..4302667caf5 100644
--- a/doc/administration/plugins.md
+++ b/doc/administration/plugins.md
@@ -33,6 +33,10 @@ Follow the steps below to set up a custom hook:
For an installation from source the path is usually
`/home/git/gitlab/plugins/`. For Omnibus installs the path is
usually `/opt/gitlab/embedded/service/gitlab-rails/plugins`.
+
+ For [highly available] configurations, your hook file should exist on each
+ application server.
+
1. Inside the `plugins` directory, create a file with a name of your choice,
without spaces or special characters.
1. Make the hook file executable and make sure it's owned by the git user.
@@ -78,3 +82,4 @@ Validating plugins from /plugins directory
[system hooks]: ../system_hooks/system_hooks.md
[webhooks]: ../user/project/integrations/webhooks.md
+[highly available]: ./high_availability/README.md \ No newline at end of file
diff --git a/doc/api/README.md b/doc/api/README.md
index ae4481b400e..9879c667150 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -13,6 +13,7 @@ following locations:
- [Broadcast Messages](broadcast_messages.md)
- [Project-level Variables](project_level_variables.md)
- [Group-level Variables](group_level_variables.md)
+- [Code Snippets](snippets.md)
- [Commits](commits.md)
- [Custom Attributes](custom_attributes.md)
- [Deployments](deployments.md)
diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md
index f05ae647577..682b90361bd 100644
--- a/doc/api/notification_settings.md
+++ b/doc/api/notification_settings.md
@@ -23,6 +23,7 @@ new_issue
reopen_issue
close_issue
reassign_issue
+issue_due
new_merge_request
push_to_merge_request
reopen_merge_request
@@ -75,6 +76,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `reopen_issue` | boolean | no | Enable/disable this notification |
| `close_issue` | boolean | no | Enable/disable this notification |
| `reassign_issue` | boolean | no | Enable/disable this notification |
+| `issue_due` | boolean | no | Enable/disable this notification |
| `new_merge_request` | boolean | no | Enable/disable this notification |
| `push_to_merge_request` | boolean | no | Enable/disable this notification |
| `reopen_merge_request` | boolean | no | Enable/disable this notification |
@@ -142,6 +144,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `reopen_issue` | boolean | no | Enable/disable this notification |
| `close_issue` | boolean | no | Enable/disable this notification |
| `reassign_issue` | boolean | no | Enable/disable this notification |
+| `issue_due` | boolean | no | Enable/disable this notification |
| `new_merge_request` | boolean | no | Enable/disable this notification |
| `push_to_merge_request` | boolean | no | Enable/disable this notification |
| `reopen_merge_request` | boolean | no | Enable/disable this notification |
@@ -166,6 +169,7 @@ Example responses:
"reopen_issue": false,
"close_issue": false,
"reassign_issue": false,
+ "issue_due": false,
"new_merge_request": false,
"push_to_merge_request": false,
"reopen_merge_request": false,
diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md
index d8f61852b11..085437c801a 100644
--- a/doc/api/project_import_export.md
+++ b/doc/api/project_import_export.md
@@ -112,13 +112,13 @@ POST /projects/import
| `file` | string | yes | The file to be uploaded |
| `path` | string | yes | Name and path for new project |
| `overwrite` | boolean | no | If there is a project with the same path the import will overwrite it. Default to false |
-| `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md)] |
+| `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md) |
-The override params passed will take precendence over all values defined inside the export file.
+The override params passed will take precedence over all values defined inside the export file.
-To upload a file from your filesystem, use the `--form` argument. This causes
+To upload a file from your file system, use the `--form` argument. This causes
cURL to post data using the header `Content-Type: multipart/form-data`.
-The `file=` parameter must point to a file on your filesystem and be preceded
+The `file=` parameter must point to a file on your file system and be preceded
by `@`. For example:
```console
diff --git a/doc/api/projects.md b/doc/api/projects.md
index a0cb5aa0820..fe21dfe23f9 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -915,6 +915,29 @@ Example response:
}
```
+## Languages
+
+Get languages used in a project with percentage value.
+
+```
+GET /projects/:id/languages
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/languages"
+```
+
+Example response:
+
+```json
+{
+ "Ruby": 66.69,
+ "JavaScript": 22.98,
+ "HTML": 7.91,
+ "CoffeeScript": 2.42
+}
+```
+
## Archive a project
Archives the project if the user is either admin or the project owner of this project. This action is
@@ -1375,4 +1398,27 @@ Read more in the [Project Badges](project_badges.md) documentation.
## Issue and merge request description templates
-The non-default [issue and merge request description templates](../user/project/description_templates.md) are managed inside the project's repository. So you can manage them via the API through the [Repositories API](repositories.md) and the [Repository Files API](repository_files.md). \ No newline at end of file
+The non-default [issue and merge request description templates](../user/project/description_templates.md) are managed inside the project's repository. So you can manage them via the API through the [Repositories API](repositories.md) and the [Repository Files API](repository_files.md).
+
+## Download snapshot of a git repository
+
+> Introduced in GitLab 10.7
+
+This endpoint may only be accessed by an administrative user.
+
+Download a snapshot of the project (or wiki, if requested) git repository. This
+snapshot is always in uncompressed [tar](https://en.wikipedia.org/wiki/Tar_(computing))
+format.
+
+If a repository is corrupted to the point where `git clone` does not work, the
+snapshot may allow some of the data to be retrieved.
+
+```
+GET /projects/:id/snapshot
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `wiki` | boolean | no | Whether to download the wiki, rather than project, repository |
+
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index 96609cd530f..5aff255c20a 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -183,7 +183,7 @@ GET /projects/:id/repository/contributors
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
-- `order_by` (optional) - Return contributors ordered by `name`, `email`, or `commits` fields. If not given contributors are ordered by commit date.
+- `order_by` (optional) - Return contributors ordered by `name`, `email`, or `commits` (orders by commit date) fields. Default is `commits`
- `sort` (optional) - Return contributors sorted in `asc` or `desc` order. Default is `asc`
Response:
diff --git a/doc/api/todos.md b/doc/api/todos.md
index dd4c737b729..27e623007cc 100644
--- a/doc/api/todos.md
+++ b/doc/api/todos.md
@@ -15,7 +15,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `action` | string | no | The action to be filtered. Can be `assigned`, `mentioned`, `build_failed`, `marked`, or `approval_required`. |
+| `action` | string | no | The action to be filtered. Can be `assigned`, `mentioned`, `build_failed`, `marked`, `approval_required`, `unmergeable` or `directly_addressed`. |
| `author_id` | integer | no | The ID of an author |
| `project_id` | integer | no | The ID of a project |
| `state` | string | no | The state of the todo. Can be either `pending` or `done` |
diff --git a/doc/api/users.md b/doc/api/users.md
index a4447e32908..ca5afa04687 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -55,6 +55,7 @@ GET /users
| --------- | ---- | -------- | ----------- |
| `order_by` | string | no | Return projects ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `two_factor` | string | no | Filter users by Two-factor authentication. Filter values are `enabled` or `disabled`. By default it returns all users |
```json
[
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 183808641c0..07b144f6ddd 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -101,12 +101,12 @@ In order to do that, follow the steps:
--registration-token REGISTRATION_TOKEN \
--executor docker \
--description "My Docker Runner" \
- --docker-image "docker:latest" \
+ --docker-image "docker:stable" \
--docker-privileged
```
The above command will register a new Runner to use the special
- `docker:latest` image which is provided by Docker. **Notice that it's using
+ `docker:stable` image which is provided by Docker. **Notice that it's using
the `privileged` mode to start the build and service containers.** If you
want to use [docker-in-docker] mode, you always have to use `privileged = true`
in your Docker containers.
@@ -120,7 +120,7 @@ In order to do that, follow the steps:
executor = "docker"
[runners.docker]
tls_verify = false
- image = "docker:latest"
+ image = "docker:stable"
privileged = true
disable_cache = false
volumes = ["/cache"]
@@ -132,7 +132,7 @@ In order to do that, follow the steps:
`docker:dind` service):
```yaml
- image: docker:latest
+ image: docker:stable
# When using dind, it's wise to use the overlayfs driver for
# improved performance.
@@ -201,12 +201,12 @@ In order to do that, follow the steps:
--registration-token REGISTRATION_TOKEN \
--executor docker \
--description "My Docker Runner" \
- --docker-image "docker:latest" \
+ --docker-image "docker:stable" \
--docker-volumes /var/run/docker.sock:/var/run/docker.sock
```
The above command will register a new Runner to use the special
- `docker:latest` image which is provided by Docker. **Notice that it's using
+ `docker:stable` image which is provided by Docker. **Notice that it's using
the Docker daemon of the Runner itself, and any containers spawned by docker
commands will be siblings of the Runner rather than children of the runner.**
This may have complications and limitations that are unsuitable for your workflow.
@@ -220,7 +220,7 @@ In order to do that, follow the steps:
executor = "docker"
[runners.docker]
tls_verify = false
- image = "docker:latest"
+ image = "docker:stable"
privileged = false
disable_cache = false
volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
@@ -232,7 +232,7 @@ In order to do that, follow the steps:
include the `docker:dind` service as when using the Docker in Docker executor):
```yaml
- image: docker:latest
+ image: docker:stable
before_script:
- docker info
@@ -286,7 +286,7 @@ any image that's used with the `--cache-from` argument must first be pulled
Here's a simple `.gitlab-ci.yml` file showing how Docker caching can be utilized:
```yaml
-image: docker:latest
+image: docker:stable
services:
- docker:dind
@@ -388,7 +388,7 @@ could look like:
```yaml
build:
- image: docker:latest
+ image: docker:stable
services:
- docker:dind
stage: build
@@ -434,7 +434,7 @@ when needed. Changes to `master` also get tagged as `latest` and deployed using
an application-specific deploy script:
```yaml
-image: docker:latest
+image: docker:stable
services:
- docker:dind
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index bc5d3840368..7c0f837ea9c 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -86,7 +86,7 @@ services](#accessing-the-services).
### How the health check of services works
Services are designed to provide additional functionality which is **network accessible**.
-It may be a database like MySQL, or Redis, and even `docker:dind` which
+It may be a database like MySQL, or Redis, and even `docker:stable-dind` which
allows you to use Docker in Docker. It can be practically anything that is
required for the CI/CD job to proceed and is accessed by network.
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index b3d9f0bc96c..517e25f00f7 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -260,6 +260,8 @@ are unsupported in environment name context:
- `CI_REGISTRY_PASSWORD`
- `CI_REPOSITORY_URL`
- `CI_ENVIRONMENT_URL`
+- `CI_DEPLOY_USER`
+- `CI_DEPLOY_PASSWORD`
GitLab Runner exposes various [environment variables][variables] when a job runs,
and as such, you can use them as environment names. Let's add another job in
diff --git a/doc/ci/examples/browser_performance.md b/doc/ci/examples/browser_performance.md
index 691370d7195..0dab07a7f80 100644
--- a/doc/ci/examples/browser_performance.md
+++ b/doc/ci/examples/browser_performance.md
@@ -17,7 +17,7 @@ performance:
variables:
URL: https://example.com
services:
- - docker:dind
+ - docker:stable-dind
script:
- mkdir gitlab-exporter
- wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
@@ -94,7 +94,7 @@ performance:
stage: performance
image: docker:git
services:
- - docker:dind
+ - docker:stable-dind
dependencies:
- review
script:
diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md
index 92317c77427..d1aa783cc9c 100644
--- a/doc/ci/examples/code_climate.md
+++ b/doc/ci/examples/code_climate.md
@@ -17,7 +17,11 @@ codequality:
- docker:stable-dind
script:
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- - docker run --env SOURCE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
+ - docker run
+ --env SOURCE_CODE="$PWD"
+ --volume "$PWD":/code
+ --volume /var/run/docker.sock:/var/run/docker.sock
+ "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
artifacts:
paths: [codeclimate.json]
```
diff --git a/doc/ci/examples/container_scanning.md b/doc/ci/examples/container_scanning.md
index c58efc7392a..eb76521cc02 100644
--- a/doc/ci/examples/container_scanning.md
+++ b/doc/ci/examples/container_scanning.md
@@ -30,6 +30,10 @@ sast:container:
- mv clair-scanner_linux_amd64 clair-scanner
- chmod +x clair-scanner
- touch clair-whitelist.yml
+ - while( ! wget -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; done
+ - retries=0
+ - echo "Waiting for clair daemon to start"
+ - while( ! wget -T 10 -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done
- ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-sast-container-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true
artifacts:
paths: [gl-sast-container-report.json]
diff --git a/doc/ci/examples/dast.md b/doc/ci/examples/dast.md
index 8df223ee560..a8720f0b7ea 100644
--- a/doc/ci/examples/dast.md
+++ b/doc/ci/examples/dast.md
@@ -42,9 +42,9 @@ dast:
allow_failure: true
script:
- mkdir /zap/wrk/
- - /zap/zap-baseline.py -J gl-dast-report.json -t $website \
- --auth-url $login_url \
- --auth-username "john.doe@example.com" \
+ - /zap/zap-baseline.py -J gl-dast-report.json -t $website
+ --auth-url $login_url
+ --auth-username "john.doe@example.com"
--auth-password "john-doe-password" || true
- cp /zap/wrk/gl-dast-report.json .
artifacts:
diff --git a/doc/ci/img/job_failure_reason.png b/doc/ci/img/job_failure_reason.png
new file mode 100644
index 00000000000..a60ce1fb21c
--- /dev/null
+++ b/doc/ci/img/job_failure_reason.png
Binary files differ
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 301cccc80a3..b16cbc61d14 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -73,6 +73,23 @@ cancel the job, retry it, or erase the job trace.
![Pipelines example](img/pipelines.png)
+## Seeing the failure reason for jobs
+
+> [Introduced][ce-17782] in GitLab 10.7.
+
+When a pipeline fails or is allowed to fail, there are several places where you
+can quickly check the reason it failed:
+
+- **In the pipeline graph** present on the pipeline detail view.
+- **In the pipeline widgets** present in the merge requests and commit pages.
+- **In the job views** present in the global and detailed views of a job.
+
+In any case, if you hover over the failed job you can see the reason it failed.
+
+![Pipeline detail](img/job_failure_reason.png)
+
+From [GitLab 10.8][ce-17814] you can also see the reason it failed on the Job detail page.
+
## Pipeline graphs
> [Introduced][ce-5742] in GitLab 8.11.
@@ -263,4 +280,6 @@ runners will not use regular runners, they must be tagged accordingly.
[ce-6242]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6242
[ce-7931]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7931
[ce-9760]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9760
+[ce-17782]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17782
+[ce-17814]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17814
[regexp]: https://gitlab.com/gitlab-org/gitlab-ce/blob/2f3dc314f42dbd79813e6251792853bc231e69dd/app/models/commit_status.rb#L99
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index 60dc2ef9ac5..821413900fd 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -298,6 +298,28 @@ Mentioned briefly earlier, but the following things of Runners can be exploited.
We're always looking for contributions that can mitigate these
[Security Considerations](https://docs.gitlab.com/runner/security/).
+### Resetting the registration token for a Project
+
+If you think that registration token for a Project was revealed, you should
+reset them. It's recommended because such token can be used to register another
+Runner to thi Project. It may be next used to obtain the values of secret
+variables or clone the project code, that normally may be unavailable for the
+attacker.
+
+To reset the token:
+
+1. Go to **Settings > CI/CD** for a specified Project
+1. Expand the **General pipelines settings** section
+1. Find the **Runner token** form field and click the **Reveal value** button
+1. Delete the value and save the form
+1. After the page is refreshed, expand the **Runners settings** section
+ and check the registration token - it should be changed
+
+From now on the old token is not valid anymore and will not allow to register
+a new Runner to the project. If you are using any tools to provision and
+register new Runners, you should now update the token that is used to the
+new value.
+
## Determining the IP address of a Runner
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17286) in GitLab 10.6.
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 4a504a98902..146df15899f 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -61,7 +61,7 @@ future GitLab releases.**
| **CI_RUNNER_EXECUTABLE_ARCH** | all | 10.6 | The OS/architecture of the GitLab Runner executable (note that this is not necessarily the same as the environment of the executor) |
| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally |
| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] |
-| **CI_PIPELINE_SOURCE** | 10.0 | all | The source for this pipeline, one of: push, web, trigger, schedule, api, external. Pipelines created before 9.5 will have unknown as source |
+| **CI_PIPELINE_SOURCE** | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `trigger`, `schedule`, `api`, and `pipeline`. For pipelines created before GitLab 9.5, this will show as `unknown` |
| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run |
| **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally |
| **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built (actually it is project folder name) |
@@ -87,6 +87,8 @@ future GitLab releases.**
| **GITLAB_USER_LOGIN** | 10.0 | all | The login username of the user who started the job |
| **GITLAB_USER_NAME** | 10.0 | all | The real name of the user who started the job |
| **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a job |
+| **CI_DEPLOY_USER** | 10.8 | all | Authentication username of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
+| **CI_DEPLOY_PASSWORD** | 10.8 | all | Authentication password of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
## 9.0 Renaming
@@ -546,6 +548,8 @@ You can find a full list of unsupported variables below:
- `CI_REGISTRY_PASSWORD`
- `CI_REPOSITORY_URL`
- `CI_ENVIRONMENT_URL`
+- `CI_DEPLOY_USER`
+- `CI_DEPLOY_PASSWORD`
These variables are also not supported in a contex of a
[dynamic environment name][dynamic-environments].
@@ -562,3 +566,4 @@ These variables are also not supported in a contex of a
[subgroups]: ../../user/group/subgroups/index.md
[builds-policies]: ../yaml/README.md#only-and-except-complex
[dynamic-environments]: ../environments.md#dynamic-environments
+[gitlab-deploy-token]: ../../user/project/deploy_tokens/index.md#gitlab-deploy-token
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 68aa64b3834..fb6d9826d08 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -308,7 +308,9 @@ except master.
## `only` and `except` (complex)
-> Introduced in GitLab 10.0
+> `refs` and `kubernetes` policies introduced in GitLab 10.0
+
+> `variables` policy introduced in 10.7
CAUTION: **Warning:**
This an _alpha_ feature, and it it subject to change at any time without
@@ -869,37 +871,29 @@ skip the download step.
- Introduced in GitLab Runner v0.7.0 for non-Windows platforms.
- Windows support was added in GitLab Runner v.1.0.0.
- From GitLab 9.2, caches are restored before artifacts.
-- Currently not all executors are supported.
+- Not all executors are [supported](https://docs.gitlab.com/runner/executors/#compatibility-chart).
- Job artifacts are only collected for successful jobs by default.
`artifacts` is used to specify a list of files and directories which should be
-attached to the job after success. You can only use paths that are within the
-project workspace. To pass artifacts between different jobs, see [dependencies](#dependencies).
-Below are some examples.
+attached to the job after success.
-Send all files in `binaries` and `.config`:
+The artifacts will be sent to GitLab after the job finishes successfully and will
+be available for download in the GitLab UI.
-```yaml
-artifacts:
- paths:
- - binaries/
- - .config
-```
+[Read more about artifacts.](../../user/project/pipelines/job_artifacts.md)
-Send all Git untracked files:
+### `artifacts:paths`
-```yaml
-artifacts:
- untracked: true
-```
+You can only use paths that are within the project workspace. To pass artifacts
+between different jobs, see [dependencies](#dependencies).
-Send all Git untracked files and files in `binaries`:
+Send all files in `binaries` and `.config`:
```yaml
artifacts:
- untracked: true
paths:
- binaries/
+ - .config
```
To disable artifact passing, define the job with empty [dependencies](#dependencies):
@@ -933,11 +927,6 @@ release-job:
- tags
```
-The artifacts will be sent to GitLab after the job finishes successfully and will
-be available for download in the GitLab UI.
-
-[Read more about artifacts.](../../user/project/pipelines/job_artifacts.md)
-
### `artifacts:name`
> Introduced in GitLab 8.6 and GitLab Runner v1.1.0.
@@ -954,26 +943,30 @@ To create an archive with a name of the current job:
job:
artifacts:
name: "$CI_JOB_NAME"
+ paths:
+ - binaries/
```
To create an archive with a name of the current branch or tag including only
-the files that are untracked by Git:
+the binaries directory:
```yaml
job:
artifacts:
name: "$CI_COMMIT_REF_NAME"
- untracked: true
+ paths:
+ - binaries/
```
To create an archive with a name of the current job and the current branch or
-tag including only the files that are untracked by Git:
+tag including only the binaries directory:
```yaml
job:
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME"
- untracked: true
+ paths:
+ - binaries/
```
To create an archive with a name of the current [stage](#stages) and branch name:
@@ -982,7 +975,8 @@ To create an archive with a name of the current [stage](#stages) and branch name
job:
artifacts:
name: "$CI_JOB_STAGE-$CI_COMMIT_REF_NAME"
- untracked: true
+ paths:
+ - binaries/
```
---
@@ -994,7 +988,8 @@ If you use **Windows Batch** to run your shell scripts you need to replace
job:
artifacts:
name: "%CI_JOB_STAGE%-%CI_COMMIT_REF_NAME%"
- untracked: true
+ paths:
+ - binaries/
```
If you use **Windows PowerShell** to run your shell scripts you need to replace
@@ -1004,7 +999,33 @@ If you use **Windows PowerShell** to run your shell scripts you need to replace
job:
artifacts:
name: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_NAME"
- untracked: true
+ paths:
+ - binaries/
+```
+
+### `artifacts:untracked`
+
+`artifacts:untracked` is used to add all Git untracked files as artifacts (along
+to the paths defined in `artifacts:paths`).
+
+NOTE: **Note:**
+To exclude the folders/files which should not be a part of `untracked` just
+add them to `.gitignore`.
+
+Send all Git untracked files:
+
+```yaml
+artifacts:
+ untracked: true
+```
+
+Send all Git untracked files and files in `binaries`:
+
+```yaml
+artifacts:
+ untracked: true
+ paths:
+ - binaries/
```
### `artifacts:when`
@@ -1552,7 +1573,7 @@ capitalization, the commit will be created but the pipeline will be skipped.
Each instance of GitLab CI has an embedded debug tool called Lint, which validates the
content of your `.gitlab-ci.yml` files. You can find the Lint under the page `ci/lint` of your
-project namespace (e.g, `http://gitlab-example.com/gitlab-org/project-123/ci/lint`)
+project namespace (e.g, `http://gitlab-example.com/gitlab-org/project-123/-/ci/lint`)
## Using reserved keywords
diff --git a/doc/development/background_migrations.md b/doc/development/background_migrations.md
index fc1b202b5eb..ce69694ab6a 100644
--- a/doc/development/background_migrations.md
+++ b/doc/development/background_migrations.md
@@ -133,11 +133,19 @@ roughly be as follows:
1. Release B:
1. Deploy code so that the application starts using the new column and stops
scheduling jobs for newly created data.
- 1. In a post-deployment migration you'll need to ensure no jobs remain. To do
- so you can use `Gitlab::BackgroundMigration.steal` to process any remaining
- jobs before continuing.
+ 1. In a post-deployment migration you'll need to ensure no jobs remain.
+ 1. Use `Gitlab::BackgroundMigration.steal` to process any remaining
+ jobs in Sidekiq.
+ 1. Reschedule the migration to be run directly (i.e. not through Sidekiq)
+ on any rows that weren't migrated by Sidekiq. This can happen if, for
+ instance, Sidekiq received a SIGKILL, or if a particular batch failed
+ enough times to be marked as dead.
1. Remove the old column.
+This may also require a bump to the [import/export version][import-export], if
+importing a project from a prior version of GitLab requires the data to be in
+the new format.
+
## Example
To explain all this, let's use the following example: the table `services` has a
@@ -296,3 +304,4 @@ for more details.
[migrations-readme]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md
[issue-rspec-hooks]: https://gitlab.com/gitlab-org/gitlab-ce/issues/35351
[reliable-sidekiq]: https://gitlab.com/gitlab-org/gitlab-ce/issues/36791
+[import-export]: ../user/project/settings/import_export.md
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
index d5a4acff481..a9fa5ae834f 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -22,7 +22,7 @@ The `merge_request` value is a reference to a merge request that adds this
entry, and the `author` key is used to give attribution to community
contributors. **Both are optional**.
The `type` field maps the category of the change,
-valid options are: added, fixed, changed, deprecated, removed, security, other. **Type field is mandatory**.
+valid options are: added, fixed, changed, deprecated, removed, security, performance, other. **Type field is mandatory**.
Community contributors and core team members are encouraged to add their name to
the `author` field. GitLab team members **should not**.
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 41e3412c7ff..0550ea527cb 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -157,6 +157,39 @@ below.
Otherwise, leave this mention out.
+### Product badges
+
+When a feature is available in EE-only tiers, add the corresponding tier according to the
+feature availability:
+
+- For GitLab Starter and GitLab.com Bronze: `**[STARTER]**`
+- For GitLab Premium and GitLab.com Silver: `**[PREMIUM]**`
+- For GitLab Ultimate and GitLab.com Gold: `**[ULTIMATE]**`
+- For GitLab Core and GitLab.com Free: `**[CORE]**`
+
+To exclude GitLab.com tiers (when the feature is not available in GitLab.com), add the
+keyword "only":
+
+- For GitLab Starter: `**[STARTER ONLY]**`
+- For GitLab Premium: `**[PREMIUM ONLY]**`
+- For GitLab Ultimate: `**[ULTIMATE ONLY]**`
+- For GitLab Core: `**[CORE ONLY]**`
+
+The tier should be ideally added to headers, so that the full badge will be displayed.
+But it can be also mentioned from paragraphs, list items, and table cells. For these cases,
+the tier mention will be represented by an orange question mark.
+E.g., `**[STARTER]**` renders **[STARTER]**, `**[STARTER ONLY]**` renders **[STARTER ONLY]**.
+
+The absence of tiers' mentions mean that the feature is available in GitLab Core,
+GitLab.com Free, and higher tiers.
+
+#### How it works
+
+Introduced by [!244](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/244),
+the special markup `**[STARTER]**` will generate a `span` element to trigger the
+badges and tooltips (`<span class="badge-trigger starter">`). When the keyword
+"only" is added, the corresponding GitLab.com badge will not be displayed.
+
### GitLab Restart
There are many cases that a restart/reconfigure of GitLab is required. To
diff --git a/doc/development/fe_guide/development_process.md b/doc/development/fe_guide/development_process.md
new file mode 100644
index 00000000000..5504a6421d5
--- /dev/null
+++ b/doc/development/fe_guide/development_process.md
@@ -0,0 +1,77 @@
+# Frontend Development Process
+
+You can find more about the organization of the frontend team in the [handbook](https://about.gitlab.com/handbook/frontend/).
+
+## Development Checklist
+
+The idea is to remind us about specific topics during the time we build a new feature or start something. This is a common practice in other industries (like pilots) that also use standardised checklists to reduce problems early on.
+
+Copy the content over to your issue or merge request and if something doesn't apply simply remove it from your current list.
+
+This checklist is intended to help us during development of bigger features/refactorings, it's not a "use it always and every point always matches" list.
+
+Please use your best judgement when to use it and please contribute new points through merge requests if something comes to your mind.
+
+---
+
+### Frontend development
+
+#### Planning development
+
+- [ ] Check the current set weight of the issue, does it fit your estimate?
+- [ ] Are all [departments](https://about.gitlab.com/handbook/engineering/#engineering-teams) that are needed from your perspective already involved in the issue? (For example is UX missing?)
+- [ ] Is the specification complete? Are you missing decisions? How about error handling/defaults/edge cases? Take your time to understand the needed implementation and go through its flow.
+- [ ] Are all necessary UX specifications available that you will need in order to implement? Are there new UX components/patterns in the designs? Then contact the UI component team early on. How should error messages or validation be handled?
+- [ ] **Library usage** Use Vuex as soon as you have even a medium state to manage, use Vue router if you need to have different views internally and want to link from the outside. Check what libraries we already have for which occassions.
+- [ ] **Plan your implementation:**
+ - [ ] **Architecture plan:** Create a plan aligned with GitLab's architecture, how you are going to do the implementation, for example Vue application setup and its components (through [onion skinning](https://gitlab.com/gitlab-org/gitlab-ce/issues/35873#note_39994091)), Store structure and data flow, which existing Vue components can you reuse. Its a good idea to go through your plan with another engineer to refine it.
+ - [ ] **Backend:** The best way is to kickoff the implementation in a call and discuss with the assigned Backend engineer what you will need from the backend and also when. Can you reuse existing API's? How is the performance with the planned architecture? Maybe create together a JSON mock object to already start with development.
+ - [ ] **Communication:** It also makes sense to have for bigger features an own slack channel (normally called #f_{feature_name}) and even weekly demo calls with all people involved.
+ - [ ] **Dependency Plan:** Are there big dependencies in the plan between you and others, then maybe create an execution diagram to show what is blocking which part and the order of the different parts.
+ - [ ] **Task list:** Create a simple checklist of the subtasks that are needed for the implementation, also consider creating even sub issues. (for example show a comment, delete a comment, update a comment, etc.). This helps you and also everyone else following the implementation
+- [ ] **Keep it small** To make it easier for you and also all reviewers try to keep merge requests small and merge into a feature branch if needed. To accomplish that you need to plan that from the start. Different methods are:
+ - [ ] **Skeleton based plan** Start with an MR that has the skeleton of the components with placeholder content. In following MRs you can fill the components with interactivity. This also makes it easier to spread out development on multiple people.
+ - [ ] **Cookie Mode** Think about hiding the feature behind a cookie flag if the implementation is on top of existing features
+ - [ ] **New route** Are you refactoring something big then you might consider adding a new route where you implement the new feature and when finished delete the current route and rename the new one. (for example 'merge_request' and 'new_merge_request')
+- [ ] **Setup** Is there any specific setup needed for your implementation (for example a kubernetes cluster)? Then let everyone know if it is not already mentioned where they can find documentation (if it doesn't exist - create it)
+- [ ] **Security** Are there any new security relevant implementations? Then please contact the security team for an app security review. If you are not sure ask our [domain expert](https://about.gitlab.com/handbook/frontend/#frontend-domain-experts)
+
+#### During development
+
+- [ ] Check off tasks on your created task list to keep everyone updated on the progress
+- [ ] [Share your work early with reviewers/maintainers](#share-your-work-early)
+- [ ] Share your work with UXer and Product Manager with Screenshots and/or [GIF's](https://about.gitlab.com/handbook/product/making-gifs/). They are easy to create for you and keep them up to date.
+- [ ] If you are blocked on something let everyone on the issue know through a comment.
+- [ ] Are you unable to work on this issue for a longer period of time, also let everyone know.
+- [ ] **Documentation** Update/add docs for the new feature, see `docs/`. Ping one of the documentation experts/reviewers
+
+#### Finishing development + Review
+
+- [ ] **Keep it in the scope** Try to focus on the actual scope and avoid a scope creep during review and keep new things to new issues.
+- [ ] **Performance** Have you checked performance? For example do the same thing with 500 comments instead of 1. Document the tests and possible findings in the MR so a reviewer can directly see it.
+- [ ] Have you tested with a variety of our [supported browsers](../../install/requirements.md#supported-web-browsers)? You can use [browserstack](https://www.browserstack.com/) to be able to access a wide variety of browsers and operating systems.
+- [ ] Did you check the mobile view?
+- [ ] Check the built webpack bundle (For the report run `WEBPACK_REPORT=true gdk run`, then open `webpack-report/index.html`) if we have unnecessary bloat due to wrong references, including libraries multiple times, etc.. If you need help contact the webpack [domain expert](https://about.gitlab.com/handbook/frontend/#frontend-domain-experts)
+- [ ] **Tests** Not only greenfield tests - Test also all bad cases that come to your mind.
+- [ ] If you have multiple MR's then also smoke test against the final merge.
+- [ ] Are there any big changes on how and especially how frequently we use the API then let production know about it
+- [ ] Smoke test of the RC on dev., staging., canary deployments and .com
+- [ ] Follow up on issues that came out of the review. Create isssues for discovered edge cases that should be covered in future iterations.
+
+---
+
+### Share your work early
+1. Before writing code, ensure your vision of the architecture is aligned with
+GitLab's architecture.
+1. Add a diagram to the issue and ask a frontend architect in the slack channel `#fe_architectural` about it.
+
+ ![Diagram of Issue Boards Architecture](img/boards_diagram.png)
+
+1. Don't take more than one week between starting work on a feature and
+sharing a Merge Request with a reviewer or a maintainer.
+
+### Vue features
+1. Follow the steps in [Vue.js Best Practices](vue.md)
+1. Follow the style guide.
+1. Only a handful of people are allowed to merge Vue related features.
+Reach out to one of Vue experts early in this process.
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index 2280cf79f86..3b4dfd50761 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -5,11 +5,15 @@ across GitLab's frontend team.
## Overview
-GitLab is built on top of [Ruby on Rails][rails] using [Haml][haml] with
-[Hamlit][hamlit]. Be wary of [the limitations that come with using
-Hamlit][hamlit-limits]. We also use [SCSS][scss] and plain JavaScript with
-modern ECMAScript standards supported through [Babel][babel] and ES module
-support through [webpack][webpack].
+GitLab is built on top of [Ruby on Rails][rails] using [Haml][haml] and also a JavaScript based Frontend with [Vue.js][vue].
+Be wary of [the limitations that come with using Hamlit][hamlit-limits]. We also use [SCSS][scss] and plain JavaScript with
+modern ECMAScript standards supported through [Babel][babel] and ES module support through [webpack][webpack].
+
+### Javascript development
+
+[Vue.js][vue] is used for particularly advanced, dynamic elements and based on previous iterations [jQuery][jquery] is used in lot of places through the application's JavaScript.
+
+We also use [Axios][axios] to handle all of our network requests.
We also utilize [webpack][webpack] to handle the bundling, minification, and
compression of our assets.
@@ -18,61 +22,29 @@ Working with our frontend assets requires Node (v6.0 or greater) and Yarn
(v1.2 or greater). You can find information on how to install these on our
[installation guide][install].
-[jQuery][jquery] is used throughout the application's JavaScript, with
-[Vue.js][vue] for particularly advanced, dynamic elements.
-
-We also use [Axios][axios] to handle all of our network requests.
-
### Browser Support
For our currently-supported browsers, see our [requirements][requirements].
---
-## Development Process
-
-### Share your work early
-1. Before writing code guarantee your vision of the architecture is aligned with
-GitLab's architecture.
-1. Add a diagram to the issue and ask a Frontend Architecture about it.
-
- ![Diagram of Issue Boards Architecture](img/boards_diagram.png)
-
-1. Don't take more than one week between starting work on a feature and
-sharing a Merge Request with a reviewer or a maintainer.
-
-### Vue features
-1. Follow the steps in [Vue.js Best Practices](vue.md)
-1. Follow the style guide.
-1. Only a handful of people are allowed to merge Vue related features.
-Reach out to one of Vue experts early in this process.
-
-
----
+## [Development Process](development_process.md)
+How we plan and execute the work on the frontend.
## [Architecture](architecture.md)
How we go about making fundamental design decisions in GitLab's frontend team
or make changes to our frontend development guidelines.
----
-
## [Testing](../testing_guide/frontend_testing.md)
-
How we write frontend tests, run the GitLab test suite, and debug test related
issues.
----
-
## [Design Patterns](design_patterns.md)
Common JavaScript design patterns in GitLab's codebase.
----
-
## [Vue.js Best Practices](vue.md)
Vue specific design patterns and practices.
----
-
## [Axios](axios.md)
Axios specific practices and gotchas.
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index c1170fa3b13..9c4b0e86351 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -523,8 +523,8 @@ export default new Vuex.Store({
```
_Note:_ If the state of the application is too complex, an individual file for the state may be better.
-#### `actions.js`
-An action commits a mutatation. In this file, we will write the actions that will call the respective mutation:
+##### `actions.js`
+An action commits a mutation. In this file, we will write the actions that will commit the respective mutation:
```javascript
import * as types from './mutation_types';
@@ -550,7 +550,7 @@ import { mapActions } from 'vuex';
};
```
-#### `getters.js`
+##### `getters.js`
Sometimes we may need to get derived state based on store state, like filtering for a specific prop. This can be done through the `getters`:
```javascript
@@ -573,7 +573,7 @@ import { mapGetters } from 'vuex';
};
```
-#### `mutations.js`
+##### `mutations.js`
The only way to actually change state in a Vuex store is by committing a mutation.
```javascript
@@ -586,7 +586,7 @@ The only way to actually change state in a Vuex store is by committing a mutatio
};
```
-#### `mutations_types.js`
+##### `mutations_types.js`
From [vuex mutations docs][vuex-mutations]:
> It is a commonly seen pattern to use constants for mutation types in various Flux implementations. This allows the code to take advantage of tooling like linters, and putting all constants in a single file allows your collaborators to get an at-a-glance view of what mutations are possible in the entire application.
@@ -661,7 +661,7 @@ describe('component', () => {
};
// populate the store
- store.dipatch('addUser', user);
+ store.dispatch('addUser', user);
vm = new Component({
store,
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index 856ef882453..b1bec84a2f3 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -131,6 +131,9 @@ There is also and alternative method to [translate messages from validation erro
### Interpolation
+Placeholders in translated text should match the code style of the respective source file.
+For example use `%{created_at}` in Ruby but `%{createdAt}` in JavaScript.
+
- In Ruby/HAML:
```ruby
@@ -141,11 +144,19 @@ There is also and alternative method to [translate messages from validation erro
```js
import { __, sprintf } from '~/locale';
- sprintf(__('Hello %{username}'), { username: 'Joe' }) => 'Hello Joe'
+
+ sprintf(__('Hello %{username}'), { username: 'Joe' }); // => 'Hello Joe'
```
-The placeholders should match the code style of the respective source file.
-For example use `%{created_at}` in Ruby but `%{createdAt}` in JavaScript.
+ By default, `sprintf` escapes the placeholder values.
+ If you want to take care of that yourself, you can pass `false` as third argument.
+
+ ```js
+ import { __, sprintf } from '~/locale';
+
+ sprintf(__('This is %{value}'), { value: '<strong>bold</strong>' }); // => 'This is &lt;strong&gt;bold&lt;/strong&gt;'
+ sprintf(__('This is %{value}'), { value: '<strong>bold</strong>' }, false); // => 'This is <strong>bold</strong>'
+ ```
### Plurals
diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index cf62314bc29..5185d843ccb 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -23,6 +23,7 @@ are very appreciative of the work done by translators and proofreaders!
- Italian
- Paolo Falomo - [GitLab](https://gitlab.com/paolofalomo), [Crowdin](https://crowdin.com/profile/paolo.falomo)
- Japanese
+ - Yamana Tokiuji - [GitLab](https://gitlab.com/tokiuji), [Crowdin](https://crowdin.com/profile/yamana)
- Korean
- Chang-Ho Cha - [GitLab](https://gitlab.com/changho-cha), [Crowdin](https://crowdin.com/profile/zzazang)
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
diff --git a/doc/development/new_fe_guide/development/components.md b/doc/development/new_fe_guide/development/components.md
index 637099d1e83..899efb398cd 100644
--- a/doc/development/new_fe_guide/development/components.md
+++ b/doc/development/new_fe_guide/development/components.md
@@ -1,3 +1,21 @@
# Components
-> TODO: Add content
+## Graphs
+
+We have a lot of graphing libraries in our codebase to render graphs. In an effort to improve maintainability, new graphs should use [D3.js](https://d3js.org/). If a new graph is fairly simple, consider implementing it in SVGs or HTML5 canvas.
+
+We chose D3 as our library going forward because of the following features:
+
+* [Tree shaking webpack capabilities.](https://github.com/d3/d3/blob/master/CHANGES.md#changes-in-d3-40)
+* [Compatible with vue.js as well as vanilla javascript.](https://github.com/d3/d3/blob/master/CHANGES.md#changes-in-d3-40)
+
+D3 is very popular across many projects outside of GitLab:
+
+* [The New York Times](https://archive.nytimes.com/www.nytimes.com/interactive/2012/02/13/us/politics/2013-budget-proposal-graphic.html)
+* [plot.ly](https://plot.ly/)
+* [Droptask](https://www.droptask.com/)
+
+Within GitLab, D3 has been used for the following notable features
+
+* [Prometheus graphs](https://docs.gitlab.com/ee/user/project/integrations/prometheus.html)
+* Contribution calendars
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index df80cd9f584..7b32e0a47e1 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -63,6 +63,8 @@ writing one](testing_levels.md#consider-not-writing-a-system-test)!
Sometimes you may need to debug Capybara tests by observing browser behavior.
+#### Live debug
+
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.
@@ -90,6 +92,54 @@ Finished in 34.51 seconds (files took 0.76702 seconds to load)
Note: `live_debug` only works on javascript enabled specs.
+#### Run `:js` spec in a visible browser
+
+Run the spec with `CHROME_HEADLESS=0`, e.g.:
+
+```
+CHROME_HEADLESS=0 bundle exec rspec some_spec.rb
+```
+
+The test will go by quickly, but this will give you an idea of what's happening.
+
+You can also add `byebug` or `binding.pry` to pause execution and step through
+the test.
+
+#### Screenshots
+
+We use the `capybara-screenshot` gem to automatically take a screenshot on
+failure. In CI you can download these files as job artifacts.
+
+Also, you can manually take screenshots at any point in a test by adding the
+methods below. Be sure to remove them when they are no longer needed! See
+https://github.com/mattheworiordan/capybara-screenshot#manual-screenshots for
+more.
+
+Add `screenshot_and_save_page` in a `:js` spec to screenshot what Capybara
+"sees", and save the page source.
+
+Add `screenshot_and_open_image` in a `:js` spec to screenshot what Capybara
+"sees", and automatically open the image.
+
+### Fast unit tests
+
+Some classes are well-isolated from Rails and you should be able to test them
+without the overhead added by the Rails environment and Bundler's `:default`
+group's gem loading. In these cases, you can `require 'fast_spec_helper'`
+instead of `require 'spec_helper'` in your test file, and your test should run
+really fast since:
+
+- Gems loading is skipped
+- Rails app boot is skipped
+- gitlab-shell and Gitaly setup are skipped
+- Test repositories setup are skipped
+
+Note that in some cases, you might have to add some `require_dependency 'foo'`
+in your file under test since Rails autoloading is not available in these cases.
+
+This shouldn't be a problem since explicitely listing dependencies should be
+considered a good practice anyway.
+
### `let` variables
GitLab's RSpec suite has made extensive use of `let` variables to reduce
@@ -281,14 +331,13 @@ All fixtures should be be placed under `spec/fixtures/`.
RSpec config files are files that change the RSpec config (i.e.
`RSpec.configure do |config|` blocks). They should be placed under
-`spec/support/config/`.
+`spec/support/`.
Each file should be related to a specific domain, e.g.
-`spec/support/config/capybara.rb`, `spec/support/config/carrierwave.rb`, etc.
+`spec/support/capybara.rb`, `spec/support/carrierwave.rb`, etc.
-Helpers can be included in the `spec/support/config/rspec.rb` file. If a
-helpers module applies only to a certain kind of specs, it should add modifiers
-to the `config.include` call. For instance if
+If a helpers module applies only to a certain kind of specs, it should add
+modifiers to the `config.include` call. For instance if
`spec/support/helpers/cycle_analytics_helpers.rb` applies to `:lib` and
`type: :model` specs only, you would write the following:
@@ -299,6 +348,14 @@ RSpec.configure do |config|
end
```
+If a config file only consists of `config.include`, you can add these
+`config.include` directly in `spec/spec_helper.rb`.
+
+For very generic helpers, consider including them in the `spec/support/rspec.rb`
+file which is used by the `spec/fast_spec_helper.rb` file. See
+[Fast unit tests](#fast-unit-tests) for more details about the
+`spec/fast_spec_helper.rb` file.
+
---
[Return to Testing documentation](index.md)
diff --git a/doc/development/testing_guide/end_to_end_tests.md b/doc/development/testing_guide/end_to_end_tests.md
index d10a797a142..21ec926414d 100644
--- a/doc/development/testing_guide/end_to_end_tests.md
+++ b/doc/development/testing_guide/end_to_end_tests.md
@@ -67,9 +67,9 @@ and examples in [the `qa/` directory][instance-qa-examples].
## Where can I ask for help?
-You can ask question in the `#qa` channel on Slack (GitLab internal) or you can
-find an issue you would like to work on in [the issue tracker][gitlab-qa-issues]
-and start a new discussion there.
+You can ask question in the `#quality` channel on Slack (GitLab internal) or
+you can find an issue you would like to work on in
+[the issue tracker][gitlab-qa-issues] and start a new discussion there.
[omnibus-gitlab]: https://gitlab.com/gitlab-org/omnibus-gitlab
[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 0c63f51cb45..af477f5ab99 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -126,13 +126,51 @@ it('tests a promise rejection', (done) => {
});
```
-#### Stubbing
+#### Stubbing and Mocking
-For unit tests, you should stub methods that are unrelated to the current unit you are testing.
-If you need to use a prototype method, instantiate an instance of the class and call it there instead of mocking the instance completely.
+Jasmine provides useful helpers `spyOn`, `spyOnProperty`, `jasmine.createSpy`,
+and `jasmine.createSpyObject` to facilitate replacing methods with dummy
+placeholders, and recalling when they are called and the arguments that are
+passed to them. These tools should be used liberally, to test for expected
+behavior, to mock responses, and to block unwanted side effects (such as a
+method that would generate a network request or alter `window.location`). The
+documentation for these methods can be found in the [jasmine introduction page](https://jasmine.github.io/2.0/introduction.html#section-Spies).
-For integration tests, you should stub methods that will effect the stability of the test if they
-execute their original behaviour. i.e. Network requests.
+Sometimes you may need to spy on a method that is directly imported by another
+module. GitLab has a custom `spyOnDependency` method which utilizes
+[babel-plugin-rewire](https://github.com/speedskater/babel-plugin-rewire) to
+achieve this. It can be used like so:
+
+```javascript
+// my_module.js
+import { visitUrl } from '~/lib/utils/url_utility';
+
+export default function doSomething() {
+ visitUrl('/foo/bar');
+}
+
+// my_module_spec.js
+import doSomething from '~/my_module';
+
+describe('my_module', () => {
+ it('does something', () => {
+ const visitUrl = spyOnDependency(doSomething, 'visitUrl');
+
+ doSomething();
+ expect(visitUrl).toHaveBeenCalledWith('/foo/bar');
+ });
+});
+```
+
+Unlike `spyOn`, `spyOnDependency` expects its first parameter to be the default
+export of a module who's import you want to stub, rather than an object which
+contains a method you wish to stub (if the module does not have a default
+export, one is be generated by the babel plugin). The second parameter is the
+name of the import you wish to change. The result of the function is a Spy
+object which can be treated like any other jasmine spy object.
+
+Further documentation on the babel rewire pluign API can be found on
+[its repository Readme doc](https://github.com/speedskater/babel-plugin-rewire#babel-plugin-rewire).
### Vue.js unit tests
@@ -152,19 +190,33 @@ is sufficient (and saves you some time).
### Live testing and focused testing
While developing locally, it may be helpful to keep karma running so that you
-can get instant feedback on as you write tests and modify code. To do this
-you can start karma with `npm run karma-start`. It will compile the javascript
+can get instant feedback on as you write tests and modify code. To do this
+you can start karma with `yarn run karma-start`. It will compile the javascript
assets and run a server at `http://localhost:9876/` where it will automatically
-run the tests on any browser which connects to it. You can enter that url on
+run the tests on any browser which connects to it. You can enter that url on
multiple browsers at once to have it run the tests on each in parallel.
While karma is running, any changes you make will instantly trigger a recompile
and retest of the entire test suite, so you can see instantly if you've broken
-a test with your changes. You can use [jasmine focused][jasmine-focus] or
+a test with your changes. You can use [jasmine focused][jasmine-focus] or
excluded tests (with `fdescribe` or `xdescribe`) to get karma to run only the
tests you want while you're working on a specific feature, but make sure to
remove these directives when you commit your code.
+It is also possible to only run karma on specific folders or files by filtering
+the run tests via the argument `--filter-spec` or short `-f`:
+
+```bash
+# Run all files
+yarn karma-start
+# Run specific spec files
+yarn karma-start --filter-spec profile/account/components/update_username_spec.js
+# Run specific spec folder
+yarn karma-start --filter-spec profile/account/components/
+# Run all specs which path contain vue_shared or vie
+yarn karma-start -f vue_shared -f vue_mr_widget
+```
+
## RSpec feature integration tests
Information on setting up and running RSpec integration tests with
@@ -176,7 +228,7 @@ Information on setting up and running RSpec integration tests with
Similar errors will be thrown if you're using JavaScript features not yet
supported by the PhantomJS test runner which is used for both Karma and RSpec
-tests. We polyfill some JavaScript objects for older browsers, but some
+tests. We polyfill some JavaScript objects for older browsers, but some
features are still unavailable:
- Array.from
@@ -188,7 +240,7 @@ features are still unavailable:
- Symbol/Symbol.iterator
- Spread
-Until these are polyfilled appropriately, they should not be used. Please
+Until these are polyfilled appropriately, they should not be used. Please
update this list with additional unsupported features.
### RSpec errors due to JavaScript
@@ -223,7 +275,7 @@ end
### Spinach errors due to missing JavaScript
NOTE: **Note:** Since we are discouraging the use of Spinach when writing new
-feature tests, you shouldn't ever need to use this. This information is kept
+feature tests, you shouldn't ever need to use this. This information is kept
available for legacy purposes only.
In Spinach, the JavaScript driver is enabled differently. In the `*.feature`
diff --git a/doc/development/testing_guide/testing_rake_tasks.md b/doc/development/testing_guide/testing_rake_tasks.md
index 60163f1a230..db8ca87e9f8 100644
--- a/doc/development/testing_guide/testing_rake_tasks.md
+++ b/doc/development/testing_guide/testing_rake_tasks.md
@@ -9,7 +9,7 @@ At a minimum, requiring the Rake helper will redirect `stdout`, include the
runtime task helpers, and include the `RakeHelpers` Spec support module.
The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make
-executing tasks simple. See `spec/support/rake_helpers.rb` for all available
+executing tasks simple. See `spec/support/helpers/rake_helpers.rb` for all available
methods.
Example:
diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md
index 2a531193adf..c9766040234 100644
--- a/doc/gitlab-basics/command-line-commands.md
+++ b/doc/gitlab-basics/command-line-commands.md
@@ -71,7 +71,7 @@ rm NAME-OF-FILE
### Remove a directory and all of its contents
```
-rm -rf NAME-OF-DIRECTORY
+rm -r NAME-OF-DIRECTORY
```
### View history in the command line
diff --git a/doc/install/README.md b/doc/install/README.md
index 9724b56910d..5dadf57ea9a 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -31,8 +31,8 @@ the hardware requirements.
- [Install GitLab on DC/OS](https://mesosphere.com/blog/gitlab-dcos/) via [GitLab-Mesosphere integration](https://about.gitlab.com/2016/09/16/announcing-gitlab-and-mesosphere/)
- [Install GitLab on Azure](azure/index.md)
- [Install GitLab on Google Cloud Platform](google_cloud_platform/index.md)
-- [Install GitLab on Google Container Engine (GKE)](https://about.gitlab.com/2017/01/23/video-tutorial-idea-to-production-on-google-container-engine-gke/): video tutorial on
-the full process of installing GitLab on Google Container Engine (GKE), pushing an application to GitLab, building the app with GitLab CI/CD, and deploying to production.
+- [Install GitLab on Google Kubernetes Engine (GKE)](https://about.gitlab.com/2017/01/23/video-tutorial-idea-to-production-on-google-container-engine-gke/): video tutorial on
+the full process of installing GitLab on Google Kubernetes Engine (GKE), pushing an application to GitLab, building the app with GitLab CI/CD, and deploying to production.
- [Install on AWS](https://about.gitlab.com/aws/)
- _Testing only!_ [DigitalOcean and Docker Machine](digitaloceandocker.md) -
Quickly test any version of GitLab on DigitalOcean using Docker Machine.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 3cf6f7b7ddf..fa5bcfa6f07 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -93,9 +93,9 @@ Is the system packaged Git too old? Remove it and compile from source.
# Download and compile from source
cd /tmp
- curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.16.2.tar.gz
- echo '9acc4339b7a2ab484eea69d705923271682b7058015219cf5a7e6ed8dee5b5fb git-2.16.2.tar.gz' | shasum -a256 -c - && tar -xzf git-2.16.2.tar.gz
- cd git-2.16.2/
+ curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.16.3.tar.gz
+ echo 'dda229e9c73f4fbb7d4324e0d993e11311673df03f73b194c554c2e9451e17cd git-2.16.3.tar.gz' | shasum -a256 -c - && tar -xzf git-2.16.3.tar.gz
+ cd git-2.16.3/
./configure
make prefix=/usr/local all
@@ -301,7 +301,7 @@ 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-6-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-7-stable gitlab
**Note:** You can change `10-6-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md
index 1f53e12d5f8..a03c49cbd89 100644
--- a/doc/install/kubernetes/gitlab_runner_chart.md
+++ b/doc/install/kubernetes/gitlab_runner_chart.md
@@ -72,6 +72,18 @@ concurrent: 10
##
checkInterval: 30
+## For RBAC support:
+rbac:
+ create: false
+
+ ## Run the gitlab-bastion container with the ability to deploy/manage containers of jobs
+ ## cluster-wide or only within namespace
+ clusterWideAccess: false
+
+ ## Use the following Kubernetes Service Account name if RBAC is disabled in this Helm chart (see rbac.create)
+ ##
+ # serviceAccountName: default
+
## Configuration for the Pods that that the runner launches for each new job
##
runners:
@@ -80,7 +92,7 @@ runners:
image: ubuntu:16.04
## Run all containers with the privileged flag enabled
- ## This will allow the docker:dind image to run if you need to run Docker
+ ## This will allow the docker:stable-dind image to run if you need to run Docker
## commands. Please read the docs before turning this on:
## ref: https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-dind
##
@@ -116,6 +128,12 @@ runners:
```
+### Enabling RBAC support
+
+If your cluster has RBAC enabled, you can choose to either have the chart create its own sevice account or provide one.
+
+To have the chart create the service account for you, set `rbac.create` to true.
+
### Controlling maximum Runner concurrency
A single GitLab Runner deployed on Kubernetes is able to execute multiple jobs in parallel by automatically starting additional Runner pods. The [`concurrent` setting](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section) controls the maximum number of pods allowed at a single time, and defaults to `10`.
@@ -147,7 +165,7 @@ enable privileged mode in `values.yaml`:
```yaml
runners:
## Run all containers with the privileged flag enabled
- ## This will allow the docker:dind image to run if you need to run Docker
+ ## This will allow the docker:stable-dind image to run if you need to run Docker
## commands. Please read the docs before turning this on:
## ref: https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-dind
##
diff --git a/doc/integration/github.md b/doc/integration/github.md
index b0d67db8b59..23bb8ef9303 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -69,7 +69,7 @@ GitHub will generate an application ID and secret key for you to use.
"name" => "github",
"app_id" => "YOUR_APP_ID",
"app_secret" => "YOUR_APP_SECRET",
- "url" => "https://github.com/",
+ "url" => "https://github.example.com/",
"args" => { "scope" => "user:email" }
}
]
@@ -125,7 +125,7 @@ For omnibus package:
"name" => "github",
"app_id" => "YOUR_APP_ID",
"app_secret" => "YOUR_APP_SECRET",
- "url" => "https://github.com/",
+ "url" => "https://github.example.com/",
"verify_ssl" => false,
"args" => { "scope" => "user:email" }
}
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index bbd2d214fe4..785cc32d590 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -498,6 +498,13 @@ more of the following options:
Read what the [backup timestamp is about](#backup-timestamp).
- `force=yes` - Does not ask if the authorized_keys file should get regenerated and assumes 'yes' for warning that database tables will be removed.
+If you are restoring into directories that are mountpoints you will need to make
+sure these directories are empty before attempting a restore. Otherwise GitLab
+will attempt to move these directories before restoring the new data and this
+would cause an error.
+
+Read more on [configuring NFS mounts](../administration/high_availability/nfs.md)
+
### Restore for installation from source
```
diff --git a/doc/security/img/outbound_requests_section.png b/doc/security/img/outbound_requests_section.png
new file mode 100644
index 00000000000..95c9c6ee771
--- /dev/null
+++ b/doc/security/img/outbound_requests_section.png
Binary files differ
diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md
index faabc53ce72..a573445ab5b 100644
--- a/doc/security/webhooks.md
+++ b/doc/security/webhooks.md
@@ -2,12 +2,19 @@
If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Webhooks.
-With [Webhooks](../user/project/integrations/webhooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
+With [Webhooks](../user/project/integrations/webhooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
Things get hairy, however, when a Webhook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the webhook is triggered and the POST request is sent.
Because Webhook requests are made by the GitLab server itself, these have complete access to everything running on the server (http://localhost:123) or within the server's local network (http://192.168.1.12:345), even if these services are otherwise protected and inaccessible from the outside world.
-If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete".
+If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete".
-To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough.
+To prevent this type of exploitation from happening, starting with GitLab 10.6, all Webhook requests to the current GitLab instance server address and/or in a private network will be forbidden by default. That means that all requests made to 127.0.0.1, ::1 and 0.0.0.0, as well as IPv4 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and IPv6 site-local (ffc0::/10) addresses won't be allowed.
+
+This behavior can be overridden by enabling the option *"Allow requests to the local network from hooks and services"* in the *"Outbound requests"* section inside the Admin area under **Settings** (`/admin/application_settings`):
+
+![Outbound requests admin settings](img/outbound_requests_section.png)
+
+>**Note:**
+*System hooks* are exempt from this protection because they are set up by admins.
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index c36609d3a86..8c4a2925356 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -405,12 +405,12 @@ into your project to enable staging and canary deployments, and more.
### Custom buildpacks
If the automatic buildpack detection fails for your project, or if you want to
-use a custom buildpack, you can override the buildpack using a project variable
-or a `.buildpack` file in your project:
+use a custom buildpack, you can override the buildpack(s) using a project variable
+or a `.buildpacks` file in your project:
- **Project variable** - Create a project variable `BUILDPACK_URL` with the URL
of the buildpack to use.
-- **`.buildpack` file** - Add a file in your project's repo called `.buildpack`
+- **`.buildpacks` file** - Add a file in your project's repo called `.buildpacks`
and add the URL of the buildpack to use on a line in the file. If you want to
use multiple buildpacks, you can enter them in, one on each line.
@@ -477,17 +477,19 @@ The following variables can be used for setting up the Auto DevOps domain,
providing a custom Helm chart, or scaling your application. PostgreSQL can be
also be customized, and you can easily use a [custom buildpack](#custom-buildpacks).
-| **Variable** | **Description** |
-| ------------ | --------------- |
-| `AUTO_DEVOPS_DOMAIN` | The [Auto DevOps domain](#auto-devops-domain); by default set automatically by the [Auto DevOps setting](#enabling-auto-devops). |
-| `AUTO_DEVOPS_CHART` | The Helm Chart used to deploy your apps; defaults to the one [provided by GitLab](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app). |
-| `PRODUCTION_REPLICAS` | The number of replicas to deploy in the production environment; defaults to 1. |
-| `CANARY_PRODUCTION_REPLICAS`| The number of canary replicas to deploy for [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) in the production environment. |
-| `POSTGRES_ENABLED` | Whether PostgreSQL is enabled; defaults to `"true"`. Set to `false` to disable the automatic deployment of PostgreSQL. |
-| `POSTGRES_USER` | The PostgreSQL user; defaults to `user`. Set it to use a custom username. |
-| `POSTGRES_PASSWORD` | The PostgreSQL password; defaults to `testing-password`. Set it to use a custom password. |
-| `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-variables-environment-variables). Set it to use a custom database name. |
-| `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142`|
+| **Variable** | **Description** |
+| ------------ | --------------- |
+| `AUTO_DEVOPS_DOMAIN` | The [Auto DevOps domain](#auto-devops-domain); by default set automatically by the [Auto DevOps setting](#enabling-auto-devops). |
+| `AUTO_DEVOPS_CHART` | The Helm Chart used to deploy your apps; defaults to the one [provided by GitLab](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app). |
+| `REPLICAS` | The number of replicas to deploy; defaults to 1. |
+| `PRODUCTION_REPLICAS` | The number of replicas to deploy in the production environment. This takes precedence over `REPLICAS`; defaults to 1. |
+| `CANARY_REPLICAS` | The number of canary replicas to deploy for [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html); defaults to 1 |
+| `CANARY_PRODUCTION_REPLICAS` | The number of canary replicas to deploy for [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) in the production environment. This takes precedence over `CANARY_REPLICAS`; defaults to 1 |
+| `POSTGRES_ENABLED` | Whether PostgreSQL is enabled; defaults to `"true"`. Set to `false` to disable the automatic deployment of PostgreSQL. |
+| `POSTGRES_USER` | The PostgreSQL user; defaults to `user`. Set it to use a custom username. |
+| `POSTGRES_PASSWORD` | The PostgreSQL password; defaults to `testing-password`. Set it to use a custom password. |
+| `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-variables-environment-variables). Set it to use a custom database name. |
+| `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142` |
TIP: **Tip:**
Set up the replica variables using a
@@ -518,8 +520,9 @@ The general rule is: `TRACK_ENV_REPLICAS`. Where:
That way, you can define your own `TRACK_ENV_REPLICAS` variables with which
you will be able to scale the pod's replicas easily.
-In the example below, the environment's name is `qa` which would result in
-looking for the `QA_REPLICAS` environment variable:
+In the example below, the environment's name is `qa` and it deploys the track
+`foo` which would result in looking for the `FOO_QA_REPLICAS` environment
+variable:
```yaml
QA testing:
@@ -527,11 +530,11 @@ QA testing:
environment:
name: qa
script:
- - deploy qa
+ - deploy foo
```
-If, in addition, there was also a `track: foo` defined in the application's Helm
-chart, like:
+The track `foo` being referenced would also need to be defined in the
+application's Helm chart, like:
```yaml
replicaCount: 1
@@ -553,8 +556,6 @@ service:
internalPort: 5000
```
-then the environment variable would be `FOO_QA_REPLICAS`.
-
## Currently supported languages
NOTE: **Note:**
diff --git a/doc/update/10.6-to-10.7.md b/doc/update/10.6-to-10.7.md
new file mode 100644
index 00000000000..4a76ae14d2e
--- /dev/null
+++ b/doc/update/10.6-to-10.7.md
@@ -0,0 +1,361 @@
+---
+comments: false
+---
+
+# From 10.6 to 10.7
+
+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
+
+NOTE: If you installed GitLab from source, make sure `rsync` is installed.
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.6.tar.gz
+echo '4e6a0f828819e15d274ae58485585fc8b7caace0 ruby-2.3.6.tar.gz' | shasum -c - && tar xzf ruby-2.3.6.tar.gz
+cd ruby-2.3.6
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab utilizes [webpack](http://webpack.js.org) to compile frontend assets.
+This requires a minimum version of node v6.0.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v6.0.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+GitLab also requires the use of yarn `>= v1.2.0` to manage JavaScript
+dependencies.
+
+```bash
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Update Go
+
+NOTE: GitLab 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 --prune
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+sudo -u git -H git checkout -- locale
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-7-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-7-stable-ee
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
+sudo -u git -H bin/compile
+```
+
+### 8. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. GitLab-Workhorse uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-workhorse
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
+sudo -u git -H make
+```
+
+### 9. Update Gitaly
+
+#### New Gitaly configuration options required
+
+In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`.
+
+```shell
+echo '
+[gitaly-ruby]
+dir = "/home/git/gitaly/ruby"
+
+[gitlab-shell]
+dir = "/home/git/gitlab-shell"
+' | sudo -u git tee -a /home/git/gitaly/config.toml
+```
+
+#### Check Gitaly configuration
+
+Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
+configuration file may contain syntax errors. The block name
+`[[storages]]`, which may occur more than once in your `config.toml`
+file, should be `[[storage]]` instead.
+
+```shell
+sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml
+```
+
+#### Compile Gitaly
+
+```shell
+cd /home/git/gitaly
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
+sudo -u git -H make
+```
+
+### 10. Update MySQL permissions
+
+If you are using MySQL you need to grant the GitLab user the necessary
+permissions on the database:
+
+```bash
+mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';"
+```
+
+If you use MySQL with replication, or just have MySQL configured with binary logging,
+you will need to also run the following on all of your MySQL servers:
+
+```bash
+mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;"
+```
+
+You can make this setting permanent by adding it to your `my.cnf`:
+
+```
+log_bin_trust_function_creators=1
+```
+
+### 11. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/10-6-stable:config/gitlab.yml.example origin/10-7-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-6-stable:lib/support/nginx/gitlab-ssl origin/10-7-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/10-6-stable:lib/support/nginx/gitlab origin/10-7-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-7-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-7-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-6-stable:lib/support/init.d/gitlab.default.example origin/10-7-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.5)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 10.5 to 10.6](10.5-to-10.6.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-7-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-7-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index eacfe2baa27..159109e8954 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -14,6 +14,10 @@ The comment area supports [Markdown] and [quick actions]. One can edit their
own comment at any time, and anyone with [Master access level][permissions] or
higher can also edit a comment made by someone else.
+You could also reply to the notification email in order to reply to a comment,
+provided that [Reply by email] is configured by your GitLab admin. This also
+supports [Markdown] and [quick actions] as if replied from the web.
+
Apart from the standard comments, you also have the option to create a comment
in the form of a resolvable or threaded discussion.
@@ -283,3 +287,4 @@ edit existing comments. Non-team members are restricted from adding or editing c
[markdown]: ../markdown.md
[quick actions]: ../project/quick_actions.md
[permissions]: ../permissions.md
+[Reply by email]: ../../administration/reply_by_email.md
diff --git a/doc/user/group/img/groups.png b/doc/user/group/img/groups.png
index 6211f999d5e..3173ddce7ff 100644
--- a/doc/user/group/img/groups.png
+++ b/doc/user/group/img/groups.png
Binary files differ
diff --git a/doc/user/group/img/new_group_from_groups.png b/doc/user/group/img/new_group_from_groups.png
index baf34244cb2..9c5dd7ebd8b 100644
--- a/doc/user/group/img/new_group_from_groups.png
+++ b/doc/user/group/img/new_group_from_groups.png
Binary files differ
diff --git a/doc/user/group/img/new_group_from_other_pages.png b/doc/user/group/img/new_group_from_other_pages.png
index 014a7088af2..77427224447 100644
--- a/doc/user/group/img/new_group_from_other_pages.png
+++ b/doc/user/group/img/new_group_from_other_pages.png
Binary files differ
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 88efddbfba8..88f4bb2ee04 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -245,10 +245,7 @@ To enable this feature, navigate to the group settings page. Select
![Checkbox for share with group lock](img/share_with_group_lock.png)
-#### Member Lock
-
-> Available in [GitLab Starter](https://about.gitlab.com/products/) and
-[GitLab.com Bronze](https://about.gitlab.com/gitlab-com/).
+#### Member Lock **[STARTER]**
With **Member Lock** it is possible to lock membership in project to the
level of members in group.
@@ -259,8 +256,8 @@ Learn more about [Member Lock](https://docs.gitlab.com/ee/user/group/index.html#
- **Projects**: view all projects within that group, add members to each project,
access each project's settings, and remove any project from the same screen.
-- **Webhooks**: configure [webhooks](../project/integrations/webhooks.md)
-and [push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html#push-rules) to your group (Push Rules is available in [GitLab Starter](https://about.gitlab.com/products/).)
+- **Webhooks**: configure [webhooks](../project/integrations/webhooks.md) to your group.
+- **Push rules**: configure [push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html#push-rules) to your group. **[STARTER]**
- **Audit Events**: view [Audit Events](https://docs.gitlab.com/ee/administration/audit_events.html#audit-events)
-for the group (GitLab admins only, available in [GitLab Starter][ee]).
+for the group. **[STARTER ONLY]**
- **Pipelines quota**: keep track of the [pipeline quota](../admin_area/settings/continuous_integration.md) for the group
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index a520279c29e..a9ba2a51242 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -15,6 +15,10 @@ GitLab [administrators](../README.md#administrator-documentation) receive all pe
To add or import a user, you can follow the
[project members documentation](../user/project/members/index.md).
+## Principles behind permissions
+
+See our [product handbook on permissions](https://about.gitlab.com/handbook/product#permissions-in-gitlab)
+
## Project members permissions
The following table depicts the various user permission levels in a project.
diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md
index 86fc58020e8..7a8b3c75690 100644
--- a/doc/user/project/deploy_tokens/index.md
+++ b/doc/user/project/deploy_tokens/index.md
@@ -23,7 +23,7 @@ You can create as many deploy tokens as you like from the settings of your proje
![Personal access tokens page](img/deploy_tokens.png)
-## Revoking a personal access token
+## Revoking a deploy token
At any time, you can revoke any deploy token by just clicking the
respective **Revoke** button under the 'Active deploy tokens' area.
@@ -71,6 +71,16 @@ docker login registry.example.com -u <username> -p <deploy_token>
Just replace `<username>` and `<deploy_token>` with the proper values. Then you can simply
pull images from your Container Registry.
+### GitLab Deploy Token
+
+> [Introduced][ce-18414] in GitLab 10.8.
+
+There's a special case when it comes to Deploy Tokens, if a user creates one
+named `gitlab-deploy-token`, the name and token of the Deploy Token will be
+automatically exposed to the CI/CD jobs as environment variables: `CI_DEPLOY_USER` and
+`CI_DEPLOY_PASSWORD`, respectively.
+
[ce-17894]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17894
[ce-11845]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845
+[ce-18414]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18414
[container registry]: ../container_registry.md
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index 5ce4ebfa811..557375a1da9 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -17,7 +17,7 @@ When you create a project in GitLab, you'll have access to a large number of
- [Issue tracker](issues/index.md): Discuss implementations with your team within issues
- [Issue Boards](issue_board.md): Organize and prioritize your workflow
- - [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards) (**Starter/Premium**): Allow your teams to create their own workflows (Issue Boards) for the same project
+ - [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards): Allow your teams to create their own workflows (Issue Boards) for the same project **[STARTER]**
- [Repositories](repository/index.md): Host your code in a fully
integrated platform
- [Branches](repository/branches/index.md): use Git branching strategies to
@@ -30,8 +30,8 @@ integrated platform
- [Deploy tokens](deploy_tokens/index.md): Manage project-based deploy tokens that allow permanent access to the repository and Container Registry.
- [Merge Requests](merge_requests/index.md): Apply your branching
strategy and get reviewed by your team
- - [Merge Request Approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) (**Starter/Premium**): Ask for approval before
- implementing a change
+ - [Merge Request Approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html): Ask for approval before
+ implementing a change **[STARTER]**
- [Fix merge conflicts from the UI](merge_requests/resolve_conflicts.md):
Your Git diff tool right from GitLab's UI
- [Review Apps](../../ci/review_apps/index.md): Live preview the results
@@ -45,6 +45,7 @@ and time spent on
templates for issue and merge request description fields for your project
- [Slash commands (quick actions)](quick_actions.md): Textual shortcuts for
common actions on issues or merge requests
+- [Web IDE](web_ide/index.md)
**GitLab CI/CD:**
diff --git a/doc/user/project/integrations/custom_issue_tracker.md b/doc/user/project/integrations/custom_issue_tracker.md
index 731291ebe84..6fc083170b6 100644
--- a/doc/user/project/integrations/custom_issue_tracker.md
+++ b/doc/user/project/integrations/custom_issue_tracker.md
@@ -15,8 +15,8 @@ in the table below.
Once you have configured and enabled Custom Issue Tracker Service you'll see a link on the GitLab project pages that takes you to that custom issue tracker.
-
## 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
+- Issues are referenced with `ANYTHING-<ID>`, where `ANYTHING` can be any string and `<ID>` is a number used in the target project of the custom integration (example `PROJECT-143`).
+- `ANYTHING` is a placeholder to differentiate against GitLab issues, which are referenced with `#<ID>`. You can use a project name or project key to replace it for example.
+- So with the example above, `PROJECT-143` would refer to `https://customissuetracker.com/project-name/143`. \ No newline at end of file
diff --git a/doc/user/project/integrations/prometheus_library/cloudwatch.md b/doc/user/project/integrations/prometheus_library/cloudwatch.md
index 34a0b97a171..bf6c0dc0e7e 100644
--- a/doc/user/project/integrations/prometheus_library/cloudwatch.md
+++ b/doc/user/project/integrations/prometheus_library/cloudwatch.md
@@ -6,7 +6,7 @@ GitLab has support for automatically detecting and monitoring AWS resources, sta
## Requirements
-The [Prometheus service](../prometheus/index.md) must be enabled.
+The [Prometheus service](../prometheus.md) must be enabled.
## Metrics supported
diff --git a/doc/user/project/integrations/prometheus_library/haproxy.md b/doc/user/project/integrations/prometheus_library/haproxy.md
index 518018e5839..cd398f7c0fd 100644
--- a/doc/user/project/integrations/prometheus_library/haproxy.md
+++ b/doc/user/project/integrations/prometheus_library/haproxy.md
@@ -5,7 +5,7 @@ GitLab has support for automatically detecting and monitoring HAProxy. This is p
## Requirements
-The [Prometheus service](../prometheus/index.md) must be enabled.
+The [Prometheus service](../prometheus.md) must be enabled.
## Metrics supported
diff --git a/doc/user/project/integrations/prometheus_library/metrics.md b/doc/user/project/integrations/prometheus_library/metrics.md
index f09ecf9ff2d..96a22316265 100644
--- a/doc/user/project/integrations/prometheus_library/metrics.md
+++ b/doc/user/project/integrations/prometheus_library/metrics.md
@@ -1,4 +1,5 @@
# Prometheus Metrics library
+
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935) in GitLab 9.0
GitLab offers automatic detection of select [Prometheus exporters](https://prometheus.io/docs/instrumenting/exporters/). Currently supported exporters are:
@@ -15,7 +16,7 @@ We have tried to surface the most important metrics for each exporter, and will
GitLab retrieves performance data from the configured Prometheus server, and attempts to identifying the presence of known metrics. Once identified, GitLab then needs to be able to map the data to a particular environment.
In order to isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do that,
-GitLab uses the defined queries and fills in the environment specific variables. Typically this involves looking for the [$CI_ENVIRONMENT_SLUG](https://docs.gitlab.com/ee/ci/variables/#predefined-variables-environment-variables), but may also include other information such as the project's Kubernetes namespace. Each search query is defined in the [exporter specific documentation](#prometheus-metrics-library).
+GitLab uses the defined queries and fills in the environment specific variables. Typically this involves looking for the [$CI_ENVIRONMENT_SLUG](../../../../ci/variables/README.md#predefined-variables-environment-variables), but may also include other information such as the project's Kubernetes namespace. Each search query is defined in the [exporter specific documentation](#prometheus-metrics-library).
## Adding to the library
diff --git a/doc/user/project/integrations/prometheus_library/nginx.md b/doc/user/project/integrations/prometheus_library/nginx.md
index 7fb8369d3c1..fea3231006b 100644
--- a/doc/user/project/integrations/prometheus_library/nginx.md
+++ b/doc/user/project/integrations/prometheus_library/nginx.md
@@ -6,7 +6,7 @@ GitLab has support for automatically detecting and monitoring NGINX. This is pro
## Requirements
-The [Prometheus service](../prometheus/index.md) must be enabled.
+The [Prometheus service](../prometheus.md) must be enabled.
## Metrics supported
diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md
index 49b34c82ae6..590b1c4275a 100644
--- a/doc/user/project/integrations/prometheus_library/nginx_ingress.md
+++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md
@@ -6,7 +6,7 @@ GitLab has support for automatically detecting and monitoring the Kubernetes NGI
## Requirements
-[Prometheus integration](../prometheus/index.md) must be active.
+[Prometheus integration](../prometheus.md) must be active.
## Metrics supported
@@ -27,7 +27,7 @@ For other deployments, there is [some configuration](#manually-setting-up-nginx-
### About managed NGINX Ingress deployments
-NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's IP](https://docs.gitlab.com/ce/user/project/clusters/index.html#getting-the-external-ip-address).
+NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's IP](../../clusters/index.md#getting-the-external-ip-address).
NGINX is configured for Prometheus monitoring, by setting:
* `enable-vts-status: "true"`, to export Prometheus metrics
@@ -51,4 +51,4 @@ Managing these settings depends on how NGINX ingress has been deployed. If you h
In order to isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do this, GitLab will search for metrics with appropriate labels. In this case, the `upstream` label must be of the form `<KUBE_NAMESPACE>-<CI_ENVIRONMENT_SLUG>-*`.
-If you have used [Auto Deploy](https://docs.gitlab.com/ee/ci/autodeploy/index.html) to deploy your app, this format will be used automatically and metrics will be detected with no action on your part.
+If you have used [Auto Deploy](../../../../topics/autodevops/index.md#auto-deploy) to deploy your app, this format will be used automatically and metrics will be detected with no action on your part.
diff --git a/doc/user/project/issues/due_dates.md b/doc/user/project/issues/due_dates.md
index e0c405353ce..1bf8b776c2e 100644
--- a/doc/user/project/issues/due_dates.md
+++ b/doc/user/project/issues/due_dates.md
@@ -35,5 +35,9 @@ Due dates also appear in your [todos list](../../../workflow/todos.md).
![Issues with due dates in the todos](img/due_dates_todos.png)
+The day before an open issue is due, an email will be sent to all participants
+of the issue. Both the due date and the day before are calculated using the
+server's timezone.
+
[ce-3614]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3614
[permissions]: ../../permissions.md#project
diff --git a/doc/user/project/issues/issues_functionalities.md b/doc/user/project/issues/issues_functionalities.md
index f2ca6a6822e..cf5cf1794ee 100644
--- a/doc/user/project/issues/issues_functionalities.md
+++ b/doc/user/project/issues/issues_functionalities.md
@@ -28,7 +28,7 @@ Comments and system notes also appear automatically in response to various actio
#### 2. Todos
- Add todo: add that issue to your [GitLab Todo](../../../workflow/todos.html) list
-- Mark done: mark that issue as done (reflects on the Todo list)
+- Mark todo as done: mark that issue as done (reflects on the Todo list)
#### 3. Assignee
@@ -41,10 +41,7 @@ it's reassigned to someone else to take it from there.
if a user is not member of that project, it can only be
assigned to them if they created the issue themselves.
-##### 3.1. Multiple Assignees
-
-> Available in [GitLab Starter](https://about.gitlab.com/products/) and
-[GitLab.com Bronze](https://about.gitlab.com/gitlab-com/).
+##### 3.1. Multiple Assignees **[STARTER]**
Often multiple people likely work on the same issue together,
which can especially be difficult to track in large teams
@@ -89,10 +86,7 @@ but they are immediately available to all projects in the group.
> **Tip:**
if the label doesn't exist yet, when you click **Edit**, it opens a dropdown menu from which you can select **Create new label**.
-#### 8. Weight
-
-> Available in [GitLab Starter](https://about.gitlab.com/products/) and
-[GitLab.com Bronze](https://about.gitlab.com/gitlab-com/).
+#### 8. Weight **[STARTER]**
- Attribute a weight (in a 0 to 9 range) to that issue. Easy to complete
should weight 1 and very hard to complete should weight 9.
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index a89a1206170..914898ea2ea 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -9,8 +9,7 @@ Labels allow you to categorize issues or merge requests using descriptive titles
In GitLab, you can create project and group labels:
- **Project labels** can be assigned to issues or merge requests in that project only.
-- **Group labels** can be assigned to any issue or merge request of any project in that group or subgroup.
-- In the [future](https://gitlab.com/gitlab-org/gitlab-ce/issues/40915), you will be able to assign group labels to issues and merge reqeusts of projects in [subgroups](../group/subgroups/index.md).
+- **Group labels** can be assigned to any issue or merge request of any project in that group or any subgroups of the group.
## Creating labels
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 3640d236db4..a6c0fd49c45 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -32,10 +32,10 @@ With GitLab merge requests, you can:
With **[GitLab Enterprise Edition][ee]**, you can also:
-- View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) (available only in GitLab Premium)
-- Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers (available in GitLab Starter)
-- [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history (available in GitLab Starter)
-- Analyze the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Starter)
+- View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) **[PREMIUM]**
+- Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers **[STARTER]**
+- [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history **[STARTER]**
+- Analyze the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) **[STARTER]**
## Use cases
@@ -43,7 +43,7 @@ A. Consider you are a software developer working in a team:
1. You checkout a new branch, and submit your changes through a merge request
1. You gather feedback from your team
-1. You work on the implementation optimizing code with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Starter)
+1. You work on the implementation optimizing code with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) **[STARTER]**
1. You build and test your changes with GitLab CI/CD
1. You request the approval from your manager
1. Your manager pushes a commit with his final review, [approves the merge request](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html), and set it to [merge when pipeline succeeds](#merge-when-pipeline-succeeds) (Merge Request Approvals are available in GitLab Starter)
@@ -56,7 +56,7 @@ B. Consider you're a web developer writing a webpage for your company's:
1. You gather feedback from your reviewers
1. Your changes are previewed with [Review Apps](../../../ci/review_apps/index.md)
1. You request your web designers for their implementation
-1. You request the [approval](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your manager (available in GitLab Starter)
+1. You request the [approval](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your manager **[STARTER]**
1. Once approved, your merge request is [squashed and merged](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html), and [deployed to staging with GitLab Pages](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) (Squash and Merge is available in GitLab Starter)
1. Your production team [cherry picks](#cherry-pick-changes) the merge commit into production
diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md
index 430fe3af1f8..61af1d2ab27 100644
--- a/doc/user/project/pages/getting_started_part_three.md
+++ b/doc/user/project/pages/getting_started_part_three.md
@@ -70,7 +70,7 @@ In case you want to point a root domain (`example.com`) to your
GitLab Pages site, deployed to `namespace.gitlab.io`, you need to
log into your domain's admin control panel and add a DNS `A` record
pointing your domain to Pages' server IP address. For projects on
-GitLab.com, this IP is `52.167.214.135`. For projects leaving in
+GitLab.com, this IP is `52.167.214.135`. For projects living in
other GitLab instances (CE or EE), please contact your sysadmin
asking for this information (which IP address is Pages server
running on your instance).
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 442fc978284..2f4ed3493c2 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -38,6 +38,7 @@ do.
| `/award :emoji:` | Toggle award for :emoji: |
| `/board_move ~column` | Move issue to column on the board |
| `/duplicate #issue` | Closes this issue and marks it as a duplicate of another issue |
-| `/move path/to/project` | Moves issue to another project |
-| `/tableflip` | Append the comment with `(╯°□°)╯︵ ┻━┻` |
-| `/shrug` | Append the comment with `¯\_(ツ)_/¯` | \ No newline at end of file
+| `/move path/to/project` | Moves issue to another project |
+| `/tableflip` | Append the comment with `(╯°□°)╯︵ ┻━┻` |
+| `/shrug` | Append the comment with `¯\_(ツ)_/¯` |
+| <code>/copy_metadata #issue &#124; !merge_request</code> | Copy labels and milestone from other issue or merge request |
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index eb0ac221e30..2c90f4b4413 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -31,7 +31,8 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version |
| ---------------- | --------------------- |
-| 10.4 to current | 0.2.2 |
+| 10.8 to current | 0.2.3 |
+| 10.4 | 0.2.2 |
| 10.3 | 0.2.1 |
| 10.0 | 0.2.0 |
| 9.4.0 | 0.1.8 |
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index 888dd0e143a..c9d2f8dc32d 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -34,7 +34,7 @@ Set up your project's merge request settings:
- Set up the merge request method (merge commit, [fast-forward merge](../merge_requests/fast_forward_merge.html)).
- Merge request [description templates](../description_templates.md#description-templates).
-- Enable [merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html#merge-request-approvals), _available in [GitLab Starter](https://about.gitlab.com/products/)_.
+- Enable [merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html#merge-request-approvals). **[STARTER]**
- Enable [merge only of pipeline succeeds](../merge_requests/merge_when_pipeline_succeeds.md).
- Enable [merge only when all discussions are resolved](../../discussions/index.md#only-allow-merge-requests-to-be-merged-if-all-discussions-are-resolved).
@@ -57,15 +57,20 @@ Here you can run housekeeping, archive, rename, transfer, or remove a project.
NOTE: **Note:**
Only project Owners and Admin users have the [permissions] to archive a project.
-An archived project will be hidden by default in the project listings.
+Archiving a project makes it read-only for all users and indicates that it is
+no longer actively maintained. Projects that have been archived can also be
+unarchived.
+
+When a project is archived, the repository, issues, merge requests and all
+other features are read-only. Archived projects are also hidden
+in project listings.
+
+To archive a project:
1. Navigate to your project's **Settings > General > Advanced settings**.
-1. Under "Archive project", hit the **Archive project** button.
+1. In the Archive project section, click the **Archive project** button.
1. Confirm the action when asked to.
-An archived project can be fully restored and will therefore retain its
-repository and all associated resources whilst in an archived state.
-
#### Renaming a repository
NOTE: **Note:**
diff --git a/doc/user/project/web_ide/img/commit_changes.png b/doc/user/project/web_ide/img/commit_changes.png
new file mode 100644
index 00000000000..b6fcbf699aa
--- /dev/null
+++ b/doc/user/project/web_ide/img/commit_changes.png
Binary files differ
diff --git a/doc/user/project/web_ide/img/enable_web_ide.png b/doc/user/project/web_ide/img/enable_web_ide.png
new file mode 100644
index 00000000000..196baa82ad2
--- /dev/null
+++ b/doc/user/project/web_ide/img/enable_web_ide.png
Binary files differ
diff --git a/doc/user/project/web_ide/img/open_web_ide.png b/doc/user/project/web_ide/img/open_web_ide.png
new file mode 100644
index 00000000000..d1192daf506
--- /dev/null
+++ b/doc/user/project/web_ide/img/open_web_ide.png
Binary files differ
diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md
new file mode 100644
index 00000000000..b7064b83c4e
--- /dev/null
+++ b/doc/user/project/web_ide/index.md
@@ -0,0 +1,33 @@
+# Web IDE
+
+> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ee/issues/4539) [GitLab Ultimate][ee] 10.4.
+> [Brought to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/issues/44157) in 10.7.
+
+The Web IDE makes it faster and easier to contribute changes to your projects
+by providing an advanced editor with commit staging.
+
+## Open the Web IDE
+
+The Web IDE can be opened when viewing a file, from the repository file list,
+and from merge requests.
+
+![Open Web IDE](img/open_web_ide.png)
+
+## Commit changes
+
+Changed files are shown on the right in the commit panel. All changes are
+automatically staged. To commit your changes, add a commit message and click
+the 'Commit Button'.
+
+![Commit changes](img/commit_changes.png)
+
+## Comparing changes
+
+Before you commit your changes, you can compare them with the previous commit
+by switching to the review mode or selecting the file from the staged files
+list.
+
+An additional review mode is available when you open a merge request, which
+shows you a preview of the merge request diff if you commit your changes.
+
+[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index 377eee69c11..104ac0cf31b 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -244,3 +244,20 @@ GitLab checks files to detect LFS pointers on push. If LFS pointers are detected
Verify that LFS in installed locally and consider a manual push with `git lfs push --all`.
If you are storing LFS files outside of GitLab you can disable LFS on the project by settting `lfs_enabled: false` with the [projects api](../../api/projects.md#edit-project).
+
+### Hosting LFS objects externally
+
+It is possible to host LFS objects externally by setting a custom LFS url with `git config -f .lfsconfig lfs.url https://example.com/<project>.git/info/lfs`.
+
+Because GitLab verifies the existence of objects referenced by LFS pointers, push will fail when LFS is enabled for the project.
+
+LFS can be disabled for a project by Owners and Masters using the [Project API](../../api/projects.md#edit-project).
+
+```bash
+curl --request PUT \
+ --url https://example.com/api/v4/projects/<PROJECT_ID> \
+ --header 'Private-Token: <YOUR_PRIVATE_TOKEN>' \
+ --data 'lfs_enabled=false'
+```
+
+Note, `<PROJECT_ID>` can also be substituted with a [namespaced path](../../api/README.md#namespaced-path-encoding).
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index c4095ee0f69..f1501c81b27 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -86,6 +86,7 @@ In most of the below cases, the notification will be sent to:
| Close issue | |
| Reassign issue | The above, plus the old assignee |
| Reopen issue | |
+| Due issue | Participants and Custom notification level with this event selected |
| New merge request | |
| Push to merge request | Participants and Custom notification level with this event selected |
| Reassign merge request | The above, plus the old assignee |
@@ -96,15 +97,14 @@ In most of the below cases, the notification will be sent to:
| Failed pipeline | The author of the pipeline |
| Successful pipeline | The author of the pipeline, if they have the custom notification setting for successful pipelines set |
-
In addition, if the title or description of an Issue or Merge Request is
changed, notifications will be sent to any **new** mentions by `@username` as
if they had been mentioned in the original text.
-You won't receive notifications for Issues, Merge Requests or Milestones
-created by yourself. You will only receive automatic notifications when
-somebody else comments or adds changes to the ones that you've created or
-mentions you.
+You won't receive notifications for Issues, Merge Requests or Milestones created
+by yourself (except when an issue is due). You will only receive automatic
+notifications when somebody else comments or adds changes to the ones that
+you've created or mentions you.
### Email Headers
@@ -122,7 +122,7 @@ Notification emails include headers that provide extra content about the notific
| X-GitLab-NotificationReason | The reason for being notified. "mentioned", "assigned", etc |
#### X-GitLab-NotificationReason
-This header holds the reason for the notification to have been sent out,
+This header holds the reason for the notification to have been sent out,
where reason can be `mentioned`, `assigned`, `own_activity`, etc.
Only one reason is sent out according to its priority:
- `own_activity`
@@ -130,7 +130,7 @@ Only one reason is sent out according to its priority:
- `mentioned`
The reason in this header will also be shown in the footer of the notification email. For example an email with the
-reason `assigned` will have this sentence in the footer:
+reason `assigned` will have this sentence in the footer:
`"You are receiving this email because you have been assigned an item on {configured GitLab hostname}"`
**Note: Only reasons listed above have been implemented so far**
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index e612646cfbc..f13d29884d4 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -92,9 +92,9 @@ corresponding **Done** button, and it will disappear from your Todo list.
![A Todo in the Todos dashboard](img/todo_list_item.png)
A Todo can also be marked as done from the issue or merge request sidebar using
-the "Mark done" button.
+the "Mark todo as done" button.
-![Mark Done from the issuable sidebar](img/todos_mark_done_sidebar.png)
+![Mark todo as done from the issuable sidebar](img/todos_mark_done_sidebar.png)
You can mark all your Todos as done at once by clicking on the **Mark all as
done** button.
diff --git a/ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb b/ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb
new file mode 100644
index 00000000000..f1e851a210b
--- /dev/null
+++ b/ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb
@@ -0,0 +1,22 @@
+module EE
+ module Ldap
+ module OmniauthCallbacksController
+ extend ::Gitlab::Utils::Override
+
+ override :sign_in_and_redirect
+ def sign_in_and_redirect(user)
+ # The counter gets incremented in `sign_in_and_redirect`
+ show_ldap_sync_flash if user.sign_in_count == 0
+
+ super
+ end
+
+ private
+
+ def show_ldap_sync_flash
+ flash[:notice] = 'LDAP sync in progress. This could take a few minutes. '\
+ 'Refresh the page to see the changes.'
+ end
+ end
+ end
+end
diff --git a/ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb b/ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
new file mode 100644
index 00000000000..0835ff35846
--- /dev/null
+++ b/ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Ldap::OmniauthCallbacksController do
+ include_context 'Ldap::OmniauthCallbacksController'
+
+ it "displays LDAP sync flash on first sign in" do
+ post provider
+
+ expect(flash[:notice]).to match(/LDAP sync in progress*/)
+ end
+
+ it "skips LDAP sync flash on subsequent sign ins" do
+ user.update!(sign_in_count: 1)
+
+ post provider
+
+ expect(flash[:notice]).to eq nil
+ end
+
+ context 'access denied' do
+ let(:valid_login?) { false }
+
+ it 'logs a failure event' do
+ stub_licensed_features(extended_audit_events: true)
+
+ expect { post provider }.to change(SecurityEvent, :count).by(1)
+ end
+ end
+end
diff --git a/features/project/builds/permissions.feature b/features/project/builds/permissions.feature
deleted file mode 100644
index db15968db06..00000000000
--- a/features/project/builds/permissions.feature
+++ /dev/null
@@ -1,54 +0,0 @@
-Feature: Project Builds Permissions
- Background:
- Given I sign in as a user
- And project exists in some group namespace
- And project has CI enabled
- And project has a recent build
-
- Scenario: I try to visit build details as guest
- Given I am member of a project with a guest role
- When I visit recent build details page
- Then page status code should be 404
-
- Scenario: I try to visit project builds page as guest
- Given I am member of a project with a guest role
- When I visit project builds page
- Then page status code should be 404
-
- Scenario: I try to visit build details of internal project without access to builds
- Given The project is internal
- And public access for builds is disabled
- When I visit recent build details page
- Then page status code should be 404
-
- Scenario: I try to visit internal project builds page without access to builds
- Given The project is internal
- And public access for builds is disabled
- When I visit project builds page
- Then page status code should be 404
-
- @javascript
- Scenario: I try to visit build details of internal project with access to builds
- Given The project is internal
- And public access for builds is enabled
- When I visit recent build details page
- Then I see details of a build
- And I see build trace
-
- Scenario: I try to visit internal project builds page with access to builds
- Given The project is internal
- And public access for builds is enabled
- When I visit project builds page
- Then I see the build
-
- Scenario: I try to download build artifacts as guest
- Given I am member of a project with a guest role
- And recent build has artifacts available
- When I access artifacts download page
- Then page status code should be 404
-
- Scenario: I try to download build artifacts as reporter
- Given I am member of a project with a reporter role
- And recent build has artifacts available
- When I access artifacts download page
- Then download of build artifacts archive starts
diff --git a/features/project/commits/branches.feature b/features/project/commits/branches.feature
deleted file mode 100644
index c57376aecff..00000000000
--- a/features/project/commits/branches.feature
+++ /dev/null
@@ -1,42 +0,0 @@
-@project_commits
-Feature: Project Commits Branches
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And project "Shop" has protected branches
-
- Scenario: I can see project all git branches
- Given I visit project branches page
- Then I should see "Shop" all branches list
-
- Scenario: I can see project protected git branches
- Given I visit project protected branches page
- Then I should see "Shop" protected branches list
-
- @javascript
- Scenario: I create a branch
- Given I visit project branches page
- And I click new branch link
- And I submit new branch form
- Then I should see new branch created
-
- @javascript
- Scenario: I delete a branch
- Given I visit project branches page
- And I filter for branch improve/awesome
- And I click branch 'improve/awesome' delete link
- Then I should not see branch 'improve/awesome'
-
- @javascript
- Scenario: I create a branch with invalid name
- Given I visit project branches page
- And I click new branch link
- And I submit new branch form with invalid name
- Then I should see new an error that branch is invalid
-
- @javascript
- Scenario: I create a branch that already exists
- Given I visit project branches page
- And I click new branch link
- And I submit new branch form with branch that already exists
- Then I should see new an error that branch already exists
diff --git a/features/project/commits/comments.feature b/features/project/commits/comments.feature
deleted file mode 100644
index fafb54b183a..00000000000
--- a/features/project/commits/comments.feature
+++ /dev/null
@@ -1,51 +0,0 @@
-@project_commits
-Feature: Project Commits Comments
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And I visit project commit page
-
- @javascript
- Scenario: I can comment on a commit
- Given I leave a comment like "XML attached"
- Then I should see a comment saying "XML attached"
-
- @javascript
- Scenario: I can't cancel the main form
- Then I should not see the cancel comment button
-
- @javascript
- Scenario: I can preview with text
- Given I write a comment like ":+1: Nice"
- Then The comment preview tab should be display rendered Markdown
-
- @javascript
- Scenario: I preview a comment
- Given I preview a comment text like "Bug fixed :smile:"
- Then I should see the comment preview
- And I should not see the comment text field
-
- @javascript
- Scenario: I can edit after preview
- Given I preview a comment text like "Bug fixed :smile:"
- Then I should see the comment write tab
-
- @javascript
- Scenario: I have a reset form after posting from preview
- Given I preview a comment text like "Bug fixed :smile:"
- And I submit the comment
- Then I should see an empty comment text field
- And I should not see the comment preview
-
- @javascript
- Scenario: I can delete a comment
- Given I leave a comment like "XML attached"
- Then I should see a comment saying "XML attached"
- And I delete a comment
- Then I should not see a comment saying "XML attached"
-
- @javascript
- Scenario: I can edit a comment with +1
- Given I leave a comment like "XML attached"
- And I edit the last comment with a +1
- Then I should see +1 in the description
diff --git a/features/project/find_file.feature b/features/project/find_file.feature
deleted file mode 100644
index ae8fa245923..00000000000
--- a/features/project/find_file.feature
+++ /dev/null
@@ -1,42 +0,0 @@
-@dashboard
-Feature: Project Find File
- Background:
- Given I sign in as a user
- And I own a project
- And I visit my project's files page
-
- @javascript
- Scenario: Navigate to find file by shortcut
- Given I press "t"
- Then I should see "find file" page
-
- Scenario: Navigate to find file
- Given I click Find File button
- Then I should see "find file" page
-
- @javascript
- Scenario: I search file
- Given I visit project find file page
- And I fill in file find with "change"
- Then I should not see ".gitignore" in files
- And I should not see ".gitmodules" in files
- And I should see "CHANGELOG" in files
- And I should not see "VERSION" in files
-
- @javascript
- Scenario: I search file that not exist
- Given I visit project find file page
- And I fill in file find with "asdfghjklqwertyuizxcvbnm"
- Then I should not see ".gitignore" in files
- And I should not see ".gitmodules" in files
- And I should not see "CHANGELOG" in files
- And I should not see "VERSION" in files
-
- @javascript
- Scenario: I search file that partially matches
- Given I visit project find file page
- And I fill in file find with "git"
- Then I should see ".gitignore" in files
- And I should see ".gitmodules" in files
- And I should not see "CHANGELOG" in files
- And I should not see "VERSION" in files
diff --git a/features/project/issues/milestones.feature b/features/project/issues/milestones.feature
deleted file mode 100644
index 77c8ed6e5bf..00000000000
--- a/features/project/issues/milestones.feature
+++ /dev/null
@@ -1,43 +0,0 @@
-@project_issues
-Feature: Project Issues Milestones
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And project "Shop" has milestone "v2.2"
- Given I visit project "Shop" milestones page
-
- Scenario: I should see active milestones
- Then I should see milestone "v2.2"
-
- Scenario: I should see milestone
- Given I click link "v2.2"
- Then I should see milestone "v2.2"
-
- @javascript
- Scenario: I create and delete new milestone
- Given I click link "New Milestone"
- And I submit new milestone "v2.3"
- Then I should see milestone "v2.3"
- Given I click button to remove milestone
- And I confirm in modal
- When I visit project "Shop" activity page
- Then I should see deleted milestone activity
-
- @javascript
- Scenario: I delete new milestone
- Given I click button to remove milestone
- And I confirm in modal
- And I should see no milestones
-
- @javascript
- Scenario: Listing closed issues
- Given the milestone has open and closed issues
- And I click link "v2.2"
- Then I should see 3 issues
-
- # Markdown
-
- Scenario: Headers inside the description should have ids generated for them.
- Given I click link "v2.2"
- # PLEASE USE the `have_header_with_correct_id_and_link(level, text, id, parent)` matcher on migrating this spec to rspec.
- Then Header "Description header" should have correct id and link
diff --git a/features/steps/project/builds/permissions.rb b/features/steps/project/builds/permissions.rb
deleted file mode 100644
index 6e9d6504fd5..00000000000
--- a/features/steps/project/builds/permissions.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class Spinach::Features::ProjectBuildsPermissions < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedBuilds
- include SharedPaths
- include RepoHelpers
-end
diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb
index c3ae33d2aa9..3ecd4c8b672 100644
--- a/features/steps/project/commits/branches.rb
+++ b/features/steps/project/commits/branches.rb
@@ -7,37 +7,14 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
click_link "All"
end
- step 'I should see "Shop" all branches list' do
- expect(page).to have_content "Branches"
- expect(page).to have_content "master"
- end
-
step 'I click link "Protected"' do
click_link "Protected"
end
- step 'I should see "Shop" protected branches list' do
- page.within ".protected-branches-list" do
- expect(page).to have_content "stable"
- expect(page).not_to have_content "master"
- end
- end
-
- step 'project "Shop" has protected branches' do
- project = Project.find_by(name: "Shop")
- create(:protected_branch, project: project, name: "stable")
- end
-
step 'I click new branch link' do
click_link "New branch"
end
- step 'I submit new branch form' do
- fill_in 'branch_name', with: 'deploy_keys'
- select_branch('master')
- click_button 'Create branch'
- end
-
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
@@ -45,40 +22,6 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
click_button 'Create branch'
end
- step 'I submit new branch form with branch that already exists' do
- fill_in 'branch_name', with: 'master'
- select_branch('master')
- click_button 'Create branch'
- end
-
- step 'I should see new branch created' do
- expect(page).to have_content 'deploy_keys'
- end
-
- step 'I should see new an error that branch is invalid' do
- expect(page).to have_content 'Branch name is invalid'
- expect(page).to have_content "can't contain spaces"
- end
-
- step 'I should see new an error that branch already exists' do
- expect(page).to have_content 'Branch already exists'
- end
-
- step 'I filter for branch improve/awesome' do
- fill_in 'branch-search', with: 'improve/awesome'
- find('#branch-search').native.send_keys(:enter)
- end
-
- step "I click branch 'improve/awesome' delete link" do
- page.within '.js-branch-improve\/awesome' do
- accept_alert { find('.btn-remove').click }
- end
- end
-
- step "I should not see branch 'improve/awesome'" do
- expect(page).to have_css('.js-branch-improve\\/awesome', visible: :hidden)
- end
-
def select_branch(branch_name)
find('.git-revision-dropdown-toggle').click
diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb
index 4ce67aa651c..30927306a4f 100644
--- a/features/steps/project/issues/milestones.rb
+++ b/features/steps/project/issues/milestones.rb
@@ -4,35 +4,6 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
include SharedPaths
include SharedMarkdown
- step 'I should see milestone "v2.2"' do
- milestone = @project.milestones.find_by(title: "v2.2")
- expect(page).to have_content(milestone.title[0..10])
- expect(page).to have_content(milestone.expires_at)
- expect(page).to have_content("Issues")
- end
-
- step 'I click link "v2.2"' do
- click_link "v2.2"
- end
-
- step 'I click link "New Milestone"' do
- page.within('.nav-controls') do
- click_link "New milestone"
- end
- end
-
- step 'I submit new milestone "v2.3"' do
- fill_in "milestone_title", with: "v2.3"
- click_button "Create milestone"
- end
-
- step 'I should see milestone "v2.3"' do
- milestone = @project.milestones.find_by(title: "v2.3")
- expect(page).to have_content(milestone.title[0..10])
- expect(page).to have_content(milestone.expires_at)
- expect(page).to have_content("Issues")
- end
-
step 'project "Shop" has milestone "v2.2"' do
project = Project.find_by(name: "Shop")
milestone = create(:milestone,
@@ -43,36 +14,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
3.times { create(:issue, project: project, milestone: milestone) }
end
- step 'the milestone has open and closed issues' do
- project = Project.find_by(name: "Shop")
- milestone = project.milestones.find_by(title: 'v2.2')
-
- # 3 Open issues created above; create one closed issue
- create(:closed_issue, project: project, milestone: milestone)
- end
-
- step 'I should see deleted milestone activity' do
- expect(page).to have_content('opened milestone in')
- expect(page).to have_content('destroyed milestone in')
- end
-
When 'I click link "All Issues"' do
click_link 'All Issues'
end
-
- step 'I should see 3 issues' do
- expect(page).to have_selector('#tab-issues li.issuable-row', count: 4)
- end
-
- step 'I click button to remove milestone' do
- click_button 'Delete'
- end
-
- step 'I confirm in modal' do
- click_button 'Delete milestone'
- end
-
- step 'I should see no milestones' do
- expect(page).to have_content('No milestones to show')
- end
end
diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb
deleted file mode 100644
index 461160b8430..00000000000
--- a/features/steps/project/project_find_file.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedProject
- include SharedProjectTab
-
- step 'I press "t"' do
- find('body').native.send_key('t')
- end
-
- step 'I click Find File button' do
- click_link 'Find file'
- end
-
- step 'I should see "find file" page' do
- ensure_active_main_tab('Repository')
- expect(page).to have_selector('.file-finder-holder', count: 1)
- end
-
- step 'I fill in Find by path with "git"' do
- ensure_active_main_tab('Repository')
- expect(page).to have_selector('.file-finder-holder', count: 1)
- end
-
- step 'I fill in file find with "git"' do
- find_file "git"
- end
-
- step 'I fill in file find with "change"' do
- find_file "change"
- end
-
- step 'I fill in file find with "asdfghjklqwertyuizxcvbnm"' do
- find_file "asdfghjklqwertyuizxcvbnm"
- end
-
- step 'I should see "VERSION" in files' do
- expect(page).to have_content("VERSION")
- end
-
- step 'I should not see "VERSION" in files' do
- expect(page).not_to have_content("VERSION")
- end
-
- step 'I should see "CHANGELOG" in files' do
- expect(page).to have_content("CHANGELOG")
- end
-
- step 'I should not see "CHANGELOG" in files' do
- expect(page).not_to have_content("CHANGELOG")
- end
-
- step 'I should see ".gitmodules" in files' do
- expect(page).to have_content(".gitmodules")
- end
-
- step 'I should not see ".gitmodules" in files' do
- expect(page).not_to have_content(".gitmodules")
- end
-
- step 'I should see ".gitignore" in files' do
- expect(page).to have_content(".gitignore")
- end
-
- step 'I should not see ".gitignore" in files' do
- expect(page).not_to have_content(".gitignore")
- end
-
- def find_file(text)
- fill_in 'file_find', with: text
- end
-end
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
index f5950145348..c2197584d8d 100644
--- a/features/steps/shared/builds.rb
+++ b/features/steps/shared/builds.rb
@@ -30,10 +30,6 @@ module SharedBuilds
visit project_job_path(@project, @build)
end
- step 'I visit project builds page' do
- visit project_jobs_path(@project)
- end
-
step 'recent build has artifacts available' do
artifacts = Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
archive = fixture_file_upload(artifacts, 'application/zip')
@@ -54,25 +50,4 @@ module SharedBuilds
expect(page.response_headers['Content-Type']).to eq 'application/zip'
expect(page.response_headers['Content-Transfer-Encoding']).to eq 'binary'
end
-
- step 'I access artifacts download page' do
- visit download_project_job_artifacts_path(@project, @build)
- end
-
- step 'I see details of a build' do
- expect(page).to have_content "Job ##{@build.id}"
- end
-
- step 'I see build trace' do
- expect(page).to have_css '#build-trace'
- end
-
- step 'I see the build' do
- page.within('.build') do
- expect(page).to have_content "##{@build.id}"
- expect(page).to have_content @build.sha[0..7]
- expect(page).to have_content @build.ref
- expect(page).to have_content @build.name
- end
- end
end
diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb
index c66280127e9..9d522936fb6 100644
--- a/features/steps/shared/markdown.rb
+++ b/features/steps/shared/markdown.rb
@@ -10,10 +10,6 @@ module SharedMarkdown
expect(find(:xpath, "#{node.path}/..").text).to eq text
end
- step 'Header "Description header" should have correct id and link' do
- header_should_have_correct_id_and_link(1, 'Description header', 'description-header')
- end
-
step 'I should not see the Markdown preview' do
expect(find('.gfm-form .js-md-preview')).not_to be_visible
end
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
index cbe1cae096e..bf1b88c60d7 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -6,70 +6,12 @@ module SharedNote
wait_for_requests if javascript_test?
end
- step 'I delete a comment' do
- page.within('.main-notes-list') do
- note = find('.note')
- note.hover
-
- find('.more-actions').click
- find('.more-actions .dropdown-menu li', match: :first)
-
- accept_confirm { find(".js-note-delete").click }
- end
- end
-
step 'I haven\'t written any comment text' do
page.within(".js-main-target-form") do
fill_in "note[note]", with: ""
end
end
- step 'I leave a comment like "XML attached"' do
- page.within(".js-main-target-form") do
- fill_in "note[note]", with: "XML attached"
- click_button "Comment"
- end
-
- wait_for_requests
- end
-
- step 'I preview a comment text like "Bug fixed :smile:"' do
- page.within(".js-main-target-form") do
- fill_in "note[note]", with: "Bug fixed :smile:"
- find('.js-md-preview-button').click
- end
- end
-
- step 'I submit the comment' do
- page.within(".js-main-target-form") do
- click_button "Comment"
- end
-
- wait_for_requests
- end
-
- step 'I write a comment like ":+1: Nice"' do
- page.within(".js-main-target-form") do
- fill_in 'note[note]', with: ':+1: Nice'
- end
- end
-
- step 'I should not see a comment saying "XML attached"' do
- expect(page).not_to have_css(".note")
- end
-
- step 'I should not see the cancel comment button' do
- page.within(".js-main-target-form") do
- should_not have_link("Cancel")
- end
- end
-
- step 'I should not see the comment preview' do
- page.within(".js-main-target-form") do
- expect(find('.js-md-preview')).not_to be_visible
- end
- end
-
step 'The comment preview tab should say there is nothing to do' do
page.within(".js-main-target-form") do
find('.js-md-preview-button').click
@@ -77,71 +19,7 @@ module SharedNote
end
end
- step 'I should not see the comment text field' do
- page.within(".js-main-target-form") do
- expect(find('.js-note-text')).not_to be_visible
- end
- end
-
- step 'I should see a comment saying "XML attached"' do
- page.within(".note") do
- expect(page).to have_content("XML attached")
- end
- end
-
- step 'I should see an empty comment text field' do
- page.within(".js-main-target-form") do
- expect(page).to have_field("note[note]", with: "")
- end
- end
-
- step 'I should see the comment write tab' do
- page.within(".js-main-target-form") do
- expect(page).to have_css('.js-md-write-button', visible: true)
- end
- end
-
- step 'The comment preview tab should be display rendered Markdown' do
- page.within(".js-main-target-form") do
- find('.js-md-preview-button').click
- expect(find('.js-md-preview')).to have_css('gl-emoji', visible: true)
- end
- end
-
- step 'I should see the comment preview' do
- page.within(".js-main-target-form") do
- expect(page).to have_css('.js-md-preview', visible: true)
- end
- end
-
step 'I should see no notes at all' do
expect(page).not_to have_css('.note')
end
-
- # Markdown
-
- step 'I edit the last comment with a +1' do
- page.within(".main-notes-list") do
- note = find('.note')
- note.hover
-
- note.find('.js-note-edit').click
- end
-
- page.find('.current-note-edit-form textarea')
-
- page.within(".current-note-edit-form") do
- fill_in 'note[note]', with: '+1 Awesome!'
- click_button 'Save comment'
- end
- wait_for_requests
- end
-
- step 'I should see +1 in the description' do
- page.within(".note") do
- expect(page).to have_content("+1 Awesome!")
- end
-
- wait_for_requests
- end
end
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index cc893b8391e..014e6ad625b 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -200,10 +200,6 @@ module SharedPaths
visit edit_project_path(@project)
end
- step "I visit my project's files page" do
- visit project_tree_path(@project, root_ref)
- end
-
step 'I visit a binary file in the repo' do
visit project_blob_path(@project,
File.join(root_ref, 'files/images/logo-black.png'))
@@ -264,10 +260,6 @@ module SharedPaths
visit project_path(project)
end
- step 'I visit project "Shop" activity page' do
- visit activity_project_path(project)
- end
-
step 'I visit project "Forked Shop" merge requests page' do
visit project_merge_requests_path(@forked_project)
end
@@ -276,14 +268,6 @@ module SharedPaths
visit edit_project_path(project)
end
- step 'I visit project branches page' do
- visit project_branches_path(@project)
- end
-
- step 'I visit project protected branches page' do
- visit project_protected_branches_path(@project)
- end
-
step 'I visit compare refs page' do
visit project_compare_index_path(@project)
end
@@ -381,10 +365,6 @@ module SharedPaths
visit project_merge_requests_path(project)
end
- step 'I visit project "Shop" milestones page' do
- visit project_milestones_path(project)
- end
-
step 'I visit project "Shop" team page' do
visit project_project_members_path(project)
end
@@ -451,12 +431,4 @@ module SharedPaths
mr = MergeRequest.find_by(title: title)
project_merge_request_path(mr.target_project, mr)
end
-
- # ----------------------------------------
- # Errors
- # ----------------------------------------
-
- step 'page status code should be 404' do
- expect(status_code).to eq 404
- end
end
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index 09969a6473f..a1945cf5f3d 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -13,11 +13,6 @@ module SharedProject
@project.add_master(@user)
end
- step "project exists in some group namespace" do
- @group = create(:group, name: 'some group')
- @project = create(:project, :repository, namespace: @group, public_builds: false)
- end
-
# Create a specific project called "Shop"
step 'I own project "Shop"' do
@project = Project.find_by(name: "Shop")
@@ -30,18 +25,6 @@ module SharedProject
end
# ----------------------------------------
- # Project permissions
- # ----------------------------------------
-
- step 'I am member of a project with a guest role' do
- @project.add_guest(@user)
- end
-
- step 'I am member of a project with a reporter role' do
- @project.add_reporter(@user)
- end
-
- # ----------------------------------------
# Visibility of archived project
# ----------------------------------------
@@ -140,18 +123,6 @@ module SharedProject
create(:label, project: project, title: 'enhancement')
end
- step 'The project is internal' do
- @project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- end
-
- step 'public access for builds is enabled' do
- @project.update(public_builds: true)
- end
-
- step 'public access for builds is disabled' do
- @project.update(public_builds: false)
- end
-
def user_owns_project(user_name:, project_name:, visibility: :private)
user = user_exists(user_name, username: user_name.gsub(/\s/, '').underscore)
project = Project.find_by(name: project_name)
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index 4e2b3c67af5..8879c9ab650 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -21,13 +21,7 @@ Capybara.register_driver :chrome do |app|
options.add_argument("no-sandbox")
# Run headless by default unless CHROME_HEADLESS specified
- unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
- options.add_argument("headless")
-
- # Chrome documentation says this flag is needed for now
- # https://developers.google.com/web/updates/2017/04/headless-chrome#cli
- options.add_argument("disable-gpu")
- end
+ options.add_argument("headless") unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
# Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252
options.add_argument("disable-dev-shm-usage") if ENV['CI'] || ENV['CI_SERVER']
diff --git a/features/support/env.rb b/features/support/env.rb
index 15211995918..8fa2fcb6e3e 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -12,7 +12,11 @@ end
WebMock.enable!
-%w(select2_helper test_env repo_helpers wait_for_requests sidekiq project_forks_helper webmock).each do |f|
+%w(select2_helper test_env repo_helpers wait_for_requests project_forks_helper).each do |f|
+ require Rails.root.join('spec', 'support', 'helpers', f)
+end
+
+%w(sidekiq webmock).each do |f|
require Rails.root.join('spec', 'support', f)
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 073471b4c4d..5139e869c71 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -154,6 +154,7 @@ module API
mount ::API::ProjectHooks
mount ::API::Projects
mount ::API::ProjectMilestones
+ mount ::API::ProjectSnapshots
mount ::API::ProjectSnippets
mount ::API::ProtectedBranches
mount ::API::Repositories
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
index 6abd575b6ad..7975f35ab1e 100644
--- a/lib/api/discussions.rb
+++ b/lib/api/discussions.rb
@@ -25,7 +25,7 @@ module API
get ":id/#{noteables_str}/:noteable_id/discussions" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
- return not_found!("Discussions") unless can?(current_user, noteable_read_ability_name(noteable), noteable)
+ break not_found!("Discussions") unless can?(current_user, noteable_read_ability_name(noteable), noteable)
notes = noteable.notes
.inc_relations_for_view
@@ -50,7 +50,7 @@ module API
notes = readable_discussion_notes(noteable, params[:discussion_id])
if notes.empty? || !can?(current_user, noteable_read_ability_name(noteable), noteable)
- return not_found!("Discussion")
+ break not_found!("Discussion")
end
discussion = Discussion.build(notes, noteable)
@@ -98,7 +98,7 @@ module API
notes = readable_discussion_notes(noteable, params[:discussion_id])
if notes.empty? || !can?(current_user, noteable_read_ability_name(noteable), noteable)
- return not_found!("Notes")
+ break not_found!("Notes")
end
present notes, with: Entities::Note
@@ -117,8 +117,8 @@ module API
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
- return not_found!("Discussion") if notes.empty?
- return bad_request!("Discussion is an individual note.") unless notes.first.part_of_discussion?
+ break not_found!("Discussion") if notes.empty?
+ break bad_request!("Discussion is an individual note.") unless notes.first.part_of_discussion?
opts = {
note: params[:body],
diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb
index 92800ce6450..55d5c7f1606 100644
--- a/lib/api/group_variables.rb
+++ b/lib/api/group_variables.rb
@@ -31,7 +31,7 @@ module API
key = params[:key]
variable = user_group.variables.find_by(key: key)
- return not_found!('GroupVariable') unless variable
+ break not_found!('GroupVariable') unless variable
present variable, with: Entities::Variable
end
@@ -67,7 +67,7 @@ module API
put ':id/variables/:key' do
variable = user_group.variables.find_by(key: params[:key])
- return not_found!('GroupVariable') unless variable
+ break not_found!('GroupVariable') unless variable
variable_params = declared_params(include_missing: false).except(:key)
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 61dab1dd5cb..b8657cd7ee4 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -103,9 +103,9 @@ module API
end
def find_project(id)
- if id =~ /^\d+$/
+ if id.is_a?(Integer) || id =~ /^\d+$/
Project.find_by(id: id)
- else
+ elsif id.include?("/")
Project.find_by_full_path(id)
end
end
diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb
index cd91df1ecd8..b74b8149834 100644
--- a/lib/api/helpers/notes_helpers.rb
+++ b/lib/api/helpers/notes_helpers.rb
@@ -64,8 +64,10 @@ module API
authorize! :create_note, noteable
parent = noteable_parent(noteable)
+
if opts[:created_at]
- opts.delete(:created_at) unless current_user.admin? || parent.owner == current_user
+ opts.delete(:created_at) unless
+ current_user.admin? || parent.owned_by?(current_user)
end
project = parent if parent.is_a?(Project)
diff --git a/lib/api/helpers/project_snapshots_helpers.rb b/lib/api/helpers/project_snapshots_helpers.rb
new file mode 100644
index 00000000000..94798a8cb51
--- /dev/null
+++ b/lib/api/helpers/project_snapshots_helpers.rb
@@ -0,0 +1,25 @@
+module API
+ module Helpers
+ module ProjectSnapshotsHelpers
+ def authorize_read_git_snapshot!
+ authenticated_with_full_private_access!
+ end
+
+ def send_git_snapshot(repository)
+ header(*Gitlab::Workhorse.send_git_snapshot(repository))
+ end
+
+ def snapshot_project
+ user_project
+ end
+
+ def snapshot_repository
+ if to_boolean(params[:wiki])
+ snapshot_project.wiki.repository
+ else
+ snapshot_project.repository
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index fcbc248fc3b..6b72caea8fd 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -50,7 +50,7 @@ module API
access_checker.check(params[:action], params[:changes])
@project ||= access_checker.project
rescue Gitlab::GitAccess::UnauthorizedError, Gitlab::GitAccess::NotFoundError => e
- return { status: false, message: e.message }
+ break { status: false, message: e.message }
end
log_user_activity(actor)
@@ -142,21 +142,21 @@ module API
if key
key.update_last_used_at
else
- return { 'success' => false, 'message' => 'Could not find the given key' }
+ break { 'success' => false, 'message' => 'Could not find the given key' }
end
if key.is_a?(DeployKey)
- return { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' }
+ break { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' }
end
user = key.user
unless user
- return { success: false, message: 'Could not find a user for the given key' }
+ break { success: false, message: 'Could not find a user for the given key' }
end
unless user.two_factor_enabled?
- return { success: false, message: 'Two-factor authentication is not enabled for this user' }
+ break { success: false, message: 'Two-factor authentication is not enabled for this user' }
end
codes = nil
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 88e7f46c92c..12ff2a1398b 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -310,7 +310,7 @@ module API
issue = find_project_issue(params[:issue_iid])
- return not_found!('UserAgentDetail') unless issue.user_agent_detail
+ break not_found!('UserAgentDetail') unless issue.user_agent_detail
present issue.user_agent_detail, with: Entities::UserAgentDetail
end
diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb
index b1adef49d46..32379d7c8ab 100644
--- a/lib/api/job_artifacts.rb
+++ b/lib/api/job_artifacts.rb
@@ -77,7 +77,7 @@ module API
build = find_build!(params[:job_id])
authorize!(:update_build, build)
- return not_found!(build) unless build.artifacts?
+ break not_found!(build) unless build.artifacts?
build.keep_artifacts!
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 60911c8d733..54d1acbd412 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -120,7 +120,7 @@ module API
build = find_build!(params[:job_id])
authorize!(:update_build, build)
- return forbidden!('Job is not retryable') unless build.retryable?
+ break forbidden!('Job is not retryable') unless build.retryable?
build = Ci::Build.retry(build, current_user)
@@ -138,7 +138,7 @@ module API
build = find_build!(params[:job_id])
authorize!(:erase_build, build)
- return forbidden!('Job is not erasable!') unless build.erasable?
+ break forbidden!('Job is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
present build, with: Entities::Job
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 3264a26f7d2..d4cc18f622b 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -189,7 +189,7 @@ module API
post ":id/merge_requests" do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42316')
- authorize! :create_merge_request, user_project
+ authorize! :create_merge_request_from, user_project
mr_params = declared_params(include_missing: false)
mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch)
diff --git a/lib/api/project_snapshots.rb b/lib/api/project_snapshots.rb
new file mode 100644
index 00000000000..71005acc587
--- /dev/null
+++ b/lib/api/project_snapshots.rb
@@ -0,0 +1,19 @@
+module API
+ class ProjectSnapshots < Grape::API
+ helpers ::API::Helpers::ProjectSnapshotsHelpers
+
+ before { authorize_read_git_snapshot! }
+
+ resource :projects do
+ desc 'Download a (possibly inconsistent) snapshot of a repository' do
+ detail 'This feature was introduced in GitLab 10.7'
+ end
+ params do
+ optional :wiki, type: Boolean, desc: 'Set to true to receive the wiki repository'
+ end
+ get ':id/snapshot' do
+ send_git_snapshot(snapshot_repository)
+ end
+ end
+ end
+end
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index 39c03c40bab..1de5551fee9 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -145,7 +145,7 @@ module API
snippet = Snippet.find_by!(id: params[:snippet_id], project_id: params[:id])
- return not_found!('UserAgentDetail') unless snippet.user_agent_detail
+ break not_found!('UserAgentDetail') unless snippet.user_agent_detail
present snippet.user_agent_detail, with: Entities::UserAgentDetail
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index d0a4a23e074..8871792060b 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -74,6 +74,11 @@ module API
present options[:with].prepare_relation(projects, options), options
end
+
+ def translate_params_for_compatibility(params)
+ params[:builds_enabled] = params.delete(:jobs_enabled) if params.key?(:jobs_enabled)
+ params
+ end
end
resource :users, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
@@ -123,7 +128,7 @@ module API
end
post do
attrs = declared_params(include_missing: false)
- attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.key?(:jobs_enabled)
+ attrs = translate_params_for_compatibility(attrs)
project = ::Projects::CreateService.new(current_user, attrs).execute
if project.saved?
@@ -155,6 +160,7 @@ module API
not_found!('User') unless user
attrs = declared_params(include_missing: false)
+ attrs = translate_params_for_compatibility(attrs)
project = ::Projects::CreateService.new(user, attrs).execute
if project.saved?
@@ -276,7 +282,7 @@ module API
authorize! :rename_project, user_project if attrs[:name].present?
authorize! :change_visibility_level, user_project if attrs[:visibility].present?
- attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.key?(:jobs_enabled)
+ attrs = translate_params_for_compatibility(attrs)
result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
@@ -338,6 +344,11 @@ module API
end
end
+ desc 'Get languages in project repository'
+ get ':id/languages' do
+ user_project.repository.languages.map { |language| language.values_at(:label, :value) }.to_h
+ end
+
desc 'Remove a project'
delete ":id" do
authorize! :remove_project, user_project
@@ -397,7 +408,7 @@ module API
end
unless user_project.allowed_to_share_with_group?
- return render_api_error!("The project sharing with group is disabled", 400)
+ break render_api_error!("The project sharing with group is disabled", 400)
end
link = user_project.project_group_links.new(declared_params(include_missing: false))
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 2396dc73f0e..bb3fa99af38 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -111,8 +111,8 @@ module API
end
params do
use :pagination
- optional :order_by, type: String, values: %w[email name commits], default: nil, desc: 'Return contributors ordered by `name` or `email` or `commits`'
- optional :sort, type: String, values: %w[asc desc], default: nil, desc: 'Sort by asc (ascending) or desc (descending)'
+ optional :order_by, type: String, values: %w[email name commits], default: 'commits', desc: 'Return contributors ordered by `name` or `email` or `commits`'
+ optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
end
get ':id/repository/contributors' do
begin
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 60aeb69e10a..4d4fbe50f9f 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -29,7 +29,7 @@ module API
project.runners.create(attributes)
end
- return forbidden! unless runner
+ break forbidden! unless runner
if runner.id
present runner, with: Entities::RunnerRegistrationDetails
@@ -83,7 +83,7 @@ module API
if current_runner.runner_queue_value_latest?(params[:last_update])
header 'X-GitLab-Last-Update', params[:last_update]
Gitlab::Metrics.add_event(:build_not_found_cached)
- return no_content!
+ break no_content!
end
new_update = current_runner.ensure_runner_queue_value
@@ -152,7 +152,7 @@ module API
stream_size = job.trace.append(request.body.read, content_range[0].to_i)
if stream_size < 0
- return error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{-stream_size}" })
+ break error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{-stream_size}" })
end
status 202
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index c736cc32021..b30305b4bc9 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -94,7 +94,7 @@ module API
end
put ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
- return not_found!('Snippet') unless snippet
+ break not_found!('Snippet') unless snippet
authorize! :update_personal_snippet, snippet
@@ -120,7 +120,7 @@ module API
end
delete ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
- return not_found!('Snippet') unless snippet
+ break not_found!('Snippet') unless snippet
authorize! :destroy_personal_snippet, snippet
@@ -135,7 +135,7 @@ module API
end
get ":id/raw" do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
- return not_found!('Snippet') unless snippet
+ break not_found!('Snippet') unless snippet
env['api.format'] = :txt
content_type 'text/plain'
@@ -153,7 +153,7 @@ module API
snippet = Snippet.find_by!(id: params[:id])
- return not_found!('UserAgentDetail') unless snippet.user_agent_detail
+ break not_found!('UserAgentDetail') unless snippet.user_agent_detail
present snippet.user_agent_detail, with: Entities::UserAgentDetail
end
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index b3709455bc3..b29e660c6e0 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -62,7 +62,7 @@ module API
authorize! :admin_build, user_project
trigger = user_project.triggers.find(params.delete(:trigger_id))
- return not_found!('Trigger') unless trigger
+ break not_found!('Trigger') unless trigger
present trigger, with: Entities::Trigger
end
@@ -99,7 +99,7 @@ module API
authorize! :admin_build, user_project
trigger = user_project.triggers.find(params.delete(:trigger_id))
- return not_found!('Trigger') unless trigger
+ break not_found!('Trigger') unless trigger
if trigger.update(declared_params(include_missing: false))
present trigger, with: Entities::Trigger
@@ -119,7 +119,7 @@ module API
authorize! :admin_build, user_project
trigger = user_project.triggers.find(params.delete(:trigger_id))
- return not_found!('Trigger') unless trigger
+ break not_found!('Trigger') unless trigger
if trigger.update(owner: current_user)
status :ok
@@ -140,7 +140,7 @@ module API
authorize! :admin_build, user_project
trigger = user_project.triggers.find(params.delete(:trigger_id))
- return not_found!('Trigger') unless trigger
+ break not_found!('Trigger') unless trigger
destroy_conditionally!(trigger)
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 3920171205f..14b8a796c8e 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -77,7 +77,7 @@ module API
authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?)
unless current_user&.admin?
- params.except!(:created_after, :created_before, :order_by, :sort)
+ params.except!(:created_after, :created_before, :order_by, :sort, :two_factor)
end
users = UsersFinder.new(current_user, params).execute
diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb
index 683b9c993cb..b49448e1e67 100644
--- a/lib/api/v3/builds.rb
+++ b/lib/api/v3/builds.rb
@@ -51,7 +51,7 @@ module API
get ':id/repository/commits/:sha/builds' do
authorize_read_builds!
- return not_found! unless user_project.commit(params[:sha])
+ break not_found! unless user_project.commit(params[:sha])
pipelines = user_project.pipelines.where(sha: params[:sha])
builds = user_project.builds.where(pipeline: pipelines).order('id DESC')
@@ -153,7 +153,7 @@ module API
build = get_build!(params[:build_id])
authorize!(:update_build, build)
- return forbidden!('Build is not retryable') unless build.retryable?
+ break forbidden!('Build is not retryable') unless build.retryable?
build = Ci::Build.retry(build, current_user)
@@ -171,7 +171,7 @@ module API
build = get_build!(params[:build_id])
authorize!(:erase_build, build)
- return forbidden!('Build is not erasable!') unless build.erasable?
+ break forbidden!('Build is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
present build, with: ::API::V3::Entities::Build
@@ -188,7 +188,7 @@ module API
build = get_build!(params[:build_id])
authorize!(:update_build, build)
- return not_found!(build) unless build.artifacts?
+ break not_found!(build) unless build.artifacts?
build.keep_artifacts!
diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb
index ce216497996..9b0f70e2bfe 100644
--- a/lib/api/v3/merge_requests.rb
+++ b/lib/api/v3/merge_requests.rb
@@ -93,7 +93,7 @@ module API
post ":id/merge_requests" do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42126')
- authorize! :create_merge_request, user_project
+ authorize! :create_merge_request_from, user_project
mr_params = declared_params(include_missing: false)
mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb
index a2df969d819..eb3dd113524 100644
--- a/lib/api/v3/projects.rb
+++ b/lib/api/v3/projects.rb
@@ -423,7 +423,7 @@ module API
end
unless user_project.allowed_to_share_with_group?
- return render_api_error!("The project sharing with group is disabled", 400)
+ break render_api_error!("The project sharing with group is disabled", 400)
end
link = user_project.project_group_links.new(declared_params(include_missing: false))
diff --git a/lib/api/v3/snippets.rb b/lib/api/v3/snippets.rb
index 85613c8ed84..1df8a20e74a 100644
--- a/lib/api/v3/snippets.rb
+++ b/lib/api/v3/snippets.rb
@@ -90,7 +90,7 @@ module API
end
put ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
- return not_found!('Snippet') unless snippet
+ break not_found!('Snippet') unless snippet
authorize! :update_personal_snippet, snippet
@@ -114,7 +114,7 @@ module API
end
delete ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
- return not_found!('Snippet') unless snippet
+ break not_found!('Snippet') unless snippet
authorize! :destroy_personal_snippet, snippet
snippet.destroy
@@ -129,7 +129,7 @@ module API
end
get ":id/raw" do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
- return not_found!('Snippet') unless snippet
+ break not_found!('Snippet') unless snippet
env['api.format'] = :txt
content_type 'text/plain'
diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb
index 34f07dfb486..969bb2a05de 100644
--- a/lib/api/v3/triggers.rb
+++ b/lib/api/v3/triggers.rb
@@ -72,7 +72,7 @@ module API
authorize! :admin_build, user_project
trigger = user_project.triggers.find_by(token: params[:token].to_s)
- return not_found!('Trigger') unless trigger
+ break not_found!('Trigger') unless trigger
present trigger, with: ::API::V3::Entities::Trigger
end
@@ -100,7 +100,7 @@ module API
authorize! :admin_build, user_project
trigger = user_project.triggers.find_by(token: params[:token].to_s)
- return not_found!('Trigger') unless trigger
+ break not_found!('Trigger') unless trigger
trigger.destroy
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index d08876ae1b9..a34de9410e8 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -31,7 +31,7 @@ module API
key = params[:key]
variable = user_project.variables.find_by(key: key)
- return not_found!('Variable') unless variable
+ break not_found!('Variable') unless variable
present variable, with: Entities::Variable
end
@@ -67,7 +67,7 @@ module API
put ':id/variables/:key' do
variable = user_project.variables.find_by(key: params[:key])
- return not_found!('Variable') unless variable
+ break not_found!('Variable') unless variable
variable_params = declared_params(include_missing: false).except(:key)
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index 88cb7e7b5a4..9895db9e451 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -53,6 +53,8 @@ module Backup
FileUtils.mv(files, timestamped_files_path)
rescue Errno::EACCES
access_denied_error(app_files_dir)
+ rescue Errno::EBUSY
+ resource_busy_error(app_files_dir)
end
end
end
diff --git a/lib/backup/helper.rb b/lib/backup/helper.rb
index a1ee0faefe9..54b9ce10b4d 100644
--- a/lib/backup/helper.rb
+++ b/lib/backup/helper.rb
@@ -13,5 +13,19 @@ module Backup
EOS
raise message
end
+
+ def resource_busy_error(path)
+ message = <<~EOS
+
+ ### NOTICE ###
+ As part of restore, the task tried to rename `#{path}` before restoring.
+ This could not be completed, perhaps `#{path}` is a mountpoint?
+
+ To complete the restore, please move the contents of `#{path}` to a
+ different location and run the restore task again.
+
+ EOS
+ raise message
+ end
end
end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 89e3f1d9076..65e06fd78c0 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -81,6 +81,8 @@ module Backup
FileUtils.mv(files, bk_repos_path)
rescue Errno::EACCES
access_denied_error(path)
+ rescue Errno::EBUSY
+ resource_busy_error(path)
end
end
end
diff --git a/lib/banzai/commit_renderer.rb b/lib/banzai/commit_renderer.rb
index f5ff95e3eb3..c351a155ae5 100644
--- a/lib/banzai/commit_renderer.rb
+++ b/lib/banzai/commit_renderer.rb
@@ -3,7 +3,7 @@ module Banzai
ATTRIBUTES = [:description, :title].freeze
def self.render(commits, project, user = nil)
- obj_renderer = ObjectRenderer.new(project, user)
+ obj_renderer = ObjectRenderer.new(user: user, default_project: project)
ATTRIBUTES.each { |attr| obj_renderer.render(commits, attr) }
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index a848154b2d4..60a12dca9d3 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -56,29 +56,29 @@ module Banzai
# Implement in child class
# Example: project.merge_requests.find
- def find_object(project, id)
+ def find_object(parent_object, id)
end
# Override if the link reference pattern produces a different ID (global
# ID vs internal ID, for instance) to the regular reference pattern.
- def find_object_from_link(project, id)
- find_object(project, id)
+ def find_object_from_link(parent_object, id)
+ find_object(parent_object, id)
end
# Implement in child class
# Example: project_merge_request_url
- def url_for_object(object, project)
+ def url_for_object(object, parent_object)
end
- def find_object_cached(project, id)
- cached_call(:banzai_find_object, id, path: [object_class, project.id]) do
- find_object(project, id)
+ def find_object_cached(parent_object, id)
+ cached_call(:banzai_find_object, id, path: [object_class, parent_object.id]) do
+ find_object(parent_object, id)
end
end
- def find_object_from_link_cached(project, id)
- cached_call(:banzai_find_object_from_link, id, path: [object_class, project.id]) do
- find_object_from_link(project, id)
+ def find_object_from_link_cached(parent_object, id)
+ cached_call(:banzai_find_object_from_link, id, path: [object_class, parent_object.id]) do
+ find_object_from_link(parent_object, id)
end
end
@@ -88,9 +88,9 @@ module Banzai
end
end
- def url_for_object_cached(object, project)
- cached_call(:banzai_url_for_object, object, path: [object_class, project.id]) do
- url_for_object(object, project)
+ def url_for_object_cached(object, parent_object)
+ cached_call(:banzai_url_for_object, object, path: [object_class, parent_object.id]) do
+ url_for_object(object, parent_object)
end
end
diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb
index 99fa2d9d8fb..01b3b0dafb9 100644
--- a/lib/banzai/filter/commit_range_reference_filter.rb
+++ b/lib/banzai/filter/commit_range_reference_filter.rb
@@ -23,6 +23,8 @@ module Banzai
end
def find_object(project, id)
+ return unless project.is_a?(Project)
+
range = CommitRange.new(id, project)
range.valid_commits? ? range : nil
diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb
index 43bf4fc6565..8cd92a1adba 100644
--- a/lib/banzai/filter/commit_reference_filter.rb
+++ b/lib/banzai/filter/commit_reference_filter.rb
@@ -17,6 +17,8 @@ module Banzai
end
def find_object(project, id)
+ return unless project.is_a?(Project)
+
if project && project.valid_repo?
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/43894
Gitlab::GitalyClient.allow_n_plus_1_calls { project.commit(id) }
diff --git a/lib/banzai/filter/issuable_state_filter.rb b/lib/banzai/filter/issuable_state_filter.rb
index 8f541dcfdb2..1a415232545 100644
--- a/lib/banzai/filter/issuable_state_filter.rb
+++ b/lib/banzai/filter/issuable_state_filter.rb
@@ -11,7 +11,8 @@ module Banzai
def call
return doc unless context[:issuable_state_filter_enabled]
- extractor = Banzai::IssuableExtractor.new(project, current_user)
+ context = RenderContext.new(project, current_user)
+ extractor = Banzai::IssuableExtractor.new(context)
issuables = extractor.extract([doc])
issuables.each do |node, issuable|
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index 1cbada818fb..a5f38046a43 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -8,8 +8,8 @@ module Banzai
Label
end
- def find_object(project, id)
- find_labels(project).find(id)
+ def find_object(parent_object, id)
+ find_labels(parent_object).find(id)
end
def self.references_in(text, pattern = Label.reference_pattern)
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
index 1a1d7dbeb3d..b144bd8cf54 100644
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -12,10 +12,14 @@ module Banzai
# 'regular' references, we need to use the global ID to disambiguate
# between group and project milestones.
def find_object(project, id)
+ return unless project.is_a?(Project)
+
find_milestone_with_finder(project, id: id)
end
def find_object_from_link(project, iid)
+ return unless project.is_a?(Project)
+
find_milestone_with_finder(project, iid: iid)
end
@@ -40,7 +44,7 @@ module Banzai
project_path = full_project_path(namespace_ref, project_ref)
project = parent_from_ref(project_path)
- return unless project
+ return unless project && project.is_a?(Project)
milestone_params = milestone_params(milestone_id, milestone_name)
diff --git a/lib/banzai/filter/redactor_filter.rb b/lib/banzai/filter/redactor_filter.rb
index 9f9882b3b40..caf11fe94c4 100644
--- a/lib/banzai/filter/redactor_filter.rb
+++ b/lib/banzai/filter/redactor_filter.rb
@@ -7,7 +7,11 @@ module Banzai
#
class RedactorFilter < HTML::Pipeline::Filter
def call
- Redactor.new(project, current_user).redact([doc]) unless context[:skip_redaction]
+ unless context[:skip_redaction]
+ context = RenderContext.new(project, current_user)
+
+ Redactor.new(context).redact([doc])
+ end
doc
end
diff --git a/lib/banzai/filter/snippet_reference_filter.rb b/lib/banzai/filter/snippet_reference_filter.rb
index 134a192c22b..881e10afb9f 100644
--- a/lib/banzai/filter/snippet_reference_filter.rb
+++ b/lib/banzai/filter/snippet_reference_filter.rb
@@ -12,6 +12,8 @@ module Banzai
end
def find_object(project, id)
+ return unless project.is_a?(Project)
+
project.snippets.find_by(id: id)
end
diff --git a/lib/banzai/issuable_extractor.rb b/lib/banzai/issuable_extractor.rb
index 49603d0b363..ae7dc71e7eb 100644
--- a/lib/banzai/issuable_extractor.rb
+++ b/lib/banzai/issuable_extractor.rb
@@ -12,11 +12,11 @@ module Banzai
[@data-reference-type="issue" or @data-reference-type="merge_request"]
).freeze
- attr_reader :project, :user
+ attr_reader :context
- def initialize(project, user)
- @project = project
- @user = user
+ # context - An instance of Banzai::RenderContext.
+ def initialize(context)
+ @context = context
end
# Returns Hash in the form { node => issuable_instance }
@@ -25,8 +25,10 @@ module Banzai
document.xpath(QUERY)
end
- issue_parser = Banzai::ReferenceParser::IssueParser.new(project, user)
- merge_request_parser = Banzai::ReferenceParser::MergeRequestParser.new(project, user)
+ issue_parser = Banzai::ReferenceParser::IssueParser.new(context)
+
+ merge_request_parser =
+ Banzai::ReferenceParser::MergeRequestParser.new(context)
issuables_for_nodes = issue_parser.records_for_nodes(nodes).merge(
merge_request_parser.records_for_nodes(nodes)
diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb
index 2691be81623..a176f1e261b 100644
--- a/lib/banzai/object_renderer.rb
+++ b/lib/banzai/object_renderer.rb
@@ -13,14 +13,13 @@ module Banzai
# As an example, rendering the attribute `note` would place the unredacted
# HTML into `note_html` and the redacted HTML into `redacted_note_html`.
class ObjectRenderer
- attr_reader :project, :user
+ attr_reader :context
- # project - A Project to use for redacting Markdown.
+ # default_project - A default Project to use for redacting Markdown.
# user - The user viewing the Markdown/HTML documents, if any.
# redaction_context - A Hash containing extra attributes to use during redaction
- def initialize(project, user = nil, redaction_context = {})
- @project = project
- @user = user
+ def initialize(default_project: nil, user: nil, redaction_context: {})
+ @context = RenderContext.new(default_project, user)
@redaction_context = base_context.merge(redaction_context)
end
@@ -48,17 +47,21 @@ module Banzai
pipeline = HTML::Pipeline.new([])
objects.map do |object|
- pipeline.to_document(Banzai.render_field(object, attribute))
+ document = pipeline.to_document(Banzai.render_field(object, attribute))
+
+ context.associate_document(document, object)
+
+ document
end
end
def post_process_documents(documents, objects, attribute)
# Called here to populate cache, refer to IssuableExtractor docs
- IssuableExtractor.new(project, user).extract(documents)
+ IssuableExtractor.new(context).extract(documents)
documents.zip(objects).map do |document, object|
- context = context_for(object, attribute)
- Banzai::Pipeline[:post_process].to_document(document, context)
+ pipeline_context = context_for(document, object, attribute)
+ Banzai::Pipeline[:post_process].to_document(document, pipeline_context)
end
end
@@ -66,20 +69,21 @@ module Banzai
#
# Returns an Array containing the redacted documents.
def redact_documents(documents)
- redactor = Redactor.new(project, user)
+ redactor = Redactor.new(context)
redactor.redact(documents)
end
# Returns a Banzai context for the given object and attribute.
- def context_for(object, attribute)
- @redaction_context.merge(object.banzai_render_context(attribute))
+ def context_for(document, object, attribute)
+ @redaction_context.merge(object.banzai_render_context(attribute)).merge(
+ project: context.project_for_node(document)
+ )
end
def base_context
{
- current_user: user,
- project: project,
+ current_user: context.current_user,
skip_redaction: true
}
end
diff --git a/lib/banzai/redactor.rb b/lib/banzai/redactor.rb
index fd457bebf03..28928d6f376 100644
--- a/lib/banzai/redactor.rb
+++ b/lib/banzai/redactor.rb
@@ -2,13 +2,15 @@ module Banzai
# Class for removing Markdown references a certain user is not allowed to
# view.
class Redactor
- attr_reader :user, :project
+ attr_reader :context
- # project - A Project to use for redacting links.
- # user - The currently logged in user (if any).
- def initialize(project, user = nil)
- @project = project
- @user = user
+ # context - An instance of `Banzai::RenderContext`.
+ def initialize(context)
+ @context = context
+ end
+
+ def user
+ context.current_user
end
# Redacts the references in the given Array of documents.
@@ -70,11 +72,11 @@ module Banzai
end
def redact_cross_project_references(documents)
- extractor = Banzai::IssuableExtractor.new(project, user)
+ extractor = Banzai::IssuableExtractor.new(context)
issuables = extractor.extract(documents)
issuables.each do |node, issuable|
- next if issuable.project == project
+ next if issuable.project == context.project_for_node(node)
node['class'] = node['class'].gsub('has-tooltip', '')
node['title'] = nil
@@ -95,7 +97,7 @@ module Banzai
end
per_type.each do |type, nodes|
- parser = Banzai::ReferenceParser[type].new(project, user)
+ parser = Banzai::ReferenceParser[type].new(context)
visible.merge(parser.nodes_visible_to_user(user, nodes))
end
diff --git a/lib/banzai/reference_extractor.rb b/lib/banzai/reference_extractor.rb
index 7e6357f8a00..78588299c18 100644
--- a/lib/banzai/reference_extractor.rb
+++ b/lib/banzai/reference_extractor.rb
@@ -10,8 +10,8 @@ module Banzai
end
def references(type, project, current_user = nil)
- processor = Banzai::ReferenceParser[type]
- .new(project, current_user)
+ context = RenderContext.new(project, current_user)
+ processor = Banzai::ReferenceParser[type].new(context)
processor.process(html_documents)
end
diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb
index 279fca8d043..68752f5bb5a 100644
--- a/lib/banzai/reference_parser/base_parser.rb
+++ b/lib/banzai/reference_parser/base_parser.rb
@@ -45,9 +45,13 @@ module Banzai
@data_attribute ||= "data-#{reference_type.to_s.dasherize}"
end
- def initialize(project = nil, current_user = nil)
- @project = project
- @current_user = current_user
+ # context - An instance of `Banzai::RenderContext`.
+ def initialize(context)
+ @context = context
+ end
+
+ def project_for_node(node)
+ context.project_for_node(node)
end
# Returns all the nodes containing references that the user can refer to.
@@ -224,7 +228,11 @@ module Banzai
private
- attr_reader :current_user, :project
+ attr_reader :context
+
+ def current_user
+ context.current_user
+ end
# When a feature is disabled or visible only for
# team members we should not allow team members
diff --git a/lib/banzai/reference_parser/commit_range_parser.rb b/lib/banzai/reference_parser/commit_range_parser.rb
index a50e6f8ef8f..2920e886938 100644
--- a/lib/banzai/reference_parser/commit_range_parser.rb
+++ b/lib/banzai/reference_parser/commit_range_parser.rb
@@ -29,6 +29,8 @@ module Banzai
end
def find_object(project, id)
+ return unless project.is_a?(Project)
+
range = CommitRange.new(id, project)
range.valid_commits? ? range : nil
diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb
index 230827129b6..6bee5ea15b9 100644
--- a/lib/banzai/reference_parser/issue_parser.rb
+++ b/lib/banzai/reference_parser/issue_parser.rb
@@ -5,15 +5,10 @@ module Banzai
def nodes_visible_to_user(user, nodes)
issues = records_for_nodes(nodes)
- issues_to_check = issues.values
+ issues_to_check, cross_project_issues = partition_issues(issues, user)
- unless can?(user, :read_cross_project)
- issues_to_check, cross_project_issues = issues_to_check.partition do |issue|
- issue.project == project
- end
- end
-
- readable_issues = Ability.issues_readable_by_user(issues_to_check, user).to_set
+ readable_issues =
+ Ability.issues_readable_by_user(issues_to_check, user).to_set
nodes.select do |node|
issue_in_node = issues[node]
@@ -25,7 +20,7 @@ module Banzai
# but not the issue.
if readable_issues.include?(issue_in_node)
true
- elsif cross_project_issues&.include?(issue_in_node)
+ elsif cross_project_issues.include?(issue_in_node)
can_read_reference?(user, issue_in_node)
else
false
@@ -33,6 +28,32 @@ module Banzai
end
end
+ # issues - A Hash mapping HTML nodes to their corresponding Issue
+ # instances.
+ # user - The current User.
+ def partition_issues(issues, user)
+ return [issues.values, []] if can?(user, :read_cross_project)
+
+ issues_to_check = []
+ cross_project_issues = []
+
+ # We manually partition the data since our input is a Hash and our
+ # output has to be an Array of issues; not an Array of (node, issue)
+ # pairs.
+ issues.each do |node, issue|
+ target =
+ if issue.project == project_for_node(node)
+ issues_to_check
+ else
+ cross_project_issues
+ end
+
+ target << issue
+ end
+
+ [issues_to_check, cross_project_issues]
+ end
+
def records_for_nodes(nodes)
@issues_for_nodes ||= grouped_objects_for_nodes(
nodes,
diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb
index 8932d4f2905..ceb7f1d165c 100644
--- a/lib/banzai/reference_parser/user_parser.rb
+++ b/lib/banzai/reference_parser/user_parser.rb
@@ -58,7 +58,7 @@ module Banzai
def can_read_project_reference?(node)
node_id = node.attr('data-project').to_i
- project && project.id == node_id
+ project_for_node(node)&.id == node_id
end
def nodes_user_can_reference(current_user, nodes)
@@ -71,6 +71,7 @@ module Banzai
nodes.select do |node|
project_id = node.attr(project_attr)
user_id = node.attr(author_attr)
+ project = project_for_node(node)
if project && project_id && project.id == project_id.to_i
true
diff --git a/lib/banzai/render_context.rb b/lib/banzai/render_context.rb
new file mode 100644
index 00000000000..e30fc9f469b
--- /dev/null
+++ b/lib/banzai/render_context.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Banzai
+ # Object storing the current user, project, and other details used when
+ # parsing Markdown references.
+ class RenderContext
+ attr_reader :current_user
+
+ # default_project - The default project to use for all documents, if any.
+ # current_user - The user viewing the document, if any.
+ def initialize(default_project = nil, current_user = nil)
+ @current_user = current_user
+ @projects = Hash.new(default_project)
+ end
+
+ # Associates an HTML document with a Project.
+ #
+ # document - The HTML document to map to a Project.
+ # object - The object that produced the HTML document.
+ def associate_document(document, object)
+ # XML nodes respond to "document" but will return a Document instance,
+ # even when they belong to a DocumentFragment.
+ document = document.document if document.fragment?
+
+ @projects[document] = object.project if object.respond_to?(:project)
+ end
+
+ def project_for_node(node)
+ @projects[node.document]
+ end
+ end
+end
diff --git a/lib/banzai/renderer/common_mark/html.rb b/lib/banzai/renderer/common_mark/html.rb
index c7a54629f31..46b609c36b0 100644
--- a/lib/banzai/renderer/common_mark/html.rb
+++ b/lib/banzai/renderer/common_mark/html.rb
@@ -9,7 +9,7 @@ module Banzai
lang_attr = lang.present? ? %Q{ lang="#{lang}"} : ''
result =
"<pre>" \
- "<code#{lang_attr}>#{html_escape(code)}</code>" \
+ "<code#{lang_attr}>#{ERB::Util.html_escape(code)}</code>" \
"</pre>"
out(result)
diff --git a/lib/banzai/renderer/redcarpet/html.rb b/lib/banzai/renderer/redcarpet/html.rb
index 94df5d8b1e1..30e815f1224 100644
--- a/lib/banzai/renderer/redcarpet/html.rb
+++ b/lib/banzai/renderer/redcarpet/html.rb
@@ -6,7 +6,7 @@ module Banzai
lang_attr = lang ? %Q{ lang="#{lang}"} : ''
"\n<pre>" \
- "<code#{lang_attr}>#{html_escape(code)}</code>" \
+ "<code#{lang_attr}>#{ERB::Util.html_escape(code)}</code>" \
"</pre>"
end
end
diff --git a/lib/declarative_policy/runner.rb b/lib/declarative_policy/runner.rb
index 77c91817382..87f14b3b0d2 100644
--- a/lib/declarative_policy/runner.rb
+++ b/lib/declarative_policy/runner.rb
@@ -77,7 +77,7 @@ module DeclarativePolicy
@state = State.new
steps_by_score do |step, score|
- return if !debug && @state.prevented?
+ break if !debug && @state.prevented?
passed = nil
case step.action
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index aa9fd36d9ff..c5498d0da1a 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -1,15 +1,34 @@
-require_dependency 'gitlab/git'
+require_dependency 'gitlab/popen'
module Gitlab
+ def self.root
+ Pathname.new(File.expand_path('..', __dir__))
+ end
+
+ def self.config
+ Settings
+ end
+
COM_URL = 'https://gitlab.com'.freeze
APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))}
+ SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}
+ VERSION = File.read(root.join("VERSION")).strip.freeze
+ REVISION = Gitlab::Popen.popen(%W(#{config.git.bin_path} log --pretty=format:%h -n 1)).first.chomp.freeze
def self.com?
- # Check `staging?` as well to keep parity with gitlab.com
- Gitlab.config.gitlab.url == COM_URL || staging?
+ # Check `gl_subdomain?` as well to keep parity with gitlab.com
+ Gitlab.config.gitlab.url == COM_URL || gl_subdomain?
+ end
+
+ def self.org?
+ Gitlab.config.gitlab.url == 'https://dev.gitlab.org'
+ end
+
+ def self.gl_subdomain?
+ SUBDOMAIN_REGEX === Gitlab.config.gitlab.url
end
- def self.staging?
- Gitlab.config.gitlab.url == 'https://staging.gitlab.com'
+ def self.dev_env_or_com?
+ Rails.env.development? || org? || com?
end
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 2a44e11efb6..8e5a985edd7 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -51,7 +51,7 @@ module Gitlab
Gitlab::Auth::UniqueIpsLimiter.limit_user! do
user = User.by_login(login)
- return if user && !user.active?
+ break if user && !user.active?
authenticators = []
diff --git a/lib/gitlab/auth/ldap/user.rb b/lib/gitlab/auth/ldap/user.rb
index 068212d9a21..922d0567d99 100644
--- a/lib/gitlab/auth/ldap/user.rb
+++ b/lib/gitlab/auth/ldap/user.rb
@@ -8,6 +8,8 @@ module Gitlab
module Auth
module LDAP
class User < Gitlab::Auth::OAuth::User
+ extend ::Gitlab::Utils::Override
+
class << self
def find_by_uid_and_provider(uid, provider)
identity = ::Identity.with_extern_uid(provider, uid).take
@@ -29,7 +31,8 @@ module Gitlab
self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider)
end
- def changed?
+ override :should_save?
+ def should_save?
gl_user.changed? || gl_user.identities.any?(&:changed?)
end
@@ -41,6 +44,10 @@ module Gitlab
Gitlab::Auth::LDAP::Access.allowed?(gl_user)
end
+ def valid_sign_in?
+ allowed? && super
+ end
+
def ldap_config
Gitlab::Auth::LDAP::Config.new(auth_hash.provider)
end
diff --git a/lib/gitlab/auth/o_auth/identity_linker.rb b/lib/gitlab/auth/o_auth/identity_linker.rb
new file mode 100644
index 00000000000..de92d7a214d
--- /dev/null
+++ b/lib/gitlab/auth/o_auth/identity_linker.rb
@@ -0,0 +1,8 @@
+module Gitlab
+ module Auth
+ module OAuth
+ class IdentityLinker < OmniauthIdentityLinkerBase
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb
index d0c6b0386ba..6c5d0788a0a 100644
--- a/lib/gitlab/auth/o_auth/user.rb
+++ b/lib/gitlab/auth/o_auth/user.rb
@@ -30,6 +30,10 @@ module Gitlab
gl_user.try(:valid?)
end
+ def valid_sign_in?
+ valid? && persisted?
+ end
+
def save(provider = 'OAuth')
raise SigninDisabledForProviderError if oauth_provider_disabled?
raise SignupDisabledError unless gl_user
@@ -64,8 +68,18 @@ module Gitlab
user
end
+ def find_and_update!
+ save if should_save?
+
+ gl_user
+ end
+
protected
+ def should_save?
+ true
+ end
+
def add_or_update_user_identities
return unless gl_user
diff --git a/lib/gitlab/auth/omniauth_identity_linker_base.rb b/lib/gitlab/auth/omniauth_identity_linker_base.rb
new file mode 100644
index 00000000000..ae365fcdfaa
--- /dev/null
+++ b/lib/gitlab/auth/omniauth_identity_linker_base.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module Auth
+ class OmniauthIdentityLinkerBase
+ attr_reader :current_user, :oauth
+
+ def initialize(current_user, oauth)
+ @current_user = current_user
+ @oauth = oauth
+ @changed = false
+ end
+
+ def link
+ save if identity.new_record?
+ end
+
+ def changed?
+ @changed
+ end
+
+ def error_message
+ identity.validate
+
+ identity.errors.full_messages.join(', ')
+ end
+
+ private
+
+ def save
+ @changed = identity.save
+ end
+
+ def identity
+ @identity ||= current_user.identities
+ .with_extern_uid(provider, uid)
+ .first_or_initialize(extern_uid: uid)
+ end
+
+ def provider
+ oauth['provider']
+ end
+
+ def uid
+ oauth['uid']
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/saml/identity_linker.rb b/lib/gitlab/auth/saml/identity_linker.rb
new file mode 100644
index 00000000000..7e4b191d512
--- /dev/null
+++ b/lib/gitlab/auth/saml/identity_linker.rb
@@ -0,0 +1,8 @@
+module Gitlab
+ module Auth
+ module Saml
+ class IdentityLinker < OmniauthIdentityLinkerBase
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/saml/user.rb b/lib/gitlab/auth/saml/user.rb
index d4024e9ec39..cb01cd8004c 100644
--- a/lib/gitlab/auth/saml/user.rb
+++ b/lib/gitlab/auth/saml/user.rb
@@ -7,6 +7,8 @@ module Gitlab
module Auth
module Saml
class User < Gitlab::Auth::OAuth::User
+ extend ::Gitlab::Utils::Override
+
def save
super('SAML')
end
@@ -21,13 +23,14 @@ module Gitlab
if external_users_enabled? && user
# Check if there is overlap between the user's groups and the external groups
# setting then set user as external or internal.
- user.external = !(auth_hash.groups & Gitlab::Auth::Saml::Config.external_groups).empty?
+ user.external = !(auth_hash.groups & saml_config.external_groups).empty?
end
user
end
- def changed?
+ override :should_save?
+ def should_save?
return true unless gl_user
gl_user.changed? || gl_user.identities.any?(&:changed?)
@@ -35,12 +38,16 @@ module Gitlab
protected
+ def saml_config
+ Gitlab::Auth::Saml::Config
+ end
+
def auto_link_saml_user?
Gitlab.config.omniauth.auto_link_saml_user
end
def external_users_enabled?
- !Gitlab::Auth::Saml::Config.external_groups.nil?
+ !saml_config.external_groups.nil?
end
def auth_hash=(auth_hash)
diff --git a/lib/gitlab/bare_repository_import/importer.rb b/lib/gitlab/bare_repository_import/importer.rb
index 1a25138e7d6..4ca5a78e068 100644
--- a/lib/gitlab/bare_repository_import/importer.rb
+++ b/lib/gitlab/bare_repository_import/importer.rb
@@ -75,10 +75,11 @@ module Gitlab
end
def mv_repo(project)
- FileUtils.mv(repo_path, File.join(project.repository_storage_path, project.disk_path + '.git'))
+ storage_path = storage_path_for_shard(project.repository_storage)
+ FileUtils.mv(repo_path, project.repository.path_to_repo)
if bare_repo.wiki_exists?
- FileUtils.mv(wiki_path, File.join(project.repository_storage_path, project.disk_path + '.wiki.git'))
+ FileUtils.mv(wiki_path, File.join(storage_path, project.disk_path + '.wiki.git'))
end
true
@@ -88,6 +89,10 @@ module Gitlab
false
end
+ def storage_path_for_shard(shard)
+ Gitlab.config.repositories.storages[shard].legacy_disk_path
+ end
+
def find_or_create_groups
return nil unless group_path.present?
diff --git a/lib/gitlab/cache/ci/project_pipeline_status.rb b/lib/gitlab/cache/ci/project_pipeline_status.rb
index dba37892863..add048d671e 100644
--- a/lib/gitlab/cache/ci/project_pipeline_status.rb
+++ b/lib/gitlab/cache/ci/project_pipeline_status.rb
@@ -40,7 +40,7 @@ module Gitlab
end
def self.cache_key_for_project(project)
- "projects/#{project.id}/pipeline_status"
+ "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:projects/#{project.id}/pipeline_status"
end
def self.update_for_pipeline(pipeline)
diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb
index c0c7c7f5b5d..c1fc70ac266 100644
--- a/lib/gitlab/ci/status/build/common.rb
+++ b/lib/gitlab/ci/status/build/common.rb
@@ -3,6 +3,14 @@ module Gitlab
module Status
module Build
module Common
+ def illustration
+ {
+ image: 'illustrations/skipped-job_empty.svg',
+ size: 'svg-430',
+ title: _('This job does not have a trace.')
+ }
+ end
+
def has_details?
can?(user, :read_build, subject)
end
diff --git a/lib/gitlab/ci/status/build/erased.rb b/lib/gitlab/ci/status/build/erased.rb
index 3a5113b16b6..495227c2ffb 100644
--- a/lib/gitlab/ci/status/build/erased.rb
+++ b/lib/gitlab/ci/status/build/erased.rb
@@ -5,7 +5,7 @@ module Gitlab
class Erased < Status::Extended
def illustration
{
- image: 'illustrations/skipped-job_empty.svg',
+ image: 'illustrations/erased-log_empty.svg',
size: 'svg-430',
title: _('Job has been erased')
}
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index cedf4171ab1..47b67930c6d 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -45,7 +45,7 @@ module Gitlab
def append(data, offset)
write do |stream|
current_length = stream.size
- return -current_length unless current_length == offset
+ break -current_length unless current_length == offset
data = job.hide_secrets(data)
stream.append(data, offset)
diff --git a/lib/gitlab/ci/trace/http_io.rb b/lib/gitlab/ci/trace/http_io.rb
index ac4308f4e2c..cff924e27ef 100644
--- a/lib/gitlab/ci/trace/http_io.rb
+++ b/lib/gitlab/ci/trace/http_io.rb
@@ -75,18 +75,28 @@ module Gitlab
end
end
- def read(length = nil)
+ def read(length = nil, outbuf = "")
out = ""
- until eof? || (length && out.length >= length)
+ length ||= size - tell
+
+ until length <= 0 || eof?
data = get_chunk
break if data.empty?
- out << data
- @tell += data.bytesize
+ chunk_bytes = [BUFFER_SIZE - chunk_offset, length].min
+ chunk_data = data.byteslice(0, chunk_bytes)
+
+ out << chunk_data
+ @tell += chunk_data.bytesize
+ length -= chunk_data.bytesize
end
- out = out[0, length] if length && out.length > length
+ # If outbuf is passed, we put the output into the buffer. This supports IO.copy_stream functionality
+ if outbuf
+ outbuf.slice!(0, outbuf.bytesize)
+ outbuf << out
+ end
out
end
@@ -158,7 +168,7 @@ module Gitlab
# Provider: GCS
# - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
# - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPOK 200
- @chunk_range ||= (chunk_start...(chunk_start + @chunk.length))
+ @chunk_range ||= (chunk_start...(chunk_start + @chunk.bytesize))
end
@chunk[chunk_offset..BUFFER_SIZE]
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index 54894a46077..187ad8b833a 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -10,7 +10,9 @@ module Gitlab
delegate :close, :tell, :seek, :size, :url, :truncate, to: :stream, allow_nil: true
- delegate :valid?, to: :stream, as: :present?, allow_nil: true
+ delegate :valid?, to: :stream, allow_nil: true
+
+ alias_method :present?, :valid?
def initialize
@stream = yield
@@ -85,7 +87,7 @@ module Gitlab
match = matches.flatten.last
coverage = match.gsub(/\d+(\.\d+)?/).first
- return coverage if coverage.present?
+ return coverage if coverage.present? # rubocop:disable Cop/AvoidReturnFromBlocks
end
nil
diff --git a/lib/gitlab/ci/variables/collection/item.rb b/lib/gitlab/ci/variables/collection/item.rb
index 23ed71db8b0..d00e5b07f95 100644
--- a/lib/gitlab/ci/variables/collection/item.rb
+++ b/lib/gitlab/ci/variables/collection/item.rb
@@ -3,12 +3,9 @@ module Gitlab
module Variables
class Collection
class Item
- def initialize(**options)
+ def initialize(key:, value:, public: true, file: false)
@variable = {
- key: options.fetch(:key),
- value: options.fetch(:value),
- public: options.fetch(:public, true),
- file: options.fetch(:files, false)
+ key: key, value: value, public: public, file: file
}
end
diff --git a/lib/gitlab/daemon.rb b/lib/gitlab/daemon.rb
index 633de9f9776..bd14c7eece3 100644
--- a/lib/gitlab/daemon.rb
+++ b/lib/gitlab/daemon.rb
@@ -30,7 +30,7 @@ module Gitlab
return unless enabled?
@mutex.synchronize do
- return thread if thread?
+ break thread if thread?
@thread = Thread.new { start_working }
end
@@ -38,7 +38,7 @@ module Gitlab
def stop
@mutex.synchronize do
- return unless thread?
+ break unless thread?
stop_working
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
index 05b86f32ce2..73971af6a74 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
@@ -62,21 +62,20 @@ module Gitlab
end
def move_repositories(namespace, old_full_path, new_full_path)
- repo_paths_for_namespace(namespace).each do |repository_storage_path|
+ repo_shards_for_namespace(namespace).each do |repository_storage|
# Ensure old directory exists before moving it
- gitlab_shell.add_namespace(repository_storage_path, old_full_path)
+ gitlab_shell.add_namespace(repository_storage, old_full_path)
- unless gitlab_shell.mv_namespace(repository_storage_path, old_full_path, new_full_path)
- message = "Exception moving path #{repository_storage_path} \
- from #{old_full_path} to #{new_full_path}"
+ unless gitlab_shell.mv_namespace(repository_storage, old_full_path, new_full_path)
+ message = "Exception moving on shard #{repository_storage} from #{old_full_path} to #{new_full_path}"
Rails.logger.error message
end
end
end
- def repo_paths_for_namespace(namespace)
+ def repo_shards_for_namespace(namespace)
projects_for_namespace(namespace).distinct.select(:repository_storage)
- .map(&:repository_storage_path)
+ .map(&:repository_storage)
end
def projects_for_namespace(namespace)
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 979225dd216..827aeb12a02 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
@@ -51,7 +51,7 @@ module Gitlab
end
def move_repository(project, old_path, new_path)
- unless gitlab_shell.mv_repository(project.repository_storage_path,
+ unless gitlab_shell.mv_repository(project.repository_storage,
old_path,
new_path)
Rails.logger.error "Error moving #{old_path} to #{new_path}"
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index 269016daac2..5c1baa19b66 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -33,10 +33,7 @@ module Gitlab
# match the blob, which is a bug. But we shouldn't fail to render
# completely in that case, even though we want to report the error.
rescue RangeError => e
- if Gitlab::Sentry.enabled?
- Gitlab::Sentry.context
- Raven.capture_exception(e)
- end
+ Gitlab::Sentry.track_exception(e, issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/45441')
end
end
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index 5fdd5dcd374..8cf59fa8e28 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -5,7 +5,7 @@ module Gitlab
CANONICAL_CE_PROJECT_URL = 'https://gitlab.com/gitlab-org/gitlab-ce'.freeze
CANONICAL_EE_REPO_URL = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
CHECK_DIR = Rails.root.join('ee_compat_check')
- IGNORED_FILES_REGEX = %r{VERSION|CHANGELOG\.md|db/schema\.rb}i.freeze
+ IGNORED_FILES_REGEX = %r{VERSION|CHANGELOG\.md|db/schema\.rb|locale/gitlab\.pot}i.freeze
PLEASE_READ_THIS_BANNER = %Q{
============================================================
===================== PLEASE READ THIS =====================
diff --git a/lib/gitlab/email/handler/create_merge_request_handler.rb b/lib/gitlab/email/handler/create_merge_request_handler.rb
index 3436306e122..2f864f2082b 100644
--- a/lib/gitlab/email/handler/create_merge_request_handler.rb
+++ b/lib/gitlab/email/handler/create_merge_request_handler.rb
@@ -23,7 +23,8 @@ module Gitlab
def execute
raise ProjectNotFound unless project
- validate_permission!(:create_merge_request)
+ validate_permission!(:create_merge_request_in)
+ validate_permission!(:create_merge_request_from)
verify_record!(
record: create_merge_request,
diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb
index 1d6f5bb5e1c..d5d35dbd97f 100644
--- a/lib/gitlab/etag_caching/middleware.rb
+++ b/lib/gitlab/etag_caching/middleware.rb
@@ -50,7 +50,7 @@ module Gitlab
status_code = Gitlab::PollingInterval.polling_enabled? ? 304 : 429
- [status_code, { 'ETag' => etag }, []]
+ [status_code, { 'ETag' => etag, 'X-Gitlab-From-Cache' => 'true' }, []]
end
def track_cache_miss(if_none_match, cached_value_present, route)
diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb
index 1b74f735679..b6eeb5d9a2b 100644
--- a/lib/gitlab/gfm/uploads_rewriter.rb
+++ b/lib/gitlab/gfm/uploads_rewriter.rb
@@ -21,7 +21,7 @@ module Gitlab
@text.gsub(@pattern) do |markdown|
file = find_file(@source_project, $~[:secret], $~[:file])
- return markdown unless file.try(:exists?)
+ break markdown unless file.try(:exists?)
new_uploader = FileUploader.new(target_project)
with_link_in_tmp_dir(file.file) do |open_tmp_file|
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index d4e893b881c..e85e87a54af 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -1,5 +1,11 @@
+require_dependency 'gitlab/encoding_helper'
+
module Gitlab
module Git
+ # 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
BLANK_SHA = ('0' * 40).freeze
TAG_REF_PREFIX = "refs/tags/".freeze
BRANCH_REF_PREFIX = "refs/heads/".freeze
diff --git a/lib/gitlab/git/attributes_parser.rb b/lib/gitlab/git/attributes_parser.rb
index d8aeabb6cba..08f4d7d4f5c 100644
--- a/lib/gitlab/git/attributes_parser.rb
+++ b/lib/gitlab/git/attributes_parser.rb
@@ -3,12 +3,8 @@ module Gitlab
# Class for parsing Git attribute files and extracting the attributes for
# file patterns.
class AttributesParser
- def initialize(attributes_data)
+ def initialize(attributes_data = "")
@data = attributes_data || ""
-
- if @data.is_a?(File)
- @patterns = parse_file
- end
end
# Returns all the Git attributes for the given path.
@@ -28,7 +24,7 @@ module Gitlab
# Returns a Hash containing the file patterns and their attributes.
def patterns
- @patterns ||= parse_file
+ @patterns ||= parse_data
end
# Parses an attribute string.
@@ -91,8 +87,8 @@ module Gitlab
private
- # Parses the Git attributes file.
- def parse_file
+ # Parses the Git attributes file contents.
+ def parse_data
pairs = []
comment = '#'
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 0fb82441bf8..fabcd46c8e9 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -486,6 +486,8 @@ module Gitlab
end
def tree_entry(path)
+ return unless path.present?
+
@repository.gitaly_migrate(:commit_tree_entry) do |is_migrated|
if is_migrated
gitaly_tree_entry(path)
diff --git a/lib/gitlab/git/committer_with_hooks.rb b/lib/gitlab/git/committer_with_hooks.rb
new file mode 100644
index 00000000000..a8a59f998cd
--- /dev/null
+++ b/lib/gitlab/git/committer_with_hooks.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module Git
+ class CommitterWithHooks < Gollum::Committer
+ attr_reader :gl_wiki
+
+ def initialize(gl_wiki, options = {})
+ @gl_wiki = gl_wiki
+ super(gl_wiki.gollum_wiki, options)
+ end
+
+ def commit
+ # TODO: Remove after 10.8
+ return super unless allowed_to_run_hooks?
+
+ result = Gitlab::Git::OperationService.new(git_user, gl_wiki.repository).with_branch(
+ @wiki.ref,
+ start_branch_name: @wiki.ref
+ ) do |start_commit|
+ super(false)
+ end
+
+ result[:newrev]
+ rescue Gitlab::Git::HooksService::PreReceiveError => e
+ message = "Custom Hook failed: #{e.message}"
+ raise Gitlab::Git::Wiki::OperationError, message
+ end
+
+ private
+
+ # TODO: Remove after 10.8
+ def allowed_to_run_hooks?
+ @options[:user_id] != 0 && @options[:username].present?
+ end
+
+ def git_user
+ @git_user ||= Gitlab::Git::User.new(@options[:username],
+ @options[:name],
+ @options[:email],
+ gitlab_id)
+ end
+
+ def gitlab_id
+ Gitlab::GlId.gl_id_from_id_value(@options[:user_id])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index a203587aec1..b58296375ef 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -249,7 +249,7 @@ module Gitlab
if size >= SIZE_LIMIT
too_large!
- return true
+ return true # rubocop:disable Cop/AvoidReturnFromBlocks
end
end
end
diff --git a/lib/gitlab/git/info_attributes.rb b/lib/gitlab/git/info_attributes.rb
deleted file mode 100644
index e79a440950b..00000000000
--- a/lib/gitlab/git/info_attributes.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# Gitaly note: JV: not sure what to make of this class. Why does it use
-# the full disk path of the repository to look up attributes This is
-# problematic in Gitaly, because Gitaly hides the full disk path to the
-# repository from gitlab-ce.
-
-module Gitlab
- module Git
- # Parses gitattributes at `$GIT_DIR/info/attributes`
- #
- # Unlike Rugged this parser only needs a single IO call (a call to `open`),
- # vastly reducing the time spent in extracting attributes.
- #
- # This class _only_ supports parsing the attributes file located at
- # `$GIT_DIR/info/attributes` as GitLab doesn't use any other files
- # (`.gitattributes` is copied to this particular path).
- #
- # Basic usage:
- #
- # attributes = Gitlab::Git::InfoAttributes.new(some_repo.path)
- #
- # attributes.attributes('README.md') # => { "eol" => "lf }
- class InfoAttributes
- delegate :attributes, :patterns, to: :parser
-
- # path - The path to the Git repository.
- def initialize(path)
- @repo_path = File.expand_path(path)
- end
-
- def parser
- @parser ||= begin
- if File.exist?(attributes_path)
- File.open(attributes_path, 'r') do |file_handle|
- AttributesParser.new(file_handle)
- end
- else
- AttributesParser.new("")
- end
- end
- end
-
- private
-
- def attributes_path
- @attributes_path ||= File.join(@repo_path, 'info/attributes')
- end
- end
- end
-end
diff --git a/lib/gitlab/git/popen.rb b/lib/gitlab/git/popen.rb
index c1767046ff0..f9f24ecc48d 100644
--- a/lib/gitlab/git/popen.rb
+++ b/lib/gitlab/git/popen.rb
@@ -25,7 +25,9 @@ module Gitlab
stdin.close
if lazy_block
- return [lazy_block.call(stdout.lazy), 0]
+ cmd_output = lazy_block.call(stdout.lazy)
+ cmd_status = 0
+ break
else
cmd_output << stdout.read
end
diff --git a/lib/gitlab/git/raw_diff_change.rb b/lib/gitlab/git/raw_diff_change.rb
new file mode 100644
index 00000000000..eb3d8819239
--- /dev/null
+++ b/lib/gitlab/git/raw_diff_change.rb
@@ -0,0 +1,60 @@
+module Gitlab
+ module Git
+ # This class behaves like a struct with fields :blob_id, :blob_size, :operation, :old_path, :new_path
+ # All those fields are (binary) strings or integers
+ class RawDiffChange
+ attr_reader :blob_id, :blob_size, :old_path, :new_path, :operation
+
+ def initialize(raw_change)
+ parse(raw_change)
+ end
+
+ private
+
+ # Input data has the following format:
+ #
+ # When a file has been modified:
+ # 7e3e39ebb9b2bf433b4ad17313770fbe4051649c 669 M\tfiles/ruby/popen.rb
+ #
+ # When a file has been renamed:
+ # 85bc2f9753afd5f4fc5d7c75f74f8d526f26b4f3 107 R060\tfiles/js/commit.js.coffee\tfiles/js/commit.coffee
+ def parse(raw_change)
+ @blob_id, @blob_size, @raw_operation, raw_paths = raw_change.split(' ', 4)
+ @operation = extract_operation
+ @old_path, @new_path = extract_paths(raw_paths)
+ end
+
+ def extract_paths(file_path)
+ case operation
+ when :renamed
+ file_path.split(/\t/)
+ when :deleted
+ [file_path, nil]
+ when :added
+ [nil, file_path]
+ else
+ [file_path, file_path]
+ end
+ end
+
+ def extract_operation
+ case @raw_operation&.first(1)
+ when 'A'
+ :added
+ when 'C'
+ :copied
+ when 'D'
+ :deleted
+ when 'M'
+ :modified
+ when 'R'
+ :renamed
+ when 'T'
+ :type_changed
+ else
+ :unknown
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index f1b575bd872..5a6e2e0b937 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -9,6 +9,7 @@ module Gitlab
include Gitlab::Git::RepositoryMirroring
include Gitlab::Git::Popen
include Gitlab::EncodingHelper
+ include Gitlab::Utils::StrongMemoize
ALLOWED_OBJECT_DIRECTORIES_VARIABLES = %w[
GIT_OBJECT_DIRECTORY
@@ -75,9 +76,6 @@ module Gitlab
end
end
- # Full path to repo
- attr_reader :path
-
# Directory name of repo
attr_reader :name
@@ -96,22 +94,26 @@ module Gitlab
@relative_path = relative_path
@gl_repository = gl_repository
- storage_path = Gitlab.config.repositories.storages[@storage].legacy_disk_path
@gitlab_projects = Gitlab::Git::GitlabProjects.new(
storage,
relative_path,
global_hooks_path: Gitlab.config.gitlab_shell.hooks_path,
logger: Rails.logger
)
- @path = File.join(storage_path, @relative_path)
+
@name = @relative_path.split("/").last
- @attributes = Gitlab::Git::InfoAttributes.new(path)
end
def ==(other)
path == other.path
end
+ def path
+ @path ||= File.join(
+ Gitlab.config.repositories.storages[@storage].legacy_disk_path, @relative_path
+ )
+ end
+
# Default branch in the repository
def root_ref
@root_ref ||= gitaly_migrate(:root_ref) do |is_enabled|
@@ -140,12 +142,12 @@ module Gitlab
end
def exists?
- Gitlab::GitalyClient.migrate(:repository_exists, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
+ Gitlab::GitalyClient.migrate(:repository_exists, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled
gitaly_repository_client.exists?
else
circuit_breaker.perform do
- File.exist?(File.join(@path, 'refs'))
+ File.exist?(File.join(path, 'refs'))
end
end
end
@@ -230,13 +232,13 @@ module Gitlab
end
end
+ def expire_has_local_branches_cache
+ clear_memoization(:has_local_branches)
+ end
+
def has_local_branches?
- gitaly_migrate(:has_local_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_repository_client.has_local_branches?
- else
- has_local_branches_rugged?
- end
+ strong_memoize(:has_local_branches) do
+ uncached_has_local_branches?
end
end
@@ -298,7 +300,8 @@ module Gitlab
#
# Ref names must start with `refs/`.
def ref_exists?(ref_name)
- gitaly_migrate(:ref_exists) do |is_enabled|
+ gitaly_migrate(:ref_exists,
+ status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_ref_exists?(ref_name)
else
@@ -559,6 +562,24 @@ module Gitlab
count_commits(from: from, to: to, **options)
end
+ # old_rev and new_rev are commit ID's
+ # the result of this method is an array of Gitlab::Git::RawDiffChange
+ def raw_changes_between(old_rev, new_rev)
+ result = []
+
+ circuit_breaker.perform do
+ Open3.pipeline_r(git_diff_cmd(old_rev, new_rev), format_git_cat_file_script, git_cat_file_cmd) do |last_stdout, wait_threads|
+ last_stdout.each_line { |line| result << ::Gitlab::Git::RawDiffChange.new(line.chomp!) }
+
+ if wait_threads.any? { |waiter| !waiter.value&.success? }
+ raise ::Gitlab::Git::Repository::GitError, "Unabled to obtain changes between #{old_rev} and #{new_rev}"
+ end
+ end
+ end
+
+ result
+ end
+
# Returns the SHA of the most recent common ancestor of +from+ and +to+
def merge_base(from, to)
gitaly_migrate(:merge_base) do |is_enabled|
@@ -993,11 +1014,32 @@ module Gitlab
raise InvalidRef
end
+ def info_attributes
+ return @info_attributes if @info_attributes
+
+ content =
+ gitaly_migrate(:get_info_attributes) do |is_enabled|
+ if is_enabled
+ gitaly_repository_client.info_attributes
+ else
+ attributes_path = File.join(File.expand_path(path), 'info', 'attributes')
+
+ if File.exist?(attributes_path)
+ File.read(attributes_path)
+ else
+ ""
+ end
+ end
+ end
+
+ @info_attributes = AttributesParser.new(content)
+ end
+
# Returns the Git attributes for the given file path.
#
# See `Gitlab::Git::Attributes` for more information.
def attributes(path)
- @attributes.attributes(path)
+ info_attributes.attributes(path)
end
def gitattribute(path, name)
@@ -1216,6 +1258,10 @@ module Gitlab
true
end
+ def create_from_snapshot(url, auth)
+ gitaly_repository_client.create_from_snapshot(url, auth)
+ end
+
def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
gitaly_migrate(:rebase) do |is_enabled|
if is_enabled
@@ -1385,7 +1431,8 @@ module Gitlab
end
def branch_names_contains_sha(sha)
- gitaly_migrate(:branch_names_contains_sha) do |is_enabled|
+ gitaly_migrate(:branch_names_contains_sha,
+ status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_ref_client.branch_names_contains_sha(sha)
else
@@ -1395,7 +1442,8 @@ module Gitlab
end
def tag_names_contains_sha(sha)
- gitaly_migrate(:tag_names_contains_sha) do |is_enabled|
+ gitaly_migrate(:tag_names_contains_sha,
+ status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_ref_client.tag_names_contains_sha(sha)
else
@@ -1428,7 +1476,7 @@ module Gitlab
return [] if empty? || safe_query.blank?
- args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{safe_query})
+ args = %W(ls-tree -r --name-status --full-tree #{ref || root_ref} -- #{safe_query})
run_git(args).first.lines.map(&:strip)
end
@@ -1517,6 +1565,16 @@ module Gitlab
private
+ def uncached_has_local_branches?
+ gitaly_migrate(:has_local_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
+ if is_enabled
+ gitaly_repository_client.has_local_branches?
+ else
+ has_local_branches_rugged?
+ end
+ end
+ end
+
def local_write_ref(ref_path, ref, old_ref: nil, shell: true)
if shell
shell_write_ref(ref_path, ref, old_ref)
@@ -2461,6 +2519,35 @@ module Gitlab
result.to_s(16)
end
+
+ def build_git_cmd(*args)
+ object_directories = alternate_object_directories.join(File::PATH_SEPARATOR)
+
+ env = { 'PWD' => self.path }
+ env['GIT_ALTERNATE_OBJECT_DIRECTORIES'] = object_directories if object_directories.present?
+
+ [
+ env,
+ ::Gitlab.config.git.bin_path,
+ *args,
+ { chdir: self.path }
+ ]
+ end
+
+ def git_diff_cmd(old_rev, new_rev)
+ old_rev = old_rev == ::Gitlab::Git::BLANK_SHA ? ::Gitlab::Git::EMPTY_TREE_ID : old_rev
+
+ build_git_cmd('diff', old_rev, new_rev, '--raw')
+ end
+
+ def git_cat_file_cmd
+ format = '%(objectname) %(objectsize) %(rest)'
+ build_git_cmd('cat-file', "--batch-check=#{format}")
+ end
+
+ def format_git_cat_file_script
+ File.expand_path('../support/format-git-cat-file-input', __FILE__)
+ end
end
end
end
diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb
index dc424a433fb..8a01f92e2af 100644
--- a/lib/gitlab/git/repository_mirroring.rb
+++ b/lib/gitlab/git/repository_mirroring.rb
@@ -26,7 +26,7 @@ module Gitlab
# 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 []
+ break []
end
name = path.split('/', 3).last
diff --git a/lib/gitlab/git/support/format-git-cat-file-input b/lib/gitlab/git/support/format-git-cat-file-input
new file mode 100755
index 00000000000..2e93c646d0f
--- /dev/null
+++ b/lib/gitlab/git/support/format-git-cat-file-input
@@ -0,0 +1,21 @@
+#!/usr/bin/env ruby
+
+# This script formats the output of the `git diff <old_rev> <new_rev> --raw`
+# command so it can be processed by `git cat-file`
+
+# We need to convert this:
+# ":100644 100644 5f53439... 85bc2f9... R060\tfiles/js/commit.js.coffee\tfiles/js/commit.coffee"
+# To:
+# "85bc2f9 R\tfiles/js/commit.js.coffee\tfiles/js/commit.coffee"
+
+ARGF.each do |line|
+ _, _, old_blob_id, new_blob_id, rest = line.split(/\s/, 5)
+
+ old_blob_id.gsub!(/[^\h]/, '')
+ new_blob_id.gsub!(/[^\h]/, '')
+
+ # We can't pass '0000000...' to `git cat-file` given it will not return info about the deleted file
+ blob_id = new_blob_id =~ /\A0+\z/ ? old_blob_id : new_blob_id
+
+ $stdout.puts "#{blob_id} #{rest}"
+end
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index 8d82820915d..84a26fe4a6f 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -2,10 +2,11 @@ module Gitlab
module Git
class Wiki
DuplicatePageError = Class.new(StandardError)
+ OperationError = Class.new(StandardError)
- CommitDetails = Struct.new(:name, :email, :message) do
+ CommitDetails = Struct.new(:user_id, :username, :name, :email, :message) do
def to_h
- { name: name, email: email, message: message }
+ { user_id: user_id, username: username, name: name, email: email, message: message }
end
end
PageBlob = Struct.new(:name)
@@ -140,6 +141,10 @@ module Gitlab
end
end
+ def gollum_wiki
+ @gollum_wiki ||= Gollum::Wiki.new(@repository.path)
+ end
+
private
# options:
@@ -158,10 +163,6 @@ module Gitlab
offset: options[:offset])
end
- def gollum_wiki
- @gollum_wiki ||= Gollum::Wiki.new(@repository.path)
- end
-
def gollum_page_by_path(page_path)
page_name = Gollum::Page.canonicalize_filename(page_path)
page_dir = File.split(page_path).first
@@ -201,12 +202,12 @@ module Gitlab
assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails)
- filename = File.basename(name)
- dir = (tmp_dir = File.dirname(name)) == '.' ? '' : tmp_dir
-
- gollum_wiki.write_page(filename, format, content, commit_details.to_h, dir)
+ with_committer_with_hooks(commit_details) do |committer|
+ filename = File.basename(name)
+ dir = (tmp_dir = File.dirname(name)) == '.' ? '' : tmp_dir
- nil
+ gollum_wiki.write_page(filename, format, content, { committer: committer }, dir)
+ end
rescue Gollum::DuplicatePageError => e
raise Gitlab::Git::Wiki::DuplicatePageError, e.message
end
@@ -214,24 +215,23 @@ module Gitlab
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
+ with_committer_with_hooks(commit_details) do |committer|
+ gollum_wiki.delete_page(gollum_page_by_path(page_path), committer: committer)
+ end
end
def gollum_update_page(page_path, title, format, content, commit_details)
assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails)
- page = gollum_page_by_path(page_path)
- committer = Gollum::Committer.new(page.wiki, commit_details.to_h)
-
- # Instead of performing two renames if the title has changed,
- # the update_page will only update the format and content and
- # the rename_page will do anything related to moving/renaming
- gollum_wiki.update_page(page, page.name, format, content, committer: committer)
- gollum_wiki.rename_page(page, title, committer: committer)
- committer.commit
- nil
+ with_committer_with_hooks(commit_details) do |committer|
+ page = gollum_page_by_path(page_path)
+ # Instead of performing two renames if the title has changed,
+ # the update_page will only update the format and content and
+ # the rename_page will do anything related to moving/renaming
+ gollum_wiki.update_page(page, page.name, format, content, committer: committer)
+ gollum_wiki.rename_page(page, title, committer: committer)
+ end
end
def gollum_find_page(title:, version: nil, dir: nil)
@@ -288,6 +288,20 @@ module Gitlab
Gitlab::Git::WikiPage.new(wiki_page, version)
end
end
+
+ def committer_with_hooks(commit_details)
+ Gitlab::Git::CommitterWithHooks.new(self, commit_details.to_h)
+ end
+
+ def with_committer_with_hooks(commit_details, &block)
+ committer = committer_with_hooks(commit_details)
+
+ yield committer
+
+ committer.commit
+
+ nil
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 456a8a1a2d6..a36e6c822f9 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -3,10 +3,6 @@ module Gitlab
class CommitService
include Gitlab::EncodingHelper
- # The ID of empty tree.
- # See http://stackoverflow.com/a/40884093/1856239 and https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012
- EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'.freeze
-
def initialize(repository)
@gitaly_repo = repository.gitaly_repository
@repository = repository
@@ -37,7 +33,7 @@ module Gitlab
def diff(from, to, options = {})
from_id = case from
when NilClass
- EMPTY_TREE_ID
+ Gitlab::Git::EMPTY_TREE_ID
else
if from.respond_to?(:oid)
# This is meant to match a Rugged::Commit. This should be impossible in
@@ -50,7 +46,7 @@ module Gitlab
to_id = case to
when NilClass
- EMPTY_TREE_ID
+ Gitlab::Git::EMPTY_TREE_ID
else
if to.respond_to?(:oid)
# This is meant to match a Rugged::Commit. This should be impossible in
@@ -352,7 +348,7 @@ module Gitlab
end
def diff_from_parent_request_params(commit, options = {})
- parent_id = commit.parent_ids.first || EMPTY_TREE_ID
+ parent_id = commit.parent_ids.first || Gitlab::Git::EMPTY_TREE_ID
diff_between_commits_request_params(parent_id, commit.id, options)
end
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 6441065f5fe..bf5a491e28d 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -50,6 +50,15 @@ module Gitlab
GitalyClient.call(@storage, :repository_service, :apply_gitattributes, request)
end
+ def info_attributes
+ request = Gitaly::GetInfoAttributesRequest.new(repository: @gitaly_repo)
+
+ response = GitalyClient.call(@storage, :repository_service, :get_info_attributes, request)
+ response.each_with_object("") do |message, attributes|
+ attributes << message.attributes
+ end
+ end
+
def fetch_remote(remote, ssh_auth:, forced:, no_tags:, timeout:, prune: true)
request = Gitaly::FetchRemoteRequest.new(
repository: @gitaly_repo, remote: remote, force: forced,
@@ -226,6 +235,22 @@ module Gitlab
)
end
+ def create_from_snapshot(http_url, http_auth)
+ request = Gitaly::CreateRepositoryFromSnapshotRequest.new(
+ repository: @gitaly_repo,
+ http_url: http_url,
+ http_auth: http_auth
+ )
+
+ GitalyClient.call(
+ @storage,
+ :repository_service,
+ :create_repository_from_snapshot,
+ request,
+ timeout: GitalyClient.default_timeout
+ )
+ end
+
def write_ref(ref_path, ref, old_ref, shell)
request = Gitaly::WriteRefRequest.new(
repository: @gitaly_repo,
diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb
index 0d8dd5cb8f4..2dfe055a496 100644
--- a/lib/gitlab/gitaly_client/wiki_service.rb
+++ b/lib/gitlab/gitaly_client/wiki_service.rb
@@ -136,7 +136,7 @@ module Gitlab
wiki_file = nil
response.each do |message|
- next unless message.name.present?
+ next unless message.name.present? || wiki_file
if wiki_file
wiki_file.raw_data << message.raw_data
@@ -200,6 +200,8 @@ module Gitlab
def gitaly_commit_details(commit_details)
Gitaly::WikiCommitDetails.new(
+ user_id: commit_details.user_id,
+ user_name: encode_binary(commit_details.username),
name: encode_binary(commit_details.name),
email: encode_binary(commit_details.email),
message: encode_binary(commit_details.message)
diff --git a/lib/gitlab/gl_id.rb b/lib/gitlab/gl_id.rb
index 624fd00367e..a53d156b41f 100644
--- a/lib/gitlab/gl_id.rb
+++ b/lib/gitlab/gl_id.rb
@@ -2,10 +2,14 @@ module Gitlab
module GlId
def self.gl_id(user)
if user.present?
- "user-#{user.id}"
+ gl_id_from_id_value(user.id)
else
- ""
+ ''
end
end
+
+ def self.gl_id_from_id_value(id)
+ "user-#{id}"
+ end
end
end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index af203ff711d..b713fa7e1cd 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -3,7 +3,7 @@ module Gitlab
extend self
# For every version update, the version history in import_export.md has to be kept up to date.
- VERSION = '0.2.2'.freeze
+ VERSION = '0.2.3'.freeze
FILENAME_LIMIT = 50
def export_path(relative_path:)
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index cd840bd5b01..0d1c4f73c6e 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -27,8 +27,6 @@ project_tree:
- :releases
- project_members:
- :user
- - lfs_file_locks:
- - :user
- merge_requests:
- notes:
- :author
@@ -66,6 +64,7 @@ project_tree:
- :project_feature
- :custom_attributes
- :project_badges
+ - :ci_cd_settings
# Only include the following attributes for the models specified.
included_attributes:
@@ -75,6 +74,8 @@ included_attributes:
- :username
author:
- :name
+ ci_cd_settings:
+ - :group_runners_enabled
# Do not include the following attributes for the models specified.
excluded_attributes:
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 598832fb2df..e3e9f156fb4 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -17,7 +17,8 @@ module Gitlab
auto_devops: :project_auto_devops,
label: :project_label,
custom_attributes: 'ProjectCustomAttribute',
- project_badges: 'Badge' }.freeze
+ project_badges: 'Badge',
+ ci_cd_settings: 'ProjectCiCdSetting' }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id closed_by_id].freeze
diff --git a/lib/gitlab/optimistic_locking.rb b/lib/gitlab/optimistic_locking.rb
index 1d9a5d1a20a..d09bce642b0 100644
--- a/lib/gitlab/optimistic_locking.rb
+++ b/lib/gitlab/optimistic_locking.rb
@@ -3,18 +3,15 @@ module Gitlab
module_function
def retry_lock(subject, retries = 100, &block)
- loop do
- begin
- ActiveRecord::Base.transaction do
- return yield(subject)
- end
- rescue ActiveRecord::StaleObjectError
- retries -= 1
- raise unless retries >= 0
-
- subject.reload
- end
+ ActiveRecord::Base.transaction do
+ yield(subject)
end
+ rescue ActiveRecord::StaleObjectError
+ retries -= 1
+ raise unless retries >= 0
+
+ subject.reload
+ retry
end
alias_method :retry_optimistic_lock, :retry_lock
diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb
index 4a22fc80f75..6381e94c1d2 100644
--- a/lib/gitlab/sentry.rb
+++ b/lib/gitlab/sentry.rb
@@ -18,6 +18,25 @@ module Gitlab
end
end
+ # This can be used for investigating exceptions that can be recovered from in
+ # code. The exception will still be raised in development and test
+ # environments.
+ #
+ # That way we can track down these exceptions with as much information as we
+ # need to resolve them.
+ #
+ # Provide an issue URL for follow up.
+ def self.track_exception(exception, issue_url: nil, extra: {})
+ if enabled?
+ extra[:issue_url] = issue_url if issue_url
+ context # Make sure we've set everything we know in the context
+
+ Raven.capture_exception(exception, extra: extra)
+ end
+
+ raise exception if should_raise?
+ end
+
def self.program_context
if Sidekiq.server?
'sidekiq'
@@ -25,5 +44,9 @@ module Gitlab
'rails'
end
end
+
+ def self.should_raise?
+ Rails.env.development? || Rails.env.test?
+ end
end
end
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 67407b651a5..156115f8a8f 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -65,11 +65,11 @@ module Gitlab
# Init new repository
#
- # storage - project's storage name
+ # storage - the shard key
# name - project disk path
#
# Ex.
- # create_repository("/path/to/storage", "gitlab/gitlab-ci")
+ # create_repository("default", "gitlab/gitlab-ci")
#
def create_repository(storage, name)
relative_path = name.dup
@@ -291,13 +291,13 @@ module Gitlab
# Add empty directory for storing repositories
#
# Ex.
- # add_namespace("/path/to/storage", "gitlab")
+ # add_namespace("default", "gitlab")
#
def add_namespace(storage, name)
Gitlab::GitalyClient.migrate(:add_namespace,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled
- gitaly_namespace_client(storage).add(name)
+ Gitlab::GitalyClient::NamespaceService.new(storage).add(name)
else
path = full_path(storage, name)
FileUtils.mkdir_p(path, mode: 0770) unless exists?(storage, name)
@@ -313,13 +313,13 @@ module Gitlab
# Every repository inside this directory will be removed too
#
# Ex.
- # rm_namespace("/path/to/storage", "gitlab")
+ # rm_namespace("default", "gitlab")
#
def rm_namespace(storage, name)
Gitlab::GitalyClient.migrate(:remove_namespace,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled
- gitaly_namespace_client(storage).remove(name)
+ Gitlab::GitalyClient::NamespaceService.new(storage).remove(name)
else
FileUtils.rm_r(full_path(storage, name), force: true)
end
@@ -338,9 +338,10 @@ module Gitlab
Gitlab::GitalyClient.migrate(:rename_namespace,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled
- gitaly_namespace_client(storage).rename(old_name, new_name)
+ Gitlab::GitalyClient::NamespaceService.new(storage)
+ .rename(old_name, new_name)
else
- return false if exists?(storage, new_name) || !exists?(storage, old_name)
+ break false if exists?(storage, new_name) || !exists?(storage, old_name)
FileUtils.mv(full_path(storage, old_name), full_path(storage, new_name))
end
@@ -374,7 +375,8 @@ module Gitlab
Gitlab::GitalyClient.migrate(:namespace_exists,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled
- gitaly_namespace_client(storage).exists?(dir_name)
+ Gitlab::GitalyClient::NamespaceService.new(storage)
+ .exists?(dir_name)
else
File.exist?(full_path(storage, dir_name))
end
@@ -398,7 +400,7 @@ module Gitlab
def full_path(storage, dir_name)
raise ArgumentError.new("Directory name can't be blank") if dir_name.blank?
- File.join(storage, dir_name)
+ File.join(Gitlab.config.repositories.storages[storage].legacy_disk_path, dir_name)
end
def gitlab_shell_projects_path
@@ -475,14 +477,6 @@ module Gitlab
Bundler.with_original_env { Popen.popen(cmd, nil, vars) }
end
- def gitaly_namespace_client(storage_path)
- storage, _value = Gitlab.config.repositories.storages.find do |storage, value|
- value.legacy_disk_path == storage_path
- end
-
- Gitlab::GitalyClient::NamespaceService.new(storage)
- end
-
def git_timeout
Gitlab.config.gitlab_shell.git_timeout
end
diff --git a/lib/gitlab/sidekiq_middleware/shutdown.rb b/lib/gitlab/sidekiq_middleware/shutdown.rb
index c2b8d6de66e..b232ac4da33 100644
--- a/lib/gitlab/sidekiq_middleware/shutdown.rb
+++ b/lib/gitlab/sidekiq_middleware/shutdown.rb
@@ -25,7 +25,7 @@ module Gitlab
# can be only one shutdown thread in the process.
def self.create_shutdown_thread
mu_synchronize do
- return unless @shutdown_thread.nil?
+ break unless @shutdown_thread.nil?
@shutdown_thread = Thread.new { yield }
end
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index 24393f96d96..8cf5d636743 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -51,7 +51,7 @@ module Gitlab
return false unless can_access_git?
if protected?(ProtectedBranch, project, ref)
- user.can?(:delete_protected_branch, project)
+ user.can?(:push_to_delete_protected_branch, project)
else
user.can?(:push_code, project)
end
@@ -63,10 +63,12 @@ module Gitlab
request_cache def can_push_to_branch?(ref)
return false unless can_access_git?
- return false unless user.can?(:push_code, project) || project.branch_allows_maintainer_push?(user, ref)
+ return false unless project
+
+ return false if !user.can?(:push_code, project) && !project.branch_allows_maintainer_push?(user, ref)
if protected?(ProtectedBranch, project, ref)
- project.user_can_push_to_empty_repo?(user) || protected_branch_accessible_to?(ref, action: :push)
+ protected_branch_accessible_to?(ref, action: :push)
else
true
end
@@ -101,6 +103,7 @@ module Gitlab
def protected_branch_accessible_to?(ref, action:)
ProtectedBranch.protected_ref_accessible_to?(
ref, user,
+ project: project,
action: action,
protected_refs: project.protected_branches)
end
@@ -108,6 +111,7 @@ module Gitlab
def protected_tag_accessible_to?(ref, action:)
ProtectedTag.protected_ref_accessible_to?(
ref, user,
+ project: project,
action: action,
protected_refs: project.protected_tags)
end
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index b0a492eaa58..aeda66763e8 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -73,6 +73,10 @@ module Gitlab
nil
end
+ def bytes_to_megabytes(bytes)
+ bytes.to_f / Numeric::MEGABYTE
+ end
+
# Used in EE
# Accepts either an Array or a String and returns an array
def ensure_array_from_string(string_or_array)
diff --git a/lib/gitlab/view/presenter/base.rb b/lib/gitlab/view/presenter/base.rb
index 841fb681435..36162faa1eb 100644
--- a/lib/gitlab/view/presenter/base.rb
+++ b/lib/gitlab/view/presenter/base.rb
@@ -20,6 +20,10 @@ module Gitlab
subject
end
+ def present(**attributes)
+ self
+ end
+
class_methods do
def presenter?
true
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 153cb2a8bb1..1f060de657d 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -81,6 +81,20 @@ module Gitlab
]
end
+ def send_git_snapshot(repository)
+ params = {
+ 'GitalyServer' => gitaly_server_hash(repository),
+ 'GetSnapshotRequest' => Gitaly::GetSnapshotRequest.new(
+ repository: repository.gitaly_repository
+ ).to_json
+ }
+
+ [
+ SEND_DATA_HEADER,
+ "git-snapshot:#{encode(params)}"
+ ]
+ end
+
def send_git_diff(repository, diff_refs)
params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
{
diff --git a/lib/rspec_flaky/config.rb b/lib/rspec_flaky/config.rb
index a17ae55910e..06e96f969f1 100644
--- a/lib/rspec_flaky/config.rb
+++ b/lib/rspec_flaky/config.rb
@@ -1,9 +1,7 @@
-require 'json'
-
module RspecFlaky
class Config
def self.generate_report?
- ENV['FLAKY_RSPEC_GENERATE_REPORT'] == 'true'
+ !!(ENV['FLAKY_RSPEC_GENERATE_REPORT'] =~ /1|true/)
end
def self.suite_flaky_examples_report_path
diff --git a/lib/rspec_flaky/flaky_examples_collection.rb b/lib/rspec_flaky/flaky_examples_collection.rb
index 973c95b0212..dea23c325be 100644
--- a/lib/rspec_flaky/flaky_examples_collection.rb
+++ b/lib/rspec_flaky/flaky_examples_collection.rb
@@ -1,11 +1,9 @@
-require 'json'
+require 'active_support/hash_with_indifferent_access'
+
+require_relative 'flaky_example'
module RspecFlaky
class FlakyExamplesCollection < SimpleDelegator
- def self.from_json(json)
- new(JSON.parse(json))
- end
-
def initialize(collection = {})
unless collection.is_a?(Hash)
raise ArgumentError, "`collection` must be a Hash, #{collection.class} given!"
@@ -22,7 +20,7 @@ module RspecFlaky
super(Hash[collection_of_flaky_examples])
end
- def to_report
+ def to_h
Hash[map { |uid, example| [uid, example.to_h] }].deep_symbolize_keys
end
diff --git a/lib/rspec_flaky/listener.rb b/lib/rspec_flaky/listener.rb
index 4a5bfec9967..5b5e4f7c7de 100644
--- a/lib/rspec_flaky/listener.rb
+++ b/lib/rspec_flaky/listener.rb
@@ -1,5 +1,11 @@
require 'json'
+require_relative 'config'
+require_relative 'example'
+require_relative 'flaky_example'
+require_relative 'flaky_examples_collection'
+require_relative 'report'
+
module RspecFlaky
class Listener
# - suite_flaky_examples: contains all the currently tracked flacky example
@@ -9,7 +15,7 @@ module RspecFlaky
attr_reader :suite_flaky_examples, :flaky_examples
def initialize(suite_flaky_examples_json = nil)
- @flaky_examples = FlakyExamplesCollection.new
+ @flaky_examples = RspecFlaky::FlakyExamplesCollection.new
@suite_flaky_examples = init_suite_flaky_examples(suite_flaky_examples_json)
end
@@ -18,47 +24,36 @@ module RspecFlaky
return unless current_example.attempts > 1
- flaky_example = suite_flaky_examples.fetch(current_example.uid) { FlakyExample.new(current_example) }
+ flaky_example = suite_flaky_examples.fetch(current_example.uid) { RspecFlaky::FlakyExample.new(current_example) }
flaky_example.update_flakiness!(last_attempts_count: current_example.attempts)
flaky_examples[current_example.uid] = flaky_example
end
def dump_summary(_)
- write_report_file(flaky_examples, RspecFlaky::Config.flaky_examples_report_path)
+ RspecFlaky::Report.new(flaky_examples).write(RspecFlaky::Config.flaky_examples_report_path)
+ # write_report_file(flaky_examples, RspecFlaky::Config.flaky_examples_report_path)
new_flaky_examples = flaky_examples - suite_flaky_examples
if new_flaky_examples.any?
Rails.logger.warn "\nNew flaky examples detected:\n"
- Rails.logger.warn JSON.pretty_generate(new_flaky_examples.to_report)
+ Rails.logger.warn JSON.pretty_generate(new_flaky_examples.to_h)
- write_report_file(new_flaky_examples, RspecFlaky::Config.new_flaky_examples_report_path)
+ RspecFlaky::Report.new(new_flaky_examples).write(RspecFlaky::Config.new_flaky_examples_report_path)
+ # write_report_file(new_flaky_examples, RspecFlaky::Config.new_flaky_examples_report_path)
end
end
- def to_report(examples)
- Hash[examples.map { |k, ex| [k, ex.to_h] }]
- end
-
private
def init_suite_flaky_examples(suite_flaky_examples_json = nil)
- unless suite_flaky_examples_json
+ if suite_flaky_examples_json
+ RspecFlaky::Report.load_json(suite_flaky_examples_json).flaky_examples
+ else
return {} unless File.exist?(RspecFlaky::Config.suite_flaky_examples_report_path)
- suite_flaky_examples_json = File.read(RspecFlaky::Config.suite_flaky_examples_report_path)
+ RspecFlaky::Report.load(RspecFlaky::Config.suite_flaky_examples_report_path).flaky_examples
end
-
- FlakyExamplesCollection.from_json(suite_flaky_examples_json)
- end
-
- def write_report_file(examples_collection, file_path)
- return unless RspecFlaky::Config.generate_report?
-
- report_path_dir = File.dirname(file_path)
- FileUtils.mkdir_p(report_path_dir) unless Dir.exist?(report_path_dir)
-
- File.write(file_path, JSON.pretty_generate(examples_collection.to_report))
end
end
end
diff --git a/lib/rspec_flaky/report.rb b/lib/rspec_flaky/report.rb
new file mode 100644
index 00000000000..a8730d3b7c7
--- /dev/null
+++ b/lib/rspec_flaky/report.rb
@@ -0,0 +1,54 @@
+require 'json'
+require 'time'
+
+require_relative 'config'
+require_relative 'flaky_examples_collection'
+
+module RspecFlaky
+ # This class is responsible for loading/saving JSON reports, and pruning
+ # outdated examples.
+ class Report < SimpleDelegator
+ OUTDATED_DAYS_THRESHOLD = 90
+
+ attr_reader :flaky_examples
+
+ def self.load(file_path)
+ load_json(File.read(file_path))
+ end
+
+ def self.load_json(json)
+ new(RspecFlaky::FlakyExamplesCollection.new(JSON.parse(json)))
+ end
+
+ def initialize(flaky_examples)
+ unless flaky_examples.is_a?(RspecFlaky::FlakyExamplesCollection)
+ raise ArgumentError, "`flaky_examples` must be a RspecFlaky::FlakyExamplesCollection, #{flaky_examples.class} given!"
+ end
+
+ @flaky_examples = flaky_examples
+ super(flaky_examples)
+ end
+
+ def write(file_path)
+ unless RspecFlaky::Config.generate_report?
+ puts "! Generating reports is disabled. To enable it, please set the `FLAKY_RSPEC_GENERATE_REPORT=1` !" # rubocop:disable Rails/Output
+ return
+ end
+
+ report_path_dir = File.dirname(file_path)
+ FileUtils.mkdir_p(report_path_dir) unless Dir.exist?(report_path_dir)
+
+ File.write(file_path, JSON.pretty_generate(flaky_examples.to_h))
+ end
+
+ def prune_outdated(days: OUTDATED_DAYS_THRESHOLD)
+ outdated_date_threshold = Time.now - (3600 * 24 * days)
+ updated_hash = flaky_examples.dup
+ .delete_if do |uid, hash|
+ hash[:last_flaky_at] && Time.parse(hash[:last_flaky_at]).to_i < outdated_date_threshold.to_i
+ end
+
+ self.class.new(RspecFlaky::FlakyExamplesCollection.new(updated_hash))
+ end
+ end
+end
diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake
index 564aa141952..cb4d5abffbc 100644
--- a/lib/tasks/cache.rake
+++ b/lib/tasks/cache.rake
@@ -6,17 +6,22 @@ namespace :cache do
desc "GitLab | Clear redis cache"
task redis: :environment do
Gitlab::Redis::Cache.with do |redis|
- cursor = REDIS_SCAN_START_STOP
- loop do
- cursor, keys = redis.scan(
- cursor,
- match: "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}*",
- count: REDIS_CLEAR_BATCH_SIZE
- )
+ cache_key_pattern = %W[#{Gitlab::Redis::Cache::CACHE_NAMESPACE}*
+ projects/*/pipeline_status]
- redis.del(*keys) if keys.any?
+ cache_key_pattern.each do |match|
+ cursor = REDIS_SCAN_START_STOP
+ loop do
+ cursor, keys = redis.scan(
+ cursor,
+ match: match,
+ count: REDIS_CLEAR_BATCH_SIZE
+ )
- break if cursor == REDIS_SCAN_START_STOP
+ redis.del(*keys) if keys.any?
+
+ break if cursor == REDIS_SCAN_START_STOP
+ end
end
end
end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index abef8cd2bcc..c04dae7446f 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -427,10 +427,7 @@ namespace :gitlab do
user = User.find_by(username: username)
if user
repo_dirs = user.authorized_projects.map do |p|
- File.join(
- p.repository_storage_path,
- "#{p.disk_path}.git"
- )
+ p.repository.path_to_repo
end
repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) }
diff --git a/lib/tasks/gitlab/list_repos.rake b/lib/tasks/gitlab/list_repos.rake
index d7f28691098..b854c34a8e5 100644
--- a/lib/tasks/gitlab/list_repos.rake
+++ b/lib/tasks/gitlab/list_repos.rake
@@ -10,9 +10,8 @@ namespace :gitlab do
end
scope.find_each do |project|
- base = File.join(project.repository_storage_path, project.disk_path)
- puts base + '.git'
- puts base + '.wiki.git'
+ puts project.repository.path_to_repo
+ puts project.wiki.repository.path_to_repo
end
end
end
diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake
index 1d903c81358..f71e69987cb 100644
--- a/lib/tasks/gitlab/setup.rake
+++ b/lib/tasks/gitlab/setup.rake
@@ -1,9 +1,20 @@
namespace :gitlab do
desc "GitLab | Setup production application"
task setup: :gitlab_environment do
+ check_gitaly_connection
setup_db
end
+ def check_gitaly_connection
+ Gitlab.config.repositories.storages.each do |name, _details|
+ Gitlab::GitalyClient::ServerService.new(name).info
+ end
+ rescue GRPC::Unavailable => ex
+ puts "Failed to connect to Gitaly...".color(:red)
+ puts "Error: #{ex}"
+ exit 1
+ end
+
def setup_db
warn_user_is_not_gitlab
diff --git a/lib/tasks/gitlab/storage.rake b/lib/tasks/gitlab/storage.rake
index 8ac73bc8ff2..6e8bd9078c8 100644
--- a/lib/tasks/gitlab/storage.rake
+++ b/lib/tasks/gitlab/storage.rake
@@ -111,7 +111,7 @@ namespace :gitlab do
puts " - #{project.full_path} (id: #{project.id})".color(:red)
- return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator
+ return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator, Cop/AvoidReturnFromBlocks
end
end
end
@@ -132,7 +132,7 @@ namespace :gitlab do
puts " - #{upload.path} (id: #{upload.id})".color(:red)
- return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator
+ return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator, Cop/AvoidReturnFromBlocks
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d7eb123b48b..17917b1176f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-04-04 18:02+0200\n"
-"PO-Revision-Date: 2018-04-04 18:02+0200\n"
+"POT-Creation-Date: 2018-04-24 13:19+0000\n"
+"PO-Revision-Date: 2018-04-24 13:19+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -241,6 +241,9 @@ msgstr ""
msgid "Allow edits from maintainers."
msgstr ""
+msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
+msgstr ""
+
msgid "Allow requests to the local network from hooks and services."
msgstr ""
@@ -322,7 +325,7 @@ msgstr ""
msgid "April"
msgstr ""
-msgid "Archived project! Repository is read-only"
+msgid "Archived project! Repository and other project resources are read-only"
msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
@@ -433,9 +436,81 @@ msgstr ""
msgid "Background jobs"
msgstr ""
+msgid "Badges"
+msgstr ""
+
+msgid "Badges|A new badge was added."
+msgstr ""
+
+msgid "Badges|Add badge"
+msgstr ""
+
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
+msgstr ""
+
+msgid "Badges|Badge image URL"
+msgstr ""
+
+msgid "Badges|Badge image preview"
+msgstr ""
+
+msgid "Badges|Delete badge"
+msgstr ""
+
+msgid "Badges|Delete badge?"
+msgstr ""
+
+msgid "Badges|Deleting the badge failed, please try again."
+msgstr ""
+
+msgid "Badges|Group Badge"
+msgstr ""
+
+msgid "Badges|Link"
+msgstr ""
+
+msgid "Badges|No badge image"
+msgstr ""
+
+msgid "Badges|No image to preview"
+msgstr ""
+
+msgid "Badges|Project Badge"
+msgstr ""
+
+msgid "Badges|Reload badge image"
+msgstr ""
+
+msgid "Badges|Save changes"
+msgstr ""
+
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
+msgstr ""
+
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
+msgstr ""
+
+msgid "Badges|The badge was deleted."
+msgstr ""
+
+msgid "Badges|The badge was saved."
+msgstr ""
+
+msgid "Badges|This group has no badges"
+msgstr ""
+
+msgid "Badges|This project has no badges"
+msgstr ""
+
+msgid "Badges|Your badges"
+msgstr ""
+
msgid "Begin with the selected commit"
msgstr ""
+msgid "Blame"
+msgstr ""
+
msgid "Branch (%{branch_count})"
msgid_plural "Branches (%{branch_count})"
msgstr[0] ""
@@ -600,12 +675,18 @@ msgstr ""
msgid "Cancel"
msgstr ""
+msgid "Cancel this job"
+msgstr ""
+
msgid "Cannot be merged automatically"
msgstr ""
msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
+msgid "Change this value to influence how frequently the GitLab UI polls for updates."
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr ""
@@ -744,6 +825,12 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
+msgstr ""
+
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
+msgstr ""
+
msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
@@ -816,7 +903,7 @@ msgstr ""
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr ""
-msgid "ClusterIntegration|Create on GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
@@ -1147,6 +1234,9 @@ msgstr ""
msgid "Confidentiality"
msgstr ""
+msgid "Configure Gitaly timeouts."
+msgstr ""
+
msgid "Configure Sidekiq job throttling."
msgstr ""
@@ -1213,6 +1303,9 @@ msgstr ""
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
msgstr ""
+msgid "ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images."
+msgstr ""
+
msgid "Continuous Integration and Deployment"
msgstr ""
@@ -1386,6 +1479,78 @@ msgstr[1] ""
msgid "Deploy Keys"
msgstr ""
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
+msgstr ""
+
+msgid "DeployTokens|Add a deploy token"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the registry images"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the repository"
+msgstr ""
+
+msgid "DeployTokens|Copy deploy token to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Copy username to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Create deploy token"
+msgstr ""
+
+msgid "DeployTokens|Created"
+msgstr ""
+
+msgid "DeployTokens|Deploy Tokens"
+msgstr ""
+
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
+msgstr ""
+
+msgid "DeployTokens|Expires"
+msgstr ""
+
+msgid "DeployTokens|Name"
+msgstr ""
+
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
+msgstr ""
+
+msgid "DeployTokens|Revoke"
+msgstr ""
+
+msgid "DeployTokens|Revoke %{name}"
+msgstr ""
+
+msgid "DeployTokens|Scopes"
+msgstr ""
+
+msgid "DeployTokens|This action cannot be undone."
+msgstr ""
+
+msgid "DeployTokens|This project has no active Deploy Tokens."
+msgstr ""
+
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
+msgstr ""
+
+msgid "DeployTokens|Use this username as a login."
+msgstr ""
+
+msgid "DeployTokens|Username"
+msgstr ""
+
+msgid "DeployTokens|You are about to revoke"
+msgstr ""
+
+msgid "DeployTokens|Your New Deploy Token"
+msgstr ""
+
+msgid "DeployTokens|Your new project deploy token has been created."
+msgstr ""
+
msgid "Description"
msgstr ""
@@ -1455,9 +1620,15 @@ msgstr ""
msgid "Editing"
msgstr ""
+msgid "Email"
+msgstr ""
+
msgid "Emails"
msgstr ""
+msgid "Embed"
+msgstr ""
+
msgid "Enable Auto DevOps"
msgstr ""
@@ -1470,6 +1641,9 @@ msgstr ""
msgid "Enable and configure Prometheus metrics."
msgstr ""
+msgid "Enable or disable version check and usage ping."
+msgstr ""
+
msgid "Enable reCAPTCHA or Akismet and set IP limits."
msgstr ""
@@ -1602,6 +1776,9 @@ msgstr ""
msgid "Failed to change the owner"
msgstr ""
+msgid "Failed to check related branches."
+msgstr ""
+
msgid "Failed to remove issue from board, please try again."
msgstr ""
@@ -1703,6 +1880,9 @@ msgstr ""
msgid "GitLab Runner section"
msgstr ""
+msgid "Gitaly"
+msgstr ""
+
msgid "Gitaly Servers"
msgstr ""
@@ -1885,6 +2065,9 @@ msgstr ""
msgid "January"
msgstr ""
+msgid "Job has been erased"
+msgstr ""
+
msgid "Jobs"
msgstr ""
@@ -1900,6 +2083,9 @@ msgstr ""
msgid "June"
msgstr ""
+msgid "Koding"
+msgstr ""
+
msgid "Kubernetes"
msgstr ""
@@ -2321,6 +2507,9 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr ""
+msgid "Online IDE integration settings."
+msgstr ""
+
msgid "Only project members can comment."
msgstr ""
@@ -2369,6 +2558,12 @@ msgstr ""
msgid "Pending"
msgstr ""
+msgid "Performance optimization"
+msgstr ""
+
+msgid "Permalink"
+msgstr ""
+
msgid "Personal Access Token"
msgstr ""
@@ -2510,12 +2705,18 @@ msgstr ""
msgid "Pipeline|with stages"
msgstr ""
+msgid "PlantUML"
+msgstr ""
+
msgid "Play"
msgstr ""
msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
msgstr ""
+msgid "Please select at least one filter to see results"
+msgstr ""
+
msgid "Please solve the reCAPTCHA"
msgstr ""
@@ -2540,6 +2741,12 @@ msgstr ""
msgid "Profiles|Account scheduled for removal."
msgstr ""
+msgid "Profiles|Change username"
+msgstr ""
+
+msgid "Profiles|Current path: %{path}"
+msgstr ""
+
msgid "Profiles|Delete Account"
msgstr ""
@@ -2558,9 +2765,21 @@ msgstr ""
msgid "Profiles|Invalid username"
msgstr ""
+msgid "Profiles|Path"
+msgstr ""
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr ""
+msgid "Profiles|Update username"
+msgstr ""
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr ""
+
+msgid "Profiles|Username successfully changed"
+msgstr ""
+
msgid "Profiles|You don't have access to delete this user."
msgstr ""
@@ -2591,6 +2810,9 @@ msgstr ""
msgid "Project '%{project_name}' was successfully updated."
msgstr ""
+msgid "Project Badges"
+msgstr ""
+
msgid "Project access must be granted explicitly to each user."
msgstr ""
@@ -2618,15 +2840,6 @@ msgstr ""
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
-msgid "ProjectFeature|Disabled"
-msgstr ""
-
-msgid "ProjectFeature|Everyone with access"
-msgstr ""
-
-msgid "ProjectFeature|Only team members"
-msgstr ""
-
msgid "ProjectFileTree|Name"
msgstr ""
@@ -2663,6 +2876,9 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "PrometheusDashboard|Time"
+msgstr ""
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
@@ -2729,6 +2945,9 @@ msgstr ""
msgid "Promote"
msgstr ""
+msgid "Promote these project milestones into a group milestone."
+msgstr ""
+
msgid "Promote to Group Label"
msgstr ""
@@ -2756,12 +2975,18 @@ msgstr ""
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
+msgid "Raw"
+msgstr ""
+
msgid "Read more"
msgstr ""
msgid "Readme"
msgstr ""
+msgid "Real-time features"
+msgstr ""
+
msgid "RefSwitcher|Branches"
msgstr ""
@@ -2828,6 +3053,12 @@ msgstr ""
msgid "Resolve discussion"
msgstr ""
+msgid "Retry this job"
+msgstr ""
+
+msgid "Retry verification"
+msgstr ""
+
msgid "Reveal value"
msgid_plural "Reveal values"
msgstr[0] ""
@@ -2839,6 +3070,9 @@ msgstr ""
msgid "Revert this merge request"
msgstr ""
+msgid "Review"
+msgstr ""
+
msgid "Reviewing"
msgstr ""
@@ -2902,6 +3136,9 @@ msgstr ""
msgid "Select Archive Format"
msgstr ""
+msgid "Select a namespace to fork the project"
+msgstr ""
+
msgid "Select a timezone"
msgstr ""
@@ -2941,6 +3178,9 @@ msgstr ""
msgid "Set default and restrict visibility levels. Configure import sources and git access protocol."
msgstr ""
+msgid "Set max session time for web terminal."
+msgstr ""
+
msgid "Set notification email for abuse reports."
msgstr ""
@@ -2962,6 +3202,9 @@ msgstr ""
msgid "Setup a specific Runner automatically"
msgstr ""
+msgid "Share"
+msgstr ""
+
msgid "Show command"
msgstr ""
@@ -3141,6 +3384,9 @@ msgstr ""
msgid "Status"
msgstr ""
+msgid "Stop this environment"
+msgstr ""
+
msgid "Stopped"
msgstr ""
@@ -3371,6 +3617,15 @@ msgstr ""
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
+msgid "This job does not have a trace."
+msgstr ""
+
+msgid "This job has been canceled"
+msgstr ""
+
+msgid "This job has been skipped"
+msgstr ""
+
msgid "This job has not been triggered yet"
msgstr ""
@@ -3392,6 +3647,9 @@ msgstr ""
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
+msgid "This page will be removed in a future release."
+msgstr ""
+
msgid "This project"
msgstr ""
@@ -3615,6 +3873,9 @@ msgstr ""
msgid "Unstar"
msgstr ""
+msgid "Unverified"
+msgstr ""
+
msgid "Up to date"
msgstr ""
@@ -3633,6 +3894,12 @@ msgstr ""
msgid "Upvotes"
msgstr ""
+msgid "Usage statistics"
+msgstr ""
+
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
+msgstr ""
+
msgid "Use the following registration token during setup:"
msgstr ""
@@ -3645,6 +3912,18 @@ msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
msgstr ""
+msgid "Various container registry settings."
+msgstr ""
+
+msgid "Various email settings."
+msgstr ""
+
+msgid "Various settings that affect GitLab performance."
+msgstr ""
+
+msgid "Verified"
+msgstr ""
+
msgid "View and edit lines"
msgstr ""
@@ -3696,6 +3975,9 @@ msgstr ""
msgid "Web IDE"
msgstr ""
+msgid "Web terminal"
+msgstr ""
+
msgid "Wiki"
msgstr ""
@@ -3938,6 +4220,9 @@ msgid_plural "days"
msgstr[0] ""
msgstr[1] ""
+msgid "deploy token"
+msgstr ""
+
msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
@@ -3988,6 +4273,9 @@ msgstr ""
msgid "mrWidget|Closes"
msgstr ""
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr ""
+
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
@@ -4081,6 +4369,9 @@ msgstr ""
msgid "mrWidget|There are merge conflicts"
msgstr ""
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr ""
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr ""
diff --git a/package.json b/package.json
index 27f612aca38..96dee321548 100644
--- a/package.json
+++ b/package.json
@@ -5,9 +5,9 @@
"eslint": "eslint --max-warnings 0 --ext .js,.vue .",
"eslint-fix": "eslint --max-warnings 0 --ext .js,.vue --fix .",
"eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html .",
- "karma": "karma start config/karma.config.js --single-run",
- "karma-coverage": "BABEL_ENV=coverage karma start config/karma.config.js --single-run",
- "karma-start": "karma start config/karma.config.js",
+ "karma": "BABEL_ENV=${BABEL_ENV:=karma} karma start --single-run true config/karma.config.js",
+ "karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js",
+ "karma-start": "BABEL_ENV=karma karma start config/karma.config.js",
"prettier-staged": "node ./scripts/frontend/prettier.js",
"prettier-staged-save": "node ./scripts/frontend/prettier.js save",
"prettier-all": "node ./scripts/frontend/prettier.js check-all",
@@ -16,7 +16,7 @@
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
- "@gitlab-org/gitlab-svgs": "^1.17.0",
+ "@gitlab-org/gitlab-svgs": "^1.18.0",
"autosize": "^4.0.0",
"axios": "^0.17.1",
"babel-core": "^6.26.0",
@@ -88,6 +88,7 @@
"vue-resource": "^1.3.5",
"vue-router": "^3.0.1",
"vue-template-compiler": "^2.5.13",
+ "vue-virtual-scroll-list": "^1.2.5",
"vuex": "^3.0.1",
"webpack": "^3.11.0",
"webpack-bundle-analyzer": "^2.10.0",
@@ -98,6 +99,10 @@
"axios-mock-adapter": "^1.10.0",
"babel-eslint": "^8.0.2",
"babel-plugin-istanbul": "^4.1.5",
+ "babel-plugin-rewire": "^1.1.0",
+ "babel-template": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "commander": "^2.15.1",
"eslint": "^3.18.0",
"eslint-config-airbnb-base": "^10.0.1",
"eslint-import-resolver-webpack": "^0.8.3",
diff --git a/qa/qa.rb b/qa/qa.rb
index 56a99c32b26..fff99a1d31b 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -31,6 +31,7 @@ module QA
autoload :Project, 'qa/factory/resource/project'
autoload :MergeRequest, 'qa/factory/resource/merge_request'
autoload :DeployKey, 'qa/factory/resource/deploy_key'
+ autoload :Branch, 'qa/factory/resource/branch'
autoload :SecretVariable, 'qa/factory/resource/secret_variable'
autoload :Runner, 'qa/factory/resource/runner'
autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token'
@@ -132,6 +133,7 @@ module QA
autoload :Repository, 'qa/page/project/settings/repository'
autoload :CICD, 'qa/page/project/settings/ci_cd'
autoload :DeployKeys, 'qa/page/project/settings/deploy_keys'
+ autoload :ProtectedBranches, 'qa/page/project/settings/protected_branches'
autoload :SecretVariables, 'qa/page/project/settings/secret_variables'
autoload :Runners, 'qa/page/project/settings/runners'
autoload :MergeRequest, 'qa/page/project/settings/merge_request'
diff --git a/qa/qa/factory/base.rb b/qa/qa/factory/base.rb
index afaa96b4541..7a532ce534b 100644
--- a/qa/qa/factory/base.rb
+++ b/qa/qa/factory/base.rb
@@ -22,7 +22,7 @@ module QA
factory.fabricate!(*args)
- return Factory::Product.populate!(factory)
+ break Factory::Product.populate!(factory)
end
end
diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb
new file mode 100644
index 00000000000..d0ef142e90d
--- /dev/null
+++ b/qa/qa/factory/resource/branch.rb
@@ -0,0 +1,73 @@
+module QA
+ module Factory
+ module Resource
+ class Branch < Factory::Base
+ attr_accessor :project, :branch_name, :allow_to_push, :protected
+
+ dependency Factory::Resource::Project, as: :project do |project|
+ project.name = 'protected-branch-project'
+ end
+
+ product :name do
+ Page::Project::Settings::Repository.act do
+ expand_protected_branches(&:last_branch_name)
+ end
+ end
+
+ product :push_allowance do
+ Page::Project::Settings::Repository.act do
+ expand_protected_branches(&:last_push_allowance)
+ end
+ end
+
+ def initialize
+ @branch_name = 'test/branch'
+ @allow_to_push = true
+ @protected = false
+ end
+
+ def fabricate!
+ project.visit!
+
+ Factory::Repository::Push.fabricate! do |resource|
+ resource.project = project
+ resource.file_name = 'kick-off.txt'
+ resource.commit_message = 'First commit'
+ end
+
+ branch = Factory::Repository::Push.fabricate! do |resource|
+ resource.project = project
+ resource.file_name = 'README.md'
+ resource.commit_message = 'Add readme'
+ resource.branch_name = "master:#{@branch_name}"
+ end
+
+ Page::Project::Show.act { wait_for_push }
+
+ # The upcoming process will make it access the Protected Branches page,
+ # select the already created branch and protect it according
+ # to `allow_to_push` variable.
+ return branch unless @protected
+
+ Page::Menu::Side.act do
+ click_repository_settings
+ end
+
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_protected_branches do |page|
+ page.select_branch(branch_name)
+
+ if allow_to_push
+ page.allow_devs_and_masters_to_push
+ else
+ page.allow_no_one_to_push
+ end
+
+ page.protect_branch
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index b3150e8f3fa..2f9f06ba277 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -1,11 +1,14 @@
require 'cgi'
require 'uri'
+require 'open3'
module QA
module Git
class Repository
include Scenario::Actable
+ attr_reader :push_error
+
def self.perform(*args)
Dir.mktmpdir do |dir|
Dir.chdir(dir) { super }
@@ -65,7 +68,8 @@ module QA
end
def push_changes(branch = 'master')
- `git push #{@uri.to_s} #{branch} #{suppress_output}`
+ # capture3 returns stdout, stderr and status.
+ _, @push_error, _ = Open3.capture3("git push #{@uri} #{branch} #{suppress_output}")
end
def commits
diff --git a/qa/qa/page/README.md b/qa/qa/page/README.md
index d38223f690d..2dbc59846e7 100644
--- a/qa/qa/page/README.md
+++ b/qa/qa/page/README.md
@@ -115,8 +115,8 @@ from within the `qa` directory.
## Where to ask for help?
-If you need more information, ask for help on `#qa` channel on Slack (GitLab
-Team only).
+If you need more information, ask for help on `#quality` channel on Slack
+(internal, GitLab Team only).
If you are not a Team Member, and you still need help to contribute, please
open an issue in GitLab QA issue tracker.
diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb
index d215518d316..89125bd2e59 100644
--- a/qa/qa/page/group/show.rb
+++ b/qa/qa/page/group/show.rb
@@ -29,7 +29,7 @@ module QA
filter_by_name(name)
wait(reload: false) do
- return false if page.has_content?('Sorry, no groups or projects matched your search')
+ break false if page.has_content?('Sorry, no groups or projects matched your search')
page.has_link?(name)
end
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index 2f2506f08fb..166861e6c4a 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -2,7 +2,7 @@ module QA
module Page
module MergeRequest
class Show < Page::Base
- view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js' do
+ view 'app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue' do
element :merge_button
element :fast_forward_message, 'Fast-forward merge without a merge commit'
end
diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb
index b183552d46c..ec61c47b3bb 100644
--- a/qa/qa/page/project/pipeline/show.rb
+++ b/qa/qa/page/project/pipeline/show.rb
@@ -20,14 +20,14 @@ module QA::Page
def running?
within('.ci-header-container') do
- return page.has_content?('running')
+ page.has_content?('running')
end
end
def has_build?(name, status: :success)
within('.pipeline-graph') do
within('.ci-job-component', text: name) do
- return has_selector?(".ci-status-icon-#{status}")
+ has_selector?(".ci-status-icon-#{status}")
end
end
end
diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb
new file mode 100644
index 00000000000..f3563401124
--- /dev/null
+++ b/qa/qa/page/project/settings/protected_branches.rb
@@ -0,0 +1,69 @@
+module QA
+ module Page
+ module Project
+ module Settings
+ class ProtectedBranches < Page::Base
+ view 'app/views/projects/protected_branches/shared/_dropdown.html.haml' do
+ element :protected_branch_select
+ element :protected_branch_dropdown
+ end
+
+ view 'app/views/projects/protected_branches/_create_protected_branch.html.haml' do
+ element :allowed_to_push_select
+ element :allowed_to_push_dropdown
+ end
+
+ view 'app/views/projects/protected_branches/shared/_branches_list.html.haml' do
+ element :protected_branches_list
+ end
+
+ view 'app/views/projects/protected_branches/shared/_protected_branch.html.haml' do
+ element :protected_branch_name
+ end
+
+ def select_branch(branch_name)
+ click_element :protected_branch_select
+
+ within_element(:protected_branch_dropdown) do
+ click_on branch_name
+ end
+ end
+
+ def allow_no_one_to_push
+ allow_to_push('No one')
+ end
+
+ def allow_devs_and_masters_to_push
+ allow_to_push('Developers + Masters')
+ end
+
+ def protect_branch
+ click_on 'Protect'
+ end
+
+ def last_branch_name
+ within_element(:protected_branches_list) do
+ all('.qa-protected-branch-name').last
+ end
+ end
+
+ def last_push_allowance
+ within_element(:protected_branches_list) do
+ all('.qa-allowed-to-push').last
+ end
+ end
+
+ private
+
+ def allow_to_push(text)
+ click_element :allowed_to_push_select
+
+ within_element(:allowed_to_push_dropdown) do
+ click_on text
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb
index 22362164a1a..30900e74e90 100644
--- a/qa/qa/page/project/settings/repository.rb
+++ b/qa/qa/page/project/settings/repository.rb
@@ -14,6 +14,12 @@ module QA
DeployKeys.perform(&block)
end
end
+
+ def expand_protected_branches(&block)
+ expand_section('Protected Branches') do
+ ProtectedBranches.perform(&block)
+ end
+ end
end
end
end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 0c7ad46d36b..c7e7ece792d 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -21,6 +21,11 @@ module QA
element :new_issue_link, "link_to 'New issue', new_project_issue_path(@project)"
end
+ view 'app/views/shared/_ref_switcher.html.haml' do
+ element :branches_select
+ element :branches_dropdown
+ end
+
def choose_repository_clone_http
choose_repository_clone('HTTP', 'http')
end
@@ -44,6 +49,18 @@ module QA
find('.qa-project-name').text
end
+ def switch_to_branch(branch_name)
+ find_element(:branches_select).click
+
+ within_element(:branches_dropdown) do
+ click_on branch_name
+ end
+ end
+
+ def last_commit_content
+ find_element(:commit_content).text
+ end
+
def new_merge_request
wait(reload: true) do
has_css?(element_selector_css(:create_merge_request))
diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb
index 341998af160..d21a9d52997 100644
--- a/qa/qa/scenario/template.rb
+++ b/qa/qa/scenario/template.rb
@@ -4,7 +4,7 @@ module QA
def self.perform(*args)
new.tap do |scenario|
yield scenario if block_given?
- return scenario.perform(*args)
+ break scenario.perform(*args)
end
end
diff --git a/qa/qa/scenario/test/sanity/selectors.rb b/qa/qa/scenario/test/sanity/selectors.rb
index c87eb5f3dfb..cff320cb751 100644
--- a/qa/qa/scenario/test/sanity/selectors.rb
+++ b/qa/qa/scenario/test/sanity/selectors.rb
@@ -31,7 +31,7 @@ module QA
current changes in this merge request.
For more help see documentation in `qa/page/README.md` file or
- ask for help on #qa channel on Slack (GitLab Team only).
+ ask for help on #quality channel on Slack (GitLab Team only).
If you are not a Team Member, and you still need help to
contribute, please open an issue in GitLab QA issue tracker.
diff --git a/qa/qa/specs/features/repository/protected_branches_spec.rb b/qa/qa/specs/features/repository/protected_branches_spec.rb
new file mode 100644
index 00000000000..88fa4994e32
--- /dev/null
+++ b/qa/qa/specs/features/repository/protected_branches_spec.rb
@@ -0,0 +1,63 @@
+module QA
+ feature 'branch protection support', :core do
+ given(:branch_name) { 'protected-branch' }
+ given(:commit_message) { 'Protected push commit message' }
+ given(:project) do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'protected-branch-project'
+ end
+ end
+ given(:location) do
+ Page::Project::Show.act do
+ choose_repository_clone_http
+ repository_location
+ end
+ end
+
+ before do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+ end
+
+ scenario 'user is able to protect a branch' do
+ protected_branch = Factory::Resource::Branch.fabricate! do |resource|
+ resource.branch_name = branch_name
+ resource.project = project
+ resource.allow_to_push = true
+ resource.protected = true
+ end
+
+ expect(protected_branch.name).to have_content(branch_name)
+ expect(protected_branch.push_allowance).to have_content('Developers + Masters')
+ end
+
+ scenario 'users without authorization cannot push to protected branch' do
+ Factory::Resource::Branch.fabricate! do |resource|
+ resource.branch_name = branch_name
+ resource.project = project
+ resource.allow_to_push = false
+ resource.protected = true
+ end
+
+ project.visit!
+
+ Git::Repository.perform do |repository|
+ repository.location = location
+ repository.use_default_credentials
+
+ repository.act do
+ clone
+ configure_identity('GitLab QA', 'root@gitlab.com')
+ checkout('protected-branch')
+ commit_file('README.md', 'readme content', 'Add a readme')
+ push_changes('protected-branch')
+ end
+
+ expect(repository.push_error)
+ .to match(/remote\: GitLab\: You are not allowed to push code to protected branches on this project/)
+ expect(repository.push_error)
+ .to match(/\[remote rejected\] #{branch_name} -> #{branch_name} \(pre-receive hook declined\)/)
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/avoid_break_from_strong_memoize.rb b/rubocop/cop/avoid_break_from_strong_memoize.rb
new file mode 100644
index 00000000000..9b436118db3
--- /dev/null
+++ b/rubocop/cop/avoid_break_from_strong_memoize.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ # Checks for break inside strong_memoize blocks.
+ # For more information see: https://gitlab.com/gitlab-org/gitlab-ce/issues/42889
+ #
+ # @example
+ # # bad
+ # strong_memoize(:result) do
+ # break if something
+ #
+ # do_an_heavy_calculation
+ # end
+ #
+ # # good
+ # strong_memoize(:result) do
+ # next if something
+ #
+ # do_an_heavy_calculation
+ # end
+ #
+ class AvoidBreakFromStrongMemoize < RuboCop::Cop::Cop
+ MSG = 'Do not use break inside strong_memoize, use next instead.'
+
+ def on_block(node)
+ block_body = node.body
+
+ return unless block_body
+ return unless node.method_name == :strong_memoize
+
+ block_body.each_node(:break) do |break_node|
+ next if container_block_for(break_node) != node
+
+ add_offense(break_node)
+ end
+ end
+
+ private
+
+ def container_block_for(current_node)
+ current_node = current_node.parent until current_node.type == :block && current_node.method_name == :strong_memoize
+
+ current_node
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/avoid_return_from_blocks.rb b/rubocop/cop/avoid_return_from_blocks.rb
new file mode 100644
index 00000000000..40b2aed019f
--- /dev/null
+++ b/rubocop/cop/avoid_return_from_blocks.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ # Checks for return inside blocks.
+ # For more information see: https://gitlab.com/gitlab-org/gitlab-ce/issues/42889
+ #
+ # @example
+ # # bad
+ # call do
+ # return if something
+ #
+ # do_something_else
+ # end
+ #
+ # # good
+ # call do
+ # break if something
+ #
+ # do_something_else
+ # end
+ #
+ class AvoidReturnFromBlocks < RuboCop::Cop::Cop
+ MSG = 'Do not return from a block, use next or break instead.'
+ DEF_METHODS = %i[define_method lambda].freeze
+ WHITELISTED_METHODS = %i[each each_filename times loop].freeze
+
+ def on_block(node)
+ block_body = node.body
+
+ return unless block_body
+ return unless top_block?(node)
+
+ block_body.each_node(:return) do |return_node|
+ next if parent_blocks(node, return_node).all?(&method(:whitelisted?))
+
+ add_offense(return_node)
+ end
+ end
+
+ private
+
+ def top_block?(node)
+ current_node = node
+ top_block = nil
+
+ while current_node && current_node.type != :def
+ top_block = current_node if current_node.type == :block
+ current_node = current_node.parent
+ end
+
+ top_block == node
+ end
+
+ def parent_blocks(node, current_node)
+ blocks = []
+
+ until node == current_node || def?(current_node)
+ blocks << current_node if current_node.type == :block
+ current_node = current_node.parent
+ end
+
+ blocks << node if node == current_node && !def?(node)
+ blocks
+ end
+
+ def def?(node)
+ node.type == :def || node.type == :defs ||
+ (node.type == :block && DEF_METHODS.include?(node.method_name))
+ end
+
+ def whitelisted?(block_node)
+ WHITELISTED_METHODS.include?(block_node.method_name)
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/gitlab/has_many_through_scope.rb b/rubocop/cop/gitlab/has_many_through_scope.rb
deleted file mode 100644
index 770a2a0529f..00000000000
--- a/rubocop/cop/gitlab/has_many_through_scope.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-require 'gitlab/styles/rubocop/model_helpers'
-
-module RuboCop
- module Cop
- module Gitlab
- class HasManyThroughScope < RuboCop::Cop::Cop
- include ::Gitlab::Styles::Rubocop::ModelHelpers
-
- MSG = 'Always provide an explicit scope calling auto_include(false) when using has_many :through'.freeze
-
- def_node_search :through?, <<~PATTERN
- (pair (sym :through) _)
- PATTERN
-
- def_node_matcher :has_many_through?, <<~PATTERN
- (send nil? :has_many ... #through?)
- PATTERN
-
- def_node_search :disables_auto_include?, <<~PATTERN
- (send _ :auto_include false)
- PATTERN
-
- def_node_matcher :scope_disables_auto_include?, <<~PATTERN
- (block (send nil? :lambda) _ #disables_auto_include?)
- PATTERN
-
- def on_send(node)
- return unless in_model?(node)
- return unless has_many_through?(node)
-
- target = node
- scope_argument = node.children[3]
-
- if scope_argument.children[0].children.last == :lambda
- return if scope_disables_auto_include?(scope_argument)
-
- target = scope_argument
- end
-
- add_offense(target, location: :expression)
- end
- end
- end
- end
-end
diff --git a/rubocop/cop/migration/safer_boolean_column.rb b/rubocop/cop/migration/safer_boolean_column.rb
index dc5c55df6fb..a7d922c752f 100644
--- a/rubocop/cop/migration/safer_boolean_column.rb
+++ b/rubocop/cop/migration/safer_boolean_column.rb
@@ -61,7 +61,7 @@ module RuboCop
return true unless opts
each_hash_node_pair(opts) do |key, value|
- return value == 'nil' if key == :default
+ break value == 'nil' if key == :default
end
end
@@ -69,7 +69,7 @@ module RuboCop
return true unless opts
each_hash_node_pair(opts) do |key, value|
- return value != 'false' if key == :null
+ break value != 'false' if key == :null
end
end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index c2254332e7d..f05990232ab 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,9 +1,10 @@
# rubocop:disable Naming/FileName
-require_relative 'cop/gitlab/has_many_through_scope'
-require_relative 'cop/gitlab/httparty'
require_relative 'cop/gitlab/module_with_instance_variables'
require_relative 'cop/gitlab/predicate_memoization'
+require_relative 'cop/gitlab/httparty'
require_relative 'cop/include_sidekiq_worker'
+require_relative 'cop/avoid_return_from_blocks'
+require_relative 'cop/avoid_break_from_strong_memoize'
require_relative 'cop/line_break_around_conditional_block'
require_relative 'cop/migration/add_column'
require_relative 'cop/migration/add_concurrent_foreign_key'
diff --git a/rubocop/spec_helpers.rb b/rubocop/spec_helpers.rb
index 6c0f0193b1a..9bf5f1e3b18 100644
--- a/rubocop/spec_helpers.rb
+++ b/rubocop/spec_helpers.rb
@@ -1,6 +1,6 @@
module RuboCop
module SpecHelpers
- SPEC_HELPERS = %w[spec_helper.rb rails_helper.rb].freeze
+ SPEC_HELPERS = %w[fast_spec_helper.rb rails_helper.rb spec_helper.rb].freeze
# Returns true if the given node originated from the spec directory.
def in_spec?(node)
diff --git a/scripts/prune-old-flaky-specs b/scripts/prune-old-flaky-specs
new file mode 100755
index 00000000000..f7451fbd428
--- /dev/null
+++ b/scripts/prune-old-flaky-specs
@@ -0,0 +1,24 @@
+#!/usr/bin/env ruby
+
+# lib/rspec_flaky/flaky_examples_collection.rb is requiring
+# `active_support/hash_with_indifferent_access`, and we install the `activesupport`
+# gem manually on the CI
+require 'rubygems'
+
+require_relative '../lib/rspec_flaky/report'
+
+report_file = ARGV.shift
+unless report_file
+ puts 'usage: prune-old-flaky-specs <report-file> <new-report-file>'
+ exit 1
+end
+
+new_report_file = ARGV.shift || report_file
+report = RspecFlaky::Report.load(report_file)
+puts "Loading #{report_file}..."
+puts "Current report has #{report.size} entries."
+
+new_report = report.prune_outdated
+
+puts "New report has #{new_report.size} entries: #{report.size - new_report.size} entries older than 90 days were removed."
+puts "Saved #{new_report_file}." if new_report.write(new_report_file)
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index cc1b1e5039e..b4fc2aa326f 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -72,11 +72,10 @@ describe Admin::ApplicationSettingsController do
expect(ApplicationSetting.current.restricted_visibility_levels).to eq([10, 20])
end
- it 'falls back to defaults when settings are omitted' do
- put :update, application_setting: {}
+ it 'updates the restricted_visibility_levels when empty array is passed' do
+ put :update, application_setting: { restricted_visibility_levels: [] }
expect(response).to redirect_to(admin_application_settings_path)
- expect(ApplicationSetting.current.default_project_visibility).to eq(Gitlab::VisibilityLevel::PRIVATE)
expect(ApplicationSetting.current.restricted_visibility_levels).to be_empty
end
end
diff --git a/spec/controllers/concerns/checks_collaboration_spec.rb b/spec/controllers/concerns/checks_collaboration_spec.rb
new file mode 100644
index 00000000000..1bd764290ae
--- /dev/null
+++ b/spec/controllers/concerns/checks_collaboration_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe ChecksCollaboration do
+ include ProjectForksHelper
+
+ let(:helper) do
+ fake_class = Class.new(ApplicationController) do
+ include ChecksCollaboration
+ end
+
+ fake_class.new
+ end
+
+ describe '#can_collaborate_with_project?' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:can?) do |user, ability, subject|
+ Ability.allowed?(user, ability, subject)
+ end
+ end
+
+ it 'is true if the user can push to the project' do
+ project.add_developer(user)
+
+ expect(helper.can_collaborate_with_project?(project)).to be_truthy
+ end
+
+ it 'is true when the user can push to a branch of the project' do
+ fake_access = double('Gitlab::UserAccess')
+ expect(fake_access).to receive(:can_push_to_branch?).with('a-branch').and_return(true)
+ expect(Gitlab::UserAccess).to receive(:new).with(user, project: project).and_return(fake_access)
+
+ expect(helper.can_collaborate_with_project?(project, ref: 'a-branch')).to be_truthy
+ end
+
+ context 'when the user has forked the project' do
+ before do
+ fork_project(project, user, namespace: user.namespace)
+ end
+
+ it 'is true' do
+ expect(helper.can_collaborate_with_project?(project)).to be_truthy
+ end
+
+ it 'is false when the project is archived' do
+ project.archived = true
+
+ expect(helper.can_collaborate_with_project?(project)).to be_falsy
+ end
+ end
+ end
+end
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 2be46049aab..be49b92d23f 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -223,11 +223,12 @@ describe Import::BitbucketController do
end
context 'user has chosen an existing nested namespace and name for the project', :postgresql do
- let(:parent_namespace) { create(:group, name: 'foo', owner: user) }
+ let(:parent_namespace) { create(:group, name: 'foo') }
let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
let(:test_name) { 'test_name' }
before do
+ parent_namespace.add_owner(user)
nested_namespace.add_owner(user)
end
@@ -273,7 +274,7 @@ describe Import::BitbucketController do
context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do
let(:test_name) { 'test_name' }
- let!(:parent_namespace) { create(:group, name: 'foo', owner: user) }
+ let!(:parent_namespace) { create(:group, name: 'foo') }
before do
parent_namespace.add_owner(user)
diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb
index e958be077c2..742f4787126 100644
--- a/spec/controllers/import/gitlab_controller_spec.rb
+++ b/spec/controllers/import/gitlab_controller_spec.rb
@@ -196,10 +196,11 @@ describe Import::GitlabController do
end
context 'user has chosen an existing nested namespace for the project', :postgresql do
- let(:parent_namespace) { create(:group, name: 'foo', owner: user) }
+ let(:parent_namespace) { create(:group, name: 'foo') }
let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
before do
+ parent_namespace.add_owner(user)
nested_namespace.add_owner(user)
end
@@ -245,7 +246,7 @@ describe Import::GitlabController do
context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do
let(:test_name) { 'test_name' }
- let!(:parent_namespace) { create(:group, name: 'foo', owner: user) }
+ let!(:parent_namespace) { create(:group, name: 'foo') }
before do
parent_namespace.add_owner(user)
diff --git a/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb b/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
new file mode 100644
index 00000000000..87c10a86cdd
--- /dev/null
+++ b/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Ldap::OmniauthCallbacksController do
+ include_context 'Ldap::OmniauthCallbacksController'
+
+ it 'allows sign in' do
+ post provider
+
+ expect(request.env['warden']).to be_authenticated
+ end
+
+ it 'respects remember me checkbox' do
+ expect do
+ post provider, remember_me: '1'
+ end.to change { user.reload.remember_created_at }.from(nil)
+ end
+
+ context 'with 2FA' do
+ let(:user) { create(:omniauth_user, :two_factor_via_otp, extern_uid: uid, provider: provider) }
+
+ it 'passes remember_me to the Devise view' do
+ post provider, remember_me: '1'
+
+ expect(assigns[:user].remember_me).to eq '1'
+ end
+ end
+
+ context 'access denied' do
+ let(:valid_login?) { false }
+
+ it 'warns the user' do
+ post provider
+
+ expect(flash[:alert]).to match(/Access denied for your LDAP account*/)
+ end
+
+ it "doesn't authenticate user" do
+ post provider
+
+ expect(request.env['warden']).not_to be_authenticated
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context 'sign up' do
+ let(:user) { double(email: 'new@example.com') }
+
+ before do
+ stub_omniauth_setting(block_auto_created_users: false)
+ end
+
+ it 'is allowed' do
+ post provider
+
+ expect(request.env['warden']).to be_authenticated
+ end
+ end
+end
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index de6ef919221..c621eb69171 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -125,7 +125,7 @@ describe ProfilesController, :request_store do
user.reload
expect(response.status).to eq(302)
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{new_username}/#{project.path}.git")).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, "#{new_username}/#{project.path}.git")).to be_truthy
end
end
@@ -143,7 +143,7 @@ describe ProfilesController, :request_store do
user.reload
expect(response.status).to eq(302)
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_truthy
expect(before_disk_path).to eq(project.disk_path)
end
end
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index c4b32dc3a09..e20623c0ac1 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -4,7 +4,11 @@ describe Projects::ForksController do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:forked_project) { Projects::ForkService.new(project, user).execute }
- let(:group) { create(:group, owner: forked_project.creator) }
+ let(:group) { create(:group) }
+
+ before do
+ group.add_owner(user)
+ end
describe 'GET index' do
def get_forks
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 01b5506b64b..ca86b0bc737 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -938,7 +938,7 @@ describe Projects::IssuesController do
end
describe 'POST create_merge_request' do
- let(:project) { create(:project, :repository) }
+ let(:project) { create(:project, :repository, :public) }
before do
project.add_developer(user)
@@ -955,6 +955,22 @@ describe Projects::IssuesController do
expect(response).to match_response_schema('merge_request')
end
+ it 'is not available when the project is archived' do
+ project.update!(archived: true)
+
+ create_merge_request
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'is not available for users who cannot create merge requests' do
+ sign_in(create(:user))
+
+ create_merge_request
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
def create_merge_request
post :create_merge_request, namespace_id: project.namespace.to_param,
project_id: project.to_param,
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index c3b71458e38..a102a3a3c8c 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -40,6 +40,30 @@ describe Projects::RepositoriesController do
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
end
+ it 'handles legacy queries with the ref specified as ref in params' do
+ get :archive, namespace_id: project.namespace, project_id: project, ref: 'feature', format: 'zip'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(assigns(:ref)).to eq('feature')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
+ end
+
+ it 'handles legacy queries with the ref specified as id in params' do
+ get :archive, namespace_id: project.namespace, project_id: project, id: 'feature', format: 'zip'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(assigns(:ref)).to eq('feature')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
+ end
+
+ it 'prioritizes the id param over the ref param when both are specified' do
+ get :archive, namespace_id: project.namespace, project_id: project, id: 'feature', ref: 'feature_conflict', format: 'zip'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(assigns(:ref)).to eq('feature')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
+ end
+
context "when the service raises an error" do
before do
allow(Gitlab::Workhorse).to receive(:send_git_archive).and_raise("Archive failed")
diff --git a/spec/db/production/settings_spec.rb b/spec/db/production/settings_spec.rb
index 79e67330854..c8d016070f5 100644
--- a/spec/db/production/settings_spec.rb
+++ b/spec/db/production/settings_spec.rb
@@ -2,10 +2,15 @@ require 'spec_helper'
require 'rainbow/ext/string'
describe 'seed production settings' do
- include StubENV
let(:settings_file) { Rails.root.join('db/fixtures/production/010_settings.rb') }
let(:settings) { Gitlab::CurrentSettings.current_application_settings }
+ before do
+ # It's important to set this variable so that we don't save a memoized
+ # (supposed to be) in-memory record in `Gitlab::CurrentSettings.in_memory_application_settings`
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ end
+
context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do
before do
stub_env('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN', '013456789')
diff --git a/spec/factories/award_emoji.rb b/spec/factories/award_emoji.rb
index a0abbbce686..d37e2bf511e 100644
--- a/spec/factories/award_emoji.rb
+++ b/spec/factories/award_emoji.rb
@@ -4,6 +4,10 @@ FactoryBot.define do
user
awardable factory: :issue
+ after(:create) do |award, evaluator|
+ award.awardable.project.add_guest(evaluator.user)
+ end
+
trait :upvote
trait :downvote do
name "thumbsdown"
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index bca7e920de4..4acc008ed38 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -62,6 +62,7 @@ FactoryBot.define do
end
trait :pending do
+ queued_at 'Di 29. Okt 09:50:59 CET 2013'
status 'pending'
end
@@ -206,7 +207,7 @@ FactoryBot.define do
options do
{
image: { name: 'ruby:2.1', entrypoint: '/bin/sh' },
- services: ['postgres', { name: 'docker:dind', entrypoint: '/bin/sh', command: 'sleep 30', alias: 'docker' }],
+ services: ['postgres', { name: 'docker:stable-dind', entrypoint: '/bin/sh', command: 'sleep 30', alias: 'docker' }],
after_script: %w(ls date),
artifacts: {
name: 'artifacts_file',
@@ -242,5 +243,10 @@ FactoryBot.define do
failed
failure_reason 1
end
+
+ trait :api_failure do
+ failed
+ failure_reason 2
+ end
end
end
diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb
index d5d819d862a..818f7b046f6 100644
--- a/spec/factories/commits.rb
+++ b/spec/factories/commits.rb
@@ -1,4 +1,4 @@
-require_relative '../support/repo_helpers'
+require_relative '../support/helpers/repo_helpers'
FactoryBot.define do
factory :commit do
diff --git a/spec/factories/deploy_tokens.rb b/spec/factories/deploy_tokens.rb
index 5fea4a9d5a6..017e866e69c 100644
--- a/spec/factories/deploy_tokens.rb
+++ b/spec/factories/deploy_tokens.rb
@@ -10,5 +10,13 @@ FactoryBot.define do
trait :revoked do
revoked true
end
+
+ trait :gitlab_deploy_token do
+ name DeployToken::GITLAB_DEPLOY_TOKEN_NAME
+ end
+
+ trait :expired do
+ expires_at { Date.today - 1.month }
+ end
end
end
diff --git a/spec/factories/gpg_key_subkeys.rb b/spec/factories/gpg_key_subkeys.rb
index 57eaaee345f..6c7db5379a9 100644
--- a/spec/factories/gpg_key_subkeys.rb
+++ b/spec/factories/gpg_key_subkeys.rb
@@ -1,5 +1,3 @@
-require_relative '../support/gpg_helpers'
-
FactoryBot.define do
factory :gpg_key_subkey do
gpg_key
diff --git a/spec/factories/gpg_keys.rb b/spec/factories/gpg_keys.rb
index b8aabf74221..51b8ddc9934 100644
--- a/spec/factories/gpg_keys.rb
+++ b/spec/factories/gpg_keys.rb
@@ -1,4 +1,4 @@
-require_relative '../support/gpg_helpers'
+require_relative '../support/helpers/gpg_helpers'
FactoryBot.define do
factory :gpg_key do
diff --git a/spec/factories/gpg_signature.rb b/spec/factories/gpg_signature.rb
index 4620caff823..b89e6ffc4b3 100644
--- a/spec/factories/gpg_signature.rb
+++ b/spec/factories/gpg_signature.rb
@@ -1,5 +1,3 @@
-require_relative '../support/gpg_helpers'
-
FactoryBot.define do
factory :gpg_signature do
commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 8c531cf5909..3b354c0d96b 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -5,6 +5,14 @@ FactoryBot.define do
type 'Group'
owner nil
+ after(:create) do |group|
+ if group.owner
+ # We could remove this after we have proper constraint:
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/43292
+ raise "Don't set owner for groups, use `group.add_owner(user)` instead"
+ end
+ end
+
trait :public do
visibility_level Gitlab::VisibilityLevel::PUBLIC
end
diff --git a/spec/factories/namespaces.rb b/spec/factories/namespaces.rb
index f94b09cff15..6feafa5ece9 100644
--- a/spec/factories/namespaces.rb
+++ b/spec/factories/namespaces.rb
@@ -2,6 +2,22 @@ FactoryBot.define do
factory :namespace do
sequence(:name) { |n| "namespace#{n}" }
path { name.downcase.gsub(/\s/, '_') }
- owner
+
+ # This is a workaround to avoid the user creating another namespace via
+ # User#ensure_namespace_correct. We should try to remove it and then
+ # we could remove this workaround
+ association :owner, factory: :user, strategy: :build
+ before(:create) do |namespace|
+ owner = namespace.owner
+
+ if owner
+ # We're changing the username here because we want to keep our path,
+ # and User#ensure_namespace_correct would change the path based on
+ # username, so we're forced to do this otherwise we'll need to change
+ # a lot of existing tests.
+ owner.username = namespace.path
+ owner.namespace = namespace
+ end
+ end
end
end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 857333f222d..40f3fa7d69b 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -1,4 +1,4 @@
-require_relative '../support/repo_helpers'
+require_relative '../support/helpers/repo_helpers'
include ActionDispatch::TestProcess
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 1761b6e2a3b..1904615778c 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -1,4 +1,4 @@
-require_relative '../support/test_env'
+require_relative '../support/helpers/test_env'
FactoryBot.define do
# Project without repository
@@ -147,7 +147,15 @@ FactoryBot.define do
# We delete hooks so that gitlab-shell will not try to authenticate with
# an API that isn't running
- FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.disk_path}.git", 'hooks'))
+ project.gitlab_shell.rm_directory(project.repository_storage,
+ File.join("#{project.disk_path}.git", 'hooks'))
+ end
+ end
+
+ trait :stubbed_repository do
+ after(:build) do |project|
+ allow(project).to receive(:empty_repo?).and_return(false)
+ allow(project.repository).to receive(:empty?).and_return(false)
end
end
@@ -165,7 +173,8 @@ FactoryBot.define do
after(:create) do |project|
raise "Failed to create repository!" unless project.create_repository
- FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.disk_path}.git", 'refs'))
+ project.gitlab_shell.rm_directory(project.repository_storage,
+ File.join("#{project.disk_path}.git", 'refs'))
end
end
diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb
new file mode 100644
index 00000000000..978113a08a4
--- /dev/null
+++ b/spec/fast_spec_helper.rb
@@ -0,0 +1,16 @@
+require 'bundler/setup'
+
+ENV['GITLAB_ENV'] = 'test'
+ENV['IN_MEMORY_APPLICATION_SETTINGS'] = 'true'
+
+unless Object.respond_to?(:require_dependency)
+ class Object
+ alias_method :require_dependency, :require
+ end
+end
+
+# Defines Settings and Gitlab.config which are at the center of the app
+require_relative '../config/settings'
+require_relative '../lib/gitlab' unless defined?(Gitlab.config)
+
+require_relative 'support/rspec'
diff --git a/spec/features/admin/admin_broadcast_messages_spec.rb b/spec/features/admin/admin_broadcast_messages_spec.rb
index 9cb351282a0..430a8d22b0f 100644
--- a/spec/features/admin/admin_broadcast_messages_spec.rb
+++ b/spec/features/admin/admin_broadcast_messages_spec.rb
@@ -45,7 +45,7 @@ feature 'Admin Broadcast Messages' do
page.within('.broadcast-message-preview') do
expect(page).to have_selector('strong', text: 'Markdown')
- expect(page).to have_selector('gl-emoji[data-name="tada"]')
+ expect(page).to have_emoji('tada')
end
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 846b8040be6..7853d2952ea 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -32,6 +32,29 @@ feature 'Admin updates settings' do
expect(find('#application_setting_visibility_level_20')).not_to be_checked
end
+ scenario 'Modify import sources' do
+ expect(Gitlab::CurrentSettings.import_sources).not_to be_empty
+
+ page.within('.as-visibility-access') do
+ Gitlab::ImportSources.options.map do |name, _|
+ uncheck name
+ end
+
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(Gitlab::CurrentSettings.import_sources).to be_empty
+
+ page.within('.as-visibility-access') do
+ check "Repo by URL"
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(Gitlab::CurrentSettings.import_sources).to eq(['git'])
+ end
+
scenario 'Change Visibility and Access Controls' do
page.within('.as-visibility-access') do
uncheck 'Project export enabled'
@@ -62,6 +85,26 @@ feature 'Admin updates settings' do
expect(page).to have_content "Application settings saved successfully"
end
+ scenario 'Modify oauth providers' do
+ expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to be_empty
+
+ page.within('.as-signin') do
+ uncheck 'Google'
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to include('google_oauth2')
+
+ page.within('.as-signin') do
+ check "Google"
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).not_to include('google_oauth2')
+ end
+
scenario 'Change Help page' do
page.within('.as-help-page') do
fill_in 'Help page text', with: 'Example text'
@@ -211,16 +254,6 @@ feature 'Admin updates settings' do
expect(find('#service_push_channel').value).to eq '#test_channel'
end
- context 'sign-in restrictions', :js do
- it 'de-activates oauth sign-in source' do
- page.within('.as-signin') do
- 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
- end
- end
-
scenario 'Change Keys settings' do
page.within('.as-visibility-access') do
select 'Are forbidden', from: 'RSA SSH keys'
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index 6769acb7c9c..e880f0096c1 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -63,6 +63,13 @@ describe 'Issue Boards new issue', :js do
page.within(first('.board .issue-count-badge-count')) do
expect(page).to have_content('1')
end
+
+ page.within(first('.card')) do
+ issue = project.issues.find_by_title('bug')
+
+ expect(page).to have_content(issue.to_reference)
+ expect(page).to have_link(issue.title, href: issue_path(issue))
+ end
end
it 'shows sidebar when creating new issue' do
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index d4c44c1adf9..4d31123a699 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -237,6 +237,22 @@ describe 'Issue Boards', :js do
end
context 'labels' do
+ it 'shows current labels when editing' do
+ click_card(card)
+
+ page.within('.labels') do
+ click_link 'Edit'
+
+ wait_for_requests
+
+ page.within('.value') do
+ expect(page).to have_selector('.label', count: 2)
+ expect(page).to have_content(development.title)
+ expect(page).to have_content(stretch.title)
+ end
+ end
+ end
+
it 'adds a single label' do
click_card(card)
@@ -296,7 +312,9 @@ describe 'Issue Boards', :js do
wait_for_requests
- click_link stretch.title
+ within('.dropdown-menu-labels') do
+ click_link stretch.title
+ end
wait_for_requests
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 986f864f0b5..257a3822503 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -89,7 +89,7 @@ feature 'Dashboard Projects' do
end
describe 'with a pipeline', :clean_gitlab_redis_shared_state do
- let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) }
+ let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha, ref: project.default_branch) }
before do
# Since the cache isn't updated when a new pipeline is created
@@ -102,7 +102,7 @@ feature 'Dashboard Projects' do
visit dashboard_projects_path
page.within('.controls') do
- expect(page).to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit)}']")
+ expect(page).to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
expect(page).to have_css('.ci-status-link')
expect(page).to have_css('.ci-status-icon-success')
expect(page).to have_link('Commit: passed')
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
index 4ffadbbcd35..3a0424d60f8 100644
--- a/spec/features/groups/show_spec.rb
+++ b/spec/features/groups/show_spec.rb
@@ -98,7 +98,7 @@ feature 'Group show page' do
it 'shows the project info' do
expect(page).to have_content(project.title)
- expect(page).to have_selector('gl-emoji[data-name="smile"]')
+ expect(page).to have_emoji('smile')
end
end
end
diff --git a/spec/features/ide_spec.rb b/spec/features/ide_spec.rb
new file mode 100644
index 00000000000..b3f24c2966d
--- /dev/null
+++ b/spec/features/ide_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe 'IDE', :js do
+ describe 'sub-groups' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:subgroup) { create(:group, parent: group) }
+ let(:subgroup_project) { create(:project, :repository, namespace: subgroup) }
+
+ before do
+ subgroup_project.add_master(user)
+ sign_in(user)
+
+ visit project_path(subgroup_project)
+
+ click_link('Web IDE')
+
+ wait_for_requests
+ end
+
+ it 'loads project in web IDE' do
+ expect(page).to have_selector('.context-header', text: subgroup_project.name)
+ end
+ end
+end
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 27551bb70ee..830c794376d 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -5,9 +5,9 @@ feature 'Issue Sidebar' do
let(:group) { create(:group, :nested) }
let(:project) { create(:project, :public, namespace: group) }
- let(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
let!(:label) { create(:label, project: project, title: 'bug') }
+ let(:issue) { create(:labeled_issue, project: project, labels: [label]) }
let!(:xss_label) { create(:label, project: project, title: '&lt;script&gt;alert("xss");&lt;&#x2F;script&gt;') }
before do
@@ -112,11 +112,18 @@ feature 'Issue Sidebar' do
context 'editing issue labels', :js do
before do
+ issue.update_attributes(labels: [label])
page.within('.block.labels') do
find('.edit-link').click
end
end
+ it 'shows the current set of labels' do
+ page.within('.issuable-show-labels') do
+ expect(page).to have_content label.title
+ end
+ end
+
it 'shows option to create a project label' do
page.within('.block.labels') do
expect(page).to have_content 'Create project'
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index 8e6493bbd93..4a44ec302fc 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -14,7 +14,7 @@ feature 'Manually create a todo item from issue', :js do
it 'creates todo when clicking button' do
page.within '.issuable-sidebar' do
click_button 'Add todo'
- expect(page).to have_content 'Mark done'
+ expect(page).to have_content 'Mark todo as done'
end
page.within '.header-content .todos-count' do
@@ -31,7 +31,7 @@ feature 'Manually create a todo item from issue', :js do
it 'marks a todo as done' do
page.within '.issuable-sidebar' do
click_button 'Add todo'
- click_button 'Mark done'
+ click_button 'Mark todo as done'
end
expect(page).to have_selector('.todos-count', visible: false)
diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb
index 3e05e7b7f38..ae41f611ddc 100644
--- a/spec/features/labels_hierarchy_spec.rb
+++ b/spec/features/labels_hierarchy_spec.rb
@@ -170,6 +170,8 @@ feature 'Labels Hierarchy', :js, :nested_groups do
context 'on issue sidebar' do
before do
+ project_1.add_developer(user)
+
visit project_issue_path(project_1, issue)
end
@@ -180,6 +182,8 @@ feature 'Labels Hierarchy', :js, :nested_groups do
let(:board) { create(:board, project: project_1) }
before do
+ project_1.add_developer(user)
+
visit project_board_path(project_1, board)
wait_for_requests
@@ -194,6 +198,8 @@ feature 'Labels Hierarchy', :js, :nested_groups do
let(:board) { create(:board, group: parent) }
before do
+ parent.add_developer(user)
+
visit group_board_path(parent, board)
wait_for_requests
@@ -211,6 +217,8 @@ feature 'Labels Hierarchy', :js, :nested_groups do
context 'on project issuable list' do
before do
+ project_1.add_developer(user)
+
visit project_issues_path(project_1)
end
@@ -237,6 +245,8 @@ feature 'Labels Hierarchy', :js, :nested_groups do
let(:board) { create(:board, project: project_1) }
before do
+ project_1.add_developer(user)
+
visit project_board_path(project_1, board)
end
@@ -247,6 +257,8 @@ feature 'Labels Hierarchy', :js, :nested_groups do
let(:board) { create(:board, group: parent) }
before do
+ parent.add_developer(user)
+
visit group_board_path(parent, board)
end
@@ -259,6 +271,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do
let(:board) { create(:board, project: project_1) }
before do
+ project_1.add_developer(user)
visit project_board_path(project_1, board)
find('.js-new-board-list').click
wait_for_requests
@@ -281,6 +294,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do
let(:board) { create(:board, group: parent) }
before do
+ parent.add_developer(user)
visit group_board_path(parent, board)
find('.js-new-board-list').click
wait_for_requests
diff --git a/spec/features/merge_request/user_awards_emoji_spec.rb b/spec/features/merge_request/user_awards_emoji_spec.rb
index 2f24cfbd9e3..859a4c65562 100644
--- a/spec/features/merge_request/user_awards_emoji_spec.rb
+++ b/spec/features/merge_request/user_awards_emoji_spec.rb
@@ -35,6 +35,14 @@ describe 'Merge request > User awards emoji', :js do
expect(page).to have_selector('.emoji-menu', count: 1)
end
+
+ describe 'the project is archived' do
+ let(:project) { create(:project, :public, :repository, :archived) }
+
+ it 'does not see award menu button' do
+ expect(page).not_to have_selector('.js-award-holder')
+ end
+ end
end
describe 'logged out' do
diff --git a/spec/features/merge_request/user_cherry_picks_spec.rb b/spec/features/merge_request/user_cherry_picks_spec.rb
index 494096b21c0..61d1bdaa95a 100644
--- a/spec/features/merge_request/user_cherry_picks_spec.rb
+++ b/spec/features/merge_request/user_cherry_picks_spec.rb
@@ -40,6 +40,14 @@ describe 'Merge request > User cherry-picks', :js do
expect(page).to have_link 'Cherry-pick'
end
+
+ it 'hides the cherry pick button for an archived project' do
+ project.update!(archived: true)
+
+ visit project_merge_request_path(project, merge_request)
+
+ expect(page).not_to have_link 'Cherry-pick'
+ end
end
end
end
diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
index b4ad4b64d8e..0fd2840c426 100644
--- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
+++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
@@ -5,7 +5,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
let(:user) { project.creator }
let(:guest) { create(:user) }
let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
- let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
+ let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, note: "| Markdown | Table |\n|-------|---------|\n| first | second |") }
let(:path) { "files/ruby/popen.rb" }
let(:position) do
Gitlab::Diff::Position.new(
@@ -111,6 +111,15 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
expect(page.find(".line-holder-placeholder")).to be_visible
expect(page.find(".timeline-content #note_#{note.id}")).to be_visible
end
+
+ it 'renders tables in lazy-loaded resolved diff dicussions' do
+ find(".timeline-content .discussion[data-discussion-id='#{note.discussion_id}'] .discussion-toggle-button").click
+
+ wait_for_requests
+
+ expect(page.find(".timeline-content #note_#{note.id}")).not_to have_css(".line_holder")
+ expect(page.find(".timeline-content #note_#{note.id}")).to have_css("tr", count: 2)
+ end
end
describe 'side-by-side view' do
diff --git a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
index 565e375600b..3b6fffb7abd 100644
--- a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
+++ b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
@@ -27,6 +27,23 @@ describe 'Merge request > User scrolls to note on load', :js do
expect(fragment_position_top).to be < (page_scroll_y + page_height)
end
+ it 'renders un-collapsed notes with diff' do
+ page.current_window.resize_to(1000, 1000)
+
+ visit "#{project_merge_request_path(project, merge_request)}#{fragment_id}"
+
+ page.execute_script "window.scrollTo(0,0)"
+
+ note_element = find(fragment_id)
+ note_container = note_element.ancestor('.js-toggle-container')
+
+ expect(note_element.visible?).to eq true
+
+ page.within note_container do
+ expect(page).not_to have_selector('.js-error-lazy-load-diff')
+ end
+ end
+
it 'expands collapsed notes' do
visit "#{project_merge_request_path(project, merge_request)}#{collapsed_fragment_id}"
note_element = find(collapsed_fragment_id)
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index 19152bf1f0f..6c51e4bbe26 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -108,4 +108,18 @@ feature 'Milestone' do
expect(page).to have_selector('.js-delete-milestone-button', count: 0)
end
end
+
+ feature 'deprecation popover', :js do
+ it 'opens deprecation popover' do
+ milestone = create(:milestone, project: project)
+
+ visit group_milestone_path(group, milestone, title: milestone.title)
+
+ expect(page).to have_selector('.milestone-deprecation-message')
+
+ find('.milestone-deprecation-message .js-popover-link').click
+
+ expect(page).to have_selector('.milestone-deprecation-message .popover')
+ end
+ end
end
diff --git a/spec/features/milestones/show_spec.rb b/spec/features/milestones/show_spec.rb
deleted file mode 100644
index 50c5e0bb65f..00000000000
--- a/spec/features/milestones/show_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require 'rails_helper'
-
-describe 'Milestone show' do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:milestone) { create(:milestone, project: project) }
- let(:labels) { create_list(:label, 2, project: project) }
- let(:issue_params) { { project: project, assignees: [user], author: user, milestone: milestone, labels: labels } }
-
- before do
- project.add_user(user, :developer)
- sign_in(user)
- end
-
- def visit_milestone
- visit project_milestone_path(project, milestone)
- end
-
- it 'avoids N+1 database queries' do
- create(:labeled_issue, issue_params)
- control = ActiveRecord::QueryRecorder.new { visit_milestone }
- create_list(:labeled_issue, 10, issue_params)
-
- expect { visit_milestone }.not_to exceed_query_limit(control)
- end
-end
diff --git a/spec/features/milestones/user_creates_milestone_spec.rb b/spec/features/milestones/user_creates_milestone_spec.rb
new file mode 100644
index 00000000000..8fd057d587c
--- /dev/null
+++ b/spec/features/milestones/user_creates_milestone_spec.rb
@@ -0,0 +1,29 @@
+require "rails_helper"
+
+describe "User creates milestone", :js do
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ visit(new_project_milestone_path(project))
+ end
+
+ it "creates milestone" do
+ TITLE = "v2.3".freeze
+
+ fill_in("Title", with: TITLE)
+ fill_in("Description", with: "# Description header")
+ click_button("Create milestone")
+
+ expect(page).to have_content(TITLE)
+ .and have_content("Issues")
+ .and have_header_with_correct_id_and_link(1, "Description header", "description-header")
+
+ visit(activity_project_path(project))
+
+ expect(page).to have_content("#{user.name} opened milestone")
+ end
+end
diff --git a/spec/features/milestones/user_deletes_milestone_spec.rb b/spec/features/milestones/user_deletes_milestone_spec.rb
new file mode 100644
index 00000000000..414702daba4
--- /dev/null
+++ b/spec/features/milestones/user_deletes_milestone_spec.rb
@@ -0,0 +1,25 @@
+require "rails_helper"
+
+describe "User deletes milestone", :js do
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+ set(:milestone) { create(:milestone, project: project) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ visit(project_milestones_path(project))
+ end
+
+ it "deletes milestone" do
+ click_button("Delete")
+ click_button("Delete milestone")
+
+ expect(page).to have_content("No milestones to show")
+
+ visit(activity_project_path(project))
+
+ expect(page).to have_content("#{user.name} destroyed milestone")
+ end
+end
diff --git a/spec/features/milestones/user_views_milestone_spec.rb b/spec/features/milestones/user_views_milestone_spec.rb
new file mode 100644
index 00000000000..83d8e2ff9e9
--- /dev/null
+++ b/spec/features/milestones/user_views_milestone_spec.rb
@@ -0,0 +1,31 @@
+require "rails_helper"
+
+describe "User views milestone" do
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+ set(:milestone) { create(:milestone, project: project) }
+ set(:labels) { create_list(:label, 2, project: project) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
+
+ it "avoids N+1 database queries" do
+ ISSUE_PARAMS = { project: project, assignees: [user], author: user, milestone: milestone, labels: labels }.freeze
+
+ create(:labeled_issue, ISSUE_PARAMS)
+
+ control = ActiveRecord::QueryRecorder.new { visit_milestone }
+
+ create(:labeled_issue, ISSUE_PARAMS)
+
+ expect { visit_milestone }.not_to exceed_query_limit(control)
+ end
+
+ private
+
+ def visit_milestone
+ visit(project_milestone_path(project, milestone))
+ end
+end
diff --git a/spec/features/milestones/user_views_milestones_spec.rb b/spec/features/milestones/user_views_milestones_spec.rb
new file mode 100644
index 00000000000..bebe40f73fd
--- /dev/null
+++ b/spec/features/milestones/user_views_milestones_spec.rb
@@ -0,0 +1,35 @@
+require "rails_helper"
+
+describe "User views milestones" do
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+ set(:milestone) { create(:milestone, project: project) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ visit(project_milestones_path(project))
+ end
+
+ it "shows milestone" do
+ expect(page).to have_content(milestone.title)
+ .and have_content(milestone.expires_at)
+ .and have_content("Issues")
+ end
+
+ context "with issues" do
+ set(:issue) { create(:issue, project: project, milestone: milestone) }
+ set(:closed_issue) { create(:closed_issue, project: project, milestone: milestone) }
+
+ it "opens milestone" do
+ click_link(milestone.title)
+
+ expect(current_path).to eq(project_milestone_path(project, milestone))
+ expect(page).to have_content(milestone.title)
+ .and have_selector("#tab-issues li.issuable-row", count: 2)
+ .and have_content(issue.title)
+ .and have_content(closed_issue.title)
+ end
+ end
+end
diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb
index a5e325ee2e3..013cdaa6479 100644
--- a/spec/features/oauth_login_spec.rb
+++ b/spec/features/oauth_login_spec.rb
@@ -28,35 +28,46 @@ feature 'OAuth Login', :js, :allow_forgery_protection do
OmniAuth.config.full_host = @omniauth_config_full_host
end
+ def login_with_provider(provider, enter_two_factor: false)
+ login_via(provider.to_s, user, uid, remember_me: remember_me)
+ enter_code(user.current_otp) if enter_two_factor
+ end
+
providers.each do |provider|
context "when the user logs in using the #{provider} provider" do
+ let(:uid) { 'my-uid' }
+ let(:remember_me) { false }
+ let(:user) { create(:omniauth_user, extern_uid: uid, provider: provider.to_s) }
+ let(:two_factor_user) { create(:omniauth_user, :two_factor, extern_uid: uid, provider: provider.to_s) }
+
+ before do
+ stub_omniauth_config(provider)
+ end
+
context 'when two-factor authentication is disabled' do
it 'logs the user in' do
- stub_omniauth_config(provider)
- user = create(:omniauth_user, extern_uid: 'my-uid', provider: provider.to_s)
- login_via(provider.to_s, user, 'my-uid')
+ login_with_provider(provider)
expect(current_path).to eq root_path
end
end
context 'when two-factor authentication is enabled' do
+ let(:user) { two_factor_user }
+
it 'logs the user in' do
- stub_omniauth_config(provider)
- user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider.to_s)
- login_via(provider.to_s, user, 'my-uid')
+ login_with_provider(provider, enter_two_factor: true)
- enter_code(user.current_otp)
expect(current_path).to eq root_path
end
end
context 'when "remember me" is checked' do
+ let(:remember_me) { true }
+
context 'when two-factor authentication is disabled' do
it 'remembers the user after a browser restart' do
- stub_omniauth_config(provider)
- user = create(:omniauth_user, extern_uid: 'my-uid', provider: provider.to_s)
- login_via(provider.to_s, user, 'my-uid', remember_me: true)
+ login_with_provider(provider)
clear_browser_session
@@ -66,11 +77,10 @@ feature 'OAuth Login', :js, :allow_forgery_protection do
end
context 'when two-factor authentication is enabled' do
+ let(:user) { two_factor_user }
+
it 'remembers the user after a browser restart' do
- stub_omniauth_config(provider)
- user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider.to_s)
- login_via(provider.to_s, user, 'my-uid', remember_me: true)
- enter_code(user.current_otp)
+ login_with_provider(provider, enter_two_factor: true)
clear_browser_session
@@ -83,9 +93,7 @@ feature 'OAuth Login', :js, :allow_forgery_protection do
context 'when "remember me" is not checked' do
context 'when two-factor authentication is disabled' do
it 'does not remember the user after a browser restart' do
- stub_omniauth_config(provider)
- user = create(:omniauth_user, extern_uid: 'my-uid', provider: provider.to_s)
- login_via(provider.to_s, user, 'my-uid', remember_me: false)
+ login_with_provider(provider)
clear_browser_session
@@ -95,11 +103,10 @@ feature 'OAuth Login', :js, :allow_forgery_protection do
end
context 'when two-factor authentication is enabled' do
+ let(:user) { two_factor_user }
+
it 'does not remember the user after a browser restart' do
- stub_omniauth_config(provider)
- user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider.to_s)
- login_via(provider.to_s, user, 'my-uid', remember_me: false)
- enter_code(user.current_otp)
+ login_with_provider(provider, enter_two_factor: true)
clear_browser_session
diff --git a/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb b/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb
index adff0a10f0e..12e07647ecd 100644
--- a/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb
+++ b/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb
@@ -99,6 +99,74 @@ describe 'User interacts with awards in an issue', :js do
click_button('Comment')
end
- expect(page).to have_selector('gl-emoji[data-name="smile"]')
+ expect(page).to have_emoji('smile')
+ end
+
+ context 'when a project is archived' do
+ let(:project) { create(:project, :archived) }
+
+ it 'hides the add award button' do
+ page.within('.awards') do
+ expect(page).not_to have_css('.js-add-award')
+ end
+ end
+ end
+
+ context 'awards on a note' do
+ let!(:note) { create(:note, noteable: issue, project: issue.project) }
+ let!(:award_emoji) { create(:award_emoji, awardable: note, name: '100') }
+
+ it 'shows the award on the note' do
+ page.within('.note-awards') do
+ expect(page).to have_emoji('100')
+ end
+ end
+
+ it 'allows adding a vote to an award' do
+ page.within('.note-awards') do
+ find('gl-emoji[data-name="100"]').click
+ end
+ wait_for_requests
+
+ expect(note.reload.award_emoji.size).to eq(2)
+ end
+
+ it 'allows adding a new emoji' do
+ page.within('.note-actions') do
+ find('a.js-add-award').click
+ end
+ page.within('.emoji-menu-content') do
+ find('gl-emoji[data-name="8ball"]').click
+ end
+ wait_for_requests
+
+ page.within('.note-awards') do
+ expect(page).to have_emoji('8ball')
+ end
+ expect(note.reload.award_emoji.size).to eq(2)
+ end
+
+ context 'when the project is archived' do
+ let(:project) { create(:project, :archived) }
+
+ it 'hides the buttons for adding new emoji' do
+ page.within('.note-awards') do
+ expect(page).not_to have_css('.award-menu-holder')
+ end
+
+ page.within('.note-actions') do
+ expect(page).not_to have_css('a.js-add-award')
+ end
+ end
+
+ it 'does not allow toggling existing emoji' do
+ page.within('.note-awards') do
+ find('gl-emoji[data-name="100"]').click
+ end
+ wait_for_requests
+
+ expect(note.reload.award_emoji.size).to eq(1)
+ end
+ end
end
end
diff --git a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb
new file mode 100644
index 00000000000..b7d063596c1
--- /dev/null
+++ b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+feature 'User creates blob in new project', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :empty_repo) }
+
+ shared_examples 'creating a file' do
+ before do
+ sign_in(user)
+ visit project_path(project)
+ end
+
+ it 'allows the user to add a new file' do
+ click_link 'New file'
+
+ find('#editor')
+ execute_script('ace.edit("editor").setValue("Hello world")')
+
+ fill_in(:file_name, with: 'dummy-file')
+
+ click_button('Commit changes')
+
+ expect(page).to have_content('The file has been successfully created')
+ end
+ end
+
+ describe 'as a master' do
+ before do
+ project.add_master(user)
+ end
+
+ it_behaves_like 'creating a file'
+ end
+
+ describe 'as an admin' do
+ let(:user) { create(:user, :admin) }
+
+ it_behaves_like 'creating a file'
+ end
+
+ describe 'as a developer' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ visit project_path(project)
+ end
+
+ it 'does not allow pushing to the default branch' do
+ expect(page).not_to have_content('New file')
+ end
+ end
+end
diff --git a/spec/features/projects/branches/user_creates_branch_spec.rb b/spec/features/projects/branches/user_creates_branch_spec.rb
new file mode 100644
index 00000000000..b706ad64954
--- /dev/null
+++ b/spec/features/projects/branches/user_creates_branch_spec.rb
@@ -0,0 +1,46 @@
+require "spec_helper"
+
+describe "User creates branch", :js do
+ include Spec::Support::Helpers::Features::BranchesHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ visit(new_project_branch_path(project))
+ end
+
+ it "creates new branch" do
+ BRANCH_NAME = "deploy_keys".freeze
+
+ create_branch(BRANCH_NAME)
+
+ expect(page).to have_content(BRANCH_NAME)
+ end
+
+ context "when branch name is invalid" do
+ it "does not create new branch" do
+ INVALID_BRANCH_NAME = "1.0 stable".freeze
+
+ fill_in("branch_name", with: INVALID_BRANCH_NAME)
+ page.find("body").click # defocus the branch_name input
+
+ select_branch("master")
+ click_button("Create branch")
+
+ expect(page).to have_content("Branch name is invalid")
+ expect(page).to have_content("can't contain spaces")
+ end
+ end
+
+ context "when branch name already exists" do
+ it "does not create new branch" do
+ create_branch("master")
+
+ expect(page).to have_content("Branch already exists")
+ end
+ end
+end
diff --git a/spec/features/projects/branches/user_deletes_branch_spec.rb b/spec/features/projects/branches/user_deletes_branch_spec.rb
new file mode 100644
index 00000000000..96f215e1606
--- /dev/null
+++ b/spec/features/projects/branches/user_deletes_branch_spec.rb
@@ -0,0 +1,23 @@
+require "spec_helper"
+
+describe "User deletes branch", :js do
+ set(:user) { create(:user) }
+ set(:project) { create(:project, :repository) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ visit(project_branches_path(project))
+ end
+
+ it "deletes branch" do
+ fill_in("branch-search", with: "improve/awesome").native.send_keys(:enter)
+
+ page.within(".js-branch-improve\\/awesome") do
+ accept_alert { find(".btn-remove").click }
+ end
+
+ expect(page).to have_css(".js-branch-improve\\/awesome", visible: :hidden)
+ end
+end
diff --git a/spec/features/projects/branches/user_views_branches_spec.rb b/spec/features/projects/branches/user_views_branches_spec.rb
new file mode 100644
index 00000000000..62ae793151c
--- /dev/null
+++ b/spec/features/projects/branches/user_views_branches_spec.rb
@@ -0,0 +1,34 @@
+require "spec_helper"
+
+describe "User views branches" do
+ set(:project) { create(:project, :repository) }
+ set(:user) { project.owner }
+
+ before do
+ sign_in(user)
+ end
+
+ context "all branches" do
+ before do
+ visit(project_branches_path(project))
+ end
+
+ it "shows branches" do
+ expect(page).to have_content("Branches").and have_content("master")
+ end
+ end
+
+ context "protected branches" do
+ set(:protected_branch) { create(:protected_branch, project: project) }
+
+ before do
+ visit(project_protected_branches_path(project))
+ end
+
+ it "shows branches" do
+ page.within(".protected-branches-list") do
+ expect(page).to have_content(protected_branch.name).and have_no_content("master")
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 2a9d9e6416c..b7ce1b9993a 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -195,6 +195,26 @@ describe 'Branches' do
expect(page).to have_content("Protected branches can be managed in project settings")
end
end
+
+ it 'shows the merge request button' do
+ visit project_branches_path(project)
+
+ page.within first('.all-branches li') do
+ expect(page).to have_content 'Merge request'
+ end
+ end
+
+ context 'when the project is archived' do
+ let(:project) { create(:project, :public, :repository, :archived) }
+
+ it 'does not show the merge request button when the project is archived' do
+ visit project_branches_path(project)
+
+ page.within first('.all-branches li') do
+ expect(page).not_to have_content 'Merge request'
+ end
+ end
+ end
end
context 'logged out' do
@@ -204,7 +224,7 @@ describe 'Branches' do
it 'does not show merge request button' do
page.within first('.all-branches li') do
- expect(page).not_to have_content 'Merge Request'
+ expect(page).not_to have_content 'Merge request'
end
end
end
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index 4d47cdb500c..dfe8e02dce0 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -33,7 +33,7 @@ feature 'Gcp Cluster', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
- click_link 'Create on GKE'
+ click_link 'Create on Google Kubernetes Engine'
end
context 'when user filled form with valid parameters' do
@@ -139,7 +139,7 @@ feature 'Gcp Cluster', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
- click_link 'Create on GKE'
+ click_link 'Create on Google Kubernetes Engine'
fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
fill_in 'cluster_name', with: 'dev-cluster'
@@ -159,7 +159,7 @@ feature 'Gcp Cluster', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
- click_link 'Create on GKE'
+ click_link 'Create on Google Kubernetes Engine'
fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
fill_in 'cluster_name', with: 'dev-cluster'
@@ -177,7 +177,7 @@ feature 'Gcp Cluster', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
- click_link 'Create on GKE'
+ click_link 'Create on Google Kubernetes Engine'
end
it 'user sees a login page' do
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index bd9f7745cf8..a251a2f4e52 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -83,7 +83,7 @@ feature 'Clusters', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
- click_link 'Create on GKE'
+ click_link 'Create on Google Kubernetes Engine'
end
it 'user sees a login page' do
diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index c4c399e3058..1df45865d6f 100644
--- a/spec/features/projects/commit/cherry_pick_spec.rb
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
@@ -89,4 +89,15 @@ describe 'Cherry-pick Commits' do
expect(page).to have_content('The commit has been successfully cherry-picked.')
end
end
+
+ context 'when the project is archived' do
+ let(:project) { create(:project, :repository, :archived, namespace: group) }
+
+ it 'does not show the cherry-pick link' do
+ find('.header-action-buttons a.dropdown-toggle').click
+
+ expect(page).not_to have_text("Cherry-pick")
+ expect(page).not_to have_css("a[href='#modal-cherry-pick-commit']")
+ end
+ end
end
diff --git a/spec/features/projects/commit/user_comments_on_commit_spec.rb b/spec/features/projects/commit/user_comments_on_commit_spec.rb
new file mode 100644
index 00000000000..5174f793367
--- /dev/null
+++ b/spec/features/projects/commit/user_comments_on_commit_spec.rb
@@ -0,0 +1,110 @@
+require "spec_helper"
+
+describe "User comments on commit", :js do
+ include Spec::Support::Helpers::Features::NotesHelpers
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ COMMENT_TEXT = "XML attached".freeze
+
+ before do
+ sign_in(user)
+ project.add_developer(user)
+
+ visit(project_commit_path(project, sample_commit.id))
+ end
+
+ context "when adding new comment" do
+ it "adds comment" do
+ EMOJI = ":+1:".freeze
+
+ page.within(".js-main-target-form") do
+ expect(page).not_to have_link("Cancel")
+
+ fill_in("note[note]", with: "#{COMMENT_TEXT} #{EMOJI}")
+
+ # Check on `Preview` tab
+ click_link("Preview")
+
+ expect(find(".js-md-preview")).to have_content(COMMENT_TEXT).and have_css("gl-emoji")
+ expect(page).not_to have_css(".js-note-text")
+
+ # Check on `Write` tab
+ click_link("Write")
+
+ expect(page).to have_field("note[note]", with: "#{COMMENT_TEXT} #{EMOJI}")
+
+ # Submit comment from the `Preview` tab to get rid of a separate `it` block
+ # which would specially tests if everything gets cleared from the note form.
+ click_link("Preview")
+ click_button("Comment")
+ end
+
+ wait_for_requests
+
+ page.within(".note") do
+ expect(page).to have_content(COMMENT_TEXT).and have_css("gl-emoji")
+ end
+
+ page.within(".js-main-target-form") do
+ expect(page).to have_field("note[note]", with: "").and have_no_css(".js-md-preview")
+ end
+ end
+ end
+
+ context "when editing comment" do
+ before do
+ add_note(COMMENT_TEXT)
+ end
+
+ it "edits comment" do
+ NEW_COMMENT_TEXT = "+1 Awesome!".freeze
+
+ page.within(".main-notes-list") do
+ note = find(".note")
+ note.hover
+
+ note.find(".js-note-edit").click
+ end
+
+ page.find(".current-note-edit-form textarea")
+
+ page.within(".current-note-edit-form") do
+ fill_in("note[note]", with: NEW_COMMENT_TEXT)
+ click_button("Save comment")
+ end
+
+ wait_for_requests
+
+ page.within(".note") do
+ expect(page).to have_content(NEW_COMMENT_TEXT)
+ end
+ end
+ end
+
+ context "when deleting comment" do
+ before do
+ add_note(COMMENT_TEXT)
+ end
+
+ it "deletes comment" do
+ page.within(".note") do
+ expect(page).to have_content(COMMENT_TEXT)
+ end
+
+ page.within(".main-notes-list") do
+ note = find(".note")
+ note.hover
+
+ find(".more-actions").click
+ find(".more-actions .dropdown-menu li", match: :first)
+
+ accept_confirm { find(".js-note-delete").click }
+ end
+
+ expect(page).not_to have_css(".note")
+ end
+ end
+end
diff --git a/spec/features/projects/commit/user_reverts_commit_spec.rb b/spec/features/projects/commit/user_reverts_commit_spec.rb
index 221f1d7757e..42844a03ea6 100644
--- a/spec/features/projects/commit/user_reverts_commit_spec.rb
+++ b/spec/features/projects/commit/user_reverts_commit_spec.rb
@@ -10,13 +10,16 @@ describe 'User reverts a commit', :js do
sign_in(user)
visit(project_commit_path(project, sample_commit.id))
+ end
+ def click_revert
find('.header-action-buttons .dropdown').click
find('a[href="#modal-revert-commit"]').click
end
context 'without creating a new merge request' do
before do
+ click_revert
page.within('#modal-revert-commit') do
uncheck('create_merge_request')
click_button('Revert')
@@ -44,6 +47,10 @@ describe 'User reverts a commit', :js do
end
context 'with creating a new merge request' do
+ before do
+ click_revert
+ end
+
it 'reverts a commit' do
page.within('#modal-revert-commit') do
click_button('Revert')
@@ -53,4 +60,14 @@ describe 'User reverts a commit', :js do
expect(page).to have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master")
end
end
+
+ context 'when the project is archived' do
+ let(:project) { create(:project, :repository, :archived, namespace: user.namespace) }
+
+ it 'does not show the revert link' do
+ find('.header-action-buttons .dropdown').click
+
+ expect(page).not_to have_link('Revert')
+ end
+ end
end
diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb
index 523a9f3f4fe..dc6e4fd27cb 100644
--- a/spec/features/projects/files/user_edits_files_spec.rb
+++ b/spec/features/projects/files/user_edits_files_spec.rb
@@ -12,6 +12,23 @@ describe 'Projects > Files > User edits files' do
sign_in(user)
end
+ shared_examples 'unavailable for an archived project' do
+ it 'does not show the edit link for an archived project', :js do
+ project.update!(archived: true)
+ visit project_tree_path(project, project.repository.root_ref)
+
+ click_link('.gitignore')
+
+ aggregate_failures 'available edit buttons' do
+ expect(page).not_to have_text('Edit')
+ expect(page).not_to have_text('Web IDE')
+
+ expect(page).not_to have_text('Replace')
+ expect(page).not_to have_text('Delete')
+ end
+ end
+ end
+
context 'when an user has write access' do
before do
project.add_master(user)
@@ -85,6 +102,8 @@ describe 'Projects > Files > User edits files' do
expect(page).to have_css('.line_holder.new')
end
+
+ it_behaves_like 'unavailable for an archived project'
end
context 'when an user does not have write access' do
@@ -168,6 +187,10 @@ describe 'Projects > Files > User edits files' do
expect(page).to have_content("From #{forked_project.full_path}")
expect(page).to have_content("into #{project2.full_path}")
end
+
+ it_behaves_like 'unavailable for an archived project' do
+ let(:project) { project2 }
+ end
end
end
end
diff --git a/spec/features/projects/files/user_find_file_spec.rb b/spec/features/projects/files/user_find_file_spec.rb
new file mode 100644
index 00000000000..df405e70dd4
--- /dev/null
+++ b/spec/features/projects/files/user_find_file_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe 'User find project file' do
+ let(:user) { create :user }
+ let(:project) { create :project, :repository }
+
+ before do
+ sign_in(user)
+ project.add_master(user)
+
+ visit project_tree_path(project, project.repository.root_ref)
+ end
+
+ def active_main_tab
+ find('.sidebar-top-level-items > li.active')
+ end
+
+ def find_file(text)
+ fill_in 'file_find', with: text
+ end
+
+ it 'navigates to find file by shortcut', :js do
+ find('body').native.send_key('t')
+
+ expect(active_main_tab).to have_content('Repository')
+ expect(page).to have_selector('.file-finder-holder', count: 1)
+ end
+
+ it 'navigates to find file' do
+ click_link 'Find file'
+
+ expect(active_main_tab).to have_content('Repository')
+ expect(page).to have_selector('.file-finder-holder', count: 1)
+ end
+
+ it 'searches CHANGELOG file', :js do
+ click_link 'Find file'
+
+ find_file 'change'
+
+ expect(page).to have_content('CHANGELOG')
+ expect(page).not_to have_content('.gitignore')
+ expect(page).not_to have_content('VERSION')
+ end
+
+ it 'does not find file when search not exist file', :js do
+ click_link 'Find file'
+
+ find_file 'asdfghjklqwertyuizxcvbnm'
+
+ expect(page).not_to have_content('CHANGELOG')
+ expect(page).not_to have_content('.gitignore')
+ expect(page).not_to have_content('VERSION')
+ end
+
+ it 'searches file by partially matches', :js do
+ click_link 'Find file'
+
+ find_file 'git'
+
+ expect(page).to have_content('.gitignore')
+ expect(page).to have_content('.gitmodules')
+ expect(page).not_to have_content('CHANGELOG')
+ expect(page).not_to have_content('VERSION')
+ end
+end
diff --git a/spec/features/projects/files/user_reads_pipeline_status_spec.rb b/spec/features/projects/files/user_reads_pipeline_status_spec.rb
new file mode 100644
index 00000000000..2fb9da2f0a2
--- /dev/null
+++ b/spec/features/projects/files/user_reads_pipeline_status_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe 'user reads pipeline status', :js do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:v110_pipeline) { create_pipeline('v1.1.0', 'success') }
+ let(:x110_pipeline) { create_pipeline('x1.1.0', 'failed') }
+
+ before do
+ project.add_master(user)
+
+ project.repository.add_tag(user, 'x1.1.0', 'v1.1.0')
+ v110_pipeline
+ x110_pipeline
+
+ sign_in(user)
+ end
+
+ shared_examples 'visiting project tree' do
+ scenario 'sees the correct pipeline status' do
+ visit project_tree_path(project, expected_pipeline.ref)
+ wait_for_requests
+
+ page.within('.blob-commit-info') do
+ expect(page).to have_link('', href: project_pipeline_path(project, expected_pipeline))
+ expect(page).to have_selector(".ci-status-icon-#{expected_pipeline.status}")
+ end
+ end
+ end
+
+ it_behaves_like 'visiting project tree' do
+ let(:expected_pipeline) { v110_pipeline }
+ end
+
+ it_behaves_like 'visiting project tree' do
+ let(:expected_pipeline) { x110_pipeline }
+ end
+
+ def create_pipeline(ref, status)
+ create(:ci_pipeline,
+ project: project,
+ ref: ref,
+ sha: project.commit(ref).sha,
+ status: status)
+ end
+end
diff --git a/spec/features/projects/files/user_uploads_files_spec.rb b/spec/features/projects/files/user_uploads_files_spec.rb
index 7a1e3a8bcce..8b212faa29d 100644
--- a/spec/features/projects/files/user_uploads_files_spec.rb
+++ b/spec/features/projects/files/user_uploads_files_spec.rb
@@ -7,11 +7,11 @@ describe 'Projects > Files > User uploads files' do
"You're not allowed to make changes to this project directly. "\
"A fork of this project has been created that you can make changes in, so you can submit a merge request."
end
- let(:project) { create(:project, :repository, name: 'Shop') }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository, name: 'Shop', creator: user) }
let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') }
let(:project_tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) }
let(:project2_tree_path_root_ref) { project_tree_path(project2, project2.repository.root_ref) }
- let(:user) { project.creator }
before do
project.add_master(user)
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 ecb7651acad..72ab2d71f35 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/issues/user_views_issue_spec.rb b/spec/features/projects/issues/user_views_issue_spec.rb
index f7f2cde3d64..4093876c289 100644
--- a/spec/features/projects/issues/user_views_issue_spec.rb
+++ b/spec/features/projects/issues/user_views_issue_spec.rb
@@ -6,11 +6,27 @@ describe "User views issue" do
set(:issue) { create(:issue, project: project, description: "# Description header", author: user) }
before do
- project.add_guest(user)
+ project.add_developer(user)
sign_in(user)
visit(project_issue_path(project, issue))
end
it { expect(page).to have_header_with_correct_id_and_link(1, "Description header", "description-header") }
+
+ it 'shows the merge request and issue actions', :aggregate_failures do
+ expect(page).to have_link('New issue')
+ expect(page).to have_button('Create merge request')
+ expect(page).to have_link('Close issue')
+ end
+
+ context 'when the project is archived' do
+ let(:project) { create(:project, :public, :archived) }
+
+ it 'hides the merge request and issue actions', :aggregate_failures do
+ expect(page).not_to have_link('New issue')
+ expect(page).not_to have_button('Create merge request')
+ expect(page).not_to have_link('Close issue')
+ end
+ end
end
diff --git a/spec/features/projects/jobs/permissions_spec.rb b/spec/features/projects/jobs/permissions_spec.rb
new file mode 100644
index 00000000000..31abadf9bd6
--- /dev/null
+++ b/spec/features/projects/jobs/permissions_spec.rb
@@ -0,0 +1,130 @@
+require 'spec_helper'
+
+describe 'Project Jobs Permissions' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group, name: 'some group') }
+ let(:project) { create(:project, :repository, namespace: group) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') }
+ let!(:job) { create(:ci_build, :running, :coverage, :trace_artifact, pipeline: pipeline) }
+
+ before do
+ sign_in(user)
+
+ project.enable_ci
+ end
+
+ describe 'jobs pages' do
+ shared_examples 'recent job page details responds with status' do |status|
+ before do
+ visit project_job_path(project, job)
+ end
+
+ it { expect(status_code).to eq(status) }
+ end
+
+ shared_examples 'project jobs page responds with status' do |status|
+ before do
+ visit project_jobs_path(project)
+ end
+
+ it { expect(status_code).to eq(status) }
+ end
+
+ context 'when public access for jobs is disabled' do
+ before do
+ project.update(public_builds: false)
+ end
+
+ context 'when user is a guest' do
+ before do
+ project.add_guest(user)
+ end
+
+ it_behaves_like 'recent job page details responds with status', 404
+ it_behaves_like 'project jobs page responds with status', 404
+ end
+
+ context 'when project is internal' do
+ before do
+ project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it_behaves_like 'recent job page details responds with status', 404
+ it_behaves_like 'project jobs page responds with status', 404
+ end
+ end
+
+ context 'when public access for jobs is enabled' do
+ before do
+ project.update(public_builds: true)
+ end
+
+ context 'when project is internal' do
+ before do
+ project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it_behaves_like 'recent job page details responds with status', 200 do
+ it 'renders job details', :js do
+ expect(page).to have_content "Job ##{job.id}"
+ expect(page).to have_css '#build-trace'
+ end
+ end
+
+ it_behaves_like 'project jobs page responds with status', 200 do
+ it 'renders job' do
+ page.within('.build') do
+ expect(page).to have_content("##{job.id}")
+ .and have_content(job.sha[0..7])
+ .and have_content(job.ref)
+ .and have_content(job.name)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe 'artifacts page' do
+ context 'when recent job has artifacts available' do
+ before do
+ artifacts = Rails.root.join('spec/fixtures/ci_build_artifacts.zip')
+ archive = fixture_file_upload(artifacts, 'application/zip')
+
+ job.update_attributes(legacy_artifacts_file: archive)
+ end
+
+ context 'when public access for jobs is disabled' do
+ before do
+ project.update(public_builds: false)
+ end
+
+ context 'when user with guest role' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'responds with 404 status' do
+ visit download_project_job_artifacts_path(project, job)
+
+ expect(status_code).to eq(404)
+ end
+ end
+
+ context 'when user with reporter role' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'starts download artifact' do
+ visit download_project_job_artifacts_path(project, job)
+
+ expect(status_code).to eq(200)
+ expect(page.response_headers['Content-Type']).to eq 'application/zip'
+ expect(page.response_headers['Content-Transfer-Encoding']).to eq 'binary'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb
index 36ebbeadd4a..786ec327b92 100644
--- a/spec/features/projects/jobs/user_browses_jobs_spec.rb
+++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb
@@ -26,7 +26,7 @@ describe 'User browses jobs' do
page.within('.nav-controls') do
ci_lint_tool_link = page.find_link('CI lint')
- expect(ci_lint_tool_link[:href]).to end_with(ci_lint_path)
+ expect(ci_lint_tool_link[:href]).to end_with(project_ci_lint_path(project))
end
end
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 749a1b81872..a00db6dd161 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -464,6 +464,17 @@ feature 'Jobs' do
expect(page).to have_content('This job has been skipped')
end
end
+
+ context 'when job is failed but has no trace' do
+ let(:job) { create(:ci_build, :failed, pipeline: pipeline) }
+
+ it 'renders empty state' do
+ visit project_job_path(project, job)
+
+ expect(job).not_to have_trace
+ expect(page).to have_content('This job does not have a trace.')
+ end
+ end
end
describe "POST /:project/jobs/:id/cancel", :js do
@@ -480,16 +491,18 @@ feature 'Jobs' do
end
end
- describe "POST /:project/jobs/:id/retry" do
+ describe "POST /:project/jobs/:id/retry", :js do
context "Job from project", :js do
before do
job.run!
+ job.cancel!
visit project_job_path(project, job)
- find('.js-cancel-job').click()
+ wait_for_requests
+
find('.js-retry-button').click
end
- it 'shows the right status and buttons', :js do
+ it 'shows the right status and buttons' do
page.within('aside.right-sidebar') do
expect(page).to have_content 'Cancel'
end
diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb
index 40689964b91..b571d5a0e26 100644
--- a/spec/features/projects/merge_request_button_spec.rb
+++ b/spec/features/projects/merge_request_button_spec.rb
@@ -45,6 +45,18 @@ feature 'Merge Request button' do
end
end
end
+
+ context 'when the project is archived' do
+ it 'hides the link' do
+ project.update!(archived: true)
+
+ visit url
+
+ within("#content-body") do
+ expect(page).not_to have_link(label)
+ end
+ end
+ end
end
context 'logged in as non-member' do
diff --git a/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb b/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb
index a41d683dbbb..f3e97bc9eb2 100644
--- a/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb
+++ b/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb
@@ -56,4 +56,12 @@ describe 'User reverts a merge request', :js do
expect(page).to have_content('The merge request has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
end
+
+ it 'cannot revert a merge requests for an archived project' do
+ project.update!(archived: true)
+
+ visit(merge_request_path(merge_request))
+
+ expect(page).not_to have_link('Revert')
+ end
end
diff --git a/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb
index bf95dbb7d09..115e548b691 100644
--- a/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb
+++ b/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb
@@ -94,6 +94,18 @@ describe 'User views open merge requests' do
end
include_examples 'shows merge requests'
+
+ it 'shows the new merge request button' do
+ expect(page).to have_link('New merge request')
+ end
+
+ context 'when the project is archived' do
+ let(:project) { create(:project, :public, :repository, :archived) }
+
+ it 'hides the new merge request button' do
+ expect(page).not_to have_link('New merge request')
+ end
+ end
end
end
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index a5954fec54b..fee6287558e 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -64,7 +64,7 @@ feature 'New project' do
end
context 'with group namespace' do
- let(:group) { create(:group, :private, owner: user) }
+ let(:group) { create(:group, :private) }
before do
group.add_owner(user)
@@ -81,7 +81,7 @@ feature 'New project' do
end
context 'with subgroup namespace' do
- let(:group) { create(:group, owner: user) }
+ let(:group) { create(:group) }
let(:subgroup) { create(:group, parent: group) }
before do
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 6e63e0f0b49..705ba78a0b7 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -517,7 +517,7 @@ describe 'Pipelines', :js do
end
it 'creates a new pipeline' do
- expect { click_on 'Create pipeline' }
+ expect { click_on 'Run pipeline' }
.to change { Ci::Pipeline.count }.by(1)
expect(Ci::Pipeline.last).to be_web
@@ -526,7 +526,7 @@ describe 'Pipelines', :js do
context 'without gitlab-ci.yml' do
before do
- click_on 'Create pipeline'
+ click_on 'Run pipeline'
end
it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
@@ -539,7 +539,7 @@ describe 'Pipelines', :js do
click_link 'master'
end
- expect { click_on 'Create pipeline' }
+ expect { click_on 'Run pipeline' }
.to change { Ci::Pipeline.count }.by(1)
end
end
@@ -557,7 +557,7 @@ describe 'Pipelines', :js do
it 'has field to add a new pipeline' do
expect(page).to have_selector('.js-branch-select')
expect(find('.js-branch-select')).to have_content project.default_branch
- expect(page).to have_content('Create for')
+ expect(page).to have_content('Run on')
end
end
diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index d9020333f28..e875a88a52b 100644
--- a/spec/features/projects/settings/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -8,6 +8,7 @@ describe "Projects > Settings > Pipelines settings" do
before do
sign_in(user)
project.add_role(user, role)
+ create(:project_auto_devops, project: project)
end
context 'for developer' do
@@ -27,10 +28,17 @@ describe "Projects > Settings > Pipelines settings" do
visit project_settings_ci_cd_path(project)
fill_in('Test coverage parsing', with: 'coverage_regex')
- click_on 'Save changes'
+
+ page.within '#js-general-pipeline-settings' do
+ click_on 'Save changes'
+ end
expect(page.status_code).to eq(200)
- expect(page).to have_button('Save changes', disabled: false)
+
+ page.within '#js-general-pipeline-settings' do
+ expect(page).to have_button('Save changes', disabled: false)
+ end
+
expect(page).to have_field('Test coverage parsing', with: 'coverage_regex')
end
@@ -38,10 +46,15 @@ describe "Projects > Settings > Pipelines settings" do
visit project_settings_ci_cd_path(project)
page.check('Auto-cancel redundant, pending pipelines')
- click_on 'Save changes'
+ page.within '#js-general-pipeline-settings' do
+ click_on 'Save changes'
+ end
expect(page.status_code).to eq(200)
- expect(page).to have_button('Save changes', disabled: false)
+
+ page.within '#js-general-pipeline-settings' do
+ expect(page).to have_button('Save changes', disabled: false)
+ end
checkbox = find_field('project_auto_cancel_pending_pipelines')
expect(checkbox).to be_checked
@@ -51,13 +64,16 @@ describe "Projects > Settings > Pipelines settings" do
it 'update auto devops settings' do
visit project_settings_ci_cd_path(project)
- fill_in('project_auto_devops_attributes_domain', with: 'test.com')
- page.choose('project_auto_devops_attributes_enabled_false')
- click_on 'Save changes'
+ page.within '#autodevops-settings' do
+ fill_in('project_auto_devops_attributes_domain', with: 'test.com')
+ page.choose('project_auto_devops_attributes_enabled_false')
+ click_on 'Save changes'
+ end
expect(page.status_code).to eq(200)
expect(project.auto_devops).to be_present
expect(project.auto_devops).not_to be_enabled
+ expect(project.auto_devops.domain).to eq('test.com')
end
end
end
diff --git a/spec/features/projects/show/user_sees_collaboration_links_spec.rb b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
new file mode 100644
index 00000000000..7b3711531c6
--- /dev/null
+++ b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe 'Projects > Show > Collaboration links' do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
+
+ it 'shows all the expected links' do
+ visit project_path(project)
+
+ # The navigation bar
+ page.within('.header-new') do
+ aggregate_failures 'dropdown links in the navigation bar' do
+ expect(page).to have_link('New issue')
+ expect(page).to have_link('New merge request')
+ expect(page).to have_link('New snippet', href: new_project_snippet_path(project))
+ end
+ end
+
+ # The project header
+ page.within('.project-home-panel') do
+ aggregate_failures 'dropdown links in the project home panel' do
+ expect(page).to have_link('New issue')
+ expect(page).to have_link('New merge request')
+ expect(page).to have_link('New snippet')
+ expect(page).to have_link('New file')
+ expect(page).to have_link('New branch')
+ expect(page).to have_link('New tag')
+ end
+ end
+
+ # The dropdown above the tree
+ page.within('.repo-breadcrumb') do
+ aggregate_failures 'dropdown links above the repo tree' do
+ expect(page).to have_link('New file')
+ expect(page).to have_link('Upload file')
+ expect(page).to have_link('New directory')
+ expect(page).to have_link('New branch')
+ expect(page).to have_link('New tag')
+ end
+ end
+
+ # The Web IDE
+ expect(page).to have_link('Web IDE')
+ end
+
+ it 'hides the links when the project is archived' do
+ project.update!(archived: true)
+
+ visit project_path(project)
+
+ page.within('.header-new') do
+ aggregate_failures 'dropdown links' do
+ expect(page).not_to have_link('New issue')
+ expect(page).not_to have_link('New merge request')
+ expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project))
+ end
+ end
+
+ page.within('.project-home-panel') do
+ aggregate_failures 'dropdown links' do
+ expect(page).not_to have_link('New issue')
+ expect(page).not_to have_link('New merge request')
+ expect(page).not_to have_link('New snippet')
+ expect(page).not_to have_link('New file')
+ expect(page).not_to have_link('New branch')
+ expect(page).not_to have_link('New tag')
+ end
+ end
+
+ page.within('.repo-breadcrumb') do
+ aggregate_failures 'dropdown links' do
+ expect(page).not_to have_link('New file')
+ expect(page).not_to have_link('Upload file')
+ expect(page).not_to have_link('New directory')
+ expect(page).not_to have_link('New branch')
+ expect(page).not_to have_link('New tag')
+ end
+ end
+
+ expect(page).not_to have_link('Web IDE')
+ end
+end
diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
index a906fa20233..e44361fbe26 100644
--- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
+++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
@@ -65,7 +65,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
describe 'Auto DevOps button' do
it '"Enable Auto DevOps" button linked to settings page' do
page.within('.project-stats') do
- expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings'))
+ expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
end
@@ -75,7 +75,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project)
page.within('.project-stats') do
- expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings'))
+ expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
end
end
@@ -212,7 +212,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
describe 'Auto DevOps button' do
it '"Enable Auto DevOps" button linked to settings page' do
page.within('.project-stats') do
- expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings'))
+ expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
end
@@ -222,7 +222,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project)
page.within('.project-stats') do
- expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings'))
+ expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
end
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
index d96c7e655ba..b242e41df1c 100644
--- a/spec/features/projects/tree/create_directory_spec.rb
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -44,6 +44,8 @@ feature 'Multi-file editor new directory', :js do
wait_for_requests
+ click_button 'Stage all'
+
fill_in('commit-message', with: 'commit message ide')
click_button('Commit')
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
index a4cbd5cf766..7d65456e049 100644
--- a/spec/features/projects/tree/create_file_spec.rb
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -34,6 +34,8 @@ feature 'Multi-file editor new file', :js do
wait_for_requests
+ click_button 'Stage all'
+
fill_in('commit-message', with: 'commit message ide')
click_button('Commit')
diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb
index fb0d8c766fe..47c5a8161d9 100644
--- a/spec/features/projects/user_uses_shortcuts_spec.rb
+++ b/spec/features/projects/user_uses_shortcuts_spec.rb
@@ -11,12 +11,12 @@ describe 'User uses shortcuts', :js do
visit(project_path(project))
end
- context 'when navigating to the Overview pages' do
+ context 'when navigating to the Project pages' do
it 'redirects to the details page' do
find('body').native.send_key('g')
find('body').native.send_key('p')
- expect(page).to have_active_navigation('Overview')
+ expect(page).to have_active_navigation('Project')
expect(page).to have_active_sub_navigation('Details')
end
@@ -24,7 +24,7 @@ describe 'User uses shortcuts', :js do
find('body').native.send_key('g')
find('body').native.send_key('e')
- expect(page).to have_active_navigation('Overview')
+ expect(page).to have_active_navigation('Project')
expect(page).to have_active_sub_navigation('Activity')
end
end
diff --git a/spec/features/projects/user_views_empty_project_spec.rb b/spec/features/projects/user_views_empty_project_spec.rb
new file mode 100644
index 00000000000..7b982301ffc
--- /dev/null
+++ b/spec/features/projects/user_views_empty_project_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe 'User views an empty project' do
+ let(:project) { create(:project, :empty_repo) }
+ let(:user) { create(:user) }
+
+ shared_examples 'allowing push to default branch' do
+ before do
+ sign_in(user)
+ visit project_path(project)
+ end
+
+ it 'shows push-to-master instructions' do
+ expect(page).to have_content('git push -u origin master')
+ end
+ end
+
+ describe 'as a master' do
+ before do
+ project.add_master(user)
+ end
+
+ it_behaves_like 'allowing push to default branch'
+ end
+
+ describe 'as an admin' do
+ let(:user) { create(:user, :admin) }
+
+ it_behaves_like 'allowing push to default branch'
+ end
+
+ describe 'as a developer' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ visit project_path(project)
+ end
+
+ it 'does not show push-to-master instructions' do
+ expect(page).not_to have_content('git push -u origin master')
+ end
+ end
+end
diff --git a/spec/features/snippets/embedded_snippet_spec.rb b/spec/features/snippets/embedded_snippet_spec.rb
new file mode 100644
index 00000000000..ab661f6fc69
--- /dev/null
+++ b/spec/features/snippets/embedded_snippet_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe 'Embedded Snippets' do
+ let(:snippet) { create(:personal_snippet, :public, file_name: 'random_dir.rb', content: content) }
+ let(:content) { "require 'fileutils'\nFileUtils.mkdir_p 'some/random_dir'\n" }
+
+ it 'loads snippet', :js do
+ script_url = "http://#{Capybara.current_session.server.host}:#{Capybara.current_session.server.port}/#{snippet_path(snippet, format: 'js')}"
+ embed_body = "<html><body><script src=\"#{script_url}\"></script></body></html>"
+
+ rack_app = proc do
+ ['200', { 'Content-Type' => 'text/html' }, [embed_body]]
+ end
+
+ server = Capybara::Server.new(rack_app)
+ server.boot
+
+ visit("http://#{server.host}:#{server.port}/embedded_snippet.html")
+
+ expect(page).to have_content("random_dir.rb")
+ expect(page).to have_content("require 'fileutils'")
+ expect(page).to have_link('Open raw')
+ expect(page).to have_link('Download')
+ end
+end
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index bc75dc5d19b..9e10bfb2adc 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -392,7 +392,7 @@ feature 'Login' do
end
def ensure_one_active_tab
- expect(page).to have_selector('.nav-tabs > li.active', count: 1)
+ expect(page).to have_selector('ul.new-session-tabs > li.active', count: 1)
end
def ensure_one_active_pane
diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb
index 375bcc9087e..796d40cb625 100644
--- a/spec/finders/group_descendants_finder_spec.rb
+++ b/spec/finders/group_descendants_finder_spec.rb
@@ -35,15 +35,6 @@ describe GroupDescendantsFinder do
expect(finder.execute).to contain_exactly(project)
end
- it 'does not include projects shared with the group' do
- project = create(:project, namespace: group)
- other_project = create(:project)
- other_project.project_group_links.create(group: group,
- group_access: ProjectGroupLink::MASTER)
-
- expect(finder.execute).to contain_exactly(project)
- end
-
context 'when archived is `true`' do
let(:params) { { archived: 'true' } }
diff --git a/spec/finders/merge_request_target_project_finder_spec.rb b/spec/finders/merge_request_target_project_finder_spec.rb
index c81bfd7932c..f302cf80ce8 100644
--- a/spec/finders/merge_request_target_project_finder_spec.rb
+++ b/spec/finders/merge_request_target_project_finder_spec.rb
@@ -19,6 +19,12 @@ describe MergeRequestTargetProjectFinder do
expect(finder.execute).to contain_exactly(forked_project)
end
+
+ it 'does not contain archived projects' do
+ base_project.update!(archived: true)
+
+ expect(finder.execute).to contain_exactly(other_fork, forked_project)
+ end
end
context 'public projects' do
diff --git a/spec/fixtures/big-image.png b/spec/fixtures/big-image.png
new file mode 100644
index 00000000000..a333363ac36
--- /dev/null
+++ b/spec/fixtures/big-image.png
Binary files differ
diff --git a/spec/fixtures/exported-project.gz b/spec/fixtures/exported-project.gz
index 352384f16c8..bef7e2ff8ee 100644
--- a/spec/fixtures/exported-project.gz
+++ b/spec/fixtures/exported-project.gz
Binary files differ
diff --git a/spec/fixtures/trace/sample_trace b/spec/fixtures/trace/sample_trace
index 55fcb9d2756..c65cf05d5ca 100644
--- a/spec/fixtures/trace/sample_trace
+++ b/spec/fixtures/trace/sample_trace
@@ -1,24 +1,24 @@
-Running with gitlab-runner 10.4.0 (857480b6)
- on docker-auto-scale-com (9a6801bd)
-Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6 ...
-Starting service postgres:9.2 ...
-Pulling docker image postgres:9.2 ...
-Using docker image postgres:9.2 ID=sha256:18cdbca56093c841d28e629eb8acd4224afe0aa4c57c839351fc181888b8a470 for postgres service...
+Running with gitlab-runner 10.6.0 (a3543a27)
+ on docker-auto-scale-com 30d62d59
+Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.16-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6 ...
+Starting service mysql:latest ...
+Pulling docker image mysql:latest ...
+Using docker image sha256:5195076672a7e30525705a18f7d352c920bbd07a5ae72b30e374081fe660a011 for mysql:latest ...
Starting service redis:alpine ...
Pulling docker image redis:alpine ...
-Using docker image redis:alpine ID=sha256:cb1ec54b370d4a91dff57d00f91fd880dc710160a58440adaa133e0f84ae999d for redis service...
+Using docker image sha256:98bd7cfc43b8ef0ff130465e3d5427c0771002c2f35a6a9b62cb2d04602bed0a for redis:alpine ...
Waiting for services to be up and running...
-Using docker image sha256:3006a02a5a6f0a116358a13bbc46ee46fb2471175efd5b7f9b1c22345ec2a8e9 for predefined container...
-Pulling docker image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6 ...
-Using docker image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6 ID=sha256:1f59be408f12738509ffe4177d65e9de6391f32461de83d9d45f58517b30af99 for build container...
-section_start:1517486886:prepare_script
-Running on runner-9a6801bd-project-13083-concurrent-0 via runner-9a6801bd-gsrm-1517484168-a8449153...
-section_end:1517486887:prepare_script
-section_start:1517486887:get_sources
-Fetching changes for 42624-gitaly-bundle-isolation-not-working-in-ci with git depth set to 20...
+Pulling docker image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.16-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6 ...
+Using docker image sha256:1b06077bb03d9d42d801b53f45701bb6a7e862ca02e1e75f30ca7fcf1270eb02 for dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.16-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6 ...
+section_start:1522927103:prepare_script
+Running on runner-30d62d59-project-13083-concurrent-0 via runner-30d62d59-prm-1522922015-ddc29478...
+section_end:1522927104:prepare_script
+section_start:1522927104:get_sources
+Fetching changes for master with git depth set to 20...
Removing .gitlab_shell_secret
Removing .gitlab_workhorse_secret
Removing .yarn-cache/
+Removing builds/2018_04/
Removing config/database.yml
Removing config/gitlab.yml
Removing config/redis.cache.yml
@@ -26,1160 +26,3420 @@ Removing config/redis.queues.yml
Removing config/redis.shared_state.yml
Removing config/resque.yml
Removing config/secrets.yml
-Removing coverage/
-Removing knapsack/
Removing log/api_json.log
Removing log/application.log
Removing log/gitaly-test.log
-Removing log/githost.log
Removing log/grpc.log
Removing log/test_json.log
-Removing node_modules/
-Removing public/assets/
-Removing rspec_flaky/
-Removing shared/tmp/
Removing tmp/tests/
Removing vendor/ruby/
-HEAD is now at 4cea24f Converted todos.js to axios
+HEAD is now at b7cbff3d Add `direct_upload` setting for artifacts
From https://gitlab.com/gitlab-org/gitlab-ce
- * [new branch] 42624-gitaly-bundle-isolation-not-working-in-ci -> origin/42624-gitaly-bundle-isolation-not-working-in-ci
-Checking out f42a5e24 as 42624-gitaly-bundle-isolation-not-working-in-ci...
+ 2dbcb9cb..641bb13b master -> origin/master
+Checking out 21488c74 as master...
Skipping Git submodules setup
-section_end:1517486896:get_sources
-section_start:1517486896:restore_cache
+section_end:1522927113:get_sources
+section_start:1522927113:restore_cache
Checking cache for ruby-2.3.6-with-yarn...
Downloading cache.zip from http://runners-cache-5-internal.gitlab.com:444/runner/project/13083/ruby-2.3.6-with-yarn
Successfully extracted cache
-section_end:1517486919:restore_cache
-section_start:1517486919:download_artifacts
-Downloading artifacts for retrieve-tests-metadata (50551658)...
-Downloading artifacts from coordinator... ok  id=50551658 responseStatus=200 OK token=HhF7y_1X
-Downloading artifacts for compile-assets (50551659)...
-Downloading artifacts from coordinator... ok  id=50551659 responseStatus=200 OK token=wTz6JrCP
-Downloading artifacts for setup-test-env (50551660)...
-Downloading artifacts from coordinator... ok  id=50551660 responseStatus=200 OK token=DTGgeVF5
+section_end:1522927128:restore_cache
+section_start:1522927128:download_artifacts
+Downloading artifacts for retrieve-tests-metadata (61303215)...
+Downloading artifacts from coordinator... ok  id=61303215 responseStatus=200 OK token=AdWPNg2R
+Downloading artifacts for compile-assets (61303216)...
+Downloading artifacts from coordinator... ok  id=61303216 responseStatus=200 OK token=iy2yYbq8
+Downloading artifacts for setup-test-env (61303217)...
+Downloading artifacts from coordinator... ok  id=61303217 responseStatus=200 OK token=ur1g79-4
WARNING: tmp/tests/gitlab-shell/.gitlab_shell_secret: chmod tmp/tests/gitlab-shell/.gitlab_shell_secret: no such file or directory (suppressing repeats)
-section_end:1517486934:download_artifacts
-section_start:1517486934:build_script
+section_end:1522927141:download_artifacts
+section_start:1522927141:build_script
$ bundle --version
Bundler version 1.16.1
+$ date
+Thu Apr 5 11:19:01 UTC 2018
$ source scripts/utils.sh
+$ date
+Thu Apr 5 11:19:01 UTC 2018
$ source scripts/prepare_build.sh
The Gemfile's dependencies are satisfied
-Successfully installed knapsack-1.15.0
+Successfully installed knapsack-1.16.0
1 gem installed
-NOTICE: database "gitlabhq_test" does not exist, skipping
-DROP DATABASE
-CREATE DATABASE
-CREATE ROLE
-GRANT
-- enable_extension("plpgsql")
- -> 0.0156s
+ -> 0.0010s
-- enable_extension("pg_trgm")
- -> 0.0156s
+ -> 0.0000s
-- create_table("abuse_reports", {:force=>:cascade})
- -> 0.0119s
+ -> 0.0401s
-- create_table("appearances", {:force=>:cascade})
- -> 0.0065s
+ -> 0.1035s
-- create_table("application_settings", {:force=>:cascade})
- -> 0.0382s
+ -> 0.0871s
-- create_table("audit_events", {:force=>:cascade})
- -> 0.0056s
+ -> 0.0539s
-- add_index("audit_events", ["entity_id", "entity_type"], {:name=>"index_audit_events_on_entity_id_and_entity_type", :using=>:btree})
- -> 0.0040s
+ -> 0.0647s
-- create_table("award_emoji", {:force=>:cascade})
- -> 0.0058s
+ -> 0.0134s
-- add_index("award_emoji", ["awardable_type", "awardable_id"], {:name=>"index_award_emoji_on_awardable_type_and_awardable_id", :using=>:btree})
- -> 0.0068s
+ -> 0.0074s
-- add_index("award_emoji", ["user_id", "name"], {:name=>"index_award_emoji_on_user_id_and_name", :using=>:btree})
- -> 0.0043s
+ -> 0.0072s
+-- create_table("badges", {:force=>:cascade})
+ -> 0.0122s
+-- add_index("badges", ["group_id"], {:name=>"index_badges_on_group_id", :using=>:btree})
+ -> 0.0086s
+-- add_index("badges", ["project_id"], {:name=>"index_badges_on_project_id", :using=>:btree})
+ -> 0.0069s
-- create_table("boards", {:force=>:cascade})
- -> 0.0049s
+ -> 0.0075s
+-- add_index("boards", ["group_id"], {:name=>"index_boards_on_group_id", :using=>:btree})
+ -> 0.0050s
-- add_index("boards", ["project_id"], {:name=>"index_boards_on_project_id", :using=>:btree})
- -> 0.0056s
+ -> 0.0051s
-- create_table("broadcast_messages", {:force=>:cascade})
- -> 0.0056s
+ -> 0.0082s
-- add_index("broadcast_messages", ["starts_at", "ends_at", "id"], {:name=>"index_broadcast_messages_on_starts_at_and_ends_at_and_id", :using=>:btree})
- -> 0.0041s
+ -> 0.0063s
-- create_table("chat_names", {:force=>:cascade})
- -> 0.0056s
+ -> 0.0084s
-- add_index("chat_names", ["service_id", "team_id", "chat_id"], {:name=>"index_chat_names_on_service_id_and_team_id_and_chat_id", :unique=>true, :using=>:btree})
- -> 0.0039s
+ -> 0.0088s
-- add_index("chat_names", ["user_id", "service_id"], {:name=>"index_chat_names_on_user_id_and_service_id", :unique=>true, :using=>:btree})
- -> 0.0036s
+ -> 0.0077s
-- create_table("chat_teams", {:force=>:cascade})
- -> 0.0068s
+ -> 0.0120s
-- add_index("chat_teams", ["namespace_id"], {:name=>"index_chat_teams_on_namespace_id", :unique=>true, :using=>:btree})
- -> 0.0098s
+ -> 0.0135s
-- create_table("ci_build_trace_section_names", {:force=>:cascade})
- -> 0.0048s
+ -> 0.0125s
-- add_index("ci_build_trace_section_names", ["project_id", "name"], {:name=>"index_ci_build_trace_section_names_on_project_id_and_name", :unique=>true, :using=>:btree})
- -> 0.0035s
+ -> 0.0087s
-- create_table("ci_build_trace_sections", {:force=>:cascade})
- -> 0.0040s
+ -> 0.0094s
-- add_index("ci_build_trace_sections", ["build_id", "section_name_id"], {:name=>"index_ci_build_trace_sections_on_build_id_and_section_name_id", :unique=>true, :using=>:btree})
- -> 0.0035s
+ -> 0.0916s
-- add_index("ci_build_trace_sections", ["project_id"], {:name=>"index_ci_build_trace_sections_on_project_id", :using=>:btree})
- -> 0.0033s
+ -> 0.0089s
+-- add_index("ci_build_trace_sections", ["section_name_id"], {:name=>"index_ci_build_trace_sections_on_section_name_id", :using=>:btree})
+ -> 0.0132s
-- create_table("ci_builds", {:force=>:cascade})
- -> 0.0062s
+ -> 0.0140s
+-- add_index("ci_builds", ["artifacts_expire_at"], {:name=>"index_ci_builds_on_artifacts_expire_at", :where=>"(artifacts_file <> ''::text)", :using=>:btree})
+ -> 0.0325s
-- add_index("ci_builds", ["auto_canceled_by_id"], {:name=>"index_ci_builds_on_auto_canceled_by_id", :using=>:btree})
- -> 0.0035s
+ -> 0.0081s
-- add_index("ci_builds", ["commit_id", "stage_idx", "created_at"], {:name=>"index_ci_builds_on_commit_id_and_stage_idx_and_created_at", :using=>:btree})
- -> 0.0032s
+ -> 0.0114s
-- add_index("ci_builds", ["commit_id", "status", "type"], {:name=>"index_ci_builds_on_commit_id_and_status_and_type", :using=>:btree})
- -> 0.0032s
+ -> 0.0119s
-- add_index("ci_builds", ["commit_id", "type", "name", "ref"], {:name=>"index_ci_builds_on_commit_id_and_type_and_name_and_ref", :using=>:btree})
- -> 0.0035s
+ -> 0.0116s
-- add_index("ci_builds", ["commit_id", "type", "ref"], {:name=>"index_ci_builds_on_commit_id_and_type_and_ref", :using=>:btree})
- -> 0.0042s
+ -> 0.0144s
-- add_index("ci_builds", ["project_id", "id"], {:name=>"index_ci_builds_on_project_id_and_id", :using=>:btree})
- -> 0.0031s
+ -> 0.0136s
-- add_index("ci_builds", ["protected"], {:name=>"index_ci_builds_on_protected", :using=>:btree})
- -> 0.0031s
+ -> 0.0113s
-- add_index("ci_builds", ["runner_id"], {:name=>"index_ci_builds_on_runner_id", :using=>:btree})
- -> 0.0033s
+ -> 0.0082s
-- add_index("ci_builds", ["stage_id"], {:name=>"index_ci_builds_on_stage_id", :using=>:btree})
- -> 0.0035s
+ -> 0.0086s
-- add_index("ci_builds", ["status", "type", "runner_id"], {:name=>"index_ci_builds_on_status_and_type_and_runner_id", :using=>:btree})
- -> 0.0031s
+ -> 0.0091s
-- add_index("ci_builds", ["status"], {:name=>"index_ci_builds_on_status", :using=>:btree})
- -> 0.0032s
+ -> 0.0081s
-- add_index("ci_builds", ["token"], {:name=>"index_ci_builds_on_token", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0103s
-- add_index("ci_builds", ["updated_at"], {:name=>"index_ci_builds_on_updated_at", :using=>:btree})
- -> 0.0047s
+ -> 0.0149s
-- add_index("ci_builds", ["user_id"], {:name=>"index_ci_builds_on_user_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0156s
+-- create_table("ci_builds_metadata", {:force=>:cascade})
+ -> 0.0134s
+-- add_index("ci_builds_metadata", ["build_id"], {:name=>"index_ci_builds_metadata_on_build_id", :unique=>true, :using=>:btree})
+ -> 0.0067s
+-- add_index("ci_builds_metadata", ["project_id"], {:name=>"index_ci_builds_metadata_on_project_id", :using=>:btree})
+ -> 0.0061s
-- create_table("ci_group_variables", {:force=>:cascade})
- -> 0.0055s
+ -> 0.0088s
-- add_index("ci_group_variables", ["group_id", "key"], {:name=>"index_ci_group_variables_on_group_id_and_key", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0073s
-- create_table("ci_job_artifacts", {:force=>:cascade})
- -> 0.0048s
+ -> 0.0089s
+-- add_index("ci_job_artifacts", ["expire_at", "job_id"], {:name=>"index_ci_job_artifacts_on_expire_at_and_job_id", :using=>:btree})
+ -> 0.0061s
-- add_index("ci_job_artifacts", ["job_id", "file_type"], {:name=>"index_ci_job_artifacts_on_job_id_and_file_type", :unique=>true, :using=>:btree})
- -> 0.0027s
+ -> 0.0077s
-- add_index("ci_job_artifacts", ["project_id"], {:name=>"index_ci_job_artifacts_on_project_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0071s
-- create_table("ci_pipeline_schedule_variables", {:force=>:cascade})
- -> 0.0044s
+ -> 0.0512s
-- add_index("ci_pipeline_schedule_variables", ["pipeline_schedule_id", "key"], {:name=>"index_ci_pipeline_schedule_variables_on_schedule_id_and_key", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0144s
-- create_table("ci_pipeline_schedules", {:force=>:cascade})
- -> 0.0047s
+ -> 0.0603s
-- add_index("ci_pipeline_schedules", ["next_run_at", "active"], {:name=>"index_ci_pipeline_schedules_on_next_run_at_and_active", :using=>:btree})
- -> 0.0029s
+ -> 0.0247s
-- add_index("ci_pipeline_schedules", ["project_id"], {:name=>"index_ci_pipeline_schedules_on_project_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0082s
-- create_table("ci_pipeline_variables", {:force=>:cascade})
- -> 0.0045s
+ -> 0.0112s
-- add_index("ci_pipeline_variables", ["pipeline_id", "key"], {:name=>"index_ci_pipeline_variables_on_pipeline_id_and_key", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0075s
-- create_table("ci_pipelines", {:force=>:cascade})
- -> 0.0057s
+ -> 0.0111s
-- add_index("ci_pipelines", ["auto_canceled_by_id"], {:name=>"index_ci_pipelines_on_auto_canceled_by_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0074s
-- add_index("ci_pipelines", ["pipeline_schedule_id"], {:name=>"index_ci_pipelines_on_pipeline_schedule_id", :using=>:btree})
- -> 0.0031s
+ -> 0.0086s
-- add_index("ci_pipelines", ["project_id", "ref", "status", "id"], {:name=>"index_ci_pipelines_on_project_id_and_ref_and_status_and_id", :using=>:btree})
- -> 0.0032s
+ -> 0.0104s
-- add_index("ci_pipelines", ["project_id", "sha"], {:name=>"index_ci_pipelines_on_project_id_and_sha", :using=>:btree})
- -> 0.0032s
+ -> 0.0107s
-- add_index("ci_pipelines", ["project_id"], {:name=>"index_ci_pipelines_on_project_id", :using=>:btree})
- -> 0.0035s
+ -> 0.0084s
-- add_index("ci_pipelines", ["status"], {:name=>"index_ci_pipelines_on_status", :using=>:btree})
- -> 0.0032s
+ -> 0.0065s
-- add_index("ci_pipelines", ["user_id"], {:name=>"index_ci_pipelines_on_user_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0071s
-- create_table("ci_runner_projects", {:force=>:cascade})
- -> 0.0035s
+ -> 0.0077s
-- add_index("ci_runner_projects", ["project_id"], {:name=>"index_ci_runner_projects_on_project_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0072s
-- add_index("ci_runner_projects", ["runner_id"], {:name=>"index_ci_runner_projects_on_runner_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0064s
-- create_table("ci_runners", {:force=>:cascade})
- -> 0.0059s
+ -> 0.0090s
-- add_index("ci_runners", ["contacted_at"], {:name=>"index_ci_runners_on_contacted_at", :using=>:btree})
- -> 0.0030s
+ -> 0.0078s
-- add_index("ci_runners", ["is_shared"], {:name=>"index_ci_runners_on_is_shared", :using=>:btree})
- -> 0.0030s
+ -> 0.0054s
-- add_index("ci_runners", ["locked"], {:name=>"index_ci_runners_on_locked", :using=>:btree})
- -> 0.0030s
+ -> 0.0052s
-- add_index("ci_runners", ["token"], {:name=>"index_ci_runners_on_token", :using=>:btree})
- -> 0.0029s
+ -> 0.0057s
-- create_table("ci_stages", {:force=>:cascade})
- -> 0.0046s
--- add_index("ci_stages", ["pipeline_id", "name"], {:name=>"index_ci_stages_on_pipeline_id_and_name", :using=>:btree})
- -> 0.0031s
+ -> 0.0059s
+-- add_index("ci_stages", ["pipeline_id", "name"], {:name=>"index_ci_stages_on_pipeline_id_and_name", :unique=>true, :using=>:btree})
+ -> 0.0054s
-- add_index("ci_stages", ["pipeline_id"], {:name=>"index_ci_stages_on_pipeline_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0045s
-- add_index("ci_stages", ["project_id"], {:name=>"index_ci_stages_on_project_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0053s
-- create_table("ci_trigger_requests", {:force=>:cascade})
- -> 0.0058s
+ -> 0.0079s
-- add_index("ci_trigger_requests", ["commit_id"], {:name=>"index_ci_trigger_requests_on_commit_id", :using=>:btree})
- -> 0.0031s
+ -> 0.0059s
-- create_table("ci_triggers", {:force=>:cascade})
- -> 0.0043s
+ -> 0.0100s
-- add_index("ci_triggers", ["project_id"], {:name=>"index_ci_triggers_on_project_id", :using=>:btree})
- -> 0.0033s
--- create_table("ci_variables", {:force=>:cascade})
-> 0.0059s
+-- create_table("ci_variables", {:force=>:cascade})
+ -> 0.0110s
-- 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})
- -> 0.0031s
+ -> 0.0066s
-- create_table("cluster_platforms_kubernetes", {:force=>:cascade})
- -> 0.0053s
+ -> 0.0082s
-- add_index("cluster_platforms_kubernetes", ["cluster_id"], {:name=>"index_cluster_platforms_kubernetes_on_cluster_id", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0047s
-- create_table("cluster_projects", {:force=>:cascade})
- -> 0.0032s
+ -> 0.0079s
-- add_index("cluster_projects", ["cluster_id"], {:name=>"index_cluster_projects_on_cluster_id", :using=>:btree})
- -> 0.0035s
+ -> 0.0045s
-- add_index("cluster_projects", ["project_id"], {:name=>"index_cluster_projects_on_project_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0044s
-- create_table("cluster_providers_gcp", {:force=>:cascade})
- -> 0.0051s
+ -> 0.0247s
-- add_index("cluster_providers_gcp", ["cluster_id"], {:name=>"index_cluster_providers_gcp_on_cluster_id", :unique=>true, :using=>:btree})
- -> 0.0034s
+ -> 0.0088s
-- create_table("clusters", {:force=>:cascade})
- -> 0.0052s
+ -> 0.0767s
-- add_index("clusters", ["enabled"], {:name=>"index_clusters_on_enabled", :using=>:btree})
- -> 0.0031s
+ -> 0.0162s
-- add_index("clusters", ["user_id"], {:name=>"index_clusters_on_user_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0216s
-- create_table("clusters_applications_helm", {:force=>:cascade})
- -> 0.0045s
+ -> 0.0379s
-- create_table("clusters_applications_ingress", {:force=>:cascade})
- -> 0.0044s
+ -> 0.0409s
-- create_table("clusters_applications_prometheus", {:force=>:cascade})
- -> 0.0047s
+ -> 0.0178s
+-- create_table("clusters_applications_runners", {:force=>:cascade})
+ -> 0.0471s
+-- add_index("clusters_applications_runners", ["cluster_id"], {:name=>"index_clusters_applications_runners_on_cluster_id", :unique=>true, :using=>:btree})
+ -> 0.0487s
+-- add_index("clusters_applications_runners", ["runner_id"], {:name=>"index_clusters_applications_runners_on_runner_id", :using=>:btree})
+ -> 0.0094s
-- create_table("container_repositories", {:force=>:cascade})
- -> 0.0050s
+ -> 0.0142s
-- add_index("container_repositories", ["project_id", "name"], {:name=>"index_container_repositories_on_project_id_and_name", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0080s
-- add_index("container_repositories", ["project_id"], {:name=>"index_container_repositories_on_project_id", :using=>:btree})
- -> 0.0032s
+ -> 0.0070s
-- create_table("conversational_development_index_metrics", {:force=>:cascade})
- -> 0.0076s
+ -> 0.0204s
-- create_table("deploy_keys_projects", {:force=>:cascade})
- -> 0.0037s
+ -> 0.0154s
-- add_index("deploy_keys_projects", ["project_id"], {:name=>"index_deploy_keys_projects_on_project_id", :using=>:btree})
- -> 0.0032s
+ -> 0.0471s
-- create_table("deployments", {:force=>:cascade})
- -> 0.0049s
+ -> 0.0191s
-- add_index("deployments", ["created_at"], {:name=>"index_deployments_on_created_at", :using=>:btree})
- -> 0.0034s
+ -> 0.0552s
-- add_index("deployments", ["environment_id", "id"], {:name=>"index_deployments_on_environment_id_and_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0294s
-- add_index("deployments", ["environment_id", "iid", "project_id"], {:name=>"index_deployments_on_environment_id_and_iid_and_project_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0408s
-- add_index("deployments", ["project_id", "iid"], {:name=>"index_deployments_on_project_id_and_iid", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0094s
-- create_table("emails", {:force=>:cascade})
- -> 0.0046s
+ -> 0.0127s
-- add_index("emails", ["confirmation_token"], {:name=>"index_emails_on_confirmation_token", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0082s
-- add_index("emails", ["email"], {:name=>"index_emails_on_email", :unique=>true, :using=>:btree})
- -> 0.0035s
+ -> 0.0110s
-- add_index("emails", ["user_id"], {:name=>"index_emails_on_user_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0079s
-- create_table("environments", {:force=>:cascade})
- -> 0.0052s
+ -> 0.0106s
-- add_index("environments", ["project_id", "name"], {:name=>"index_environments_on_project_id_and_name", :unique=>true, :using=>:btree})
- -> 0.0031s
+ -> 0.0086s
-- add_index("environments", ["project_id", "slug"], {:name=>"index_environments_on_project_id_and_slug", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0076s
-- create_table("events", {:force=>:cascade})
- -> 0.0046s
+ -> 0.0122s
-- add_index("events", ["action"], {:name=>"index_events_on_action", :using=>:btree})
- -> 0.0032s
--- add_index("events", ["author_id"], {:name=>"index_events_on_author_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0068s
+-- add_index("events", ["author_id", "project_id"], {:name=>"index_events_on_author_id_and_project_id", :using=>:btree})
+ -> 0.0081s
-- add_index("events", ["project_id", "id"], {:name=>"index_events_on_project_id_and_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0064s
-- add_index("events", ["target_type", "target_id"], {:name=>"index_events_on_target_type_and_target_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0087s
-- create_table("feature_gates", {:force=>:cascade})
- -> 0.0046s
+ -> 0.0105s
-- add_index("feature_gates", ["feature_key", "key", "value"], {:name=>"index_feature_gates_on_feature_key_and_key_and_value", :unique=>true, :using=>:btree})
- -> 0.0031s
+ -> 0.0080s
-- create_table("features", {:force=>:cascade})
- -> 0.0041s
+ -> 0.0086s
-- add_index("features", ["key"], {:name=>"index_features_on_key", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0058s
-- create_table("fork_network_members", {:force=>:cascade})
- -> 0.0033s
+ -> 0.0081s
-- add_index("fork_network_members", ["fork_network_id"], {:name=>"index_fork_network_members_on_fork_network_id", :using=>:btree})
- -> 0.0033s
+ -> 0.0056s
-- add_index("fork_network_members", ["project_id"], {:name=>"index_fork_network_members_on_project_id", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0053s
-- create_table("fork_networks", {:force=>:cascade})
- -> 0.0049s
+ -> 0.0081s
-- add_index("fork_networks", ["root_project_id"], {:name=>"index_fork_networks_on_root_project_id", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0051s
-- create_table("forked_project_links", {:force=>:cascade})
- -> 0.0032s
+ -> 0.0070s
-- add_index("forked_project_links", ["forked_to_project_id"], {:name=>"index_forked_project_links_on_forked_to_project_id", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0061s
-- create_table("gcp_clusters", {:force=>:cascade})
- -> 0.0074s
+ -> 0.0090s
-- add_index("gcp_clusters", ["project_id"], {:name=>"index_gcp_clusters_on_project_id", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0073s
-- create_table("gpg_key_subkeys", {:force=>:cascade})
- -> 0.0042s
+ -> 0.0092s
-- add_index("gpg_key_subkeys", ["fingerprint"], {:name=>"index_gpg_key_subkeys_on_fingerprint", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0063s
-- add_index("gpg_key_subkeys", ["gpg_key_id"], {:name=>"index_gpg_key_subkeys_on_gpg_key_id", :using=>:btree})
- -> 0.0032s
+ -> 0.0603s
-- add_index("gpg_key_subkeys", ["keyid"], {:name=>"index_gpg_key_subkeys_on_keyid", :unique=>true, :using=>:btree})
- -> 0.0027s
+ -> 0.0705s
-- create_table("gpg_keys", {:force=>:cascade})
- -> 0.0042s
+ -> 0.0235s
-- add_index("gpg_keys", ["fingerprint"], {:name=>"index_gpg_keys_on_fingerprint", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0220s
-- add_index("gpg_keys", ["primary_keyid"], {:name=>"index_gpg_keys_on_primary_keyid", :unique=>true, :using=>:btree})
- -> 0.0026s
+ -> 0.0329s
-- add_index("gpg_keys", ["user_id"], {:name=>"index_gpg_keys_on_user_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0087s
-- create_table("gpg_signatures", {:force=>:cascade})
- -> 0.0054s
+ -> 0.0126s
-- add_index("gpg_signatures", ["commit_sha"], {:name=>"index_gpg_signatures_on_commit_sha", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0105s
-- add_index("gpg_signatures", ["gpg_key_id"], {:name=>"index_gpg_signatures_on_gpg_key_id", :using=>:btree})
- -> 0.0026s
+ -> 0.0094s
-- add_index("gpg_signatures", ["gpg_key_primary_keyid"], {:name=>"index_gpg_signatures_on_gpg_key_primary_keyid", :using=>:btree})
- -> 0.0029s
+ -> 0.0100s
-- add_index("gpg_signatures", ["gpg_key_subkey_id"], {:name=>"index_gpg_signatures_on_gpg_key_subkey_id", :using=>:btree})
- -> 0.0032s
+ -> 0.0079s
-- add_index("gpg_signatures", ["project_id"], {:name=>"index_gpg_signatures_on_project_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0081s
-- create_table("group_custom_attributes", {:force=>:cascade})
- -> 0.0044s
+ -> 0.0092s
-- add_index("group_custom_attributes", ["group_id", "key"], {:name=>"index_group_custom_attributes_on_group_id_and_key", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0086s
-- add_index("group_custom_attributes", ["key", "value"], {:name=>"index_group_custom_attributes_on_key_and_value", :using=>:btree})
- -> 0.0028s
+ -> 0.0071s
-- create_table("identities", {:force=>:cascade})
- -> 0.0043s
+ -> 0.0114s
-- add_index("identities", ["user_id"], {:name=>"index_identities_on_user_id", :using=>:btree})
- -> 0.0034s
+ -> 0.0064s
+-- create_table("internal_ids", {:id=>:bigserial, :force=>:cascade})
+ -> 0.0097s
+-- add_index("internal_ids", ["usage", "project_id"], {:name=>"index_internal_ids_on_usage_and_project_id", :unique=>true, :using=>:btree})
+ -> 0.0073s
-- create_table("issue_assignees", {:id=>false, :force=>:cascade})
- -> 0.0013s
+ -> 0.0127s
-- add_index("issue_assignees", ["issue_id", "user_id"], {:name=>"index_issue_assignees_on_issue_id_and_user_id", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0110s
-- add_index("issue_assignees", ["user_id"], {:name=>"index_issue_assignees_on_user_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0079s
-- create_table("issue_metrics", {:force=>:cascade})
- -> 0.0032s
+ -> 0.0098s
-- add_index("issue_metrics", ["issue_id"], {:name=>"index_issue_metrics", :using=>:btree})
- -> 0.0029s
+ -> 0.0053s
-- create_table("issues", {:force=>:cascade})
- -> 0.0051s
+ -> 0.0090s
-- add_index("issues", ["author_id"], {:name=>"index_issues_on_author_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0056s
-- add_index("issues", ["confidential"], {:name=>"index_issues_on_confidential", :using=>:btree})
- -> 0.0029s
+ -> 0.0055s
-- add_index("issues", ["description"], {:name=>"index_issues_on_description_trigram", :using=>:gin, :opclasses=>{"description"=>"gin_trgm_ops"}})
- -> 0.0022s
+ -> 0.0006s
-- add_index("issues", ["milestone_id"], {:name=>"index_issues_on_milestone_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0061s
-- add_index("issues", ["moved_to_id"], {:name=>"index_issues_on_moved_to_id", :where=>"(moved_to_id IS NOT NULL)", :using=>:btree})
- -> 0.0030s
+ -> 0.0051s
-- add_index("issues", ["project_id", "created_at", "id", "state"], {:name=>"index_issues_on_project_id_and_created_at_and_id_and_state", :using=>:btree})
- -> 0.0039s
+ -> 0.0069s
-- 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})
- -> 0.0031s
+ -> 0.0073s
-- add_index("issues", ["project_id", "iid"], {:name=>"index_issues_on_project_id_and_iid", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0060s
-- add_index("issues", ["project_id", "updated_at", "id", "state"], {:name=>"index_issues_on_project_id_and_updated_at_and_id_and_state", :using=>:btree})
- -> 0.0035s
+ -> 0.0094s
-- add_index("issues", ["relative_position"], {:name=>"index_issues_on_relative_position", :using=>:btree})
- -> 0.0030s
+ -> 0.0070s
-- add_index("issues", ["state"], {:name=>"index_issues_on_state", :using=>:btree})
- -> 0.0027s
+ -> 0.0078s
-- add_index("issues", ["title"], {:name=>"index_issues_on_title_trigram", :using=>:gin, :opclasses=>{"title"=>"gin_trgm_ops"}})
- -> 0.0021s
+ -> 0.0007s
-- add_index("issues", ["updated_at"], {:name=>"index_issues_on_updated_at", :using=>:btree})
- -> 0.0030s
+ -> 0.0068s
-- add_index("issues", ["updated_by_id"], {:name=>"index_issues_on_updated_by_id", :where=>"(updated_by_id IS NOT NULL)", :using=>:btree})
- -> 0.0028s
+ -> 0.0066s
-- create_table("keys", {:force=>:cascade})
- -> 0.0048s
+ -> 0.0087s
-- add_index("keys", ["fingerprint"], {:name=>"index_keys_on_fingerprint", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0066s
-- add_index("keys", ["user_id"], {:name=>"index_keys_on_user_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0063s
-- create_table("label_links", {:force=>:cascade})
- -> 0.0041s
+ -> 0.0073s
-- add_index("label_links", ["label_id"], {:name=>"index_label_links_on_label_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0050s
-- add_index("label_links", ["target_id", "target_type"], {:name=>"index_label_links_on_target_id_and_target_type", :using=>:btree})
- -> 0.0028s
+ -> 0.0062s
-- create_table("label_priorities", {:force=>:cascade})
- -> 0.0031s
+ -> 0.0073s
-- add_index("label_priorities", ["priority"], {:name=>"index_label_priorities_on_priority", :using=>:btree})
- -> 0.0028s
+ -> 0.0058s
-- add_index("label_priorities", ["project_id", "label_id"], {:name=>"index_label_priorities_on_project_id_and_label_id", :unique=>true, :using=>:btree})
- -> 0.0027s
+ -> 0.0056s
-- create_table("labels", {:force=>:cascade})
- -> 0.0046s
+ -> 0.0087s
-- add_index("labels", ["group_id", "project_id", "title"], {:name=>"index_labels_on_group_id_and_project_id_and_title", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0074s
-- add_index("labels", ["project_id"], {:name=>"index_labels_on_project_id", :using=>:btree})
- -> 0.0032s
+ -> 0.0061s
-- add_index("labels", ["template"], {:name=>"index_labels_on_template", :where=>"template", :using=>:btree})
- -> 0.0027s
+ -> 0.0060s
-- add_index("labels", ["title"], {:name=>"index_labels_on_title", :using=>:btree})
- -> 0.0030s
+ -> 0.0076s
-- add_index("labels", ["type", "project_id"], {:name=>"index_labels_on_type_and_project_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0061s
+-- create_table("lfs_file_locks", {:force=>:cascade})
+ -> 0.0078s
+-- add_index("lfs_file_locks", ["project_id", "path"], {:name=>"index_lfs_file_locks_on_project_id_and_path", :unique=>true, :using=>:btree})
+ -> 0.0067s
+-- add_index("lfs_file_locks", ["user_id"], {:name=>"index_lfs_file_locks_on_user_id", :using=>:btree})
+ -> 0.0060s
-- create_table("lfs_objects", {:force=>:cascade})
- -> 0.0040s
+ -> 0.0109s
-- add_index("lfs_objects", ["oid"], {:name=>"index_lfs_objects_on_oid", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0059s
-- create_table("lfs_objects_projects", {:force=>:cascade})
- -> 0.0035s
+ -> 0.0091s
-- add_index("lfs_objects_projects", ["project_id"], {:name=>"index_lfs_objects_projects_on_project_id", :using=>:btree})
- -> 0.0025s
+ -> 0.0060s
-- create_table("lists", {:force=>:cascade})
- -> 0.0033s
+ -> 0.0115s
-- add_index("lists", ["board_id", "label_id"], {:name=>"index_lists_on_board_id_and_label_id", :unique=>true, :using=>:btree})
- -> 0.0026s
+ -> 0.0055s
-- add_index("lists", ["label_id"], {:name=>"index_lists_on_label_id", :using=>:btree})
- -> 0.0026s
+ -> 0.0055s
-- create_table("members", {:force=>:cascade})
- -> 0.0046s
+ -> 0.0140s
-- add_index("members", ["access_level"], {:name=>"index_members_on_access_level", :using=>:btree})
- -> 0.0028s
+ -> 0.0067s
-- add_index("members", ["invite_token"], {:name=>"index_members_on_invite_token", :unique=>true, :using=>:btree})
- -> 0.0027s
+ -> 0.0069s
-- add_index("members", ["requested_at"], {:name=>"index_members_on_requested_at", :using=>:btree})
- -> 0.0025s
+ -> 0.0057s
-- add_index("members", ["source_id", "source_type"], {:name=>"index_members_on_source_id_and_source_type", :using=>:btree})
- -> 0.0027s
+ -> 0.0057s
-- add_index("members", ["user_id"], {:name=>"index_members_on_user_id", :using=>:btree})
- -> 0.0026s
+ -> 0.0073s
-- create_table("merge_request_diff_commits", {:id=>false, :force=>:cascade})
- -> 0.0027s
+ -> 0.0087s
-- add_index("merge_request_diff_commits", ["merge_request_diff_id", "relative_order"], {:name=>"index_merge_request_diff_commits_on_mr_diff_id_and_order", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0151s
-- add_index("merge_request_diff_commits", ["sha"], {:name=>"index_merge_request_diff_commits_on_sha", :using=>:btree})
- -> 0.0029s
+ -> 0.0057s
-- create_table("merge_request_diff_files", {:id=>false, :force=>:cascade})
- -> 0.0027s
+ -> 0.0094s
-- add_index("merge_request_diff_files", ["merge_request_diff_id", "relative_order"], {:name=>"index_merge_request_diff_files_on_mr_diff_id_and_order", :unique=>true, :using=>:btree})
- -> 0.0027s
+ -> 0.0138s
-- create_table("merge_request_diffs", {:force=>:cascade})
- -> 0.0042s
+ -> 0.0077s
-- add_index("merge_request_diffs", ["merge_request_id", "id"], {:name=>"index_merge_request_diffs_on_merge_request_id_and_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0060s
-- create_table("merge_request_metrics", {:force=>:cascade})
- -> 0.0034s
+ -> 0.0098s
-- add_index("merge_request_metrics", ["first_deployed_to_production_at"], {:name=>"index_merge_request_metrics_on_first_deployed_to_production_at", :using=>:btree})
- -> 0.0028s
+ -> 0.0060s
-- add_index("merge_request_metrics", ["merge_request_id"], {:name=>"index_merge_request_metrics", :using=>:btree})
- -> 0.0025s
+ -> 0.0050s
-- add_index("merge_request_metrics", ["pipeline_id"], {:name=>"index_merge_request_metrics_on_pipeline_id", :using=>:btree})
- -> 0.0026s
+ -> 0.0045s
-- create_table("merge_requests", {:force=>:cascade})
-> 0.0066s
-- add_index("merge_requests", ["assignee_id"], {:name=>"index_merge_requests_on_assignee_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0072s
-- add_index("merge_requests", ["author_id"], {:name=>"index_merge_requests_on_author_id", :using=>:btree})
- -> 0.0026s
+ -> 0.0050s
-- add_index("merge_requests", ["created_at"], {:name=>"index_merge_requests_on_created_at", :using=>:btree})
- -> 0.0026s
+ -> 0.0053s
-- add_index("merge_requests", ["description"], {:name=>"index_merge_requests_on_description_trigram", :using=>:gin, :opclasses=>{"description"=>"gin_trgm_ops"}})
- -> 0.0020s
+ -> 0.0008s
-- add_index("merge_requests", ["head_pipeline_id"], {:name=>"index_merge_requests_on_head_pipeline_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0053s
-- add_index("merge_requests", ["latest_merge_request_diff_id"], {:name=>"index_merge_requests_on_latest_merge_request_diff_id", :using=>:btree})
- -> 0.0025s
+ -> 0.0048s
-- add_index("merge_requests", ["merge_user_id"], {:name=>"index_merge_requests_on_merge_user_id", :where=>"(merge_user_id IS NOT NULL)", :using=>:btree})
- -> 0.0029s
+ -> 0.0051s
-- add_index("merge_requests", ["milestone_id"], {:name=>"index_merge_requests_on_milestone_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0055s
-- add_index("merge_requests", ["source_branch"], {:name=>"index_merge_requests_on_source_branch", :using=>:btree})
- -> 0.0026s
+ -> 0.0055s
-- add_index("merge_requests", ["source_project_id", "source_branch"], {:name=>"index_merge_requests_on_source_project_and_branch_state_opened", :where=>"((state)::text = 'opened'::text)", :using=>:btree})
- -> 0.0029s
+ -> 0.0061s
-- add_index("merge_requests", ["source_project_id", "source_branch"], {:name=>"index_merge_requests_on_source_project_id_and_source_branch", :using=>:btree})
- -> 0.0031s
+ -> 0.0068s
-- add_index("merge_requests", ["target_branch"], {:name=>"index_merge_requests_on_target_branch", :using=>:btree})
- -> 0.0028s
+ -> 0.0054s
-- add_index("merge_requests", ["target_project_id", "iid"], {:name=>"index_merge_requests_on_target_project_id_and_iid", :unique=>true, :using=>:btree})
- -> 0.0027s
+ -> 0.0061s
-- 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})
- -> 0.0029s
+ -> 0.0077s
-- add_index("merge_requests", ["title"], {:name=>"index_merge_requests_on_title", :using=>:btree})
- -> 0.0026s
+ -> 0.0105s
-- add_index("merge_requests", ["title"], {:name=>"index_merge_requests_on_title_trigram", :using=>:gin, :opclasses=>{"title"=>"gin_trgm_ops"}})
- -> 0.0020s
+ -> 0.0008s
-- add_index("merge_requests", ["updated_by_id"], {:name=>"index_merge_requests_on_updated_by_id", :where=>"(updated_by_id IS NOT NULL)", :using=>:btree})
- -> 0.0029s
+ -> 0.0074s
-- create_table("merge_requests_closing_issues", {:force=>:cascade})
- -> 0.0031s
+ -> 0.0125s
-- add_index("merge_requests_closing_issues", ["issue_id"], {:name=>"index_merge_requests_closing_issues_on_issue_id", :using=>:btree})
- -> 0.0026s
+ -> 0.0064s
-- add_index("merge_requests_closing_issues", ["merge_request_id"], {:name=>"index_merge_requests_closing_issues_on_merge_request_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0061s
-- create_table("milestones", {:force=>:cascade})
- -> 0.0044s
+ -> 0.0064s
-- add_index("milestones", ["description"], {:name=>"index_milestones_on_description_trigram", :using=>:gin, :opclasses=>{"description"=>"gin_trgm_ops"}})
- -> 0.0022s
+ -> 0.0007s
-- add_index("milestones", ["due_date"], {:name=>"index_milestones_on_due_date", :using=>:btree})
- -> 0.0033s
+ -> 0.0053s
-- add_index("milestones", ["group_id"], {:name=>"index_milestones_on_group_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0068s
-- add_index("milestones", ["project_id", "iid"], {:name=>"index_milestones_on_project_id_and_iid", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0057s
-- add_index("milestones", ["title"], {:name=>"index_milestones_on_title", :using=>:btree})
- -> 0.0026s
+ -> 0.0051s
-- add_index("milestones", ["title"], {:name=>"index_milestones_on_title_trigram", :using=>:gin, :opclasses=>{"title"=>"gin_trgm_ops"}})
- -> 0.0021s
+ -> 0.0006s
-- create_table("namespaces", {:force=>:cascade})
- -> 0.0068s
+ -> 0.0083s
-- add_index("namespaces", ["created_at"], {:name=>"index_namespaces_on_created_at", :using=>:btree})
- -> 0.0030s
+ -> 0.0061s
-- add_index("namespaces", ["name", "parent_id"], {:name=>"index_namespaces_on_name_and_parent_id", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0062s
-- add_index("namespaces", ["name"], {:name=>"index_namespaces_on_name_trigram", :using=>:gin, :opclasses=>{"name"=>"gin_trgm_ops"}})
- -> 0.0020s
+ -> 0.0006s
-- add_index("namespaces", ["owner_id"], {:name=>"index_namespaces_on_owner_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0061s
-- add_index("namespaces", ["parent_id", "id"], {:name=>"index_namespaces_on_parent_id_and_id", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0072s
-- add_index("namespaces", ["path"], {:name=>"index_namespaces_on_path", :using=>:btree})
- -> 0.0031s
+ -> 0.0056s
-- add_index("namespaces", ["path"], {:name=>"index_namespaces_on_path_trigram", :using=>:gin, :opclasses=>{"path"=>"gin_trgm_ops"}})
- -> 0.0019s
+ -> 0.0006s
-- add_index("namespaces", ["require_two_factor_authentication"], {:name=>"index_namespaces_on_require_two_factor_authentication", :using=>:btree})
- -> 0.0029s
+ -> 0.0061s
-- add_index("namespaces", ["type"], {:name=>"index_namespaces_on_type", :using=>:btree})
- -> 0.0032s
--- create_table("notes", {:force=>:cascade})
-> 0.0055s
+-- create_table("notes", {:force=>:cascade})
+ -> 0.0092s
-- add_index("notes", ["author_id"], {:name=>"index_notes_on_author_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0072s
-- add_index("notes", ["commit_id"], {:name=>"index_notes_on_commit_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0057s
-- add_index("notes", ["created_at"], {:name=>"index_notes_on_created_at", :using=>:btree})
- -> 0.0029s
+ -> 0.0065s
-- add_index("notes", ["discussion_id"], {:name=>"index_notes_on_discussion_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0064s
-- add_index("notes", ["line_code"], {:name=>"index_notes_on_line_code", :using=>:btree})
- -> 0.0029s
+ -> 0.0078s
-- add_index("notes", ["note"], {:name=>"index_notes_on_note_trigram", :using=>:gin, :opclasses=>{"note"=>"gin_trgm_ops"}})
- -> 0.0024s
+ -> 0.0006s
-- add_index("notes", ["noteable_id", "noteable_type"], {:name=>"index_notes_on_noteable_id_and_noteable_type", :using=>:btree})
- -> 0.0029s
+ -> 0.0102s
-- add_index("notes", ["noteable_type"], {:name=>"index_notes_on_noteable_type", :using=>:btree})
- -> 0.0030s
+ -> 0.0092s
-- add_index("notes", ["project_id", "noteable_type"], {:name=>"index_notes_on_project_id_and_noteable_type", :using=>:btree})
- -> 0.0027s
+ -> 0.0082s
-- add_index("notes", ["updated_at"], {:name=>"index_notes_on_updated_at", :using=>:btree})
- -> 0.0026s
+ -> 0.0062s
-- create_table("notification_settings", {:force=>:cascade})
- -> 0.0053s
+ -> 0.0088s
-- add_index("notification_settings", ["source_id", "source_type"], {:name=>"index_notification_settings_on_source_id_and_source_type", :using=>:btree})
- -> 0.0028s
+ -> 0.0405s
-- add_index("notification_settings", ["user_id", "source_id", "source_type"], {:name=>"index_notifications_on_user_id_and_source_id_and_source_type", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0677s
-- add_index("notification_settings", ["user_id"], {:name=>"index_notification_settings_on_user_id", :using=>:btree})
- -> 0.0031s
+ -> 0.1199s
-- create_table("oauth_access_grants", {:force=>:cascade})
- -> 0.0042s
+ -> 0.0140s
-- add_index("oauth_access_grants", ["token"], {:name=>"index_oauth_access_grants_on_token", :unique=>true, :using=>:btree})
- -> 0.0031s
+ -> 0.0076s
-- create_table("oauth_access_tokens", {:force=>:cascade})
- -> 0.0051s
+ -> 0.0167s
-- add_index("oauth_access_tokens", ["refresh_token"], {:name=>"index_oauth_access_tokens_on_refresh_token", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0098s
-- add_index("oauth_access_tokens", ["resource_owner_id"], {:name=>"index_oauth_access_tokens_on_resource_owner_id", :using=>:btree})
- -> 0.0025s
+ -> 0.0074s
-- add_index("oauth_access_tokens", ["token"], {:name=>"index_oauth_access_tokens_on_token", :unique=>true, :using=>:btree})
- -> 0.0026s
+ -> 0.0078s
-- create_table("oauth_applications", {:force=>:cascade})
- -> 0.0049s
+ -> 0.0112s
-- add_index("oauth_applications", ["owner_id", "owner_type"], {:name=>"index_oauth_applications_on_owner_id_and_owner_type", :using=>:btree})
- -> 0.0030s
+ -> 0.0079s
-- add_index("oauth_applications", ["uid"], {:name=>"index_oauth_applications_on_uid", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0114s
-- create_table("oauth_openid_requests", {:force=>:cascade})
- -> 0.0048s
+ -> 0.0102s
-- create_table("pages_domains", {:force=>:cascade})
- -> 0.0052s
+ -> 0.0102s
-- add_index("pages_domains", ["domain"], {:name=>"index_pages_domains_on_domain", :unique=>true, :using=>:btree})
- -> 0.0027s
+ -> 0.0067s
+-- add_index("pages_domains", ["project_id", "enabled_until"], {:name=>"index_pages_domains_on_project_id_and_enabled_until", :using=>:btree})
+ -> 0.0114s
-- add_index("pages_domains", ["project_id"], {:name=>"index_pages_domains_on_project_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0066s
+-- add_index("pages_domains", ["verified_at", "enabled_until"], {:name=>"index_pages_domains_on_verified_at_and_enabled_until", :using=>:btree})
+ -> 0.0073s
+-- add_index("pages_domains", ["verified_at"], {:name=>"index_pages_domains_on_verified_at", :using=>:btree})
+ -> 0.0063s
-- create_table("personal_access_tokens", {:force=>:cascade})
- -> 0.0056s
+ -> 0.0084s
-- add_index("personal_access_tokens", ["token"], {:name=>"index_personal_access_tokens_on_token", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0075s
-- add_index("personal_access_tokens", ["user_id"], {:name=>"index_personal_access_tokens_on_user_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0066s
-- create_table("project_authorizations", {:id=>false, :force=>:cascade})
- -> 0.0018s
+ -> 0.0087s
-- add_index("project_authorizations", ["project_id"], {:name=>"index_project_authorizations_on_project_id", :using=>:btree})
- -> 0.0033s
+ -> 0.0056s
-- add_index("project_authorizations", ["user_id", "project_id", "access_level"], {:name=>"index_project_authorizations_on_user_id_project_id_access_level", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0075s
-- create_table("project_auto_devops", {:force=>:cascade})
- -> 0.0043s
+ -> 0.0079s
-- add_index("project_auto_devops", ["project_id"], {:name=>"index_project_auto_devops_on_project_id", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0067s
-- create_table("project_custom_attributes", {:force=>:cascade})
- -> 0.0047s
+ -> 0.0071s
-- add_index("project_custom_attributes", ["key", "value"], {:name=>"index_project_custom_attributes_on_key_and_value", :using=>:btree})
- -> 0.0030s
+ -> 0.0060s
-- add_index("project_custom_attributes", ["project_id", "key"], {:name=>"index_project_custom_attributes_on_project_id_and_key", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0069s
-- create_table("project_features", {:force=>:cascade})
- -> 0.0038s
+ -> 0.0100s
-- add_index("project_features", ["project_id"], {:name=>"index_project_features_on_project_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0069s
-- create_table("project_group_links", {:force=>:cascade})
- -> 0.0036s
+ -> 0.0117s
-- add_index("project_group_links", ["group_id"], {:name=>"index_project_group_links_on_group_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0121s
-- add_index("project_group_links", ["project_id"], {:name=>"index_project_group_links_on_project_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0076s
-- create_table("project_import_data", {:force=>:cascade})
- -> 0.0049s
+ -> 0.0084s
-- add_index("project_import_data", ["project_id"], {:name=>"index_project_import_data_on_project_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0058s
-- create_table("project_statistics", {:force=>:cascade})
- -> 0.0046s
+ -> 0.0075s
-- add_index("project_statistics", ["namespace_id"], {:name=>"index_project_statistics_on_namespace_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0054s
-- add_index("project_statistics", ["project_id"], {:name=>"index_project_statistics_on_project_id", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0054s
-- create_table("projects", {:force=>:cascade})
- -> 0.0090s
+ -> 0.0077s
-- add_index("projects", ["ci_id"], {:name=>"index_projects_on_ci_id", :using=>:btree})
- -> 0.0033s
+ -> 0.0070s
-- add_index("projects", ["created_at"], {:name=>"index_projects_on_created_at", :using=>:btree})
- -> 0.0030s
+ -> 0.0060s
-- add_index("projects", ["creator_id"], {:name=>"index_projects_on_creator_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0071s
-- add_index("projects", ["description"], {:name=>"index_projects_on_description_trigram", :using=>:gin, :opclasses=>{"description"=>"gin_trgm_ops"}})
- -> 0.0022s
+ -> 0.0009s
+-- add_index("projects", ["id"], {:name=>"index_projects_on_id_partial_for_visibility", :unique=>true, :where=>"(visibility_level = ANY (ARRAY[10, 20]))", :using=>:btree})
+ -> 0.0062s
-- add_index("projects", ["last_activity_at"], {:name=>"index_projects_on_last_activity_at", :using=>:btree})
- -> 0.0032s
+ -> 0.0060s
-- add_index("projects", ["last_repository_check_failed"], {:name=>"index_projects_on_last_repository_check_failed", :using=>:btree})
- -> 0.0030s
+ -> 0.0063s
-- add_index("projects", ["last_repository_updated_at"], {:name=>"index_projects_on_last_repository_updated_at", :using=>:btree})
- -> 0.0031s
+ -> 0.0633s
-- add_index("projects", ["name"], {:name=>"index_projects_on_name_trigram", :using=>:gin, :opclasses=>{"name"=>"gin_trgm_ops"}})
- -> 0.0022s
+ -> 0.0012s
-- add_index("projects", ["namespace_id"], {:name=>"index_projects_on_namespace_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0167s
-- add_index("projects", ["path"], {:name=>"index_projects_on_path", :using=>:btree})
- -> 0.0028s
+ -> 0.0222s
-- add_index("projects", ["path"], {:name=>"index_projects_on_path_trigram", :using=>:gin, :opclasses=>{"path"=>"gin_trgm_ops"}})
- -> 0.0023s
+ -> 0.0010s
-- add_index("projects", ["pending_delete"], {:name=>"index_projects_on_pending_delete", :using=>:btree})
- -> 0.0029s
+ -> 0.0229s
-- add_index("projects", ["repository_storage"], {:name=>"index_projects_on_repository_storage", :using=>:btree})
- -> 0.0026s
+ -> 0.0173s
-- add_index("projects", ["runners_token"], {:name=>"index_projects_on_runners_token", :using=>:btree})
- -> 0.0034s
+ -> 0.0167s
-- add_index("projects", ["star_count"], {:name=>"index_projects_on_star_count", :using=>:btree})
- -> 0.0028s
+ -> 0.0491s
-- add_index("projects", ["visibility_level"], {:name=>"index_projects_on_visibility_level", :using=>:btree})
- -> 0.0027s
+ -> 0.0598s
-- create_table("protected_branch_merge_access_levels", {:force=>:cascade})
- -> 0.0042s
+ -> 0.1964s
-- add_index("protected_branch_merge_access_levels", ["protected_branch_id"], {:name=>"index_protected_branch_merge_access", :using=>:btree})
- -> 0.0029s
+ -> 0.1112s
-- create_table("protected_branch_push_access_levels", {:force=>:cascade})
- -> 0.0037s
+ -> 0.0195s
-- add_index("protected_branch_push_access_levels", ["protected_branch_id"], {:name=>"index_protected_branch_push_access", :using=>:btree})
- -> 0.0030s
+ -> 0.0069s
-- create_table("protected_branches", {:force=>:cascade})
- -> 0.0048s
+ -> 0.0113s
-- add_index("protected_branches", ["project_id"], {:name=>"index_protected_branches_on_project_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0071s
-- create_table("protected_tag_create_access_levels", {:force=>:cascade})
- -> 0.0037s
+ -> 0.0180s
-- add_index("protected_tag_create_access_levels", ["protected_tag_id"], {:name=>"index_protected_tag_create_access", :using=>:btree})
- -> 0.0029s
+ -> 0.0068s
-- add_index("protected_tag_create_access_levels", ["user_id"], {:name=>"index_protected_tag_create_access_levels_on_user_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0077s
-- create_table("protected_tags", {:force=>:cascade})
- -> 0.0051s
+ -> 0.0115s
-- add_index("protected_tags", ["project_id"], {:name=>"index_protected_tags_on_project_id", :using=>:btree})
- -> 0.0034s
+ -> 0.0081s
-- create_table("push_event_payloads", {:id=>false, :force=>:cascade})
- -> 0.0030s
+ -> 0.0108s
-- add_index("push_event_payloads", ["event_id"], {:name=>"index_push_event_payloads_on_event_id", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0189s
-- create_table("redirect_routes", {:force=>:cascade})
- -> 0.0049s
+ -> 0.0106s
-- add_index("redirect_routes", ["path"], {:name=>"index_redirect_routes_on_path", :unique=>true, :using=>:btree})
- -> 0.0031s
+ -> 0.0075s
-- add_index("redirect_routes", ["source_type", "source_id"], {:name=>"index_redirect_routes_on_source_type_and_source_id", :using=>:btree})
- -> 0.0034s
+ -> 0.0099s
-- create_table("releases", {:force=>:cascade})
- -> 0.0043s
+ -> 0.0126s
-- add_index("releases", ["project_id", "tag"], {:name=>"index_releases_on_project_id_and_tag", :using=>:btree})
- -> 0.0032s
+ -> 0.0066s
-- add_index("releases", ["project_id"], {:name=>"index_releases_on_project_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0060s
-- create_table("routes", {:force=>:cascade})
- -> 0.0055s
+ -> 0.0091s
-- add_index("routes", ["path"], {:name=>"index_routes_on_path", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0073s
-- add_index("routes", ["path"], {:name=>"index_routes_on_path_text_pattern_ops", :using=>:btree, :opclasses=>{"path"=>"varchar_pattern_ops"}})
- -> 0.0026s
+ -> 0.0004s
-- add_index("routes", ["source_type", "source_id"], {:name=>"index_routes_on_source_type_and_source_id", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0111s
-- create_table("sent_notifications", {:force=>:cascade})
- -> 0.0048s
+ -> 0.0093s
-- add_index("sent_notifications", ["reply_key"], {:name=>"index_sent_notifications_on_reply_key", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0060s
-- create_table("services", {:force=>:cascade})
- -> 0.0091s
+ -> 0.0099s
-- add_index("services", ["project_id"], {:name=>"index_services_on_project_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0068s
-- add_index("services", ["template"], {:name=>"index_services_on_template", :using=>:btree})
- -> 0.0031s
+ -> 0.0076s
-- create_table("snippets", {:force=>:cascade})
- -> 0.0050s
+ -> 0.0073s
-- add_index("snippets", ["author_id"], {:name=>"index_snippets_on_author_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0055s
-- add_index("snippets", ["file_name"], {:name=>"index_snippets_on_file_name_trigram", :using=>:gin, :opclasses=>{"file_name"=>"gin_trgm_ops"}})
- -> 0.0020s
+ -> 0.0006s
-- add_index("snippets", ["project_id"], {:name=>"index_snippets_on_project_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0058s
-- add_index("snippets", ["title"], {:name=>"index_snippets_on_title_trigram", :using=>:gin, :opclasses=>{"title"=>"gin_trgm_ops"}})
- -> 0.0020s
+ -> 0.0005s
-- add_index("snippets", ["updated_at"], {:name=>"index_snippets_on_updated_at", :using=>:btree})
- -> 0.0026s
+ -> 0.0100s
-- add_index("snippets", ["visibility_level"], {:name=>"index_snippets_on_visibility_level", :using=>:btree})
- -> 0.0026s
+ -> 0.0091s
-- create_table("spam_logs", {:force=>:cascade})
- -> 0.0048s
+ -> 0.0129s
-- create_table("subscriptions", {:force=>:cascade})
- -> 0.0041s
+ -> 0.0094s
-- add_index("subscriptions", ["subscribable_id", "subscribable_type", "user_id", "project_id"], {:name=>"index_subscriptions_on_subscribable_and_user_id_and_project_id", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0107s
-- create_table("system_note_metadata", {:force=>:cascade})
- -> 0.0040s
+ -> 0.0138s
-- add_index("system_note_metadata", ["note_id"], {:name=>"index_system_note_metadata_on_note_id", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0060s
-- create_table("taggings", {:force=>:cascade})
- -> 0.0047s
+ -> 0.0121s
-- add_index("taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], {:name=>"taggings_idx", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0078s
+-- add_index("taggings", ["tag_id"], {:name=>"index_taggings_on_tag_id", :using=>:btree})
+ -> 0.0058s
-- add_index("taggings", ["taggable_id", "taggable_type", "context"], {:name=>"index_taggings_on_taggable_id_and_taggable_type_and_context", :using=>:btree})
- -> 0.0025s
+ -> 0.0059s
+-- add_index("taggings", ["taggable_id", "taggable_type"], {:name=>"index_taggings_on_taggable_id_and_taggable_type", :using=>:btree})
+ -> 0.0056s
-- create_table("tags", {:force=>:cascade})
- -> 0.0044s
+ -> 0.0063s
-- add_index("tags", ["name"], {:name=>"index_tags_on_name", :unique=>true, :using=>:btree})
- -> 0.0026s
+ -> 0.0055s
-- create_table("timelogs", {:force=>:cascade})
- -> 0.0033s
+ -> 0.0061s
-- add_index("timelogs", ["issue_id"], {:name=>"index_timelogs_on_issue_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0063s
-- add_index("timelogs", ["merge_request_id"], {:name=>"index_timelogs_on_merge_request_id", :using=>:btree})
- -> 0.0033s
+ -> 0.0052s
-- add_index("timelogs", ["user_id"], {:name=>"index_timelogs_on_user_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0055s
-- create_table("todos", {:force=>:cascade})
- -> 0.0043s
+ -> 0.0065s
-- add_index("todos", ["author_id"], {:name=>"index_todos_on_author_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0081s
-- add_index("todos", ["commit_id"], {:name=>"index_todos_on_commit_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0085s
-- add_index("todos", ["note_id"], {:name=>"index_todos_on_note_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0083s
-- add_index("todos", ["project_id"], {:name=>"index_todos_on_project_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0094s
-- add_index("todos", ["target_type", "target_id"], {:name=>"index_todos_on_target_type_and_target_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0070s
+-- add_index("todos", ["user_id", "id"], {:name=>"index_todos_on_user_id_and_id_done", :where=>"((state)::text = 'done'::text)", :using=>:btree})
+ -> 0.0099s
+-- add_index("todos", ["user_id", "id"], {:name=>"index_todos_on_user_id_and_id_pending", :where=>"((state)::text = 'pending'::text)", :using=>:btree})
+ -> 0.0080s
-- add_index("todos", ["user_id"], {:name=>"index_todos_on_user_id", :using=>:btree})
- -> 0.0026s
+ -> 0.0061s
-- create_table("trending_projects", {:force=>:cascade})
- -> 0.0030s
--- add_index("trending_projects", ["project_id"], {:name=>"index_trending_projects_on_project_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0081s
+-- add_index("trending_projects", ["project_id"], {:name=>"index_trending_projects_on_project_id", :unique=>true, :using=>:btree})
+ -> 0.0046s
-- create_table("u2f_registrations", {:force=>:cascade})
- -> 0.0048s
+ -> 0.0063s
-- add_index("u2f_registrations", ["key_handle"], {:name=>"index_u2f_registrations_on_key_handle", :using=>:btree})
- -> 0.0029s
+ -> 0.0052s
-- add_index("u2f_registrations", ["user_id"], {:name=>"index_u2f_registrations_on_user_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0072s
-- create_table("uploads", {:force=>:cascade})
- -> 0.0044s
+ -> 0.0067s
-- add_index("uploads", ["checksum"], {:name=>"index_uploads_on_checksum", :using=>:btree})
- -> 0.0028s
+ -> 0.0046s
-- add_index("uploads", ["model_id", "model_type"], {:name=>"index_uploads_on_model_id_and_model_type", :using=>:btree})
- -> 0.0027s
--- add_index("uploads", ["path"], {:name=>"index_uploads_on_path", :using=>:btree})
- -> 0.0028s
+ -> 0.0049s
+-- add_index("uploads", ["uploader", "path"], {:name=>"index_uploads_on_uploader_and_path", :using=>:btree})
+ -> 0.0052s
-- create_table("user_agent_details", {:force=>:cascade})
- -> 0.0051s
+ -> 0.0059s
-- add_index("user_agent_details", ["subject_id", "subject_type"], {:name=>"index_user_agent_details_on_subject_id_and_subject_type", :using=>:btree})
- -> 0.0028s
+ -> 0.0052s
+-- create_table("user_callouts", {:force=>:cascade})
+ -> 0.0059s
+-- add_index("user_callouts", ["user_id", "feature_name"], {:name=>"index_user_callouts_on_user_id_and_feature_name", :unique=>true, :using=>:btree})
+ -> 0.0094s
+-- add_index("user_callouts", ["user_id"], {:name=>"index_user_callouts_on_user_id", :using=>:btree})
+ -> 0.0064s
-- create_table("user_custom_attributes", {:force=>:cascade})
- -> 0.0044s
+ -> 0.0086s
-- add_index("user_custom_attributes", ["key", "value"], {:name=>"index_user_custom_attributes_on_key_and_value", :using=>:btree})
- -> 0.0027s
+ -> 0.0080s
-- add_index("user_custom_attributes", ["user_id", "key"], {:name=>"index_user_custom_attributes_on_user_id_and_key", :unique=>true, :using=>:btree})
- -> 0.0026s
--- create_table("user_synced_attributes_metadata", {:force=>:cascade})
+ -> 0.0066s
+-- create_table("user_interacted_projects", {:id=>false, :force=>:cascade})
+ -> 0.0108s
+-- add_index("user_interacted_projects", ["project_id", "user_id"], {:name=>"index_user_interacted_projects_on_project_id_and_user_id", :unique=>true, :using=>:btree})
+ -> 0.0114s
+-- add_index("user_interacted_projects", ["user_id"], {:name=>"index_user_interacted_projects_on_user_id", :using=>:btree})
-> 0.0056s
+-- create_table("user_synced_attributes_metadata", {:force=>:cascade})
+ -> 0.0115s
-- add_index("user_synced_attributes_metadata", ["user_id"], {:name=>"index_user_synced_attributes_metadata_on_user_id", :unique=>true, :using=>:btree})
- -> 0.0027s
+ -> 0.0054s
-- create_table("users", {:force=>:cascade})
- -> 0.0134s
+ -> 0.0111s
-- add_index("users", ["admin"], {:name=>"index_users_on_admin", :using=>:btree})
- -> 0.0030s
+ -> 0.0065s
-- add_index("users", ["confirmation_token"], {:name=>"index_users_on_confirmation_token", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0065s
-- add_index("users", ["created_at"], {:name=>"index_users_on_created_at", :using=>:btree})
- -> 0.0034s
+ -> 0.0068s
-- add_index("users", ["email"], {:name=>"index_users_on_email", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0066s
-- add_index("users", ["email"], {:name=>"index_users_on_email_trigram", :using=>:gin, :opclasses=>{"email"=>"gin_trgm_ops"}})
- -> 0.0431s
+ -> 0.0011s
-- add_index("users", ["ghost"], {:name=>"index_users_on_ghost", :using=>:btree})
- -> 0.0051s
+ -> 0.0063s
-- add_index("users", ["incoming_email_token"], {:name=>"index_users_on_incoming_email_token", :using=>:btree})
- -> 0.0044s
+ -> 0.0057s
-- add_index("users", ["name"], {:name=>"index_users_on_name", :using=>:btree})
- -> 0.0044s
+ -> 0.0056s
-- add_index("users", ["name"], {:name=>"index_users_on_name_trigram", :using=>:gin, :opclasses=>{"name"=>"gin_trgm_ops"}})
- -> 0.0034s
+ -> 0.0011s
-- add_index("users", ["reset_password_token"], {:name=>"index_users_on_reset_password_token", :unique=>true, :using=>:btree})
- -> 0.0044s
+ -> 0.0055s
-- add_index("users", ["rss_token"], {:name=>"index_users_on_rss_token", :using=>:btree})
- -> 0.0046s
+ -> 0.0068s
-- add_index("users", ["state"], {:name=>"index_users_on_state", :using=>:btree})
- -> 0.0040s
+ -> 0.0067s
-- add_index("users", ["username"], {:name=>"index_users_on_username", :using=>:btree})
- -> 0.0046s
+ -> 0.0072s
-- add_index("users", ["username"], {:name=>"index_users_on_username_trigram", :using=>:gin, :opclasses=>{"username"=>"gin_trgm_ops"}})
- -> 0.0044s
+ -> 0.0012s
-- create_table("users_star_projects", {:force=>:cascade})
- -> 0.0055s
+ -> 0.0100s
-- add_index("users_star_projects", ["project_id"], {:name=>"index_users_star_projects_on_project_id", :using=>:btree})
- -> 0.0037s
+ -> 0.0061s
-- add_index("users_star_projects", ["user_id", "project_id"], {:name=>"index_users_star_projects_on_user_id_and_project_id", :unique=>true, :using=>:btree})
- -> 0.0044s
+ -> 0.0068s
-- create_table("web_hook_logs", {:force=>:cascade})
- -> 0.0060s
+ -> 0.0097s
-- add_index("web_hook_logs", ["web_hook_id"], {:name=>"index_web_hook_logs_on_web_hook_id", :using=>:btree})
- -> 0.0034s
+ -> 0.0057s
-- create_table("web_hooks", {:force=>:cascade})
- -> 0.0120s
+ -> 0.0080s
-- add_index("web_hooks", ["project_id"], {:name=>"index_web_hooks_on_project_id", :using=>:btree})
- -> 0.0038s
+ -> 0.0062s
-- add_index("web_hooks", ["type"], {:name=>"index_web_hooks_on_type", :using=>:btree})
- -> 0.0036s
+ -> 0.0065s
+-- add_foreign_key("badges", "namespaces", {:column=>"group_id", :on_delete=>:cascade})
+ -> 0.0158s
+-- add_foreign_key("badges", "projects", {:on_delete=>:cascade})
+ -> 0.0140s
+-- add_foreign_key("boards", "namespaces", {:column=>"group_id", :on_delete=>:cascade})
+ -> 0.0138s
-- add_foreign_key("boards", "projects", {:name=>"fk_f15266b5f9", :on_delete=>:cascade})
- -> 0.0030s
+ -> 0.0118s
-- add_foreign_key("chat_teams", "namespaces", {:on_delete=>:cascade})
- -> 0.0021s
+ -> 0.0130s
-- add_foreign_key("ci_build_trace_section_names", "projects", {:on_delete=>:cascade})
- -> 0.0022s
+ -> 0.0131s
-- add_foreign_key("ci_build_trace_sections", "ci_build_trace_section_names", {:column=>"section_name_id", :name=>"fk_264e112c66", :on_delete=>:cascade})
- -> 0.0018s
+ -> 0.0210s
-- add_foreign_key("ci_build_trace_sections", "ci_builds", {:column=>"build_id", :name=>"fk_4ebe41f502", :on_delete=>:cascade})
- -> 0.0024s
+ -> 0.0823s
-- add_foreign_key("ci_build_trace_sections", "projects", {:on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0942s
-- add_foreign_key("ci_builds", "ci_pipelines", {:column=>"auto_canceled_by_id", :name=>"fk_a2141b1522", :on_delete=>:nullify})
- -> 0.0023s
+ -> 0.1346s
-- add_foreign_key("ci_builds", "ci_stages", {:column=>"stage_id", :name=>"fk_3a9eaa254d", :on_delete=>:cascade})
- -> 0.0020s
+ -> 0.0506s
-- add_foreign_key("ci_builds", "projects", {:name=>"fk_befce0568a", :on_delete=>:cascade})
- -> 0.0024s
+ -> 0.0403s
+-- add_foreign_key("ci_builds_metadata", "ci_builds", {:column=>"build_id", :on_delete=>:cascade})
+ -> 0.0160s
+-- add_foreign_key("ci_builds_metadata", "projects", {:on_delete=>:cascade})
+ -> 0.0165s
-- add_foreign_key("ci_group_variables", "namespaces", {:column=>"group_id", :name=>"fk_33ae4d58d8", :on_delete=>:cascade})
- -> 0.0024s
+ -> 0.0153s
-- add_foreign_key("ci_job_artifacts", "ci_builds", {:column=>"job_id", :on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0160s
-- add_foreign_key("ci_job_artifacts", "projects", {:on_delete=>:cascade})
- -> 0.0020s
+ -> 0.0278s
-- add_foreign_key("ci_pipeline_schedule_variables", "ci_pipeline_schedules", {:column=>"pipeline_schedule_id", :name=>"fk_41c35fda51", :on_delete=>:cascade})
- -> 0.0027s
+ -> 0.0193s
-- add_foreign_key("ci_pipeline_schedules", "projects", {:name=>"fk_8ead60fcc4", :on_delete=>:cascade})
- -> 0.0022s
+ -> 0.0184s
-- add_foreign_key("ci_pipeline_schedules", "users", {:column=>"owner_id", :name=>"fk_9ea99f58d2", :on_delete=>:nullify})
- -> 0.0025s
+ -> 0.0158s
-- add_foreign_key("ci_pipeline_variables", "ci_pipelines", {:column=>"pipeline_id", :name=>"fk_f29c5f4380", :on_delete=>:cascade})
- -> 0.0018s
+ -> 0.0097s
-- add_foreign_key("ci_pipelines", "ci_pipeline_schedules", {:column=>"pipeline_schedule_id", :name=>"fk_3d34ab2e06", :on_delete=>:nullify})
- -> 0.0019s
+ -> 0.0693s
-- add_foreign_key("ci_pipelines", "ci_pipelines", {:column=>"auto_canceled_by_id", :name=>"fk_262d4c2d19", :on_delete=>:nullify})
- -> 0.0029s
+ -> 0.1599s
-- add_foreign_key("ci_pipelines", "projects", {:name=>"fk_86635dbd80", :on_delete=>:cascade})
- -> 0.0023s
+ -> 0.1505s
-- add_foreign_key("ci_runner_projects", "projects", {:name=>"fk_4478a6f1e4", :on_delete=>:cascade})
- -> 0.0036s
+ -> 0.0984s
-- add_foreign_key("ci_stages", "ci_pipelines", {:column=>"pipeline_id", :name=>"fk_fb57e6cc56", :on_delete=>:cascade})
- -> 0.0017s
+ -> 0.1152s
-- add_foreign_key("ci_stages", "projects", {:name=>"fk_2360681d1d", :on_delete=>:cascade})
- -> 0.0020s
+ -> 0.1062s
-- add_foreign_key("ci_trigger_requests", "ci_triggers", {:column=>"trigger_id", :name=>"fk_b8ec8b7245", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0455s
-- add_foreign_key("ci_triggers", "projects", {:name=>"fk_e3e63f966e", :on_delete=>:cascade})
- -> 0.0021s
+ -> 0.0725s
-- add_foreign_key("ci_triggers", "users", {:column=>"owner_id", :name=>"fk_e8e10d1964", :on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0774s
-- add_foreign_key("ci_variables", "projects", {:name=>"fk_ada5eb64b3", :on_delete=>:cascade})
- -> 0.0021s
+ -> 0.0626s
-- add_foreign_key("cluster_platforms_kubernetes", "clusters", {:on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0529s
-- add_foreign_key("cluster_projects", "clusters", {:on_delete=>:cascade})
- -> 0.0018s
+ -> 0.0678s
-- add_foreign_key("cluster_projects", "projects", {:on_delete=>:cascade})
- -> 0.0020s
+ -> 0.0391s
-- add_foreign_key("cluster_providers_gcp", "clusters", {:on_delete=>:cascade})
- -> 0.0017s
+ -> 0.0328s
-- add_foreign_key("clusters", "users", {:on_delete=>:nullify})
- -> 0.0018s
+ -> 0.1266s
-- add_foreign_key("clusters_applications_helm", "clusters", {:on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0489s
+-- add_foreign_key("clusters_applications_ingress", "clusters", {:name=>"fk_753a7b41c1", :on_delete=>:cascade})
+ -> 0.0565s
+-- add_foreign_key("clusters_applications_prometheus", "clusters", {:name=>"fk_557e773639", :on_delete=>:cascade})
+ -> 0.0174s
+-- add_foreign_key("clusters_applications_runners", "ci_runners", {:column=>"runner_id", :name=>"fk_02de2ded36", :on_delete=>:nullify})
+ -> 0.0182s
+-- add_foreign_key("clusters_applications_runners", "clusters", {:on_delete=>:cascade})
+ -> 0.0208s
-- add_foreign_key("container_repositories", "projects")
- -> 0.0020s
+ -> 0.0186s
-- add_foreign_key("deploy_keys_projects", "projects", {:name=>"fk_58a901ca7e", :on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0140s
-- add_foreign_key("deployments", "projects", {:name=>"fk_b9a3851b82", :on_delete=>:cascade})
- -> 0.0021s
+ -> 0.0328s
-- add_foreign_key("environments", "projects", {:name=>"fk_d1c8c1da6a", :on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0221s
-- add_foreign_key("events", "projects", {:on_delete=>:cascade})
- -> 0.0020s
+ -> 0.0212s
-- add_foreign_key("events", "users", {:column=>"author_id", :name=>"fk_edfd187b6f", :on_delete=>:cascade})
- -> 0.0020s
+ -> 0.0150s
-- add_foreign_key("fork_network_members", "fork_networks", {:on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0134s
-- add_foreign_key("fork_network_members", "projects", {:column=>"forked_from_project_id", :name=>"fk_b01280dae4", :on_delete=>:nullify})
- -> 0.0019s
+ -> 0.0200s
-- add_foreign_key("fork_network_members", "projects", {:on_delete=>:cascade})
- -> 0.0018s
+ -> 0.0162s
-- add_foreign_key("fork_networks", "projects", {:column=>"root_project_id", :name=>"fk_e7b436b2b5", :on_delete=>:nullify})
- -> 0.0018s
+ -> 0.0138s
-- add_foreign_key("forked_project_links", "projects", {:column=>"forked_to_project_id", :name=>"fk_434510edb0", :on_delete=>:cascade})
- -> 0.0018s
+ -> 0.0137s
-- add_foreign_key("gcp_clusters", "projects", {:on_delete=>:cascade})
- -> 0.0029s
+ -> 0.0148s
-- add_foreign_key("gcp_clusters", "services", {:on_delete=>:nullify})
- -> 0.0022s
+ -> 0.0216s
-- add_foreign_key("gcp_clusters", "users", {:on_delete=>:nullify})
- -> 0.0019s
+ -> 0.0156s
-- add_foreign_key("gpg_key_subkeys", "gpg_keys", {:on_delete=>:cascade})
- -> 0.0017s
+ -> 0.0139s
-- add_foreign_key("gpg_keys", "users", {:on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0142s
-- add_foreign_key("gpg_signatures", "gpg_key_subkeys", {:on_delete=>:nullify})
- -> 0.0016s
+ -> 0.0216s
-- add_foreign_key("gpg_signatures", "gpg_keys", {:on_delete=>:nullify})
- -> 0.0016s
+ -> 0.0211s
-- add_foreign_key("gpg_signatures", "projects", {:on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0215s
-- add_foreign_key("group_custom_attributes", "namespaces", {:column=>"group_id", :on_delete=>:cascade})
- -> 0.0014s
+ -> 0.0174s
+-- add_foreign_key("internal_ids", "projects", {:on_delete=>:cascade})
+ -> 0.0143s
-- add_foreign_key("issue_assignees", "issues", {:name=>"fk_b7d881734a", :on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0139s
-- add_foreign_key("issue_assignees", "users", {:name=>"fk_5e0c8d9154", :on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0138s
-- add_foreign_key("issue_metrics", "issues", {:on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0106s
-- add_foreign_key("issues", "issues", {:column=>"moved_to_id", :name=>"fk_a194299be1", :on_delete=>:nullify})
- -> 0.0014s
+ -> 0.0366s
-- add_foreign_key("issues", "milestones", {:name=>"fk_96b1dd429c", :on_delete=>:nullify})
- -> 0.0016s
+ -> 0.0309s
-- add_foreign_key("issues", "projects", {:name=>"fk_899c8f3231", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0314s
-- add_foreign_key("issues", "users", {:column=>"author_id", :name=>"fk_05f1e72feb", :on_delete=>:nullify})
- -> 0.0015s
+ -> 0.0504s
+-- add_foreign_key("issues", "users", {:column=>"closed_by_id", :name=>"fk_c63cbf6c25", :on_delete=>:nullify})
+ -> 0.0428s
-- add_foreign_key("issues", "users", {:column=>"updated_by_id", :name=>"fk_ffed080f01", :on_delete=>:nullify})
- -> 0.0017s
+ -> 0.0333s
-- add_foreign_key("label_priorities", "labels", {:on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0143s
-- add_foreign_key("label_priorities", "projects", {:on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0160s
-- add_foreign_key("labels", "namespaces", {:column=>"group_id", :on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0176s
-- add_foreign_key("labels", "projects", {:name=>"fk_7de4989a69", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0216s
+-- add_foreign_key("lfs_file_locks", "projects", {:on_delete=>:cascade})
+ -> 0.0144s
+-- add_foreign_key("lfs_file_locks", "users", {:on_delete=>:cascade})
+ -> 0.0178s
-- add_foreign_key("lists", "boards", {:name=>"fk_0d3f677137", :on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0161s
-- add_foreign_key("lists", "labels", {:name=>"fk_7a5553d60f", :on_delete=>:cascade})
- -> 0.0014s
+ -> 0.0137s
-- add_foreign_key("members", "users", {:name=>"fk_2e88fb7ce9", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0171s
-- add_foreign_key("merge_request_diff_commits", "merge_request_diffs", {:on_delete=>:cascade})
- -> 0.0014s
+ -> 0.0143s
-- add_foreign_key("merge_request_diff_files", "merge_request_diffs", {:on_delete=>:cascade})
- -> 0.0014s
+ -> 0.0106s
-- add_foreign_key("merge_request_diffs", "merge_requests", {:name=>"fk_8483f3258f", :on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0119s
-- add_foreign_key("merge_request_metrics", "ci_pipelines", {:column=>"pipeline_id", :on_delete=>:cascade})
- -> 0.0017s
+ -> 0.0163s
-- add_foreign_key("merge_request_metrics", "merge_requests", {:on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0204s
-- add_foreign_key("merge_request_metrics", "users", {:column=>"latest_closed_by_id", :name=>"fk_ae440388cc", :on_delete=>:nullify})
- -> 0.0015s
+ -> 0.0196s
-- add_foreign_key("merge_request_metrics", "users", {:column=>"merged_by_id", :name=>"fk_7f28d925f3", :on_delete=>:nullify})
- -> 0.0015s
+ -> 0.0202s
-- add_foreign_key("merge_requests", "ci_pipelines", {:column=>"head_pipeline_id", :name=>"fk_fd82eae0b9", :on_delete=>:nullify})
- -> 0.0014s
+ -> 0.0394s
-- add_foreign_key("merge_requests", "merge_request_diffs", {:column=>"latest_merge_request_diff_id", :name=>"fk_06067f5644", :on_delete=>:nullify})
- -> 0.0014s
+ -> 0.0532s
-- add_foreign_key("merge_requests", "milestones", {:name=>"fk_6a5165a692", :on_delete=>:nullify})
- -> 0.0015s
+ -> 0.0291s
-- add_foreign_key("merge_requests", "projects", {:column=>"source_project_id", :name=>"fk_3308fe130c", :on_delete=>:nullify})
- -> 0.0017s
+ -> 0.0278s
-- add_foreign_key("merge_requests", "projects", {:column=>"target_project_id", :name=>"fk_a6963e8447", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0367s
-- add_foreign_key("merge_requests", "users", {:column=>"assignee_id", :name=>"fk_6149611a04", :on_delete=>:nullify})
- -> 0.0016s
+ -> 0.0327s
-- add_foreign_key("merge_requests", "users", {:column=>"author_id", :name=>"fk_e719a85f8a", :on_delete=>:nullify})
- -> 0.0017s
+ -> 0.0337s
-- add_foreign_key("merge_requests", "users", {:column=>"merge_user_id", :name=>"fk_ad525e1f87", :on_delete=>:nullify})
- -> 0.0018s
+ -> 0.0517s
-- add_foreign_key("merge_requests", "users", {:column=>"updated_by_id", :name=>"fk_641731faff", :on_delete=>:nullify})
- -> 0.0017s
+ -> 0.0335s
-- add_foreign_key("merge_requests_closing_issues", "issues", {:on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0167s
-- add_foreign_key("merge_requests_closing_issues", "merge_requests", {:on_delete=>:cascade})
- -> 0.0014s
+ -> 0.0191s
-- add_foreign_key("milestones", "namespaces", {:column=>"group_id", :name=>"fk_95650a40d4", :on_delete=>:cascade})
- -> 0.0014s
+ -> 0.0206s
-- add_foreign_key("milestones", "projects", {:name=>"fk_9bd0a0c791", :on_delete=>:cascade})
- -> 0.0017s
+ -> 0.0221s
-- add_foreign_key("notes", "projects", {:name=>"fk_99e097b079", :on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0332s
-- add_foreign_key("oauth_openid_requests", "oauth_access_grants", {:column=>"access_grant_id", :name=>"fk_oauth_openid_requests_oauth_access_grants_access_grant_id"})
- -> 0.0014s
+ -> 0.0128s
-- add_foreign_key("pages_domains", "projects", {:name=>"fk_ea2f6dfc6f", :on_delete=>:cascade})
- -> 0.0021s
+ -> 0.0220s
-- add_foreign_key("personal_access_tokens", "users")
- -> 0.0016s
+ -> 0.0187s
-- add_foreign_key("project_authorizations", "projects", {:on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0149s
-- add_foreign_key("project_authorizations", "users", {:on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0167s
-- add_foreign_key("project_auto_devops", "projects", {:on_delete=>:cascade})
- -> 0.0026s
+ -> 0.0142s
-- add_foreign_key("project_custom_attributes", "projects", {:on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0218s
-- add_foreign_key("project_features", "projects", {:name=>"fk_18513d9b92", :on_delete=>:cascade})
- -> 0.0020s
+ -> 0.0204s
-- add_foreign_key("project_group_links", "projects", {:name=>"fk_daa8cee94c", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0174s
-- add_foreign_key("project_import_data", "projects", {:name=>"fk_ffb9ee3a10", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0138s
-- add_foreign_key("project_statistics", "projects", {:on_delete=>:cascade})
- -> 0.0021s
+ -> 0.0125s
-- add_foreign_key("protected_branch_merge_access_levels", "protected_branches", {:name=>"fk_8a3072ccb3", :on_delete=>:cascade})
- -> 0.0014s
+ -> 0.0157s
-- add_foreign_key("protected_branch_push_access_levels", "protected_branches", {:name=>"fk_9ffc86a3d9", :on_delete=>:cascade})
- -> 0.0014s
+ -> 0.0112s
-- add_foreign_key("protected_branches", "projects", {:name=>"fk_7a9c6d93e7", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0122s
-- add_foreign_key("protected_tag_create_access_levels", "namespaces", {:column=>"group_id"})
- -> 0.0016s
+ -> 0.0131s
-- add_foreign_key("protected_tag_create_access_levels", "protected_tags", {:name=>"fk_f7dfda8c51", :on_delete=>:cascade})
- -> 0.0013s
+ -> 0.0168s
-- add_foreign_key("protected_tag_create_access_levels", "users")
- -> 0.0018s
+ -> 0.0221s
-- add_foreign_key("protected_tags", "projects", {:name=>"fk_8e4af87648", :on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0135s
-- add_foreign_key("push_event_payloads", "events", {:name=>"fk_36c74129da", :on_delete=>:cascade})
- -> 0.0013s
+ -> 0.0107s
-- add_foreign_key("releases", "projects", {:name=>"fk_47fe2a0596", :on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0131s
-- add_foreign_key("services", "projects", {:name=>"fk_71cce407f9", :on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0142s
-- add_foreign_key("snippets", "projects", {:name=>"fk_be41fd4bb7", :on_delete=>:cascade})
- -> 0.0017s
+ -> 0.0178s
-- add_foreign_key("subscriptions", "projects", {:on_delete=>:cascade})
- -> 0.0018s
+ -> 0.0160s
-- add_foreign_key("system_note_metadata", "notes", {:name=>"fk_d83a918cb1", :on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0156s
-- add_foreign_key("timelogs", "issues", {:name=>"fk_timelogs_issues_issue_id", :on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0183s
-- add_foreign_key("timelogs", "merge_requests", {:name=>"fk_timelogs_merge_requests_merge_request_id", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0198s
+-- add_foreign_key("todos", "notes", {:name=>"fk_91d1f47b13", :on_delete=>:cascade})
+ -> 0.0276s
-- add_foreign_key("todos", "projects", {:name=>"fk_45054f9c45", :on_delete=>:cascade})
- -> 0.0018s
+ -> 0.0175s
+-- add_foreign_key("todos", "users", {:column=>"author_id", :name=>"fk_ccf0373936", :on_delete=>:cascade})
+ -> 0.0182s
+-- add_foreign_key("todos", "users", {:name=>"fk_d94154aa95", :on_delete=>:cascade})
+ -> 0.0184s
-- add_foreign_key("trending_projects", "projects", {:on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0338s
-- add_foreign_key("u2f_registrations", "users")
- -> 0.0017s
+ -> 0.0176s
+-- add_foreign_key("user_callouts", "users", {:on_delete=>:cascade})
+ -> 0.0160s
-- add_foreign_key("user_custom_attributes", "users", {:on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0191s
+-- add_foreign_key("user_interacted_projects", "projects", {:name=>"fk_722ceba4f7", :on_delete=>:cascade})
+ -> 0.0171s
+-- add_foreign_key("user_interacted_projects", "users", {:name=>"fk_0894651f08", :on_delete=>:cascade})
+ -> 0.0155s
-- add_foreign_key("user_synced_attributes_metadata", "users", {:on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0164s
-- add_foreign_key("users_star_projects", "projects", {:name=>"fk_22cd27ddfc", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0180s
-- add_foreign_key("web_hook_logs", "web_hooks", {:on_delete=>:cascade})
- -> 0.0014s
+ -> 0.0164s
-- add_foreign_key("web_hooks", "projects", {:name=>"fk_0c8ca6d9d1", :on_delete=>:cascade})
- -> 0.0017s
+ -> 0.0172s
-- initialize_schema_migrations_table()
- -> 0.0112s
+ -> 0.0212s
+Adding limits to schema.rb for mysql
+-- column_exists?(:merge_request_diffs, :st_commits)
+ -> 0.0010s
+-- column_exists?(:merge_request_diffs, :st_diffs)
+ -> 0.0006s
+-- change_column(:snippets, :content, :text, {:limit=>2147483647})
+ -> 0.0308s
+-- change_column(:notes, :st_diff, :text, {:limit=>2147483647})
+ -> 0.0366s
+-- change_column(:snippets, :content_html, :text, {:limit=>2147483647})
+ -> 0.0272s
+-- change_column(:merge_request_diff_files, :diff, :text, {:limit=>2147483647})
+ -> 0.0170s
+$ date
+Thu Apr 5 11:19:41 UTC 2018
$ JOB_NAME=( $CI_JOB_NAME )
$ export CI_NODE_INDEX=${JOB_NAME[-2]}
$ export CI_NODE_TOTAL=${JOB_NAME[-1]}
$ export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
$ export KNAPSACK_GENERATE_REPORT=true
+$ export SUITE_FLAKY_RSPEC_REPORT_PATH=${FLAKY_RSPEC_SUITE_REPORT_PATH}
+$ export FLAKY_RSPEC_REPORT_PATH=rspec_flaky/all_${JOB_NAME[0]}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+$ export NEW_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/new_${JOB_NAME[0]}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+$ export FLAKY_RSPEC_GENERATE_REPORT=true
$ export CACHE_CLASSES=true
-$ cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
+$ cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
+$ [[ -f $FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_REPORT_PATH}
+$ [[ -f $NEW_FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${NEW_FLAKY_RSPEC_REPORT_PATH}
$ scripts/gitaly-test-spawn
-Gem.path: ["/root/.gem/ruby/2.3.0", "/usr/local/lib/ruby/gems/2.3.0", "/usr/local/bundle"]
-ENV['BUNDLE_GEMFILE']: nil
-ENV['RUBYOPT']: nil
-bundle config in /builds/gitlab-org/gitlab-ce
-scripts/gitaly-test-spawn:10:in `<main>': undefined local variable or method `gitaly_dir' for main:Object (NameError)
-Did you mean? gitaly_dir
-Settings are listed in order of priority. The top value will be used.
-retry
-Set for your local app (/usr/local/bundle/config): 3
+59
+$ knapsack rspec "--color --format documentation"
+
+Report specs:
+spec/services/todo_service_spec.rb
+spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
+spec/controllers/projects/merge_requests_controller_spec.rb
+spec/controllers/groups_controller_spec.rb
+spec/features/projects/import_export/import_file_spec.rb
+spec/lib/gitlab/middleware/go_spec.rb
+spec/services/groups/transfer_service_spec.rb
+spec/features/projects/blobs/edit_spec.rb
+spec/services/boards/lists/move_service_spec.rb
+spec/services/create_deployment_service_spec.rb
+spec/controllers/groups/milestones_controller_spec.rb
+spec/helpers/groups_helper_spec.rb
+spec/requests/api/v3/todos_spec.rb
+spec/models/project_services/teamcity_service_spec.rb
+spec/lib/gitlab/conflict/file_spec.rb
+spec/lib/banzai/filter/snippet_reference_filter_spec.rb
+spec/finders/autocomplete_users_finder_spec.rb
+spec/models/service_spec.rb
+spec/services/test_hooks/project_service_spec.rb
+spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb
+spec/finders/runner_jobs_finder_spec.rb
+spec/features/projects/snippets_spec.rb
+spec/requests/api/v3/environments_spec.rb
+spec/requests/api/namespaces_spec.rb
+spec/services/merge_requests/get_urls_service_spec.rb
+spec/models/lfs_file_lock_spec.rb
+spec/lib/gitlab/ci/config/entry/boolean_spec.rb
+
+Leftover specs:
+
+Knapsack report generator started!
+
+==> Setting up GitLab Shell...
+ GitLab Shell setup in 0.307428917 seconds...
+
+==> Setting up Gitaly...
+ Gitaly setup in 0.000135767 seconds...
+
+TodoService
+ updates cached counts when a todo is created
+ Issues
+ #new_issue
+ creates a todo if assigned
+ does not create a todo if unassigned
+ creates a todo if assignee is the current user
+ creates a todo for each valid mentioned user
+ creates a directly addressed todo for each valid addressed user
+ creates correct todos for each valid user based on the type of mention
+ does not create todo if user can not see the issue when issue is confidential
+ does not create directly addressed todo if user cannot see the issue when issue is confidential
+ when a private group is mentioned
+ creates a todo for group members
+ #update_issue
+ creates a todo for each valid mentioned user not included in skip_users
+ creates a todo for each valid user not included in skip_users based on the type of mention
+ creates a directly addressed todo for each valid addressed user not included in skip_users
+ does not create a todo if user was already mentioned and todo is pending
+ does not create a todo if user was already mentioned and todo is done
+ does not create a directly addressed todo if user was already mentioned or addressed and todo is pending
+ does not create a directly addressed todo if user was already mentioned or addressed and todo is done
+ does not create todo if user can not see the issue when issue is confidential
+ does not create a directly addressed todo if user can not see the issue when issue is confidential
+ issues with a task list
+ does not create todo when tasks are marked as completed
+ does not create directly addressed todo when tasks are marked as completed
+ does not raise an error when description not change
+ #close_issue
+ marks related pending todos to the target for the user as done
+ #destroy_target
+ refreshes the todos count cache for users with todos on the target
+ does not refresh the todos count cache for users with only done todos on the target
+ yields the target to the caller
+ #reassigned_issue
+ creates a pending todo for new assignee
+ does not create a todo if unassigned
+ creates a todo if new assignee is the current user
+ #mark_pending_todos_as_done
+ marks related pending todos to the target for the user as done
+ cached counts
+ updates when todos change
+ #mark_todos_as_done
+ behaves like updating todos state
+ updates related todos for the user with the new_state
+ returns the updated ids
+ cached counts
+ updates when todos change
+ #mark_todos_as_done_by_ids
+ behaves like updating todos state
+ updates related todos for the user with the new_state
+ returns the updated ids
+ cached counts
+ updates when todos change
+ #mark_todos_as_pending
+ behaves like updating todos state
+ updates related todos for the user with the new_state
+ returns the updated ids
+ cached counts
+ updates when todos change
+ #mark_todos_as_pending_by_ids
+ behaves like updating todos state
+ updates related todos for the user with the new_state
+ returns the updated ids
+ cached counts
+ updates when todos change
+ #new_note
+ mark related pending todos to the noteable for the note author as done
+ does not mark related pending todos it is a system note
+ creates a todo for each valid mentioned user
+ creates a todo for each valid user based on the type of mention
+ creates a directly addressed todo for each valid addressed user
+ does not create todo if user can not see the issue when leaving a note on a confidential issue
+ does not create a directly addressed todo if user can not see the issue when leaving a note on a confidential issue
+ does not create todo when leaving a note on snippet
+ on commit
+ creates a todo for each valid mentioned user when leaving a note on commit
+ creates a directly addressed todo for each valid mentioned user when leaving a note on commit
+ #mark_todo
+ creates a todo from a issue
+ #todo_exists?
+ returns false when no todo exist for the given issuable
+ returns true when a todo exist for the given issuable
+ Merge Requests
+ #new_merge_request
+ creates a pending todo if assigned
+ does not create a todo if unassigned
+ does not create a todo if assignee is the current user
+ creates a todo for each valid mentioned user
+ creates a todo for each valid user based on the type of mention
+ creates a directly addressed todo for each valid addressed user
+ #update_merge_request
+ creates a todo for each valid mentioned user not included in skip_users
+ creates a todo for each valid user not included in skip_users based on the type of mention
+ creates a directly addressed todo for each valid addressed user not included in skip_users
+ does not create a todo if user was already mentioned and todo is pending
+ does not create a todo if user was already mentioned and todo is done
+ does not create a directly addressed todo if user was already mentioned or addressed and todo is pending
+ does not create a directly addressed todo if user was already mentioned or addressed and todo is done
+ with a task list
+ does not create todo when tasks are marked as completed
+ does not create directly addressed todo when tasks are marked as completed
+ does not raise an error when description not change
+ #close_merge_request
+ marks related pending todos to the target for the user as done
+ #reassigned_merge_request
+ creates a pending todo for new assignee
+ does not create a todo if unassigned
+ creates a todo if new assignee is the current user
+ does not create a todo for guests
+ does not create a directly addressed todo for guests
+ #merge_merge_request
+ marks related pending todos to the target for the user as done
+ does not create todo for guests
+ does not create directly addressed todo for guests
+ #new_award_emoji
+ marks related pending todos to the target for the user as done
+ #merge_request_build_failed
+ creates a pending todo for the merge request author
+ creates a pending todo for merge_user
+ #merge_request_push
+ marks related pending todos to the target for the user as done
+ #merge_request_became_unmergeable
+ creates a pending todo for a merge_user
+ #mark_todo
+ creates a todo from a merge request
+ #new_note
+ creates a todo for mentioned user on new diff note
+ creates a directly addressed todo for addressed user on new diff note
+ creates a todo for mentioned user on legacy diff note
+ does not create todo for guests
+ #update_note
+ creates a todo for each valid mentioned user not included in skip_users
+ creates a todo for each valid user not included in skip_users based on the type of mention
+ creates a directly addressed todo for each valid addressed user not included in skip_users
+ does not create a todo if user was already mentioned and todo is pending
+ does not create a todo if user was already mentioned and todo is done
+ does not create a directly addressed todo if user was already mentioned or addressed and todo is pending
+ does not create a directly addressed todo if user was already mentioned or addressed and todo is done
+ #mark_todos_as_done
+ marks a relation of todos as done
+ marks an array of todos as done
+ returns the ids of updated todos
+ when some of the todos are done already
+ returns the ids of those still pending
+ returns an empty array if all are done
+ #mark_todos_as_done_by_ids
+ marks an array of todo ids as done
+ marks a single todo id as done
+ caches the number of todos of a user
+
+Gitlab::ImportExport::ProjectTreeSaver
+ saves the project tree into a json object
+ saves project successfully
+ JSON
+ saves the correct json
+ has milestones
+ has merge requests
+ has merge request's milestones
+ has merge request's source branch SHA
+ has merge request's target branch SHA
+ has events
+ has snippets
+ has snippet notes
+ has releases
+ has issues
+ has issue comments
+ has issue assignees
+ has author on issue comments
+ has project members
+ has merge requests diffs
+ has merge request diff files
+ has merge request diff commits
+ has merge requests comments
+ has author on merge requests comments
+ has pipeline stages
+ has pipeline statuses
+ has pipeline builds
+ has no when YML attributes but only the DB column
+ has pipeline commits
+ has ci pipeline notes
+ has labels with no associations
+ has labels associated to records
+ has project and group labels
+ has priorities associated to labels
+ saves the correct service type
+ saves the properties for a service
+ has project feature
+ has custom attributes
+ has badges
+ does not complain about non UTF-8 characters in MR diff files
+ with description override
+ overrides the project description
+ group members
+ does not export group members if it has no permission
+ does not export group members as master
+ exports group members as group owner
+ as admin
+ exports group members as admin
+ exports group members as project members
+ project attributes
+ contains the html description
+ does not contain the runners token
+
+Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits
+ #perform
+ when the diff IDs passed do not exist
+ does not raise
+ when the merge request diff has no serialised commits or diffs
+ does not raise
+ processing multiple merge request diffs
+ when BUFFER_ROWS is exceeded
+ inserts commit rows in chunks of BUFFER_ROWS
+ inserts diff rows in chunks of DIFF_FILE_BUFFER_ROWS
+ when BUFFER_ROWS is not exceeded
+ only updates once
+ when some rows were already inserted due to a previous failure
+ does not raise
+ logs a message
+ ends up with the correct rows
+ when the merge request diff update fails
+ raises an error
+ logs the error
+ still adds diff commits
+ still adds diff files
+ when the merge request diff has valid commits and diffs
+ creates correct entries in the merge_request_diff_commits table
+ creates correct entries in the merge_request_diff_files table
+ sets the st_commits and st_diffs columns to nil
+ when the merge request diff has diffs but no commits
+ creates correct entries in the merge_request_diff_commits table
+ creates correct entries in the merge_request_diff_files table
+ sets the st_commits and st_diffs columns to nil
+ when the merge request diffs do not have too_large set
+ creates correct entries in the merge_request_diff_commits table
+ creates correct entries in the merge_request_diff_files table
+ sets the st_commits and st_diffs columns to nil
+ when the merge request diffs do not have a_mode and b_mode set
+ creates correct entries in the merge_request_diff_commits table
+ creates correct entries in the merge_request_diff_files table
+ sets the st_commits and st_diffs columns to nil
+ when the merge request diffs have binary content
+ creates correct entries in the merge_request_diff_commits table
+ creates correct entries in the merge_request_diff_files table
+ sets the st_commits and st_diffs columns to nil
+ when the merge request diff has commits, but no diffs
+ creates correct entries in the merge_request_diff_commits table
+ creates correct entries in the merge_request_diff_files table
+ sets the st_commits and st_diffs columns to nil
+ when the merge request diffs have invalid content
+ creates correct entries in the merge_request_diff_commits table
+ creates correct entries in the merge_request_diff_files table
+ sets the st_commits and st_diffs columns to nil
+ when the merge request diffs are Rugged::Patch instances
+ creates correct entries in the merge_request_diff_commits table
+ creates correct entries in the merge_request_diff_files table
+ sets the st_commits and st_diffs columns to nil
+ when the merge request diffs are Rugged::Diff::Delta instances
+ creates correct entries in the merge_request_diff_commits table
+ creates correct entries in the merge_request_diff_files table
+ sets the st_commits and st_diffs columns to nil
+
+Projects::MergeRequestsController
+ GET commit_change_content
+ renders commit_change_content template
+ GET show
+ behaves like loads labels
+ loads labels into the @labels variable
+ as html
+ renders merge request page
+ loads notes
+ with special_role FIRST_TIME_CONTRIBUTOR
+ as json
+ with basic serializer param
+ renders basic MR entity as json
+ with widget serializer param
+ renders widget MR entity as json
+ when no serialiser was passed
+ renders widget MR entity as json
+ as diff
+ triggers workhorse to serve the request
+ as patch
+ triggers workhorse to serve the request
+ GET index
+ behaves like issuables list meta-data
+ creates indexed meta-data object for issuable notes and votes count
+ when given empty collection
+ doesn't execute any queries with false conditions
+ when page param
+ redirects to last_page if page number is larger than number of pages
+ redirects to specified page
+ does not redirect to external sites when provided a host field
+ when filtering by opened state
+ with opened merge requests
+ lists those merge requests
+ with reopened merge requests
+ lists those merge requests
+ PUT update
+ changing the assignee
+ limits the attributes exposed on the assignee
+ when user does not have access to update issue
+ responds with 404
+ there is no source project
+ closes MR without errors
+ allows editing of a closed merge request
+ does not allow to update target branch closed merge request
+ behaves like update invalid issuable
+ when updating causes conflicts
+ renders edit when format is html
+ renders json error message when format is json
+ when updating an invalid issuable
+ renders edit when merge request is invalid
+ POST merge
+ when user cannot access
+ returns 404
+ when the merge request is not mergeable
+ returns :failed
+ when the sha parameter does not match the source SHA
+ returns :sha_mismatch
+ when the sha parameter matches the source SHA
+ returns :success
+ starts the merge immediately
+ when the pipeline succeeds is passed
+ returns :merge_when_pipeline_succeeds
+ sets the MR to merge when the pipeline succeeds
+ when project.only_allow_merge_if_pipeline_succeeds? is true
+ returns :merge_when_pipeline_succeeds
+ and head pipeline is not the current one
+ returns :failed
+ only_allow_merge_if_all_discussions_are_resolved? setting
+ when enabled
+ with unresolved discussion
+ returns :failed
+ with all discussions resolved
+ returns :success
+ when disabled
+ with unresolved discussion
+ returns :success
+ with all discussions resolved
+ returns :success
+ DELETE destroy
+ denies access to users unless they're admin or project owner
+ when the user is owner
+ deletes the merge request
+ delegates the update of the todos count cache to TodoService
+ GET commits
+ renders the commits template to a string
+ GET pipelines
+ responds with serialized pipelines
+ POST remove_wip
+ removes the wip status
+ renders MergeRequest as JSON
+ POST cancel_merge_when_pipeline_succeeds
+ calls MergeRequests::MergeWhenPipelineSucceedsService
+ should respond with numeric status code success
+ renders MergeRequest as JSON
+ POST assign_related_issues
+ shows a flash message on success
+ correctly pluralizes flash message on success
+ calls MergeRequests::AssignIssuesService
+ is skipped when not signed in
+ GET ci_environments_status
+ the environment is from a forked project
+ links to the environment on that project
+ GET pipeline_status.json
+ when head_pipeline exists
+ return a detailed head_pipeline status in json
+ when head_pipeline does not exist
+ return empty
+ POST #rebase
+ successfully
+ enqeues a RebaseWorker
+ with a forked project
+ user cannot push to source branch
+ returns 404
+ user can push to source branch
+ returns 200
+
+GroupsController
+ GET #show
+ as html
+ assigns whether or not a group has children
+ as atom
+ assigns events for all the projects in the group
+ GET #new
+ when creating subgroups
+ and can_create_group is true
+ and logged in as Admin
+ behaves like member with ability to create subgroups
+ renders the new page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Owner
+ behaves like member with ability to create subgroups
+ renders the new page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Guest
+ behaves like member without ability to create subgroups
+ renders the 404 page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Developer
+ behaves like member without ability to create subgroups
+ renders the 404 page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Master
+ behaves like member without ability to create subgroups
+ renders the 404 page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and can_create_group is false
+ and logged in as Admin
+ behaves like member with ability to create subgroups
+ renders the new page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Owner
+ behaves like member with ability to create subgroups
+ renders the new page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Guest
+ behaves like member without ability to create subgroups
+ renders the 404 page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Developer
+ behaves like member without ability to create subgroups
+ renders the 404 page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Master
+ behaves like member without ability to create subgroups
+ renders the 404 page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ GET #activity
+ as json
+ includes all projects in event feed
+ POST #create
+ when creating subgroups
+ and can_create_group is true
+ and logged in as Owner
+ creates the subgroup (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Developer
+ renders the new template (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and can_create_group is false
+ and logged in as Owner
+ creates the subgroup (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Developer
+ renders the new template (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ when creating a top level group
+ and can_create_group is enabled
+ creates the Group
+ and can_create_group is disabled
+ does not create the Group
+ GET #index
+ as a user
+ redirects to Groups Dashboard
+ as a guest
+ redirects to Explore Groups
+ GET #issues
+ sorting by votes
+ sorts most popular issues
+ sorts least popular issues
+ GET #merge_requests
+ sorting by votes
+ sorts most popular merge requests
+ sorts least popular merge requests
+ DELETE #destroy
+ as another user
+ returns 404
+ as the group owner
+ schedules a group destroy
+ redirects to the root path
+ PUT update
+ updates the path successfully
+ does not update the path on error
+ #ensure_canonical_path
+ for a GET request
+ when requesting groups at the root path
+ when requesting the canonical path with different casing
+ redirects to the correct casing
+ when requesting a redirected path
+ redirects to the canonical path
+ when the old group path is a substring of the scheme or host
+ does not modify the requested host
+ when the old group path is substring of groups
+ does not modify the /groups part of the path
+ when requesting groups under the /groups path
+ when requesting the canonical path
+ non-show path
+ with exactly matching casing
+ does not redirect
+ with different casing
+ redirects to the correct casing
+ show path
+ with exactly matching casing
+ does not redirect
+ with different casing
+ redirects to the correct casing at the root path
+ when requesting a redirected path
+ redirects to the canonical path
+ when the old group path is a substring of the scheme or host
+ does not modify the requested host
+ when the old group path is substring of groups
+ does not modify the /groups part of the path
+ when the old group path is substring of groups plus the new path
+ does not modify the /groups part of the path
+ for a POST request
+ when requesting the canonical path with different casing
+ does not 404
+ does not redirect to the correct casing
+ when requesting a redirected path
+ returns not found
+ for a DELETE request
+ when requesting the canonical path with different casing
+ does not 404
+ does not redirect to the correct casing
+ when requesting a redirected path
+ returns not found
+ PUT transfer
+ when transfering to a subgroup goes right
+ should return a notice (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should redirect to the new path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when converting to a root group goes right
+ should return a notice (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should redirect to the new path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ When the transfer goes wrong
+ should return an alert (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should redirect to the current path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the user is not allowed to transfer the group
+ should be denied (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+
+Import/Export - project import integration test
+Starting the Capybara driver server...
+ invalid project
+ when selecting the namespace
+ prefilled the path
+ user imports an exported project successfully
+ path is not prefilled
+ user imports an exported project successfully
+
+Gitlab::Middleware::Go
+ #call
+ when go-get=0
+ skips go-import generation
+ when go-get=1
+ with SSH disabled
+ with simple 2-segment project path
+ with subpackages
+ returns the full project path
+ without subpackages
+ returns the full project path
+ with a nested project path
+ with subpackages
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ with a subpackage that is not a valid project path
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ without subpackages
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ with a bogus path
+ skips go-import generation
+ with HTTP disabled
+ with simple 2-segment project path
+ with subpackages
+ returns the full project path
+ without subpackages
+ returns the full project path
+ with a nested project path
+ with subpackages
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ with a subpackage that is not a valid project path
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ without subpackages
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ with a bogus path
+ skips go-import generation
+ with nothing disabled
+ with simple 2-segment project path
+ with subpackages
+ returns the full project path
+ without subpackages
+ returns the full project path
+ with a nested project path
+ with subpackages
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ with a subpackage that is not a valid project path
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ without subpackages
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ with a bogus path
+ skips go-import generation
+ with nothing disabled (blank string)
+ with simple 2-segment project path
+ with subpackages
+ returns the full project path
+ without subpackages
+ returns the full project path
+ with a nested project path
+ with subpackages
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ with a subpackage that is not a valid project path
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ without subpackages
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ with a bogus path
+ skips go-import generation
+
+Groups::TransferService
+ #execute
+ when transforming a group into a root group
+ behaves like ensuring allowed transfer for a group
+ with other database than PostgreSQL
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when there's an exception on Gitlab shell directories
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the group is already a root group
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the user does not have the right policies
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when there is a group with the same path
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the group is a subgroup and the transfer is valid
+ should update group attributes (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should update group children path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should update group projects path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when transferring a subgroup into another group
+ behaves like ensuring allowed transfer for a group
+ with other database than PostgreSQL
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when there's an exception on Gitlab shell directories
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the new parent group is the same as the previous parent group
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the user does not have the right policies
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the parent has a group with the same path
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the parent group has a project with the same path
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the group is allowed to be transferred
+ should update visibility for the group based on the parent group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should update parent group to the new parent (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should return the group as children of the new parent (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should create a redirect for the group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the group has a lower visibility than the parent group
+ should not update the visibility for the group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the group has a higher visibility than the parent group
+ should update visibility level based on the parent group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when transferring a group with group descendants
+ should update subgroups path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should create redirects for the subgroups (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the new parent has a higher visibility than the children
+ should not update the children visibility (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the new parent has a lower visibility than the children
+ should update children visibility to match the new parent (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when transferring a group with project descendants
+ should update projects path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should create permanent redirects for the projects (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the new parent has a higher visibility than the projects
+ should not update projects visibility (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the new parent has a lower visibility than the projects
+ should update projects visibility to match the new parent (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when transferring a group with subgroups & projects descendants
+ should update subgroups path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should update projects path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should create redirect for the subgroups and projects (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when transfering a group with nested groups and projects
+ should update subgroups path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should update projects path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should create redirect for the subgroups and projects (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when updating the group goes wrong
+ should restore group and projects visibility (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+
+Editing file blob
+ as a developer
+ from MR diff
+ returns me to the mr
+ from blob file path
+ updates content
+ previews content
+ visit blob edit
+ redirects to sign in and returns
+ as developer
+ redirects to sign in and returns
+ as guest
+ redirects to sign in and returns
+ as developer
+ on some branch
+ shows blob editor with same branch
+ with protected branch
+ shows blob editor with patch branch
+ as master
+ shows blob editor with same branch
+
+Boards::Lists::MoveService
+ #execute
+ when board parent is a project
+ behaves like lists move service
+ keeps position of lists when list type is closed
+ when list type is set to label
+ keeps position of lists when new position is nil
+ keeps position of lists when new positon is equal to old position
+ keeps position of lists when new positon is negative
+ keeps position of lists when new positon is equal to number of labels lists
+ keeps position of lists when new positon is greater than number of labels lists
+ increments position of intermediate lists when new positon is equal to first position
+ decrements position of intermediate lists when new positon is equal to last position
+ decrements position of intermediate lists when new position is greater than old position
+ increments position of intermediate lists when new position is lower than old position
+ when board parent is a group
+ behaves like lists move service
+ keeps position of lists when list type is closed
+ when list type is set to label
+ keeps position of lists when new position is nil
+ keeps position of lists when new positon is equal to old position
+ keeps position of lists when new positon is negative
+ keeps position of lists when new positon is equal to number of labels lists
+ keeps position of lists when new positon is greater than number of labels lists
+ increments position of intermediate lists when new positon is equal to first position
+ decrements position of intermediate lists when new positon is equal to last position
+ decrements position of intermediate lists when new position is greater than old position
+ increments position of intermediate lists when new position is lower than old position
+
+CreateDeploymentService
+ #execute
+ when environment exists
+ creates a deployment
+ when environment does not exist
+ does not create a deployment
+ when start action is defined
+ and environment is stopped
+ makes environment available
+ creates a deployment
+ when stop action is defined
+ and environment is available
+ makes environment stopped
+ does not create a deployment
+ when variables are used
+ creates a new deployment
+ does not create a new environment
+ updates external url
+ when project was removed
+ does not create deployment or environment
+ #expanded_environment_url
+ when yaml environment uses $CI_COMMIT_REF_NAME
+ should eq "http://review/master"
+ when yaml environment uses $CI_ENVIRONMENT_SLUG
+ should eq "http://review/prod-slug"
+ when yaml environment uses yaml_variables containing symbol keys
+ should eq "http://review/host"
+ when yaml environment does not have url
+ returns the external_url from persisted environment
+ processing of builds
+ without environment specified
+ behaves like does not create deployment
+ does not create a new deployment
+ does not call a service
+ when environment is specified
+ when job succeeds
+ behaves like creates deployment
+ creates a new deployment
+ calls a service
+ is set as deployable
+ updates environment URL
+ when job fails
+ behaves like does not create deployment
+ does not create a new deployment
+ does not call a service
+ when job is retried
+ behaves like creates deployment
+ creates a new deployment
+ calls a service
+ is set as deployable
+ updates environment URL
+ merge request metrics
+ while updating the 'first_deployed_to_production_at' time
+ for merge requests merged before the current deploy
+ sets the time if the deploy's environment is 'production'
+ doesn't set the time if the deploy's environment is not 'production'
+ does not raise errors if the merge request does not have a metrics record
+ for merge requests merged before the previous deploy
+ if the 'first_deployed_to_production_at' time is already set
+ does not overwrite the older 'first_deployed_to_production_at' time
+ if the 'first_deployed_to_production_at' time is not already set
+ does not overwrite the older 'first_deployed_to_production_at' time
+
+Groups::MilestonesController
+ #index
+ shows group milestones page
+ as JSON
+ lists legacy group milestones and group milestones
+ #show
+ when there is a title parameter
+ searchs for a legacy group milestone
+ when there is not a title parameter
+ searchs for a group milestone
+ behaves like milestone tabs
+ #merge_requests
+ as html
+ redirects to milestone#show
+ as json
+ renders the merge requests tab template to a string
+ #participants
+ as html
+ redirects to milestone#show
+ as json
+ renders the participants tab template to a string
+ #labels
+ as html
+ redirects to milestone#show
+ as json
+ renders the labels tab template to a string
+ #create
+ creates group milestone with Chinese title
+ #update
+ updates group milestone
+ legacy group milestones
+ updates only group milestones state
+ #ensure_canonical_path
+ for a GET request
+ when requesting the canonical path
+ non-show path
+ with exactly matching casing
+ does not redirect
+ with different casing
+ redirects to the correct casing
+ show path
+ with exactly matching casing
+ does not redirect
+ with different casing
+ redirects to the correct casing
+ when requesting a redirected path
+ redirects to the canonical path
+ when the old group path is a substring of the scheme or host
+ does not modify the requested host
+ when the old group path is substring of groups
+ does not modify the /groups part of the path
+ when the old group path is substring of groups plus the new path
+ does not modify the /groups part of the path
+ for a non-GET request
+ when requesting the canonical path with different casing
+ does not 404
+ does not redirect to the correct casing
+ when requesting a redirected path
+ returns not found
+
+GroupsHelper
+ group_icon
+ returns an url for the avatar
+ group_icon_url
+ returns an url for the avatar
+ gives default avatar_icon when no avatar is present
+ group_lfs_status
+ only one project in group
+ returns all projects as enabled
+ returns all projects as disabled
+ more than one project in group
+ LFS enabled in group
+ returns both projects as enabled
+ returns only one as enabled
+ LFS disabled in group
+ returns both projects as disabled
+ returns only one as disabled
+ group_title
+ outputs the groups in the correct order (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ #share_with_group_lock_help_text
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :root_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :sub_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :sub_sub_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :root_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :sub_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :sub_sub_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :root_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :subgroup
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :sub_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :subgroup
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :sub_sub_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :subgroup
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :root_owner, help_text: :ancestor_locked_but_you_can_override, linked_ancestor: :subgroup
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :sub_owner, help_text: :ancestor_locked_but_you_can_override, linked_ancestor: :subgroup
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :sub_sub_owner, help_text: :ancestor_locked_so_ask_the_owner, linked_ancestor: :subgroup
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :root_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :sub_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :sub_sub_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :root_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :sub_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :sub_sub_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :root_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :root_group
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :sub_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :root_group
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :sub_sub_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :root_group
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :root_owner, help_text: :ancestor_locked_but_you_can_override, linked_ancestor: :root_group
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :sub_owner, help_text: :ancestor_locked_so_ask_the_owner, linked_ancestor: :root_group
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :sub_sub_owner, help_text: :ancestor_locked_so_ask_the_owner, linked_ancestor: :root_group
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ #group_sidebar_links
+ returns all the expected links
+ includes settings when the user can admin the group
+ excludes cross project features when the user cannot read cross project
+
+API::V3::Todos
+ DELETE /todos/:id
+ when unauthenticated
+ returns authentication error
+ when authenticated
+ marks a todo as done
+ updates todos cache
+ returns 404 if the todo does not belong to the current user
+ DELETE /todos
+ when unauthenticated
+ returns authentication error
+ when authenticated
+ marks all todos as done
+ updates todos cache
+
+TeamcityService
+ Associations
+ should belong to project
+ should have one service_hook
+ Validations
+ when service is active
+ should validate that :build_type cannot be empty/falsy
+ should validate that :teamcity_url cannot be empty/falsy
+ behaves like issue tracker service URL attribute
+ should allow :teamcity_url to be ‹"https://example.com"›
+ should not allow :teamcity_url to be ‹"example.com"›
+ should not allow :teamcity_url to be ‹"ftp://example.com"›
+ should not allow :teamcity_url to be ‹"herp-and-derp"›
+ #username
+ does not validate the presence of username if password is nil
+ validates the presence of username if password is present
+ #password
+ does not validate the presence of password if username is nil
+ validates the presence of password if username is present
+ when service is inactive
+ should not validate that :build_type cannot be empty/falsy
+ should not validate that :teamcity_url cannot be empty/falsy
+ should not validate that :username cannot be empty/falsy
+ should not validate that :password cannot be empty/falsy
+ Callbacks
+ before_update :reset_password
+ saves password if new url is set together with password when no password was previously set
+ when a password was previously set
+ resets password if url changed
+ does not reset password if username changed
+ does not reset password if new url is set together with password, even if it's the same password
+ #build_page
+ returns the contents of the reactive cache
+ #commit_status
+ returns the contents of the reactive cache
+ #calculate_reactive_cache
+ build_page
+ returns a specific URL when status is 500
+ returns a build URL when teamcity_url has no trailing slash
+ teamcity_url has trailing slash
+ returns a build URL
+ commit_status
+ sets commit status to :error when status is 500
+ sets commit status to "pending" when status is 404
+ sets commit status to "success" when build status contains SUCCESS
+ sets commit status to "failed" when build status contains FAILURE
+ sets commit status to "pending" when build status contains Pending
+ sets commit status to :error when build status is unknown
+
+Gitlab::Conflict::File
+ #resolve_lines
+ raises ResolutionError when passed a hash without resolutions for all sections
+ when resolving everything to the same side
+ has the correct number of lines
+ has content matching the chosen lines
+ with mixed resolutions
+ has the correct number of lines
+ returns a file containing only the chosen parts of the resolved sections
+ #highlight_lines!
+ modifies the existing lines
+ is called implicitly when rich_text is accessed on a line
+ sets the rich_text of the lines matching the text content
+ highlights the lines correctly
+ #sections
+ only inserts match lines when there is a gap between sections
+ sets conflict to false for sections with only unchanged lines
+ only includes a maximum of CONTEXT_LINES (plus an optional match line) in context sections
+ sets conflict to true for sections with only changed lines
+ adds unique IDs to conflict sections, and not to other sections
+ with an example file
+ sets the correct match line headers
+ does not add match lines where they are not needed
+ creates context sections of the correct length
+ #as_json
+ includes the blob path for the file
+ includes the blob icon for the file
+ with the full_content option passed
+ includes the full content of the conflict
+ includes the detected language of the conflict file
+
+Banzai::Filter::SnippetReferenceFilter
+ requires project context
+ ignores valid references contained inside 'pre' element
+ ignores valid references contained inside 'code' element
+ ignores valid references contained inside 'a' element
+ ignores valid references contained inside 'style' element
+ internal reference
+ links to a valid reference
+ links with adjacent text
+ ignores invalid snippet IDs
+ includes a title attribute
+ escapes the title attribute
+ includes default classes
+ includes a data-project attribute
+ includes a data-snippet attribute
+ supports an :only_path context
+ cross-project / cross-namespace complete reference
+ links to a valid reference
+ link has valid text
+ has valid text
+ ignores invalid snippet IDs on the referenced project
+ cross-project / same-namespace complete reference
+ links to a valid reference
+ link has valid text
+ has valid text
+ ignores invalid snippet IDs on the referenced project
+ cross-project shorthand reference
+ links to a valid reference
+ link has valid text
+ has valid text
+ ignores invalid snippet IDs on the referenced project
+ cross-project URL reference
+ links to a valid reference
+ links with adjacent text
+ ignores invalid snippet IDs on the referenced project
+ group context
+ links to a valid reference
+
+AutocompleteUsersFinder
+ #execute
+ should contain exactly #<User id:2126 @johndoe>, #<User id:2128 @user2119>, #<User id:2129 @user2120>, and #<User id:2130 @user2121>
+ when current_user not passed or nil
+ should contain exactly
+ when project passed
+ should contain exactly #<User id:2140 @user2127>
+ when author_id passed
+ should contain exactly #<User id:2146 @user2131> and #<User id:2142 @notsorandom>
+ when group passed and project not passed
+ should contain exactly #<User id:2147 @johndoe>
+ when passed a subgroup
+ includes users from parent groups as well (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ when filtered by search
+ should contain exactly #<User id:2152 @johndoe>
+ when filtered by skip_users
+ should contain exactly #<User id:2157 @johndoe> and #<User id:2159 @user2138>
+ when todos exist
+ when filtered by todo_filter without todo_state_filter
+ should contain exactly
+ when filtered by todo_filter with pending todo_state_filter
+ should contain exactly #<User id:2175 @johndoe>
+ when filtered by todo_filter with done todo_state_filter
+ should contain exactly #<User id:2190 @user2163>
+ when filtered by current_user
+ should contain exactly #<User id:2202 @notsorandom>, #<User id:2201 @johndoe>, #<User id:2203 @user2174>, and #<User id:2204 @user2175>
+ when filtered by author_id
+ should contain exactly #<User id:2206 @notsorandom>, #<User id:2205 @johndoe>, #<User id:2207 @user2176>, #<User id:2208 @user2177>, and #<User id:2209 @user2178>
+
+Service
+ Associations
+ should belong to project
+ should have one service_hook
+ Validations
+ should validate that :type cannot be empty/falsy
+ Scopes
+ .confidential_note_hooks
+ includes services where confidential_note_events is true
+ excludes services where confidential_note_events is false
+ Test Button
+ #can_test?
+ when repository is not empty
+ returns true
+ when repository is empty
+ returns true
+ #test
+ when repository is not empty
+ test runs execute
+ when repository is empty
+ test runs execute
+ Template
+ .build_from_template
+ when template is invalid
+ sets service template to inactive when template is invalid
+ for pushover service
+ is prefilled for projects pushover service
+ has all fields prefilled
+ {property}_changed?
+ returns false when the property has not been assigned a new value
+ returns true when the property has been assigned a different value
+ returns true when the property has been assigned a different value twice
+ returns false when the property has been re-assigned the same value
+ returns false when the property has been assigned a new value then saved
+ {property}_touched?
+ returns false when the property has not been assigned a new value
+ returns true when the property has been assigned a different value
+ returns true when the property has been assigned a different value twice
+ returns true when the property has been re-assigned the same value
+ returns false when the property has been assigned a new value then saved
+ {property}_was
+ returns nil when the property has not been assigned a new value
+ returns the previous value when the property has been assigned a different value
+ returns initial value when the property has been re-assigned the same value
+ returns initial value when the property has been assigned multiple values
+ returns nil when the property has been assigned a new value then saved
+ initialize service with no properties
+ does not raise error
+ creates the properties
+ callbacks
+ on create
+ updates the has_external_issue_tracker boolean
+ on update
+ updates the has_external_issue_tracker boolean
+ #deprecated?
+ should return false by default
+ #deprecation_message
+ should be empty by default
+ .find_by_template
+ returns service template
+ #api_field_names
+ filters out sensitive fields
+
+TestHooks::ProjectService
+ #execute
+ hook with not implemented test
+ returns error message
+ push_events
+ returns error message if not enough data
+ executes hook
+ tag_push_events
+ returns error message if not enough data
+ executes hook
+ note_events
+ returns error message if not enough data
+ executes hook
+ issues_events
+ returns error message if not enough data
+ executes hook
+ confidential_issues_events
+ returns error message if not enough data
+ executes hook
+ merge_requests_events
+ returns error message if not enough data
+ executes hook
+ job_events
+ returns error message if not enough data
+ executes hook
+ pipeline_events
+ returns error message if not enough data
+ executes hook
+ wiki_page_events
+ returns error message if wiki disabled
+ returns error message if not enough data
+ executes hook
+
+User views an open merge request
+ when a merge request does not have repository
+ renders both the title and the description
+ when a merge request has repository
+ when rendering description preview
+ renders empty description preview
+ renders description preview
+ when the branch is rebased on the target
+ does not show diverged commits count
+ when the branch is diverged on the target
+ shows diverged commits count
+
+RunnerJobsFinder
+ #execute
+ when params is empty
+ returns all jobs assigned to Runner
+ when params contains status
+ when status is created
+ returns matched job
+ when status is pending
+ returns matched job
+ when status is running
+ returns matched job
+ when status is success
+ returns matched job
+ when status is failed
+ returns matched job
+ when status is canceled
+ returns matched job
+ when status is skipped
+ returns matched job
+ when status is manual
+ returns matched job
+
+Project snippets
+ when the project has snippets
+ pagination
+ behaves like paginated snippets
+ is limited to 20 items per page
+ clicking on the link to the second page
+ shows the remaining snippets
+ list content
+ contains all project snippets
+ when submitting a note
+ should have autocomplete
+ should have zen mode
+
+API::V3::Environments
+ GET /projects/:id/environments
+ as member of the project
+ returns project environments
+ behaves like a paginated resources
+ has pagination headers
+ as non member
+ returns a 404 status code
+ POST /projects/:id/environments
+ as a member
+ creates a environment with valid params
+ requires name to be passed
+ returns a 400 if environment already exists
+ returns a 400 if slug is specified
+ a non member
+ rejects the request
+ returns a 400 when the required params are missing
+ PUT /projects/:id/environments/:environment_id
+ returns a 200 if name and external_url are changed
+ won't allow slug to be changed
+ won't update the external_url if only the name is passed
+ returns a 404 if the environment does not exist
+ DELETE /projects/:id/environments/:environment_id
+ as a master
+ returns a 200 for an existing environment
+ returns a 404 for non existing id
+ a non member
+ rejects the request
+
+API::Namespaces
+ GET /namespaces
+ when unauthenticated
+ returns authentication error
+ when authenticated as admin
+ returns correct attributes
+ admin: returns an array of all namespaces
+ admin: returns an array of matched namespaces
+ when authenticated as a regular user
+ returns correct attributes when user can admin group
+ returns correct attributes when user cannot admin group
+ user: returns an array of namespaces
+ admin: returns an array of matched namespaces
+ GET /namespaces/:id
+ when unauthenticated
+ returns authentication error
+ when authenticated as regular user
+ when requested namespace is not owned by user
+ when requesting group
+ returns not-found
+ when requesting personal namespace
+ returns not-found
+ when requested namespace is owned by user
+ behaves like namespace reader
+ when namespace exists
+ when requested by ID
+ when requesting group
+ behaves like can access namespace
+ returns namespace details
+ when requesting personal namespace
+ behaves like can access namespace
+ returns namespace details
+ when requested by path
+ when requesting group
+ behaves like can access namespace
+ returns namespace details
+ when requesting personal namespace
+ behaves like can access namespace
+ returns namespace details
+ when namespace doesn't exist
+ returns not-found
+ when authenticated as admin
+ when requested namespace is not owned by user
+ when requesting group
+ behaves like can access namespace
+ returns namespace details
+ when requesting personal namespace
+ behaves like can access namespace
+ returns namespace details
+ when requested namespace is owned by user
+ behaves like namespace reader
+ when namespace exists
+ when requested by ID
+ when requesting group
+ behaves like can access namespace
+ returns namespace details
+ when requesting personal namespace
+ behaves like can access namespace
+ returns namespace details
+ when requested by path
+ when requesting group
+ behaves like can access namespace
+ returns namespace details
+ when requesting personal namespace
+ behaves like can access namespace
+ returns namespace details
+ when namespace doesn't exist
+ returns not-found
+
+MergeRequests::GetUrlsService
+ #execute
+ pushing to default branch
+ behaves like no_merge_request_url
+ returns no URL
+ pushing to project with MRs disabled
+ behaves like no_merge_request_url
+ returns no URL
+ pushing one completely new branch
+ behaves like new_merge_request_link
+ returns url to create new merge request
+ pushing to existing branch but no merge request
+ behaves like new_merge_request_link
+ returns url to create new merge request
+ pushing to deleted branch
+ behaves like no_merge_request_url
+ returns no URL
+ pushing to existing branch and merge request opened
+ behaves like show_merge_request_url
+ returns url to view merge request
+ pushing to existing branch and merge request is reopened
+ behaves like show_merge_request_url
+ returns url to view merge request
+ pushing to existing branch from forked project
+ behaves like show_merge_request_url
+ returns url to view merge request
+ pushing to existing branch and merge request is closed
+ behaves like new_merge_request_link
+ returns url to create new merge request
+ pushing to existing branch and merge request is merged
+ behaves like new_merge_request_link
+ returns url to create new merge request
+ pushing new branch and existing branch (with merge request created) at once
+ returns 2 urls for both creating new and showing merge request
+ when printing_merge_request_link_enabled is false
+ returns empty array
+
+LfsFileLock
+ should belong to project
+ should belong to user
+ should validate that :project_id cannot be empty/falsy
+ should validate that :user_id cannot be empty/falsy
+ should validate that :path cannot be empty/falsy
+ #can_be_unlocked_by?
+ when it's forced
+ can be unlocked by the author
+ can be unlocked by a master
+ can't be unlocked by other user
+ when it isn't forced
+ can be unlocked by the author
+ can't be unlocked by a master
+ can't be unlocked by other user
+
+Gitlab::Ci::Config::Entry::Boolean
+ validations
+ when entry config value is valid
+ #value
+ returns key value
+ #valid?
+ is valid
+ when entry value is not valid
+ #errors
+ saves errors
+Knapsack report was generated. Preview:
+{
+ "spec/services/todo_service_spec.rb": 53.71851348876953,
+ "spec/lib/gitlab/import_export/project_tree_saver_spec.rb": 48.39624857902527,
+ "spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb": 35.17360734939575,
+ "spec/controllers/projects/merge_requests_controller_spec.rb": 25.50887441635132,
+ "spec/controllers/groups_controller_spec.rb": 13.007296323776245,
+ "spec/features/projects/import_export/import_file_spec.rb": 16.827879428863525,
+ "spec/lib/gitlab/middleware/go_spec.rb": 12.497276306152344,
+ "spec/features/projects/blobs/edit_spec.rb": 11.511932134628296,
+ "spec/services/boards/lists/move_service_spec.rb": 8.695446491241455,
+ "spec/services/create_deployment_service_spec.rb": 6.754847526550293,
+ "spec/controllers/groups/milestones_controller_spec.rb": 6.8740551471710205,
+ "spec/helpers/groups_helper_spec.rb": 0.9002459049224854,
+ "spec/requests/api/v3/todos_spec.rb": 6.5924904346466064,
+ "spec/models/project_services/teamcity_service_spec.rb": 2.9881808757781982,
+ "spec/lib/gitlab/conflict/file_spec.rb": 5.294132709503174,
+ "spec/lib/banzai/filter/snippet_reference_filter_spec.rb": 4.118850469589233,
+ "spec/finders/autocomplete_users_finder_spec.rb": 3.864232063293457,
+ "spec/models/service_spec.rb": 3.1697962284088135,
+ "spec/services/test_hooks/project_service_spec.rb": 4.167759656906128,
+ "spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb": 4.707003355026245,
+ "spec/finders/runner_jobs_finder_spec.rb": 3.2137575149536133,
+ "spec/features/projects/snippets_spec.rb": 3.631467580795288,
+ "spec/requests/api/v3/environments_spec.rb": 2.314746856689453,
+ "spec/requests/api/namespaces_spec.rb": 2.352935314178467,
+ "spec/services/merge_requests/get_urls_service_spec.rb": 2.8039824962615967,
+ "spec/models/lfs_file_lock_spec.rb": 0.7295050621032715,
+ "spec/lib/gitlab/ci/config/entry/boolean_spec.rb": 0.007024049758911133
+}
+
+Knapsack global time execution for tests: 04m 49s
+
+Pending: (Failures listed here are expected and do not affect your suite's status)
+
+ 1) GroupsController GET #new when creating subgroups and can_create_group is true and logged in as Admin behaves like member with ability to create subgroups renders the new page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:15
+
+ 2) GroupsController GET #new when creating subgroups and can_create_group is true and logged in as Owner behaves like member with ability to create subgroups renders the new page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:15
+
+ 3) GroupsController GET #new when creating subgroups and can_create_group is true and logged in as Guest behaves like member without ability to create subgroups renders the 404 page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:25
+
+ 4) GroupsController GET #new when creating subgroups and can_create_group is true and logged in as Developer behaves like member without ability to create subgroups renders the 404 page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:25
+
+ 5) GroupsController GET #new when creating subgroups and can_create_group is true and logged in as Master behaves like member without ability to create subgroups renders the 404 page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:25
+
+ 6) GroupsController GET #new when creating subgroups and can_create_group is false and logged in as Admin behaves like member with ability to create subgroups renders the new page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:15
+
+ 7) GroupsController GET #new when creating subgroups and can_create_group is false and logged in as Owner behaves like member with ability to create subgroups renders the new page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:15
+
+ 8) GroupsController GET #new when creating subgroups and can_create_group is false and logged in as Guest behaves like member without ability to create subgroups renders the 404 page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:25
+
+ 9) GroupsController GET #new when creating subgroups and can_create_group is false and logged in as Developer behaves like member without ability to create subgroups renders the 404 page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:25
+
+ 10) GroupsController GET #new when creating subgroups and can_create_group is false and logged in as Master behaves like member without ability to create subgroups renders the 404 page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:25
+
+ 11) GroupsController POST #create when creating subgroups and can_create_group is true and logged in as Owner creates the subgroup
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:117
+
+ 12) GroupsController POST #create when creating subgroups and can_create_group is true and logged in as Developer renders the new template
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:129
+
+ 13) GroupsController POST #create when creating subgroups and can_create_group is false and logged in as Owner creates the subgroup
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:117
+
+ 14) GroupsController POST #create when creating subgroups and can_create_group is false and logged in as Developer renders the new template
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:129
+
+ 15) GroupsController PUT transfer when transfering to a subgroup goes right should return a notice
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:516
+
+ 16) GroupsController PUT transfer when transfering to a subgroup goes right should redirect to the new path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:520
+
+ 17) GroupsController PUT transfer when converting to a root group goes right should return a notice
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:535
+
+ 18) GroupsController PUT transfer when converting to a root group goes right should redirect to the new path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:539
+
+ 19) GroupsController PUT transfer When the transfer goes wrong should return an alert
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:557
+
+ 20) GroupsController PUT transfer When the transfer goes wrong should redirect to the current path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:561
+
+ 21) GroupsController PUT transfer when the user is not allowed to transfer the group should be denied
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:577
+
+ 22) Groups::TransferService#execute when transforming a group into a root group behaves like ensuring allowed transfer for a group with other database than PostgreSQL should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:15
+
+ 23) Groups::TransferService#execute when transforming a group into a root group behaves like ensuring allowed transfer for a group with other database than PostgreSQL should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:19
+
+ 24) Groups::TransferService#execute when transforming a group into a root group behaves like ensuring allowed transfer for a group when there's an exception on Gitlab shell directories should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:33
+
+ 25) Groups::TransferService#execute when transforming a group into a root group behaves like ensuring allowed transfer for a group when there's an exception on Gitlab shell directories should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:37
+
+ 26) Groups::TransferService#execute when transforming a group into a root group when the group is already a root group should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:53
+
+ 27) Groups::TransferService#execute when transforming a group into a root group when the user does not have the right policies should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:62
+
+ 28) Groups::TransferService#execute when transforming a group into a root group when the user does not have the right policies should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:66
+
+ 29) Groups::TransferService#execute when transforming a group into a root group when there is a group with the same path should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:79
+
+ 30) Groups::TransferService#execute when transforming a group into a root group when there is a group with the same path should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:83
+
+ 31) Groups::TransferService#execute when transforming a group into a root group when the group is a subgroup and the transfer is valid should update group attributes
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:99
+
+ 32) Groups::TransferService#execute when transforming a group into a root group when the group is a subgroup and the transfer is valid should update group children path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:103
+
+ 33) Groups::TransferService#execute when transforming a group into a root group when the group is a subgroup and the transfer is valid should update group projects path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:109
+
+ 34) Groups::TransferService#execute when transferring a subgroup into another group behaves like ensuring allowed transfer for a group with other database than PostgreSQL should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:15
+
+ 35) Groups::TransferService#execute when transferring a subgroup into another group behaves like ensuring allowed transfer for a group with other database than PostgreSQL should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:19
+
+ 36) Groups::TransferService#execute when transferring a subgroup into another group behaves like ensuring allowed transfer for a group when there's an exception on Gitlab shell directories should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:33
+
+ 37) Groups::TransferService#execute when transferring a subgroup into another group behaves like ensuring allowed transfer for a group when there's an exception on Gitlab shell directories should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:37
+
+ 38) Groups::TransferService#execute when transferring a subgroup into another group when the new parent group is the same as the previous parent group should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:125
+
+ 39) Groups::TransferService#execute when transferring a subgroup into another group when the new parent group is the same as the previous parent group should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:129
+
+ 40) Groups::TransferService#execute when transferring a subgroup into another group when the user does not have the right policies should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:138
+
+ 41) Groups::TransferService#execute when transferring a subgroup into another group when the user does not have the right policies should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:142
+
+ 42) Groups::TransferService#execute when transferring a subgroup into another group when the parent has a group with the same path should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:155
+
+ 43) Groups::TransferService#execute when transferring a subgroup into another group when the parent has a group with the same path should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:159
+
+ 44) Groups::TransferService#execute when transferring a subgroup into another group when the parent group has a project with the same path should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:174
+
+ 45) Groups::TransferService#execute when transferring a subgroup into another group when the parent group has a project with the same path should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:178
+
+ 46) Groups::TransferService#execute when transferring a subgroup into another group when the group is allowed to be transferred should update visibility for the group based on the parent group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:212
+
+ 47) Groups::TransferService#execute when transferring a subgroup into another group when the group is allowed to be transferred should update parent group to the new parent
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:216
+
+ 48) Groups::TransferService#execute when transferring a subgroup into another group when the group is allowed to be transferred should return the group as children of the new parent
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:220
+
+ 49) Groups::TransferService#execute when transferring a subgroup into another group when the group is allowed to be transferred should create a redirect for the group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:225
+
+ 50) Groups::TransferService#execute when transferring a subgroup into another group when the group is allowed to be transferred when the group has a lower visibility than the parent group should not update the visibility for the group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:194
+
+ 51) Groups::TransferService#execute when transferring a subgroup into another group when the group is allowed to be transferred when the group has a higher visibility than the parent group should update visibility level based on the parent group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:205
+
+ 52) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with group descendants should update subgroups path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:239
+
+ 53) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with group descendants should create redirects for the subgroups
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:246
+
+ 54) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with group descendants when the new parent has a higher visibility than the children should not update the children visibility
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:253
+
+ 55) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with group descendants when the new parent has a lower visibility than the children should update children visibility to match the new parent
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:264
+
+ 56) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with project descendants should update projects path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:282
+
+ 57) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with project descendants should create permanent redirects for the projects
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:289
+
+ 58) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with project descendants when the new parent has a higher visibility than the projects should not update projects visibility
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:296
+
+ 59) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with project descendants when the new parent has a lower visibility than the projects should update projects visibility to match the new parent
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:307
+
+ 60) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with subgroups & projects descendants should update subgroups path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:327
+
+ 61) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with subgroups & projects descendants should update projects path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:334
+
+ 62) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with subgroups & projects descendants should create redirect for the subgroups and projects
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:341
+
+ 63) Groups::TransferService#execute when transferring a subgroup into another group when transfering a group with nested groups and projects should update subgroups path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:363
+
+ 64) Groups::TransferService#execute when transferring a subgroup into another group when transfering a group with nested groups and projects should update projects path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:375
+
+ 65) Groups::TransferService#execute when transferring a subgroup into another group when transfering a group with nested groups and projects should create redirect for the subgroups and projects
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:383
+
+ 66) Groups::TransferService#execute when transferring a subgroup into another group when updating the group goes wrong should restore group and projects visibility
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:405
+
+ 67) GroupsHelper group_title outputs the groups in the correct order
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:106
+
+ 68) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :root_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 69) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :sub_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 70) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :sub_sub_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 71) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :root_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 72) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :sub_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 73) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :sub_sub_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 74) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :root_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :subgroup has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 75) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :sub_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :subgroup has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 76) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :sub_sub_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :subgroup has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 77) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :root_owner, help_text: :ancestor_locked_but_you_can_override, linked_ancestor: :subgroup has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 78) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :sub_owner, help_text: :ancestor_locked_but_you_can_override, linked_ancestor: :subgroup has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 79) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :sub_sub_owner, help_text: :ancestor_locked_so_ask_the_owner, linked_ancestor: :subgroup has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 80) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :root_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 81) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :sub_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 82) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :sub_sub_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 83) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :root_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 84) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :sub_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
-path
-Set for your local app (/usr/local/bundle/config): "vendor"
-Set via BUNDLE_PATH: "/usr/local/bundle"
+ 85) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :sub_sub_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
-jobs
-Set for your local app (/usr/local/bundle/config): "2"
+ 86) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :root_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :root_group has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
-clean
-Set for your local app (/usr/local/bundle/config): "true"
+ 87) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :sub_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :root_group has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
-without
-Set for your local app (/usr/local/bundle/config): [:production]
+ 88) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :sub_sub_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :root_group has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
-silence_root_warning
-Set via BUNDLE_SILENCE_ROOT_WARNING: true
+ 89) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :root_owner, help_text: :ancestor_locked_but_you_can_override, linked_ancestor: :root_group has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
-app_config
-Set via BUNDLE_APP_CONFIG: "/usr/local/bundle"
+ 90) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :sub_owner, help_text: :ancestor_locked_so_ask_the_owner, linked_ancestor: :root_group has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
-install_flags
-Set via BUNDLE_INSTALL_FLAGS: "--without=production --jobs=2 --path=vendor --retry=3 --quiet"
+ 91) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :sub_sub_owner, help_text: :ancestor_locked_so_ask_the_owner, linked_ancestor: :root_group has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
-bin
-Set via BUNDLE_BIN: "/usr/local/bundle/bin"
+ 92) AutocompleteUsersFinder#execute when passed a subgroup includes users from parent groups as well
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/finders/autocomplete_users_finder_spec.rb:55
-gemfile
-Set via BUNDLE_GEMFILE: "/builds/gitlab-org/gitlab-ce/Gemfile"
+Finished in 5 minutes 7 seconds (files took 16.6 seconds to load)
+819 examples, 0 failures, 92 pending
-section_end:1517486961:build_script
-section_start:1517486961:after_script
-section_end:1517486962:after_script
-section_start:1517486962:upload_artifacts
+section_end:1522927514:build_script
+section_start:1522927514:after_script
+Running after script...
+$ date
+Thu Apr 5 11:25:14 UTC 2018
+section_end:1522927515:after_script
+section_start:1522927515:archive_cache
+Not uploading cache ruby-2.3.6-with-yarn due to policy
+section_end:1522927516:archive_cache
+section_start:1522927516:upload_artifacts
Uploading artifacts...
-WARNING: coverage/: no matching files 
+coverage/: found 5 matching files 
knapsack/: found 5 matching files 
+rspec_flaky/: found 4 matching files 
WARNING: tmp/capybara/: no matching files 
-Uploading artifacts to coordinator... ok  id=50551722 responseStatus=201 Created token=XkN753rp
-section_end:1517486963:upload_artifacts
-ERROR: Job failed: exit code 1
- \ No newline at end of file
+Uploading artifacts to coordinator... ok  id=61303283 responseStatus=201 Created token=rusBKvxM
+section_end:1522927520:upload_artifacts
+Job succeeded
+
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
index 6c4f7050ee0..143b28728a3 100644
--- a/spec/helpers/gitlab_routing_helper_spec.rb
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -89,4 +89,19 @@ describe GitlabRoutingHelper do
expect(preview_markdown_path(project)).to eq("/#{project.full_path}/preview_markdown")
end
end
+
+ describe '#edit_milestone_path' do
+ it 'returns group milestone edit path when given entity parent is a Group' do
+ group = create(:group)
+ milestone = create(:milestone, group: group)
+
+ expect(edit_milestone_path(milestone)).to eq("/groups/#{group.path}/-/milestones/#{milestone.iid}/edit")
+ end
+
+ it 'returns project milestone edit path when given entity parent is not a Group' do
+ milestone = create(:milestone, group: nil)
+
+ expect(edit_milestone_path(milestone)).to eq("/#{milestone.project.full_path}/milestones/#{milestone.iid}/edit")
+ end
+ end
end
diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb
index 2f23ed55d99..93d8e672f8c 100644
--- a/spec/helpers/icons_helper_spec.rb
+++ b/spec/helpers/icons_helper_spec.rb
@@ -162,4 +162,11 @@ describe IconsHelper do
expect(file_type_icon_class('file', 0, 'CHANGELOG')).to eq 'file-text-o'
end
end
+
+ describe '#external_snippet_icon' do
+ it 'returns external snippet icon' do
+ expect(external_snippet_icon('download').to_s)
+ .to eq("<span class=\"gl-snippet-icon gl-snippet-icon-download\"></span>")
+ end
+ end
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 4224cea4652..7b59fde999d 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -22,11 +22,15 @@ describe IssuablesHelper do
end
describe '#issuable_labels_tooltip' do
- it 'returns label text' do
+ it 'returns label text with no labels' do
+ expect(issuable_labels_tooltip([])).to eq("Labels")
+ end
+
+ it 'returns label text with labels within max limit' do
expect(issuable_labels_tooltip([label])).to eq(label.title)
end
- it 'returns label text' do
+ it 'returns label text with labels exceeding max limit' do
expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more")
end
end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index aeef5352333..8bb2e234e9a 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -96,13 +96,32 @@ describe IssuesHelper do
describe '#award_state_class' do
let!(:upvote) { create(:award_emoji) }
+ let(:awardable) { upvote.awardable }
+ let(:user) { upvote.user }
+
+ before do
+ allow(helper).to receive(:can?) do |*args|
+ Ability.allowed?(*args)
+ end
+ end
it "returns disabled string for unauthenticated user" do
- expect(award_state_class(AwardEmoji.all, nil)).to eq("disabled")
+ expect(helper.award_state_class(awardable, AwardEmoji.all, nil)).to eq("disabled")
+ end
+
+ it "returns disabled for a user that does not have access to the awardable" do
+ expect(helper.award_state_class(awardable, AwardEmoji.all, build(:user))).to eq("disabled")
end
it "returns active string for author" do
- expect(award_state_class(AwardEmoji.all, upvote.user)).to eq("active")
+ expect(helper.award_state_class(awardable, AwardEmoji.all, upvote.user)).to eq("active")
+ end
+
+ it "is blank for a user that has access to the awardable" do
+ user = build(:user)
+ expect(helper).to receive(:can?).with(user, :award_emoji, awardable).and_return(true)
+
+ expect(helper.award_state_class(awardable, AwardEmoji.all, user)).to be_blank
end
end
@@ -144,4 +163,26 @@ describe IssuesHelper do
end
end
end
+
+ describe '#show_new_issue_link?' do
+ before do
+ allow(helper).to receive(:current_user)
+ end
+
+ it 'is false when no project there is no project' do
+ expect(helper.show_new_issue_link?(nil)).to be_falsey
+ end
+
+ it 'is true when there is a project and no logged in user' do
+ expect(helper.show_new_issue_link?(build(:project))).to be_truthy
+ end
+
+ it 'is true when the current user does not have access to the project' do
+ project = build(:project)
+ allow(helper).to receive(:current_user).and_return(project.owner)
+
+ expect(helper).to receive(:can?).with(project.owner, :create_issue, project).and_return(true)
+ expect(helper.show_new_issue_link?(project)).to be_truthy
+ end
+ end
end
diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb
index 70b4a89cb86..f5185cb2857 100644
--- a/spec/helpers/milestones_helper_spec.rb
+++ b/spec/helpers/milestones_helper_spec.rb
@@ -83,58 +83,4 @@ describe MilestonesHelper do
end
end
end
-
- describe '#milestone_remaining_days' do
- around do |example|
- Timecop.freeze(Time.utc(2017, 3, 17)) { example.run }
- end
-
- context 'when less than 31 days remaining' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) }
-
- it 'returns days remaining' do
- expect(milestone_remaining).to eq("<strong>12</strong> days remaining")
- end
- end
-
- context 'when less than 1 year and more than 30 days remaining' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) }
-
- it 'returns months remaining' do
- expect(milestone_remaining).to eq("<strong>2</strong> months remaining")
- end
- end
-
- context 'when more than 1 year remaining' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) }
-
- it 'returns years remaining' do
- expect(milestone_remaining).to eq("<strong>1</strong> year remaining")
- end
- end
-
- context 'when milestone is expired' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.days.ago.utc)) }
-
- it 'returns "Past due"' do
- expect(milestone_remaining).to eq("<strong>Past due</strong>")
- end
- end
-
- context 'when milestone has start_date in the future' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) }
-
- it 'returns "Upcoming"' do
- expect(milestone_remaining).to eq("<strong>Upcoming</strong>")
- end
- end
-
- context 'when milestone has start_date in the past' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.ago.utc)) }
-
- it 'returns days elapsed' do
- expect(milestone_remaining).to eq("<strong>2</strong> days elapsed")
- end
- end
- end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index ce96e90e2d7..8fcb175416f 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -274,16 +274,16 @@ describe ProjectsHelper do
end
end
- describe '#sanitized_import_error' do
+ describe '#sanitizerepo_repo_path' do
let(:project) { create(:project, :repository) }
+ let(:storage_path) { Gitlab.config.repositories.storages.default.legacy_disk_path }
before do
- allow(project).to receive(:repository_storage_path).and_return('/base/repo/path')
allow(Settings.shared).to receive(:[]).with('path').and_return('/base/repo/export/path')
end
it 'removes the repo path' do
- repo = '/base/repo/path/namespace/test.git'
+ repo = "#{storage_path}/namespace/test.git"
import_error = "Could not clone #{repo}\n"
expect(sanitize_repo_path(project, import_error)).to eq('Could not clone [REPOS PATH]/namespace/test.git')
@@ -322,74 +322,6 @@ describe ProjectsHelper do
end
end
- describe "#project_feature_access_select" do
- let(:project) { create(:project, :public) }
- let(:user) { create(:user) }
-
- context "when project is internal or public" do
- it "shows all options" do
- helper.instance_variable_set(:@project, project)
- result = helper.project_feature_access_select(:issues_access_level)
- expect(result).to include("Disabled")
- expect(result).to include("Only team members")
- expect(result).to include("Everyone with access")
- end
- end
-
- context "when project is private" do
- before do
- project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
-
- it "shows only allowed options" do
- helper.instance_variable_set(:@project, project)
- result = helper.project_feature_access_select(:issues_access_level)
- expect(result).to include("Disabled")
- expect(result).to include("Only team members")
- expect(result).to have_selector('option[disabled]', text: "Everyone with access")
- end
- end
-
- context "when project moves from public to private" do
- before do
- project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
-
- it "shows the highest allowed level selected" do
- helper.instance_variable_set(:@project, project)
- result = helper.project_feature_access_select(:issues_access_level)
-
- expect(result).to include("Disabled")
- expect(result).to include("Only team members")
- expect(result).to have_selector('option[disabled]', text: "Everyone with access")
- expect(result).to have_selector('option[selected]', text: "Only team members")
- end
- end
- end
-
- describe "#visibility_select_options" do
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
-
- before do
- allow(helper).to receive(:current_user).and_return(user)
-
- stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
- end
-
- it "does not include the Public restricted level" do
- expect(helper.send(:visibility_select_options, project, Gitlab::VisibilityLevel::PRIVATE)).not_to include('Public')
- end
-
- it "includes the Internal level" do
- expect(helper.send(:visibility_select_options, project, Gitlab::VisibilityLevel::PRIVATE)).to include('Internal')
- end
-
- it "includes the Private level" do
- expect(helper.send(:visibility_select_options, project, Gitlab::VisibilityLevel::PRIVATE)).to include('Private')
- end
- end
-
describe '#get_project_nav_tabs' do
let(:project) { create(:project) }
let(:user) { create(:user) }
diff --git a/spec/helpers/snippets_helper_spec.rb b/spec/helpers/snippets_helper_spec.rb
new file mode 100644
index 00000000000..0323ffb641c
--- /dev/null
+++ b/spec/helpers/snippets_helper_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe SnippetsHelper do
+ include IconsHelper
+
+ describe '#embedded_snippet_raw_button' do
+ it 'gives view raw button of embedded snippets for project snippets' do
+ @snippet = create(:project_snippet, :public)
+
+ expect(embedded_snippet_raw_button.to_s).to eq("<a class=\"btn\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Open raw\" href=\"#{raw_project_snippet_url(@snippet.project, @snippet)}\">#{external_snippet_icon('doc_code')}</a>")
+ end
+
+ it 'gives view raw button of embedded snippets for personal snippets' do
+ @snippet = create(:personal_snippet, :public)
+
+ expect(embedded_snippet_raw_button.to_s).to eq("<a class=\"btn\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Open raw\" href=\"#{raw_snippet_url(@snippet)}\">#{external_snippet_icon('doc_code')}</a>")
+ end
+ end
+
+ describe '#embedded_snippet_download_button' do
+ it 'gives download button of embedded snippets for project snippets' do
+ @snippet = create(:project_snippet, :public)
+
+ expect(embedded_snippet_download_button.to_s).to eq("<a class=\"btn\" target=\"_blank\" title=\"Download\" rel=\"noopener noreferrer\" href=\"#{raw_project_snippet_url(@snippet.project, @snippet, inline: false)}\">#{external_snippet_icon('download')}</a>")
+ end
+
+ it 'gives download button of embedded snippets for personal snippets' do
+ @snippet = create(:personal_snippet, :public)
+
+ expect(embedded_snippet_download_button.to_s).to eq("<a class=\"btn\" target=\"_blank\" title=\"Download\" rel=\"noopener noreferrer\" href=\"#{raw_snippet_url(@snippet, inline: false)}\">#{external_snippet_icon('download')}</a>")
+ end
+ end
+end
diff --git a/spec/initializers/gollum_spec.rb b/spec/initializers/gollum_spec.rb
deleted file mode 100644
index adf824a8947..00000000000
--- a/spec/initializers/gollum_spec.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-require 'spec_helper'
-
-describe 'gollum' do
- let(:project) { create(:project) }
- let(:user) { project.owner }
- let(:wiki) { ProjectWiki.new(project, user) }
- let(:gollum_wiki) { Gollum::Wiki.new(wiki.repository.path) }
-
- before do
- create_page(page_name, 'content1')
- end
-
- after do
- destroy_page(page_name)
- end
-
- context 'with simple paths' do
- let(:page_name) { 'page1' }
-
- it 'returns the entry hash if it matches the file name' do
- expect(tree_entry(page_name)).not_to be_nil
- end
-
- it 'returns nil if the path does not fit completely' do
- expect(tree_entry("foo/#{page_name}")).to be_nil
- end
- end
-
- context 'with complex paths' do
- let(:page_name) { '/foo/bar/page2' }
-
- it 'returns the entry hash if it matches the file name' do
- expect(tree_entry(page_name)).not_to be_nil
- end
-
- it 'returns nil if the path does not fit completely' do
- expect(tree_entry("foo1/bar/page2")).to be_nil
- expect(tree_entry("foo/bar1/page2")).to be_nil
- end
- end
-
- def tree_entry(name)
- gollum_wiki.repo.git.tree_entry(wiki_commits[0].commit, name + '.md')
- end
-
- def wiki_commits
- gollum_wiki.repo.commits
- end
-
- def commit_details
- Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit")
- end
-
- def create_page(name, content)
- wiki.wiki.write_page(name, :markdown, content, commit_details)
- end
-
- def destroy_page(name)
- page = wiki.find_page(name).page
- wiki.delete_page(page, "test commit")
- end
-end
diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc
index 3d922021978..9eb0e732572 100644
--- a/spec/javascripts/.eslintrc
+++ b/spec/javascripts/.eslintrc
@@ -18,6 +18,7 @@
"sandbox": false,
"setFixtures": false,
"setStyleFixtures": false,
+ "spyOnDependency": false,
"spyOnEvent": false,
"ClassSpecHelper": false
},
diff --git a/spec/javascripts/activities_spec.js b/spec/javascripts/activities_spec.js
index 909a1bf76bc..5dbdcd24296 100644
--- a/spec/javascripts/activities_spec.js
+++ b/spec/javascripts/activities_spec.js
@@ -3,24 +3,30 @@
import $ from 'jquery';
import 'vendor/jquery.endless-scroll';
import Activities from '~/activities';
+import Pager from '~/pager';
-(() => {
+describe('Activities', () => {
window.gon || (window.gon = {});
const fixtureTemplate = 'static/event_filter.html.raw';
const filters = [
{
id: 'all',
- }, {
+ },
+ {
id: 'push',
name: 'push events',
- }, {
+ },
+ {
id: 'merged',
name: 'merge events',
- }, {
+ },
+ {
id: 'comments',
- }, {
+ },
+ {
id: 'team',
- }];
+ },
+ ];
function getEventName(index) {
const filter = filters[index];
@@ -32,31 +38,34 @@ import Activities from '~/activities';
return `#${filter.id}_event_filter`;
}
- describe('Activities', () => {
- beforeEach(() => {
- loadFixtures(fixtureTemplate);
- new Activities();
- });
-
- for (let i = 0; i < filters.length; i += 1) {
- ((i) => {
- describe(`when selecting ${getEventName(i)}`, () => {
- beforeEach(() => {
- $(getSelector(i)).click();
- });
-
- for (let x = 0; x < filters.length; x += 1) {
- ((x) => {
- const shouldHighlight = i === x;
- const testName = shouldHighlight ? 'should highlight' : 'should not highlight';
-
- it(`${testName} ${getEventName(x)}`, () => {
- expect($(getSelector(x)).parent().hasClass('active')).toEqual(shouldHighlight);
- });
- })(x);
- }
- });
- })(i);
- }
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+ spyOn(Pager, 'init').and.stub();
+ new Activities();
});
-})();
+
+ for (let i = 0; i < filters.length; i += 1) {
+ (i => {
+ describe(`when selecting ${getEventName(i)}`, () => {
+ beforeEach(() => {
+ $(getSelector(i)).click();
+ });
+
+ for (let x = 0; x < filters.length; x += 1) {
+ (x => {
+ const shouldHighlight = i === x;
+ const testName = shouldHighlight ? 'should highlight' : 'should not highlight';
+
+ it(`${testName} ${getEventName(x)}`, () => {
+ expect(
+ $(getSelector(x))
+ .parent()
+ .hasClass('active'),
+ ).toEqual(shouldHighlight);
+ });
+ })(x);
+ }
+ });
+ })(i);
+ }
+});
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index c37c62c63dd..d03836d10f9 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import '~/behaviors/quick_submit';
-describe('Quick Submit behavior', () => {
+describe('Quick Submit behavior', function () {
const keydownEvent = (options = { keyCode: 13, metaKey: true }) => $.Event('keydown', options);
preloadFixtures('merge_requests/merge_request_with_task_list.html.raw');
diff --git a/spec/javascripts/blob/blob_file_dropzone_spec.js b/spec/javascripts/blob/blob_file_dropzone_spec.js
index 0b1de504435..346f795c3f5 100644
--- a/spec/javascripts/blob/blob_file_dropzone_spec.js
+++ b/spec/javascripts/blob/blob_file_dropzone_spec.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import BlobFileDropzone from '~/blob/blob_file_dropzone';
-describe('BlobFileDropzone', () => {
+describe('BlobFileDropzone', function () {
preloadFixtures('blob/show.html.raw');
beforeEach(() => {
diff --git a/spec/javascripts/branches/branches_delete_modal_spec.js b/spec/javascripts/branches/branches_delete_modal_spec.js
new file mode 100644
index 00000000000..b223b8e2c0a
--- /dev/null
+++ b/spec/javascripts/branches/branches_delete_modal_spec.js
@@ -0,0 +1,40 @@
+import $ from 'jquery';
+import DeleteModal from '~/branches/branches_delete_modal';
+
+describe('branches delete modal', () => {
+ describe('setDisableDeleteButton', () => {
+ let submitSpy;
+ let $deleteButton;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div id="modal-delete-branch">
+ <form>
+ <button type="submit" class="js-delete-branch">Delete</button>
+ </form>
+ </div>
+ `);
+ $deleteButton = $('.js-delete-branch');
+ submitSpy = jasmine.createSpy('submit').and.callFake(event => event.preventDefault());
+ $('#modal-delete-branch form').on('submit', submitSpy);
+ // eslint-disable-next-line no-new
+ new DeleteModal();
+ });
+
+ it('does not submit if button is disabled', () => {
+ $deleteButton.attr('disabled', true);
+
+ $deleteButton.click();
+
+ expect(submitSpy).not.toHaveBeenCalled();
+ });
+
+ it('submits if button is not disabled', () => {
+ $deleteButton.attr('disabled', false);
+
+ $deleteButton.click();
+
+ expect(submitSpy).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js
index 2abf52a1676..8427e8a0ba7 100644
--- a/spec/javascripts/collapsed_sidebar_todo_spec.js
+++ b/spec/javascripts/collapsed_sidebar_todo_spec.js
@@ -85,7 +85,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
setTimeout(() => {
expect(
document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
- ).toBe('Mark done');
+ ).toBe('Mark todo as done');
done();
});
@@ -97,7 +97,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
setTimeout(() => {
expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('data-original-title'),
- ).toBe('Mark done');
+ ).toBe('Mark todo as done');
done();
});
@@ -128,13 +128,13 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
.catch(done.fail);
});
- it('updates aria-label to mark done', (done) => {
+ it('updates aria-label to mark todo as done', (done) => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
setTimeout(() => {
expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
- ).toBe('Mark done');
+ ).toBe('Mark todo as done');
done();
});
@@ -147,7 +147,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
.then(() => {
expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
- ).toBe('Mark done');
+ ).toBe('Mark todo as done');
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
})
diff --git a/spec/javascripts/comment_type_toggle_spec.js b/spec/javascripts/comment_type_toggle_spec.js
index dfd0810d52e..0ba709298c5 100644
--- a/spec/javascripts/comment_type_toggle_spec.js
+++ b/spec/javascripts/comment_type_toggle_spec.js
@@ -1,5 +1,4 @@
import CommentTypeToggle from '~/comment_type_toggle';
-import * as dropLabSrc from '~/droplab/drop_lab';
import InputSetter from '~/droplab/plugins/input_setter';
describe('CommentTypeToggle', function () {
@@ -59,14 +58,14 @@ describe('CommentTypeToggle', function () {
this.droplab = jasmine.createSpyObj('droplab', ['init']);
- spyOn(dropLabSrc, 'default').and.returnValue(this.droplab);
+ this.droplabConstructor = spyOnDependency(CommentTypeToggle, 'DropLab').and.returnValue(this.droplab);
spyOn(this.commentTypeToggle, 'setConfig').and.returnValue(this.config);
CommentTypeToggle.prototype.initDroplab.call(this.commentTypeToggle);
});
it('should instantiate a DropLab instance', function () {
- expect(dropLabSrc.default).toHaveBeenCalled();
+ expect(this.droplabConstructor).toHaveBeenCalled();
});
it('should set .droplab', function () {
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index 53820770f3f..819ed7896ca 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -4,7 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import pipelinesTable from '~/commit/pipelines/pipelines_table.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-describe('Pipelines table in Commits and Merge requests', () => {
+describe('Pipelines table in Commits and Merge requests', function () {
const jsonFixtureName = 'pipelines/pipelines.json';
let pipeline;
let PipelinesTable;
diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js
index 977298b9221..60d100e8544 100644
--- a/spec/javascripts/commits_spec.js
+++ b/spec/javascripts/commits_spec.js
@@ -3,6 +3,7 @@ import 'vendor/jquery.endless-scroll';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import CommitsList from '~/commits';
+import Pager from '~/pager';
describe('Commits List', () => {
let commitsList;
@@ -14,6 +15,7 @@ describe('Commits List', () => {
</form>
<ol id="commits-list"></ol>
`);
+ spyOn(Pager, 'init').and.stub();
commitsList = new CommitsList(25);
});
@@ -68,9 +70,10 @@ describe('Commits List', () => {
mock.restore();
});
- it('should save the last search string', (done) => {
+ it('should save the last search string', done => {
commitsList.searchField.val('GitLab');
- commitsList.filterResults()
+ commitsList
+ .filterResults()
.then(() => {
expect(ajaxSpy).toHaveBeenCalled();
expect(commitsList.lastSearch).toEqual('GitLab');
@@ -80,8 +83,9 @@ describe('Commits List', () => {
.catch(done.fail);
});
- it('should not make ajax call if the input does not change', (done) => {
- commitsList.filterResults()
+ it('should not make ajax call if the input does not change', done => {
+ commitsList
+ .filterResults()
.then(() => {
expect(ajaxSpy).not.toHaveBeenCalled();
expect(commitsList.lastSearch).toEqual('');
diff --git a/spec/javascripts/droplab/hook_spec.js b/spec/javascripts/droplab/hook_spec.js
index 3d39bd0812b..5eed1db2750 100644
--- a/spec/javascripts/droplab/hook_spec.js
+++ b/spec/javascripts/droplab/hook_spec.js
@@ -1,5 +1,4 @@
import Hook from '~/droplab/hook';
-import * as dropdownSrc from '~/droplab/drop_down';
describe('Hook', function () {
describe('class constructor', function () {
@@ -10,7 +9,7 @@ describe('Hook', function () {
this.config = {};
this.dropdown = {};
- spyOn(dropdownSrc, 'default').and.returnValue(this.dropdown);
+ this.dropdownConstructor = spyOnDependency(Hook, 'DropDown').and.returnValue(this.dropdown);
this.hook = new Hook(this.trigger, this.list, this.plugins, this.config);
});
@@ -24,7 +23,7 @@ describe('Hook', function () {
});
it('should call DropDown constructor', function () {
- expect(dropdownSrc.default).toHaveBeenCalledWith(this.list, this.config);
+ expect(this.dropdownConstructor).toHaveBeenCalledWith(this.list, this.config);
});
it('should set .type', function () {
diff --git a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
index 480c138b9db..2ab6a0077b5 100644
--- a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
+++ b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
@@ -3,12 +3,11 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import {
getSelector,
- togglePopover,
dismiss,
- mouseleave,
- mouseenter,
inserted,
} from '~/feature_highlight/feature_highlight_helper';
+import { togglePopover } from '~/shared/popover';
+
import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
describe('feature highlight helper', () => {
@@ -19,110 +18,6 @@ describe('feature highlight helper', () => {
});
});
- describe('togglePopover', () => {
- describe('togglePopover(true)', () => {
- it('returns true when popover is shown', () => {
- const context = {
- hasClass: () => false,
- popover: () => {},
- toggleClass: () => {},
- };
-
- expect(togglePopover.call(context, true)).toEqual(true);
- });
-
- it('returns false when popover is already shown', () => {
- const context = {
- hasClass: () => true,
- };
-
- expect(togglePopover.call(context, true)).toEqual(false);
- });
-
- it('shows popover', (done) => {
- const context = {
- hasClass: () => false,
- popover: () => {},
- toggleClass: () => {},
- };
-
- spyOn(context, 'popover').and.callFake((method) => {
- expect(method).toEqual('show');
- done();
- });
-
- togglePopover.call(context, true);
- });
-
- it('adds disable-animation and js-popover-show class', (done) => {
- const context = {
- hasClass: () => false,
- popover: () => {},
- toggleClass: () => {},
- };
-
- spyOn(context, 'toggleClass').and.callFake((classNames, show) => {
- expect(classNames).toEqual('disable-animation js-popover-show');
- expect(show).toEqual(true);
- done();
- });
-
- togglePopover.call(context, true);
- });
- });
-
- describe('togglePopover(false)', () => {
- it('returns true when popover is hidden', () => {
- const context = {
- hasClass: () => true,
- popover: () => {},
- toggleClass: () => {},
- };
-
- expect(togglePopover.call(context, false)).toEqual(true);
- });
-
- it('returns false when popover is already hidden', () => {
- const context = {
- hasClass: () => false,
- };
-
- expect(togglePopover.call(context, false)).toEqual(false);
- });
-
- it('hides popover', (done) => {
- const context = {
- hasClass: () => true,
- popover: () => {},
- toggleClass: () => {},
- };
-
- spyOn(context, 'popover').and.callFake((method) => {
- expect(method).toEqual('hide');
- done();
- });
-
- togglePopover.call(context, false);
- });
-
- it('removes disable-animation and js-popover-show class', (done) => {
- const context = {
- hasClass: () => true,
- popover: () => {},
- toggleClass: () => {},
- };
-
- spyOn(context, 'toggleClass').and.callFake((classNames, show) => {
- expect(classNames).toEqual('disable-animation js-popover-show');
- expect(show).toEqual(false);
- done();
- });
-
- togglePopover.call(context, false);
- });
- });
- });
-
describe('dismiss', () => {
let mock;
const context = {
@@ -163,56 +58,6 @@ describe('feature highlight helper', () => {
});
});
- describe('mouseleave', () => {
- it('calls hide popover if .popover:hover is false', () => {
- const fakeJquery = {
- length: 0,
- };
-
- spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
- spyOn(togglePopover, 'call');
- mouseleave();
- expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), false);
- });
-
- it('does not call hide popover if .popover:hover is true', () => {
- const fakeJquery = {
- length: 1,
- };
-
- spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
- spyOn(togglePopover, 'call');
- mouseleave();
- expect(togglePopover.call).not.toHaveBeenCalledWith(false);
- });
- });
-
- describe('mouseenter', () => {
- const context = {};
-
- it('shows popover', () => {
- spyOn(togglePopover, 'call').and.returnValue(false);
- mouseenter.call(context);
- expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), true);
- });
-
- it('registers mouseleave event if popover is showed', (done) => {
- spyOn(togglePopover, 'call').and.returnValue(true);
- spyOn($.fn, 'on').and.callFake((eventName) => {
- expect(eventName).toEqual('mouseleave');
- done();
- });
- mouseenter.call(context);
- });
-
- it('does not register mouseleave event if popover is not showed', () => {
- spyOn(togglePopover, 'call').and.returnValue(false);
- const spy = spyOn($.fn, 'on').and.callFake(() => {});
- mouseenter.call(context);
- expect(spy).not.toHaveBeenCalled();
- });
- });
-
describe('inserted', () => {
it('registers click event callback', (done) => {
const context = {
diff --git a/spec/javascripts/feature_highlight/feature_highlight_spec.js b/spec/javascripts/feature_highlight/feature_highlight_spec.js
index d2dd39d49d1..ec46d4f905a 100644
--- a/spec/javascripts/feature_highlight/feature_highlight_spec.js
+++ b/spec/javascripts/feature_highlight/feature_highlight_spec.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import * as featureHighlightHelper from '~/feature_highlight/feature_highlight_helper';
import * as featureHighlight from '~/feature_highlight/feature_highlight';
+import * as popover from '~/shared/popover';
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
@@ -29,7 +29,6 @@ describe('feature highlight', () => {
mock = new MockAdapter(axios);
mock.onGet('/test').reply(200);
spyOn(window, 'addEventListener');
- spyOn(window, 'removeEventListener');
featureHighlight.setupFeatureHighlightPopover('test', 0);
});
@@ -45,14 +44,14 @@ describe('feature highlight', () => {
});
it('setup mouseenter', () => {
- const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call');
+ const toggleSpy = spyOn(popover.togglePopover, 'call');
$(selector).trigger('mouseenter');
expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), true);
});
it('setup debounced mouseleave', (done) => {
- const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call');
+ const toggleSpy = spyOn(popover.togglePopover, 'call');
$(selector).trigger('mouseleave');
// Even though we've set the debounce to 0ms, setTimeout is needed for the debounce
@@ -64,12 +63,7 @@ describe('feature highlight', () => {
it('setup show.bs.popover', () => {
$(selector).trigger('show.bs.popover');
- expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function));
- });
-
- it('setup hide.bs.popover', () => {
- $(selector).trigger('hide.bs.popover');
- expect(window.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function));
+ expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), { once: true });
});
it('removes disabled attribute', () => {
@@ -85,7 +79,7 @@ describe('feature highlight', () => {
it('toggles when clicked', () => {
$(selector).trigger('mouseenter');
const popoverId = $(selector).attr('aria-describedby');
- const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call');
+ const toggleSpy = spyOn(popover.togglePopover, 'call');
$(`#${popoverId} .dismiss-feature-highlight`).click();
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index 95d02974bdc..8fcee36beb8 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -1,5 +1,3 @@
-import * as urlUtils from '~/lib/utils/url_utility';
-import * as recentSearchesStoreSrc from '~/filtered_search/stores/recent_searches_store';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
@@ -11,7 +9,7 @@ import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dro
import FilteredSearchManager from '~/filtered_search/filtered_search_manager';
import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
-describe('Filtered Search Manager', () => {
+describe('Filtered Search Manager', function () {
let input;
let manager;
let tokensContainer;
@@ -74,18 +72,19 @@ describe('Filtered Search Manager', () => {
describe('class constructor', () => {
const isLocalStorageAvailable = 'isLocalStorageAvailable';
+ let RecentSearchesStoreSpy;
beforeEach(() => {
spyOn(RecentSearchesService, 'isAvailable').and.returnValue(isLocalStorageAvailable);
- spyOn(recentSearchesStoreSrc, 'default');
spyOn(RecentSearchesRoot.prototype, 'render');
+ RecentSearchesStoreSpy = spyOnDependency(FilteredSearchManager, 'RecentSearchesStore');
});
it('should instantiate RecentSearchesStore with isLocalStorageAvailable', () => {
manager = new FilteredSearchManager({ page });
expect(RecentSearchesService.isAvailable).toHaveBeenCalled();
- expect(recentSearchesStoreSrc.default).toHaveBeenCalledWith({
+ expect(RecentSearchesStoreSpy).toHaveBeenCalledWith({
isLocalStorageAvailable,
allowedKeys: FilteredSearchTokenKeys.getKeys(),
});
@@ -164,7 +163,7 @@ describe('Filtered Search Manager', () => {
it('should search with a single word', (done) => {
input.value = 'searchTerm';
- spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
+ spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=searchTerm`);
done();
});
@@ -175,7 +174,7 @@ describe('Filtered Search Manager', () => {
it('should search with multiple words', (done) => {
input.value = 'awesome search terms';
- spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
+ spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
done();
});
@@ -186,7 +185,7 @@ describe('Filtered Search Manager', () => {
it('should search with special characters', (done) => {
input.value = '~!@#$%^&*()_+{}:<>,.?/';
- spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
+ spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
done();
});
@@ -200,7 +199,7 @@ describe('Filtered Search Manager', () => {
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
`);
- spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
+ spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&label_name[]=bug`);
done();
});
diff --git a/spec/javascripts/filtered_search/recent_searches_root_spec.js b/spec/javascripts/filtered_search/recent_searches_root_spec.js
index d8ba6de5f45..1e6272bad0b 100644
--- a/spec/javascripts/filtered_search/recent_searches_root_spec.js
+++ b/spec/javascripts/filtered_search/recent_searches_root_spec.js
@@ -1,11 +1,11 @@
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
-import * as vueSrc from 'vue';
describe('RecentSearchesRoot', () => {
describe('render', () => {
let recentSearchesRoot;
let data;
let template;
+ let VueSpy;
beforeEach(() => {
recentSearchesRoot = {
@@ -14,7 +14,7 @@ describe('RecentSearchesRoot', () => {
},
};
- spyOn(vueSrc, 'default').and.callFake((options) => {
+ VueSpy = spyOnDependency(RecentSearchesRoot, 'Vue').and.callFake((options) => {
data = options.data;
template = options.template;
});
@@ -23,7 +23,7 @@ describe('RecentSearchesRoot', () => {
});
it('should instantiate Vue', () => {
- expect(vueSrc.default).toHaveBeenCalled();
+ expect(VueSpy).toHaveBeenCalled();
expect(data()).toBe(recentSearchesRoot.store.state);
expect(template).toContain(':is-local-storage-available="isLocalStorageAvailable"');
});
diff --git a/spec/javascripts/fixtures/linked_tabs.html.haml b/spec/javascripts/fixtures/linked_tabs.html.haml
index 93c0cf97ff0..c38fe8b1f25 100644
--- a/spec/javascripts/fixtures/linked_tabs.html.haml
+++ b/spec/javascripts/fixtures/linked_tabs.html.haml
@@ -1,4 +1,4 @@
-%ul.nav.nav-tabs.linked-tabs
+%ul.nav-links.new-session-tabs.linked-tabs
%li
%a{ href: 'foo/bar/1', data: { target: 'div#tab1', action: 'tab1', toggle: 'tab' } }
Tab 1
diff --git a/spec/javascripts/fixtures/signin_tabs.html.haml b/spec/javascripts/fixtures/signin_tabs.html.haml
index 12b8d423cbe..2e00fe7865e 100644
--- a/spec/javascripts/fixtures/signin_tabs.html.haml
+++ b/spec/javascripts/fixtures/signin_tabs.html.haml
@@ -1,5 +1,5 @@
-%ul.nav-tabs
+%ul.nav-links.new-session-tabs
+ %li.active
+ %a{ href: '#ldap' } LDAP
%li
- %a.active{ id: 'standard', href: '#standard'} Standard
- %li
- %a{ id: 'ldap', href: '#ldap'} Ldap
+ %a{ href: '#login-pane'} Standard
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index 5393502196e..7f9c4811fba 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -1,9 +1,8 @@
/* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */
import $ from 'jquery';
-import '~/gl_dropdown';
+import GLDropdown from '~/gl_dropdown';
import '~/lib/utils/common_utils';
-import * as urlUtils from '~/lib/utils/url_utility';
describe('glDropdown', function describeDropdown() {
preloadFixtures('static/gl_dropdown.html.raw');
@@ -138,13 +137,13 @@ describe('glDropdown', function describeDropdown() {
expect(this.dropdownContainerElement).toHaveClass('open');
const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
navigateWithKeys('down', randomIndex, () => {
- spyOn(urlUtils, 'visitUrl').and.stub();
+ const visitUrl = spyOnDependency(GLDropdown, 'visitUrl').and.stub();
navigateWithKeys('enter', null, () => {
expect(this.dropdownContainerElement).not.toHaveClass('open');
const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
expect(link).toHaveClass('is-active');
const linkedLocation = link.attr('href');
- if (linkedLocation && linkedLocation !== '#') expect(urlUtils.visitUrl).toHaveBeenCalledWith(linkedLocation);
+ if (linkedLocation && linkedLocation !== '#') expect(visitUrl).toHaveBeenCalledWith(linkedLocation);
});
});
});
diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js
index d8428bd0e08..2b92c485f41 100644
--- a/spec/javascripts/groups/components/app_spec.js
+++ b/spec/javascripts/groups/components/app_spec.js
@@ -1,7 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
-import * as utils from '~/lib/utils/url_utility';
import appComponent from '~/groups/components/app.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue';
@@ -177,7 +176,7 @@ describe('AppComponent', () => {
it('should fetch groups for provided page details and update window state', (done) => {
spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups));
spyOn(vm, 'updateGroups').and.callThrough();
- spyOn(utils, 'mergeUrlParams').and.callThrough();
+ const mergeUrlParams = spyOnDependency(appComponent, 'mergeUrlParams').and.callThrough();
spyOn(window.history, 'replaceState');
spyOn($, 'scrollTo');
@@ -193,7 +192,7 @@ describe('AppComponent', () => {
setTimeout(() => {
expect(vm.isLoading).toBe(false);
expect($.scrollTo).toHaveBeenCalledWith(0);
- expect(utils.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String));
+ expect(mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String));
expect(window.history.replaceState).toHaveBeenCalledWith({
page: jasmine.any(String),
}, jasmine.any(String), jasmine.any(String));
diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js
index e3c942597a3..49a139855c8 100644
--- a/spec/javascripts/groups/components/group_item_spec.js
+++ b/spec/javascripts/groups/components/group_item_spec.js
@@ -1,5 +1,4 @@
import Vue from 'vue';
-import * as urlUtils from '~/lib/utils/url_utility';
import groupItemComponent from '~/groups/components/group_item.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import eventHub from '~/groups/event_hub';
@@ -135,13 +134,13 @@ describe('GroupItemComponent', () => {
const group = Object.assign({}, mockParentGroupItem);
group.childrenCount = 0;
const newVm = createComponent(group);
- spyOn(urlUtils, 'visitUrl').and.stub();
+ const visitUrl = spyOnDependency(groupItemComponent, 'visitUrl').and.stub();
spyOn(eventHub, '$emit');
newVm.onClickRowGroup(event);
setTimeout(() => {
expect(eventHub.$emit).not.toHaveBeenCalled();
- expect(urlUtils.visitUrl).toHaveBeenCalledWith(newVm.group.relativePath);
+ expect(visitUrl).toHaveBeenCalledWith(newVm.group.relativePath);
done();
}, 0);
});
diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js b/spec/javascripts/helpers/class_spec_helper_spec.js
index 1415ffb7eb3..fa104ae5bcd 100644
--- a/spec/javascripts/helpers/class_spec_helper_spec.js
+++ b/spec/javascripts/helpers/class_spec_helper_spec.js
@@ -2,7 +2,7 @@
import './class_spec_helper';
-describe('ClassSpecHelper', () => {
+describe('ClassSpecHelper', function () {
describe('itShouldBeAStaticMethod', () => {
beforeEach(() => {
class TestClass {
diff --git a/spec/javascripts/helpers/vue_component_helper.js b/spec/javascripts/helpers/vue_component_helper.js
index 257c9f5526a..e0fe18e5560 100644
--- a/spec/javascripts/helpers/vue_component_helper.js
+++ b/spec/javascripts/helpers/vue_component_helper.js
@@ -1,3 +1,18 @@
-export default function removeBreakLine (data) {
- return data.replace(/\r?\n|\r/g, ' ');
-}
+/**
+ * Replaces line break with an empty space
+ * @param {*} data
+ */
+export const removeBreakLine = data => data.replace(/\r?\n|\r/g, ' ');
+
+/**
+ * Removes line breaks, spaces and trims the given text
+ * @param {String} str
+ * @returns {String}
+ */
+export const trimText = str =>
+ str
+ .replace(/\r?\n|\r/g, '')
+ .replace(/\s\s+/g, ' ')
+ .trim();
+
+export const removeWhitespace = str => str.replace(/\s\s+/g, ' ');
diff --git a/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js
new file mode 100644
index 00000000000..b80d08de7b1
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js
@@ -0,0 +1,95 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import emptyState from '~/ide/components/commit_sidebar/empty_state.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { resetStore } from '../../helpers';
+
+describe('IDE commit panel empty state', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(emptyState);
+
+ vm = createComponentWithStore(Component, store, {
+ noChangesStateSvgPath: 'no-changes',
+ committedStateSvgPath: 'committed-state',
+ });
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ describe('statusSvg', () => {
+ it('uses noChangesStateSvgPath when commit message is empty', () => {
+ expect(vm.statusSvg).toBe('no-changes');
+ expect(vm.$el.querySelector('img').getAttribute('src')).toBe(
+ 'no-changes',
+ );
+ });
+
+ it('uses committedStateSvgPath when commit message exists', done => {
+ vm.$store.state.lastCommitMsg = 'testing';
+
+ Vue.nextTick(() => {
+ expect(vm.statusSvg).toBe('committed-state');
+ expect(vm.$el.querySelector('img').getAttribute('src')).toBe(
+ 'committed-state',
+ );
+
+ done();
+ });
+ });
+ });
+
+ it('renders no changes text when last commit message is empty', () => {
+ expect(vm.$el.textContent).toContain('No changes');
+ });
+
+ it('renders last commit message when it exists', done => {
+ vm.$store.state.lastCommitMsg = 'testing commit message';
+
+ Vue.nextTick(() => {
+ expect(vm.$el.textContent).toContain('testing commit message');
+
+ done();
+ });
+ });
+
+ describe('toggle button', () => {
+ it('calls store action', () => {
+ spyOn(vm, 'toggleRightPanelCollapsed');
+
+ vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
+
+ expect(vm.toggleRightPanelCollapsed).toHaveBeenCalled();
+ });
+
+ it('renders collapsed class', done => {
+ vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
+
+ done();
+ });
+ });
+ });
+
+ describe('collapsed state', () => {
+ beforeEach(done => {
+ vm.$store.state.rightPanelCollapsed = true;
+
+ Vue.nextTick(done);
+ });
+
+ it('does not render text & svg', () => {
+ expect(vm.$el.querySelector('img')).toBeNull();
+ expect(vm.$el.textContent).not.toContain('No changes');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
index 5b402886b55..9af3c15a4e3 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
@@ -3,6 +3,7 @@ import store from '~/ide/stores';
import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file } from '../../helpers';
+import { removeWhitespace } from '../../../helpers/vue_component_helper';
describe('Multi-file editor commit sidebar list collapsed', () => {
let vm;
@@ -10,10 +11,17 @@ describe('Multi-file editor commit sidebar list collapsed', () => {
beforeEach(() => {
const Component = Vue.extend(listCollapsed);
- vm = createComponentWithStore(Component, store);
-
- vm.$store.state.changedFiles.push(file('file1'), file('file2'));
- vm.$store.state.changedFiles[0].tempFile = true;
+ vm = createComponentWithStore(Component, store, {
+ files: [
+ {
+ ...file('file1'),
+ tempFile: true,
+ },
+ file('file2'),
+ ],
+ iconName: 'staged',
+ title: 'Staged',
+ });
vm.$mount();
});
@@ -23,6 +31,42 @@ describe('Multi-file editor commit sidebar list collapsed', () => {
});
it('renders added & modified files count', () => {
- expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toBe('1 1');
+ expect(removeWhitespace(vm.$el.textContent).trim()).toBe('1 1');
+ });
+
+ describe('addedFilesLength', () => {
+ it('returns an length of temp files', () => {
+ expect(vm.addedFilesLength).toBe(1);
+ });
+ });
+
+ describe('modifiedFilesLength', () => {
+ it('returns an length of modified files', () => {
+ expect(vm.modifiedFilesLength).toBe(1);
+ });
+ });
+
+ describe('addedFilesIconClass', () => {
+ it('includes multi-file-addition when addedFiles is not empty', () => {
+ expect(vm.addedFilesIconClass).toContain('multi-file-addition');
+ });
+
+ it('excludes multi-file-addition when addedFiles is empty', () => {
+ vm.files = [];
+
+ expect(vm.addedFilesIconClass).not.toContain('multi-file-addition');
+ });
+ });
+
+ describe('modifiedFilesClass', () => {
+ it('includes multi-file-modified when addedFiles is not empty', () => {
+ expect(vm.modifiedFilesClass).toContain('multi-file-modified');
+ });
+
+ it('excludes multi-file-modified when addedFiles is empty', () => {
+ vm.files = [];
+
+ expect(vm.modifiedFilesClass).not.toContain('multi-file-modified');
+ });
});
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
index 509434e4300..cc7e0a3f26d 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
+import store from '~/ide/stores';
import listItem from '~/ide/components/commit_sidebar/list_item.vue';
import router from '~/ide/ide_router';
-import store from '~/ide/stores';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file, resetStore } from '../../helpers';
@@ -18,6 +18,7 @@ describe('Multi-file editor commit sidebar list item', () => {
vm = createComponentWithStore(Component, store, {
file: f,
+ actionComponent: 'stage-button',
}).$mount();
});
@@ -31,22 +32,18 @@ describe('Multi-file editor commit sidebar list item', () => {
expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path);
});
- it('calls discardFileChanges when clicking discard button', () => {
- spyOn(vm, 'discardFileChanges');
-
- vm.$el.querySelector('.multi-file-discard-btn').click();
-
- expect(vm.discardFileChanges).toHaveBeenCalled();
+ it('renders actionn button', () => {
+ expect(vm.$el.querySelector('.multi-file-discard-btn')).not.toBeNull();
});
it('opens a closed file in the editor when clicking the file path', done => {
- spyOn(vm, 'openFileInEditor').and.callThrough();
+ spyOn(vm, 'openPendingTab').and.callThrough();
spyOn(router, 'push');
vm.$el.querySelector('.multi-file-commit-list-path').click();
setTimeout(() => {
- expect(vm.openFileInEditor).toHaveBeenCalled();
+ expect(vm.openPendingTab).toHaveBeenCalled();
expect(router.push).toHaveBeenCalled();
done();
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_spec.js
index a62c0a28340..62fc3f90ad1 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import store from '~/ide/stores';
import commitSidebarList from '~/ide/components/commit_sidebar/list.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import { file } from '../../helpers';
+import { file, resetStore } from '../../helpers';
describe('Multi-file editor commit sidebar list', () => {
let vm;
@@ -13,6 +13,10 @@ describe('Multi-file editor commit sidebar list', () => {
vm = createComponentWithStore(Component, store, {
title: 'Staged',
fileList: [],
+ iconName: 'staged',
+ action: 'stageAllChanges',
+ actionBtnText: 'stage all',
+ itemActionComponent: 'stage-button',
});
vm.$store.state.rightPanelCollapsed = false;
@@ -22,6 +26,8 @@ describe('Multi-file editor commit sidebar list', () => {
afterEach(() => {
vm.$destroy();
+
+ resetStore(vm.$store);
});
describe('with a list of files', () => {
@@ -38,6 +44,12 @@ describe('Multi-file editor commit sidebar list', () => {
});
});
+ describe('empty files array', () => {
+ it('renders no changes text when empty', () => {
+ expect(vm.$el.textContent).toContain('No changes');
+ });
+ });
+
describe('collapsed', () => {
beforeEach(done => {
vm.$store.state.rightPanelCollapsed = true;
@@ -50,4 +62,32 @@ describe('Multi-file editor commit sidebar list', () => {
expect(vm.$el.querySelector('.help-block')).toBeNull();
});
});
+
+ describe('with toggle', () => {
+ beforeEach(done => {
+ spyOn(vm, 'toggleRightPanelCollapsed');
+
+ vm.showToggle = true;
+
+ Vue.nextTick(done);
+ });
+
+ it('calls setPanelCollapsedStatus when clickin toggle', () => {
+ vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
+
+ expect(vm.toggleRightPanelCollapsed).toHaveBeenCalled();
+ });
+ });
+
+ describe('action button', () => {
+ beforeEach(() => {
+ spyOn(vm, 'stageAllChanges');
+ });
+
+ it('calls store action when clicked', () => {
+ vm.$el.querySelector('.ide-staged-action-btn').click();
+
+ expect(vm.stageAllChanges).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js b/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js
new file mode 100644
index 00000000000..d62d58101d6
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js
@@ -0,0 +1,174 @@
+import Vue from 'vue';
+import CommitMessageField from '~/ide/components/commit_sidebar/message_field.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('IDE commit message field', () => {
+ const Component = Vue.extend(CommitMessageField);
+ let vm;
+
+ beforeEach(() => {
+ setFixtures('<div id="app"></div>');
+
+ vm = createComponent(
+ Component,
+ {
+ text: '',
+ },
+ '#app',
+ );
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('adds is-focused class on focus', done => {
+ vm.$el.querySelector('textarea').focus();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.is-focused')).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('removed is-focused class on blur', done => {
+ vm.$el.querySelector('textarea').focus();
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.is-focused')).not.toBeNull();
+
+ vm.$el.querySelector('textarea').blur();
+
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(vm.$el.querySelector('.is-focused')).toBeNull();
+
+ done();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('emits input event on input', () => {
+ spyOn(vm, '$emit');
+
+ const textarea = vm.$el.querySelector('textarea');
+ textarea.value = 'testing';
+
+ textarea.dispatchEvent(new Event('input'));
+
+ expect(vm.$emit).toHaveBeenCalledWith('input', 'testing');
+ });
+
+ describe('highlights', () => {
+ describe('subject line', () => {
+ it('does not highlight less than 50 characters', done => {
+ vm.text = 'text less than 50 chars';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.highlights span').textContent).toContain(
+ 'text less than 50 chars',
+ );
+ expect(vm.$el.querySelector('mark').style.display).toBe('none');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('highlights characters over 50 length', done => {
+ vm.text =
+ 'text less than 50 chars that should not highlighted. text more than 50 should be highlighted';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.highlights span').textContent).toContain(
+ 'text less than 50 chars that should not highlighte',
+ );
+ expect(vm.$el.querySelector('mark').style.display).not.toBe('none');
+ expect(vm.$el.querySelector('mark').textContent).toBe(
+ 'd. text more than 50 should be highlighted',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('body text', () => {
+ it('does not highlight body text less tan 72 characters', done => {
+ vm.text = 'subject line\nbody content';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
+ expect(vm.$el.querySelectorAll('mark')[1].style.display).toBe('none');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('highlights body text more than 72 characters', done => {
+ vm.text =
+ 'subject line\nbody content that will be highlighted when it is more than 72 characters in length';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
+ expect(vm.$el.querySelectorAll('mark')[1].style.display).not.toBe('none');
+ expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('highlights body text & subject line', done => {
+ vm.text =
+ 'text less than 50 chars that should not highlighted\nbody content that will be highlighted when it is more than 72 characters in length';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
+ expect(vm.$el.querySelectorAll('mark').length).toBe(2);
+
+ expect(vm.$el.querySelectorAll('mark')[0].textContent).toContain('d');
+ expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('scrolling textarea', () => {
+ it('updates transform of highlights', done => {
+ vm.text = 'subject line\n\n\n\n\n\n\n\n\n\n\nbody content';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ vm.$el.querySelector('textarea').scrollTo(0, 50);
+
+ vm.handleScroll();
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.scrollTop).toBe(50);
+ expect(vm.$el.querySelector('.highlights').style.transform).toBe(
+ 'translate3d(0px, -50px, 0px)',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
index 4e8243439f3..21bfe4be52f 100644
--- a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
@@ -69,19 +69,6 @@ describe('IDE commit sidebar radio group', () => {
});
});
- it('renders helpText tooltip', done => {
- vm.helpText = 'help text';
-
- Vue.nextTick(() => {
- const help = vm.$el.querySelector('.help-block');
-
- expect(help).not.toBeNull();
- expect(help.getAttribute('data-original-title')).toBe('help text');
-
- done();
- });
- });
-
describe('with input', () => {
beforeEach(done => {
vm.$destroy();
diff --git a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js
new file mode 100644
index 00000000000..6bf8710bda7
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js
@@ -0,0 +1,46 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import stageButton from '~/ide/components/commit_sidebar/stage_button.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { file, resetStore } from '../../helpers';
+
+describe('IDE stage file button', () => {
+ let vm;
+ let f;
+
+ beforeEach(() => {
+ const Component = Vue.extend(stageButton);
+ f = file();
+
+ vm = createComponentWithStore(Component, store, {
+ path: f.path,
+ });
+
+ spyOn(vm, 'stageChange');
+ spyOn(vm, 'discardFileChanges');
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders button to discard & stage', () => {
+ expect(vm.$el.querySelectorAll('.btn').length).toBe(2);
+ });
+
+ it('calls store with stage button', () => {
+ vm.$el.querySelectorAll('.btn')[0].click();
+
+ expect(vm.stageChange).toHaveBeenCalledWith(f.path);
+ });
+
+ it('calls store with discard button', () => {
+ vm.$el.querySelectorAll('.btn')[1].click();
+
+ expect(vm.discardFileChanges).toHaveBeenCalledWith(f.path);
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js
new file mode 100644
index 00000000000..917bbb9fb46
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import unstageButton from '~/ide/components/commit_sidebar/unstage_button.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { file, resetStore } from '../../helpers';
+
+describe('IDE unstage file button', () => {
+ let vm;
+ let f;
+
+ beforeEach(() => {
+ const Component = Vue.extend(unstageButton);
+ f = file();
+
+ vm = createComponentWithStore(Component, store, {
+ path: f.path,
+ });
+
+ spyOn(vm, 'unstageChange');
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders button to unstage', () => {
+ expect(vm.$el.querySelectorAll('.btn').length).toBe(1);
+ });
+
+ it('calls store with unnstage button', () => {
+ vm.$el.querySelector('.btn').click();
+
+ expect(vm.unstageChange).toHaveBeenCalledWith(f.path);
+ });
+});
diff --git a/spec/javascripts/ide/components/file_finder/index_spec.js b/spec/javascripts/ide/components/file_finder/index_spec.js
new file mode 100644
index 00000000000..4f208e946d2
--- /dev/null
+++ b/spec/javascripts/ide/components/file_finder/index_spec.js
@@ -0,0 +1,308 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import FindFileComponent from '~/ide/components/file_finder/index.vue';
+import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
+import router from '~/ide/ide_router';
+import { file, resetStore } from '../../helpers';
+import { mountComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+
+describe('IDE File finder item spec', () => {
+ const Component = Vue.extend(FindFileComponent);
+ let vm;
+
+ beforeEach(done => {
+ setFixtures('<div id="app"></div>');
+
+ vm = mountComponentWithStore(Component, {
+ store,
+ el: '#app',
+ props: {
+ index: 0,
+ },
+ });
+
+ setTimeout(done);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ describe('with entries', () => {
+ beforeEach(done => {
+ Vue.set(vm.$store.state.entries, 'folder', {
+ ...file('folder'),
+ path: 'folder',
+ type: 'folder',
+ });
+
+ Vue.set(vm.$store.state.entries, 'index.js', {
+ ...file('index.js'),
+ path: 'index.js',
+ type: 'blob',
+ url: '/index.jsurl',
+ });
+
+ Vue.set(vm.$store.state.entries, 'component.js', {
+ ...file('component.js'),
+ path: 'component.js',
+ type: 'blob',
+ });
+
+ setTimeout(done);
+ });
+
+ it('renders list of blobs', () => {
+ expect(vm.$el.textContent).toContain('index.js');
+ expect(vm.$el.textContent).toContain('component.js');
+ expect(vm.$el.textContent).not.toContain('folder');
+ });
+
+ it('filters entries', done => {
+ vm.searchText = 'index';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.textContent).toContain('index.js');
+ expect(vm.$el.textContent).not.toContain('component.js');
+
+ done();
+ });
+ });
+
+ it('shows clear button when searchText is not empty', done => {
+ vm.searchText = 'index';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.dropdown-input-clear').classList).toContain('show');
+ expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden');
+
+ done();
+ });
+ });
+
+ it('clear button resets searchText', done => {
+ vm.searchText = 'index';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ vm.$el.querySelector('.dropdown-input-clear').click();
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.searchText).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('clear button focues search input', done => {
+ spyOn(vm.$refs.searchInput, 'focus');
+ vm.searchText = 'index';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ vm.$el.querySelector('.dropdown-input-clear').click();
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.$refs.searchInput.focus).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ describe('listShowCount', () => {
+ it('returns 1 when no filtered entries exist', done => {
+ vm.searchText = 'testing 123';
+
+ vm.$nextTick(() => {
+ expect(vm.listShowCount).toBe(1);
+
+ done();
+ });
+ });
+
+ it('returns entries length when not filtered', () => {
+ expect(vm.listShowCount).toBe(2);
+ });
+ });
+
+ describe('listHeight', () => {
+ it('returns 55 when entries exist', () => {
+ expect(vm.listHeight).toBe(55);
+ });
+
+ it('returns 33 when entries dont exist', done => {
+ vm.searchText = 'testing 123';
+
+ vm.$nextTick(() => {
+ expect(vm.listHeight).toBe(33);
+
+ done();
+ });
+ });
+ });
+
+ describe('filteredBlobsLength', () => {
+ it('returns length of filtered blobs', done => {
+ vm.searchText = 'index';
+
+ vm.$nextTick(() => {
+ expect(vm.filteredBlobsLength).toBe(1);
+
+ done();
+ });
+ });
+ });
+
+ describe('watches', () => {
+ describe('searchText', () => {
+ it('resets focusedIndex when updated', done => {
+ vm.focusedIndex = 1;
+ vm.searchText = 'test';
+
+ vm.$nextTick(() => {
+ expect(vm.focusedIndex).toBe(0);
+
+ done();
+ });
+ });
+ });
+
+ describe('fileFindVisible', () => {
+ it('returns searchText when false', done => {
+ vm.searchText = 'test';
+ vm.$store.state.fileFindVisible = true;
+
+ vm
+ .$nextTick()
+ .then(() => {
+ vm.$store.state.fileFindVisible = false;
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.searchText).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('openFile', () => {
+ beforeEach(() => {
+ spyOn(router, 'push');
+ spyOn(vm, 'toggleFileFinder');
+ });
+
+ it('closes file finder', () => {
+ vm.openFile(vm.$store.state.entries['index.js']);
+
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+ });
+
+ it('pushes to router', () => {
+ vm.openFile(vm.$store.state.entries['index.js']);
+
+ expect(router.push).toHaveBeenCalledWith('/project/index.jsurl');
+ });
+ });
+
+ describe('onKeyup', () => {
+ it('opens file on enter key', done => {
+ const event = new CustomEvent('keyup');
+ event.keyCode = ENTER_KEY_CODE;
+
+ spyOn(vm, 'openFile');
+
+ vm.$refs.searchInput.dispatchEvent(event);
+
+ vm.$nextTick(() => {
+ expect(vm.openFile).toHaveBeenCalledWith(vm.$store.state.entries['index.js']);
+
+ done();
+ });
+ });
+
+ it('closes file finder on esc key', done => {
+ const event = new CustomEvent('keyup');
+ event.keyCode = ESC_KEY_CODE;
+
+ spyOn(vm, 'toggleFileFinder');
+
+ vm.$refs.searchInput.dispatchEvent(event);
+
+ vm.$nextTick(() => {
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
+
+ describe('onKeyDown', () => {
+ let el;
+
+ beforeEach(() => {
+ el = vm.$refs.searchInput;
+ });
+
+ describe('up key', () => {
+ const event = new CustomEvent('keydown');
+ event.keyCode = UP_KEY_CODE;
+
+ it('resets to last index when at top', () => {
+ el.dispatchEvent(event);
+
+ expect(vm.focusedIndex).toBe(1);
+ });
+
+ it('minus 1 from focusedIndex', () => {
+ vm.focusedIndex = 1;
+
+ el.dispatchEvent(event);
+
+ expect(vm.focusedIndex).toBe(0);
+ });
+ });
+
+ describe('down key', () => {
+ const event = new CustomEvent('keydown');
+ event.keyCode = DOWN_KEY_CODE;
+
+ it('resets to first index when at bottom', () => {
+ vm.focusedIndex = 1;
+ el.dispatchEvent(event);
+
+ expect(vm.focusedIndex).toBe(0);
+ });
+
+ it('adds 1 to focusedIndex', () => {
+ el.dispatchEvent(event);
+
+ expect(vm.focusedIndex).toBe(1);
+ });
+ });
+ });
+ });
+
+ describe('without entries', () => {
+ it('renders loading text when loading', done => {
+ store.state.loading = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.textContent).toContain('Loading...');
+
+ done();
+ });
+ });
+
+ it('renders no files text', () => {
+ expect(vm.$el.textContent).toContain('No files found.');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/file_finder/item_spec.js b/spec/javascripts/ide/components/file_finder/item_spec.js
new file mode 100644
index 00000000000..0f1116c6912
--- /dev/null
+++ b/spec/javascripts/ide/components/file_finder/item_spec.js
@@ -0,0 +1,140 @@
+import Vue from 'vue';
+import ItemComponent from '~/ide/components/file_finder/item.vue';
+import { file } from '../../helpers';
+import createComponent from '../../../helpers/vue_mount_component_helper';
+
+describe('IDE File finder item spec', () => {
+ const Component = Vue.extend(ItemComponent);
+ let vm;
+ let localFile;
+
+ beforeEach(() => {
+ localFile = {
+ ...file(),
+ name: 'test file',
+ path: 'test/file',
+ };
+
+ vm = createComponent(Component, {
+ file: localFile,
+ focused: true,
+ searchText: '',
+ index: 0,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders file name & path', () => {
+ expect(vm.$el.textContent).toContain('test file');
+ expect(vm.$el.textContent).toContain('test/file');
+ });
+
+ describe('focused', () => {
+ it('adds is-focused class', () => {
+ expect(vm.$el.classList).toContain('is-focused');
+ });
+
+ it('does not have is-focused class when not focused', done => {
+ vm.focused = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.classList).not.toContain('is-focused');
+
+ done();
+ });
+ });
+ });
+
+ describe('changed file icon', () => {
+ it('does not render when not a changed or temp file', () => {
+ expect(vm.$el.querySelector('.diff-changed-stats')).toBe(null);
+ });
+
+ it('renders when a changed file', done => {
+ vm.file.changed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
+
+ done();
+ });
+ });
+
+ it('renders when a temp file', done => {
+ vm.file.tempFile = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
+
+ done();
+ });
+ });
+ });
+
+ it('emits event when clicked', () => {
+ spyOn(vm, '$emit');
+
+ vm.$el.click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('click', vm.file);
+ });
+
+ describe('path', () => {
+ let el;
+
+ beforeEach(done => {
+ vm.searchText = 'file';
+
+ el = vm.$el.querySelector('.diff-changed-file-path');
+
+ vm.$nextTick(done);
+ });
+
+ it('highlights text', () => {
+ expect(el.querySelectorAll('.highlighted').length).toBe(4);
+ });
+
+ it('adds ellipsis to long text', done => {
+ vm.file.path = new Array(70)
+ .fill()
+ .map((_, i) => `${i}-`)
+ .join('');
+
+ vm.$nextTick(() => {
+ expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`);
+ done();
+ });
+ });
+ });
+
+ describe('name', () => {
+ let el;
+
+ beforeEach(done => {
+ vm.searchText = 'file';
+
+ el = vm.$el.querySelector('.diff-changed-file-name');
+
+ vm.$nextTick(done);
+ });
+
+ it('highlights text', () => {
+ expect(el.querySelectorAll('.highlighted').length).toBe(4);
+ });
+
+ it('does not add ellipsis to long text', done => {
+ vm.file.name = new Array(70)
+ .fill()
+ .map((_, i) => `${i}-`)
+ .join('');
+
+ vm.$nextTick(() => {
+ expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js
index 5bd890094cc..7bfcfc90572 100644
--- a/spec/javascripts/ide/components/ide_spec.js
+++ b/spec/javascripts/ide/components/ide_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import Mousetrap from 'mousetrap';
import store from '~/ide/stores';
import ide from '~/ide/components/ide.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
@@ -38,4 +39,68 @@ describe('ide component', () => {
done();
});
});
+
+ describe('file finder', () => {
+ beforeEach(done => {
+ spyOn(vm, 'toggleFileFinder');
+
+ vm.$store.state.fileFindVisible = true;
+
+ vm.$nextTick(done);
+ });
+
+ it('calls toggleFileFinder on `t` key press', done => {
+ Mousetrap.trigger('t');
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('calls toggleFileFinder on `command+p` key press', done => {
+ Mousetrap.trigger('command+p');
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('calls toggleFileFinder on `ctrl+p` key press', done => {
+ Mousetrap.trigger('ctrl+p');
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('always allows `command+p` to trigger toggleFileFinder', () => {
+ expect(
+ vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'command+p'),
+ ).toBe(false);
+ });
+
+ it('always allows `ctrl+p` to trigger toggleFileFinder', () => {
+ expect(
+ vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'ctrl+p'),
+ ).toBe(false);
+ });
+
+ it('onlys handles `t` when focused in input-field', () => {
+ expect(
+ vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'),
+ ).toBe(true);
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js
index 113ade269e9..768f6e99bf1 100644
--- a/spec/javascripts/ide/components/repo_commit_section_spec.js
+++ b/spec/javascripts/ide/components/repo_commit_section_spec.js
@@ -28,16 +28,34 @@ describe('RepoCommitSection', () => {
},
};
+ const files = [file('file1'), file('file2')].map(f =>
+ Object.assign(f, {
+ type: 'blob',
+ }),
+ );
+
vm.$store.state.rightPanelCollapsed = false;
vm.$store.state.currentBranch = 'master';
- vm.$store.state.changedFiles = [file('file1'), file('file2')];
+ vm.$store.state.changedFiles = [...files];
vm.$store.state.changedFiles.forEach(f =>
Object.assign(f, {
changed: true,
+ content: 'changedFile testing',
+ }),
+ );
+
+ vm.$store.state.stagedFiles = [{ ...files[0] }, { ...files[1] }];
+ vm.$store.state.stagedFiles.forEach(f =>
+ Object.assign(f, {
+ changed: true,
content: 'testing',
}),
);
+ vm.$store.state.changedFiles.forEach(f => {
+ vm.$store.state.entries[f.path] = f;
+ });
+
return vm.$mount();
}
@@ -94,20 +112,93 @@ describe('RepoCommitSection', () => {
...vm.$el.querySelectorAll('.multi-file-commit-list li'),
];
const submitCommit = vm.$el.querySelector('form .btn');
+ const allFiles = vm.$store.state.changedFiles.concat(
+ vm.$store.state.stagedFiles,
+ );
expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull();
- expect(changedFileElements.length).toEqual(2);
+ expect(changedFileElements.length).toEqual(4);
changedFileElements.forEach((changedFile, i) => {
- expect(changedFile.textContent.trim()).toContain(
- vm.$store.state.changedFiles[i].path,
- );
+ expect(changedFile.textContent.trim()).toContain(allFiles[i].path);
});
expect(submitCommit.disabled).toBeTruthy();
expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull();
});
+ it('adds changed files into staged files', done => {
+ vm.$el.querySelector('.ide-staged-action-btn').click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.ide-commit-list-container').textContent,
+ ).toContain('No changes');
+
+ done();
+ });
+ });
+
+ it('stages a single file', done => {
+ vm.$el.querySelector('.multi-file-discard-btn .btn').click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el
+ .querySelector('.ide-commit-list-container')
+ .querySelectorAll('li').length,
+ ).toBe(1);
+
+ done();
+ });
+ });
+
+ it('discards a single file', done => {
+ vm.$el.querySelectorAll('.multi-file-discard-btn .btn')[1].click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.ide-commit-list-container').textContent,
+ ).not.toContain('file1');
+ expect(
+ vm.$el
+ .querySelector('.ide-commit-list-container')
+ .querySelectorAll('li').length,
+ ).toBe(1);
+
+ done();
+ });
+ });
+
+ it('removes all staged files', done => {
+ vm.$el.querySelectorAll('.ide-staged-action-btn')[1].click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelectorAll('.ide-commit-list-container')[1].textContent,
+ ).toContain('No changes');
+
+ done();
+ });
+ });
+
+ it('unstages a single file', done => {
+ vm.$el
+ .querySelectorAll('.multi-file-discard-btn')[2]
+ .querySelector('.btn')
+ .click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el
+ .querySelectorAll('.ide-commit-list-container')[1]
+ .querySelectorAll('li').length,
+ ).toBe(1);
+
+ done();
+ });
+ });
+
it('updates commitMessage in store on input', done => {
const textarea = vm.$el.querySelector('textarea');
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
index 63a3d2c6cd5..b06a6c62a1c 100644
--- a/spec/javascripts/ide/components/repo_editor_spec.js
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -1,9 +1,12 @@
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
import repoEditor from '~/ide/components/repo_editor.vue';
import monacoLoader from '~/ide/monaco_loader';
import Editor from '~/ide/lib/editor';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
import { file, resetStore } from '../helpers';
describe('RepoEditor', () => {
@@ -35,7 +38,7 @@ describe('RepoEditor', () => {
resetStore(vm.$store);
- Editor.editorInstance.modelManager.dispose();
+ Editor.editorInstance.dispose();
});
it('renders an ide container', done => {
@@ -79,16 +82,30 @@ describe('RepoEditor', () => {
});
describe('when file is markdown and viewer mode is review', () => {
+ let mock;
+
beforeEach(done => {
+ mock = new MockAdapter(axios);
+
+ vm.file.projectId = 'namespace/project';
vm.file.previewMode = {
id: 'markdown',
previewTitle: 'Preview Markdown',
};
+ vm.file.content = 'testing 123';
vm.$store.state.viewer = 'diff';
+ mock.onPost(/(.*)\/preview_markdown/).reply(200, {
+ body: '<p>testing 123</p>',
+ });
+
vm.$nextTick(done);
});
+ afterEach(() => {
+ mock.restore();
+ });
+
it('renders an Edit and a Preview Tab', done => {
Vue.nextTick(() => {
const tabs = vm.$el.querySelectorAll('.ide-mode-tabs .nav-links li');
@@ -99,6 +116,26 @@ describe('RepoEditor', () => {
done();
});
});
+
+ it('renders markdown for tempFile', done => {
+ vm.file.tempFile = true;
+ vm.file.path = `${vm.file.path}.md`;
+ vm.$store.state.entries[vm.file.path] = vm.file;
+
+ vm
+ .$nextTick()
+ .then(() => {
+ vm.$el.querySelectorAll('.ide-mode-tabs .nav-links a')[1].click();
+ })
+ .then(setTimeoutPromise)
+ .then(() => {
+ expect(vm.$el.querySelector('.preview-container').innerHTML).toContain(
+ '<p>testing 123</p>',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
describe('when open file is binary and not raw', () => {
@@ -163,7 +200,7 @@ describe('RepoEditor', () => {
vm.setupEditor();
- expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file);
+ expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file, null);
expect(vm.model).not.toBeNull();
});
@@ -185,7 +222,7 @@ describe('RepoEditor', () => {
vm.setupEditor();
expect(vm.editor.onPositionChange).toHaveBeenCalled();
- expect(vm.model.events.size).toBe(1);
+ expect(vm.model.events.size).toBe(2);
});
it('updates state when model content changed', done => {
@@ -197,6 +234,20 @@ describe('RepoEditor', () => {
done();
});
});
+
+ it('sets head model as staged file', () => {
+ spyOn(vm.editor, 'createModel').and.callThrough();
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.$store.state.stagedFiles.push({ ...vm.file, key: 'staged' });
+ vm.file.staged = true;
+ vm.file.key = `unstaged-${vm.file.key}`;
+
+ vm.setupEditor();
+
+ expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file, vm.$store.state.stagedFiles[0]);
+ });
});
describe('editor updateDimensions', () => {
diff --git a/spec/javascripts/ide/components/repo_loading_file_spec.js b/spec/javascripts/ide/components/repo_loading_file_spec.js
index 8f9644216bc..7c20b8302f9 100644
--- a/spec/javascripts/ide/components/repo_loading_file_spec.js
+++ b/spec/javascripts/ide/components/repo_loading_file_spec.js
@@ -27,7 +27,7 @@ describe('RepoLoadingFile', () => {
const lines = [...container.querySelectorAll(':scope > div')];
expect(container).toBeTruthy();
- expect(lines.length).toEqual(6);
+ expect(lines.length).toEqual(3);
assertLines(lines);
});
}
diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js
index 8fc2fccb64c..7a6c22b6d27 100644
--- a/spec/javascripts/ide/lib/common/model_spec.js
+++ b/spec/javascripts/ide/lib/common/model_spec.js
@@ -30,6 +30,19 @@ describe('Multi-file editor library model', () => {
expect(model.baseModel).not.toBeNull();
});
+ it('creates model with head file to compare against', () => {
+ const f = file('path');
+ model.dispose();
+
+ model = new Model(monaco, f, {
+ ...f,
+ content: '123 testing',
+ });
+
+ expect(model.head).not.toBeNull();
+ expect(model.getOriginalModel().getValue()).toBe('123 testing');
+ });
+
it('adds eventHub listener', () => {
expect(eventHub.$on).toHaveBeenCalledWith(
`editor.update.model.dispose.${model.file.key}`,
@@ -70,13 +83,6 @@ describe('Multi-file editor library model', () => {
});
describe('onChange', () => {
- it('caches event by path', () => {
- model.onChange(() => {});
-
- expect(model.events.size).toBe(1);
- expect(model.events.keys().next().value).toBe(model.file.key);
- });
-
it('calls callback on change', done => {
const spy = jasmine.createSpy();
model.onChange(spy);
@@ -119,5 +125,15 @@ describe('Multi-file editor library model', () => {
jasmine.anything(),
);
});
+
+ it('calls onDispose callback', () => {
+ const disposeSpy = jasmine.createSpy();
+
+ model.onDispose(disposeSpy);
+
+ model.dispose();
+
+ expect(disposeSpy).toHaveBeenCalled();
+ });
});
});
diff --git a/spec/javascripts/ide/lib/decorations/controller_spec.js b/spec/javascripts/ide/lib/decorations/controller_spec.js
index aec325e26a9..e1c4ca570b6 100644
--- a/spec/javascripts/ide/lib/decorations/controller_spec.js
+++ b/spec/javascripts/ide/lib/decorations/controller_spec.js
@@ -117,4 +117,33 @@ describe('Multi-file editor library decorations controller', () => {
expect(controller.editorDecorations.size).toBe(0);
});
});
+
+ describe('hasDecorations', () => {
+ it('returns true when decorations are cached', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ expect(controller.hasDecorations(model)).toBe(true);
+ });
+
+ it('returns false when no model decorations exist', () => {
+ expect(controller.hasDecorations(model)).toBe(false);
+ });
+ });
+
+ describe('removeDecorations', () => {
+ beforeEach(() => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+ controller.decorate(model);
+ });
+
+ it('removes cached decorations', () => {
+ expect(controller.decorations.size).not.toBe(0);
+ expect(controller.editorDecorations.size).not.toBe(0);
+
+ controller.removeDecorations(model);
+
+ expect(controller.decorations.size).toBe(0);
+ expect(controller.editorDecorations.size).toBe(0);
+ });
+ });
});
diff --git a/spec/javascripts/ide/lib/diff/controller_spec.js b/spec/javascripts/ide/lib/diff/controller_spec.js
index ff73240734e..fd8ab3b4f1d 100644
--- a/spec/javascripts/ide/lib/diff/controller_spec.js
+++ b/spec/javascripts/ide/lib/diff/controller_spec.js
@@ -3,10 +3,7 @@ import monacoLoader from '~/ide/monaco_loader';
import editor from '~/ide/lib/editor';
import ModelManager from '~/ide/lib/common/model_manager';
import DecorationsController from '~/ide/lib/decorations/controller';
-import DirtyDiffController, {
- getDiffChangeType,
- getDecorator,
-} from '~/ide/lib/diff/controller';
+import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller';
import { computeDiff } from '~/ide/lib/diff/diff';
import { file } from '../../helpers';
@@ -90,6 +87,14 @@ describe('Multi-file editor library dirty diff controller', () => {
expect(model.onChange).toHaveBeenCalled();
});
+ it('adds dispose event callback', () => {
+ spyOn(model, 'onDispose');
+
+ controller.attachModel(model);
+
+ expect(model.onDispose).toHaveBeenCalled();
+ });
+
it('calls throttledComputeDiff on change', () => {
spyOn(controller, 'throttledComputeDiff');
@@ -99,6 +104,12 @@ describe('Multi-file editor library dirty diff controller', () => {
expect(controller.throttledComputeDiff).toHaveBeenCalled();
});
+
+ it('caches model', () => {
+ controller.attachModel(model);
+
+ expect(controller.models.has(model.url)).toBe(true);
+ });
});
describe('computeDiff', () => {
@@ -116,14 +127,22 @@ describe('Multi-file editor library dirty diff controller', () => {
});
describe('reDecorate', () => {
- it('calls decorations controller decorate', () => {
+ it('calls computeDiff when no decorations are cached', () => {
+ spyOn(controller, 'computeDiff');
+
+ controller.reDecorate(model);
+
+ expect(controller.computeDiff).toHaveBeenCalledWith(model);
+ });
+
+ it('calls decorate when decorations are cached', () => {
spyOn(controller.decorationsController, 'decorate');
+ controller.decorationsController.decorations.set(model.url, 'test');
+
controller.reDecorate(model);
- expect(controller.decorationsController.decorate).toHaveBeenCalledWith(
- model,
- );
+ expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model);
});
});
@@ -133,16 +152,15 @@ describe('Multi-file editor library dirty diff controller', () => {
controller.decorate({ data: { changes: [], path: model.path } });
- expect(
- controller.decorationsController.addDecorations,
- ).toHaveBeenCalledWith(model, 'dirtyDiff', jasmine.anything());
+ expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith(
+ model,
+ 'dirtyDiff',
+ jasmine.anything(),
+ );
});
it('adds decorations into editor', () => {
- const spy = spyOn(
- controller.decorationsController.editor.instance,
- 'deltaDecorations',
- );
+ const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations');
controller.decorate({
data: { changes: computeDiff('123', '1234'), path: model.path },
@@ -181,16 +199,22 @@ describe('Multi-file editor library dirty diff controller', () => {
});
it('removes worker event listener', () => {
- spyOn(
- controller.dirtyDiffWorker,
- 'removeEventListener',
- ).and.callThrough();
+ spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough();
controller.dispose();
- expect(
- controller.dirtyDiffWorker.removeEventListener,
- ).toHaveBeenCalledWith('message', jasmine.anything());
+ expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith(
+ 'message',
+ jasmine.anything(),
+ );
+ });
+
+ it('clears cached models', () => {
+ controller.attachModel(model);
+
+ model.dispose();
+
+ expect(controller.models.size).toBe(0);
});
});
});
diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js
index 75e6f0f54ec..530bdfa2759 100644
--- a/spec/javascripts/ide/lib/editor_spec.js
+++ b/spec/javascripts/ide/lib/editor_spec.js
@@ -88,7 +88,7 @@ describe('Multi-file editor library', () => {
instance.createModel('FILE');
- expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE');
+ expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE', null);
});
});
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 479ed7ce49e..ce5c525bed7 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -1,9 +1,12 @@
import Vue from 'vue';
import store from '~/ide/stores';
+import * as actions from '~/ide/stores/actions/file';
+import * as types from '~/ide/stores/mutation_types';
import service from '~/ide/services';
import router from '~/ide/ide_router';
import eventHub from '~/ide/eventhub';
import { file, resetStore } from '../../helpers';
+import testAction from '../../../helpers/vuex_action_helper';
describe('IDE store file actions', () => {
beforeEach(() => {
@@ -402,6 +405,7 @@ describe('IDE store file actions', () => {
beforeEach(() => {
spyOn(eventHub, '$on');
+ spyOn(eventHub, '$emit');
tmpFile = file();
tmpFile.content = 'testing';
@@ -460,6 +464,57 @@ describe('IDE store file actions', () => {
})
.catch(done.fail);
});
+
+ it('pushes route for active file', done => {
+ tmpFile.active = true;
+ store.state.openFiles.push(tmpFile);
+
+ store
+ .dispatch('discardFileChanges', tmpFile.path)
+ .then(() => {
+ expect(router.push).toHaveBeenCalledWith(`/project${tmpFile.url}`);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('emits eventHub event to dispose cached model', done => {
+ store
+ .dispatch('discardFileChanges', tmpFile.path)
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalled();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('stageChange', () => {
+ it('calls STAGE_CHANGE with file path', done => {
+ testAction(
+ actions.stageChange,
+ 'path',
+ store.state,
+ [{ type: types.STAGE_CHANGE, payload: 'path' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('unstageChange', () => {
+ it('calls UNSTAGE_CHANGE with file path', done => {
+ testAction(
+ actions.unstageChange,
+ 'path',
+ store.state,
+ [{ type: types.UNSTAGE_CHANGE, payload: 'path' }],
+ [],
+ done,
+ );
+ });
});
describe('openPendingTab', () => {
@@ -476,7 +531,7 @@ describe('IDE store file actions', () => {
it('makes file pending in openFiles', done => {
store
- .dispatch('openPendingTab', f)
+ .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
.then(() => {
expect(store.state.openFiles[0].pending).toBe(true);
})
@@ -486,7 +541,7 @@ describe('IDE store file actions', () => {
it('returns true when opened', done => {
store
- .dispatch('openPendingTab', f)
+ .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
.then(added => {
expect(added).toBe(true);
})
@@ -498,7 +553,7 @@ describe('IDE store file actions', () => {
store.state.currentBranchId = 'master';
store
- .dispatch('openPendingTab', f)
+ .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
.then(() => {
expect(router.push).toHaveBeenCalledWith('/project/123/tree/master/');
})
@@ -512,7 +567,7 @@ describe('IDE store file actions', () => {
store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line
store
- .dispatch('openPendingTab', f)
+ .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
.then(() => {
expect(scrollToTabSpy).toHaveBeenCalled();
store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line
@@ -527,7 +582,7 @@ describe('IDE store file actions', () => {
store.state.viewer = 'diff';
store
- .dispatch('openPendingTab', f)
+ .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
.then(added => {
expect(added).toBe(false);
})
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index cec572f4507..b6eadf56f9d 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -1,7 +1,9 @@
-import * as urlUtils from '~/lib/utils/url_utility';
+import actions, { stageAllChanges, unstageAllChanges, toggleFileFinder } from '~/ide/stores/actions';
import store from '~/ide/stores';
+import * as types from '~/ide/stores/mutation_types';
import router from '~/ide/ide_router';
import { resetStore, file } from '../helpers';
+import testAction from '../../helpers/vuex_action_helper';
describe('Multi-file store actions', () => {
beforeEach(() => {
@@ -14,12 +16,12 @@ describe('Multi-file store actions', () => {
describe('redirectToUrl', () => {
it('calls visitUrl', done => {
- spyOn(urlUtils, 'visitUrl');
+ const visitUrl = spyOnDependency(actions, 'visitUrl');
store
.dispatch('redirectToUrl', 'test')
.then(() => {
- expect(urlUtils.visitUrl).toHaveBeenCalledWith('test');
+ expect(visitUrl).toHaveBeenCalledWith('test');
done();
})
@@ -191,9 +193,7 @@ describe('Multi-file store actions', () => {
})
.then(f => {
expect(f.tempFile).toBeTruthy();
- expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(
- 1,
- );
+ expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1);
done();
})
@@ -292,6 +292,42 @@ describe('Multi-file store actions', () => {
});
});
+ describe('stageAllChanges', () => {
+ it('adds all files from changedFiles to stagedFiles', done => {
+ store.state.changedFiles.push(file(), file('new'));
+
+ testAction(
+ stageAllChanges,
+ null,
+ store.state,
+ [
+ { type: types.STAGE_CHANGE, payload: store.state.changedFiles[0].path },
+ { type: types.STAGE_CHANGE, payload: store.state.changedFiles[1].path },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('unstageAllChanges', () => {
+ it('removes all files from stagedFiles after unstaging', done => {
+ store.state.stagedFiles.push(file(), file('new'));
+
+ testAction(
+ unstageAllChanges,
+ null,
+ store.state,
+ [
+ { type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[0].path },
+ { type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[1].path },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
describe('updateViewer', () => {
it('updates viewer state', done => {
store
@@ -303,4 +339,17 @@ describe('Multi-file store actions', () => {
.catch(done.fail);
});
});
+
+ describe('toggleFileFinder', () => {
+ it('commits TOGGLE_FILE_FINDER', done => {
+ testAction(
+ toggleFileFinder,
+ true,
+ null,
+ [{ type: 'TOGGLE_FILE_FINDER', payload: true }],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
index 33733b97dff..b6b4dd28729 100644
--- a/spec/javascripts/ide/stores/getters_spec.js
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -37,19 +37,11 @@ describe('IDE store getters', () => {
expect(modifiedFiles.length).toBe(1);
expect(modifiedFiles[0].name).toBe('changed');
});
- });
-
- describe('addedFiles', () => {
- it('returns a list of added files', () => {
- localState.openFiles.push(file());
- localState.changedFiles.push(file('added'));
- localState.changedFiles[0].changed = true;
- localState.changedFiles[0].tempFile = true;
- const modifiedFiles = getters.addedFiles(localState);
+ it('returns angle left when collapsed', () => {
+ localState.rightPanelCollapsed = true;
- expect(modifiedFiles.length).toBe(1);
- expect(modifiedFiles[0].name).toBe('added');
+ expect(getters.collapseButtonIcon(localState)).toBe('angle-double-left');
});
});
@@ -72,4 +64,24 @@ describe('IDE store getters', () => {
expect(getters.currentMergeRequest(localState)).toBeNull();
});
});
+
+ describe('allBlobs', () => {
+ beforeEach(() => {
+ Object.assign(localState.entries, {
+ index: { type: 'blob', name: 'index', lastOpenedAt: 0 },
+ app: { type: 'blob', name: 'blob', lastOpenedAt: 0 },
+ folder: { type: 'folder', name: 'folder', lastOpenedAt: 0 },
+ });
+ });
+
+ it('returns only blobs', () => {
+ expect(getters.allBlobs(localState).length).toBe(2);
+ });
+
+ it('returns list sorted by lastOpenedAt', () => {
+ localState.entries.app.lastOpenedAt = new Date().getTime();
+
+ expect(getters.allBlobs(localState)[0].name).toBe('blob');
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
index 90ded940227..b2b4b85ca42 100644
--- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -1,7 +1,7 @@
+import actions from '~/ide/stores/actions';
import store from '~/ide/stores';
import service from '~/ide/services';
import router from '~/ide/ide_router';
-import * as urlUtils from '~/lib/utils/url_utility';
import eventHub from '~/ide/eventhub';
import * as consts from '~/ide/stores/modules/commit/constants';
import { resetStore, file } from 'spec/ide/helpers';
@@ -133,10 +133,7 @@ describe('IDE commit module actions', () => {
store
.dispatch('commit/checkCommitStatus')
.then(() => {
- expect(service.getBranchData).toHaveBeenCalledWith(
- 'abcproject',
- 'master',
- );
+ expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master');
done();
})
@@ -212,14 +209,14 @@ describe('IDE commit module actions', () => {
},
},
};
- store.state.changedFiles.push(f, {
+ store.state.stagedFiles.push(f, {
...file('changedFile2'),
changed: true,
});
- store.state.openFiles = store.state.changedFiles;
+ store.state.openFiles = store.state.stagedFiles;
- store.state.changedFiles.forEach(changedFile => {
- store.state.entries[changedFile.path] = changedFile;
+ store.state.stagedFiles.forEach(stagedFile => {
+ store.state.entries[stagedFile.path] = stagedFile;
});
});
@@ -230,9 +227,7 @@ describe('IDE commit module actions', () => {
branch,
})
.then(() => {
- expect(
- store.state.projects.abcproject.branches.master.workingReference,
- ).toBe(data.id);
+ expect(store.state.projects.abcproject.branches.master.workingReference).toBe(data.id);
})
.then(done)
.catch(done.fail);
@@ -253,19 +248,6 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
- it('removes all changed files', done => {
- store
- .dispatch('commit/updateFilesAfterCommit', {
- data,
- branch,
- })
- .then(() => {
- expect(store.state.changedFiles.length).toBe(0);
- })
- .then(done)
- .catch(done.fail);
- });
-
it('sets files commit data', done => {
store
.dispatch('commit/updateFilesAfterCommit', {
@@ -299,10 +281,10 @@ describe('IDE commit module actions', () => {
branch,
})
.then(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith(
- `editor.update.model.content.${f.path}`,
- f.content,
- );
+ expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.content.${f.key}`, {
+ content: f.content,
+ changed: false,
+ });
})
.then(done)
.catch(done.fail);
@@ -317,26 +299,7 @@ describe('IDE commit module actions', () => {
branch,
})
.then(() => {
- expect(router.push).toHaveBeenCalledWith(
- `/project/abcproject/blob/master/${f.path}`,
- );
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('resets stores commit actions', done => {
- store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
-
- store
- .dispatch('commit/updateFilesAfterCommit', {
- data,
- branch,
- })
- .then(() => {
- expect(store.state.commit.commitAction).not.toBe(
- consts.COMMIT_TO_NEW_BRANCH,
- );
+ expect(router.push).toHaveBeenCalledWith(`/project/abcproject/blob/master/${f.path}`);
})
.then(done)
.catch(done.fail);
@@ -344,8 +307,10 @@ describe('IDE commit module actions', () => {
});
describe('commitChanges', () => {
+ let visitUrl;
+
beforeEach(() => {
- spyOn(urlUtils, 'visitUrl');
+ visitUrl = spyOnDependency(actions, 'visitUrl');
document.body.innerHTML += '<div class="flash-container"></div>';
@@ -359,12 +324,22 @@ describe('IDE commit module actions', () => {
},
},
};
- store.state.changedFiles.push(file('changed'));
- store.state.changedFiles[0].active = true;
+
+ const f = {
+ ...file('changed'),
+ type: 'blob',
+ active: true,
+ };
+ store.state.stagedFiles.push(f);
+ store.state.changedFiles = [
+ {
+ ...f,
+ },
+ ];
store.state.openFiles = store.state.changedFiles;
- store.state.openFiles.forEach(f => {
- store.state.entries[f.path] = f;
+ store.state.openFiles.forEach(localF => {
+ store.state.entries[localF.path] = localF;
});
store.state.commit.commitAction = '2';
@@ -444,11 +419,11 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
- it('adds commit data to changed files', done => {
+ it('adds commit data to files', done => {
store
.dispatch('commit/commitChanges')
.then(() => {
- expect(store.state.openFiles[0].lastCommit.message).toBe(
+ expect(store.state.entries[store.state.openFiles[0].path].lastCommit.message).toBe(
'test message',
);
@@ -457,24 +432,63 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
- it('redirects to new merge request page', done => {
- spyOn(eventHub, '$on');
-
- store.state.commit.commitAction = '3';
+ it('resets stores commit actions', done => {
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
store
.dispatch('commit/commitChanges')
.then(() => {
- expect(urlUtils.visitUrl).toHaveBeenCalledWith(
- `webUrl/merge_requests/new?merge_request[source_branch]=${
- store.getters['commit/newBranchName']
- }&merge_request[target_branch]=master`,
- );
+ expect(store.state.commit.commitAction).not.toBe(consts.COMMIT_TO_NEW_BRANCH);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
- done();
+ it('removes all staged files', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(store.state.stagedFiles.length).toBe(0);
})
+ .then(done)
.catch(done.fail);
});
+
+ describe('merge request', () => {
+ it('redirects to new merge request page', done => {
+ spyOn(eventHub, '$on');
+
+ store.state.commit.commitAction = '3';
+
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(visitUrl).toHaveBeenCalledWith(
+ `webUrl/merge_requests/new?merge_request[source_branch]=${
+ store.getters['commit/newBranchName']
+ }&merge_request[target_branch]=master`,
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('resets changed files before redirecting', done => {
+ spyOn(eventHub, '$on');
+
+ store.state.commit.commitAction = '3';
+
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(store.state.stagedFiles.length).toBe(0);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
});
describe('failed', () => {
diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
index e396284ec2c..55580f046ad 100644
--- a/spec/javascripts/ide/stores/modules/commit/getters_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
@@ -34,17 +34,17 @@ describe('IDE commit module getters', () => {
discardDraftButtonDisabled: false,
};
const rootState = {
- changedFiles: ['a'],
+ stagedFiles: ['a'],
};
- it('returns false when discardDraftButtonDisabled is false & changedFiles is not empty', () => {
+ it('returns false when discardDraftButtonDisabled is false & stagedFiles is not empty', () => {
expect(
getters.commitButtonDisabled(state, localGetters, rootState),
).toBeFalsy();
});
- it('returns true when discardDraftButtonDisabled is false & changedFiles is empty', () => {
- rootState.changedFiles.length = 0;
+ it('returns true when discardDraftButtonDisabled is false & stagedFiles is empty', () => {
+ rootState.stagedFiles.length = 0;
expect(
getters.commitButtonDisabled(state, localGetters, rootState),
@@ -61,7 +61,7 @@ describe('IDE commit module getters', () => {
it('returns true when discardDraftButtonDisabled is false & changedFiles is not empty', () => {
localGetters.discardDraftButtonDisabled = false;
- rootState.changedFiles.length = 0;
+ rootState.stagedFiles.length = 0;
expect(
getters.commitButtonDisabled(state, localGetters, rootState),
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
index bf9d5166d0a..6fba934810d 100644
--- a/spec/javascripts/ide/stores/mutations/file_spec.js
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -8,7 +8,10 @@ describe('IDE store file mutations', () => {
beforeEach(() => {
localState = state();
- localFile = file();
+ localFile = {
+ ...file(),
+ type: 'blob',
+ };
localState.entries[localFile.path] = localFile;
});
@@ -183,6 +186,49 @@ describe('IDE store file mutations', () => {
});
});
+ describe('STAGE_CHANGE', () => {
+ it('adds file into stagedFiles array', () => {
+ mutations.STAGE_CHANGE(localState, localFile.path);
+
+ expect(localState.stagedFiles.length).toBe(1);
+ expect(localState.stagedFiles[0]).toEqual(localFile);
+ });
+
+ it('updates stagedFile if it is already staged', () => {
+ mutations.STAGE_CHANGE(localState, localFile.path);
+
+ localFile.raw = 'testing 123';
+
+ mutations.STAGE_CHANGE(localState, localFile.path);
+
+ expect(localState.stagedFiles.length).toBe(1);
+ expect(localState.stagedFiles[0].raw).toEqual('testing 123');
+ });
+ });
+
+ describe('UNSTAGE_CHANGE', () => {
+ let f;
+
+ beforeEach(() => {
+ f = {
+ ...file(),
+ type: 'blob',
+ staged: true,
+ };
+
+ localState.stagedFiles.push(f);
+ localState.changedFiles.push(f);
+ localState.entries[f.path] = f;
+ });
+
+ it('removes from stagedFiles array', () => {
+ mutations.UNSTAGE_CHANGE(localState, f.path);
+
+ expect(localState.stagedFiles.length).toBe(0);
+ expect(localState.changedFiles.length).toBe(1);
+ });
+ });
+
describe('TOGGLE_FILE_CHANGED', () => {
it('updates file changed status', () => {
mutations.TOGGLE_FILE_CHANGED(localState, {
diff --git a/spec/javascripts/ide/stores/mutations/tree_spec.js b/spec/javascripts/ide/stores/mutations/tree_spec.js
index e6c085eaff6..67e9f7509da 100644
--- a/spec/javascripts/ide/stores/mutations/tree_spec.js
+++ b/spec/javascripts/ide/stores/mutations/tree_spec.js
@@ -55,6 +55,16 @@ describe('Multi-file store tree mutations', () => {
expect(tree.tree[1].name).toBe('submodule');
expect(tree.tree[2].name).toBe('blob');
});
+
+ it('keeps loading state', () => {
+ mutations.CREATE_TREE(localState, { treePath: 'project/master' });
+ mutations.SET_DIRECTORY_DATA(localState, {
+ data,
+ treePath: 'project/master',
+ });
+
+ expect(localState.trees['project/master'].loading).toBe(true);
+ });
});
describe('REMOVE_ALL_CHANGES_FILES', () => {
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
index 38162a470ad..575039e755e 100644
--- a/spec/javascripts/ide/stores/mutations_spec.js
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -69,6 +69,16 @@ describe('Multi-file store mutations', () => {
});
});
+ describe('CLEAR_STAGED_CHANGES', () => {
+ it('clears stagedFiles array', () => {
+ localState.stagedFiles.push('a');
+
+ mutations.CLEAR_STAGED_CHANGES(localState);
+
+ expect(localState.stagedFiles.length).toBe(0);
+ });
+ });
+
describe('UPDATE_VIEWER', () => {
it('sets viewer state', () => {
mutations.UPDATE_VIEWER(localState, 'diff');
@@ -76,4 +86,12 @@ describe('Multi-file store mutations', () => {
expect(localState.viewer).toBe('diff');
});
});
+
+ describe('TOGGLE_FILE_FINDER', () => {
+ it('updates fileFindVisible', () => {
+ mutations.TOGGLE_FILE_FINDER(localState, true);
+
+ expect(localState.fileFindVisible).toBe(true);
+ });
+ });
});
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index d5a87b5ce20..bf1f0c822fe 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -2,7 +2,6 @@ import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import '~/behaviors/markdown/render_gfm';
-import * as urlUtils from '~/lib/utils/url_utility';
import issuableApp from '~/issue_show/components/app.vue';
import eventHub from '~/issue_show/event_hub';
import setTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
@@ -174,7 +173,7 @@ describe('Issuable output', () => {
});
it('does not redirect if issue has not moved', (done) => {
- spyOn(urlUtils, 'visitUrl');
+ const visitUrl = spyOnDependency(issuableApp, 'visitUrl');
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
data: {
@@ -187,16 +186,13 @@ describe('Issuable output', () => {
vm.updateIssuable();
setTimeout(() => {
- expect(
- urlUtils.visitUrl,
- ).not.toHaveBeenCalled();
-
+ expect(visitUrl).not.toHaveBeenCalled();
done();
});
});
it('redirects if returned web_url has changed', (done) => {
- spyOn(urlUtils, 'visitUrl');
+ const visitUrl = spyOnDependency(issuableApp, 'visitUrl');
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
data: {
@@ -209,10 +205,7 @@ describe('Issuable output', () => {
vm.updateIssuable();
setTimeout(() => {
- expect(
- urlUtils.visitUrl,
- ).toHaveBeenCalledWith('/testing-issue-move');
-
+ expect(visitUrl).toHaveBeenCalledWith('/testing-issue-move');
done();
});
});
@@ -340,7 +333,7 @@ describe('Issuable output', () => {
describe('deleteIssuable', () => {
it('changes URL when deleted', (done) => {
- spyOn(urlUtils, 'visitUrl');
+ const visitUrl = spyOnDependency(issuableApp, 'visitUrl');
spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
data: {
@@ -352,16 +345,13 @@ describe('Issuable output', () => {
vm.deleteIssuable();
setTimeout(() => {
- expect(
- urlUtils.visitUrl,
- ).toHaveBeenCalledWith('/test');
-
+ expect(visitUrl).toHaveBeenCalledWith('/test');
done();
});
});
it('stops polling when deleting', (done) => {
- spyOn(urlUtils, 'visitUrl');
+ spyOnDependency(issuableApp, 'visitUrl');
spyOn(vm.poll, 'stop').and.callThrough();
spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
@@ -377,7 +367,6 @@ describe('Issuable output', () => {
expect(
vm.poll.stop,
).toHaveBeenCalledWith();
-
done();
});
});
diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js
index d96151a8a3a..889c8545faa 100644
--- a/spec/javascripts/issue_show/components/description_spec.js
+++ b/spec/javascripts/issue_show/components/description_spec.js
@@ -1,7 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
-import descriptionComponent from '~/issue_show/components/description.vue';
-import * as taskList from '~/task_list';
+import Description from '~/issue_show/components/description.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Description component', () => {
@@ -17,7 +16,7 @@ describe('Description component', () => {
};
beforeEach(() => {
- DescriptionComponent = Vue.extend(descriptionComponent);
+ DescriptionComponent = Vue.extend(Description);
if (!document.querySelector('.issuable-meta')) {
const metaData = document.createElement('div');
@@ -82,18 +81,20 @@ describe('Description component', () => {
});
describe('TaskList', () => {
+ let TaskList;
+
beforeEach(() => {
vm = mountComponent(DescriptionComponent, Object.assign({}, props, {
issuableType: 'issuableType',
}));
- spyOn(taskList, 'default');
+ TaskList = spyOnDependency(Description, 'TaskList');
});
it('re-inits the TaskList when description changed', (done) => {
vm.descriptionHtml = 'changed';
setTimeout(() => {
- expect(taskList.default).toHaveBeenCalled();
+ expect(TaskList).toHaveBeenCalled();
done();
});
});
@@ -103,7 +104,7 @@ describe('Description component', () => {
vm.descriptionHtml = 'changed';
setTimeout(() => {
- expect(taskList.default).not.toHaveBeenCalled();
+ expect(TaskList).not.toHaveBeenCalled();
done();
});
});
@@ -112,7 +113,7 @@ describe('Description component', () => {
vm.descriptionHtml = 'changed';
setTimeout(() => {
- expect(taskList.default).toHaveBeenCalledWith({
+ expect(TaskList).toHaveBeenCalledWith({
dataType: 'issuableType',
fieldName: 'description',
selector: '.detail-page-description',
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index f37426a72d4..047ecab27db 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -92,6 +92,7 @@ describe('Issue', function() {
function mockCanCreateBranch(canCreateBranch) {
mock.onGet(/(.*)\/can_create_branch$/).reply(200, {
can_create_branch: canCreateBranch,
+ suggested_branch_name: 'foo-99',
});
}
diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js
index c6bbacf237a..da00b615c9b 100644
--- a/spec/javascripts/job_spec.js
+++ b/spec/javascripts/job_spec.js
@@ -2,7 +2,6 @@ import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
-import * as urlUtils from '~/lib/utils/url_utility';
import '~/lib/utils/datetime_utility';
import Job from '~/job';
import '~/breakpoints';
@@ -22,7 +21,7 @@ describe('Job', () => {
beforeEach(() => {
loadFixtures('builds/build-with-artifacts.html.raw');
- spyOn(urlUtils, 'visitUrl');
+ spyOnDependency(Job, 'visitUrl');
response = {};
diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js
index 0961605ce5c..4f861c39d3f 100644
--- a/spec/javascripts/jobs/header_spec.js
+++ b/spec/javascripts/jobs/header_spec.js
@@ -36,14 +36,28 @@ describe('Job details header', () => {
},
isLoading: false,
};
-
- vm = mountComponent(HeaderComponent, props);
});
afterEach(() => {
vm.$destroy();
});
+ describe('job reason', () => {
+ it('should not render the reason when reason is absent', () => {
+ vm = mountComponent(HeaderComponent, props);
+
+ expect(vm.shouldRenderReason).toBe(false);
+ });
+
+ it('should render the reason when reason is present', () => {
+ props.job.callout_message = 'There is an unknown failure, please try again';
+
+ vm = mountComponent(HeaderComponent, props);
+
+ expect(vm.shouldRenderReason).toBe(true);
+ });
+ });
+
describe('triggered job', () => {
beforeEach(() => {
vm = mountComponent(HeaderComponent, props);
@@ -51,14 +65,17 @@ describe('Job details header', () => {
it('should render provided job information', () => {
expect(
- vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(),
+ vm.$el
+ .querySelector('.header-main-content')
+ .textContent.replace(/\s+/g, ' ')
+ .trim(),
).toEqual('failed Job #123 triggered 3 weeks ago by Foo');
});
it('should render new issue link', () => {
- expect(
- vm.$el.querySelector('.js-new-issue').getAttribute('href'),
- ).toEqual(props.job.new_issue_path);
+ expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(
+ props.job.new_issue_path,
+ );
});
});
@@ -68,7 +85,10 @@ describe('Job details header', () => {
vm = mountComponent(HeaderComponent, props);
expect(
- vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(),
+ vm.$el
+ .querySelector('.header-main-content')
+ .textContent.replace(/\s+/g, ' ')
+ .trim(),
).toEqual('failed Job #123 created 3 weeks ago by Foo');
});
});
diff --git a/spec/javascripts/jobs/sidebar_details_block_spec.js b/spec/javascripts/jobs/sidebar_details_block_spec.js
index 602dae514b1..9c4454252ce 100644
--- a/spec/javascripts/jobs/sidebar_details_block_spec.js
+++ b/spec/javascripts/jobs/sidebar_details_block_spec.js
@@ -31,10 +31,25 @@ describe('Sidebar details block', () => {
});
});
+ describe("when user can't retry", () => {
+ it('should not render a retry button', () => {
+ vm = new SidebarComponent({
+ propsData: {
+ job: {},
+ canUserRetry: false,
+ isLoading: true,
+ },
+ }).$mount();
+
+ expect(vm.$el.querySelector('.js-retry-job')).toBeNull();
+ });
+ });
+
beforeEach(() => {
vm = new SidebarComponent({
propsData: {
job,
+ canUserRetry: true,
isLoading: false,
},
}).$mount();
@@ -42,7 +57,9 @@ describe('Sidebar details block', () => {
describe('actions', () => {
it('should render link to new issue', () => {
- expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(job.new_issue_path);
+ expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(
+ job.new_issue_path,
+ );
expect(vm.$el.querySelector('.js-new-issue').textContent.trim()).toEqual('New issue');
});
@@ -57,43 +74,35 @@ describe('Sidebar details block', () => {
describe('information', () => {
it('should render merge request link', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-mr')),
- ).toEqual('Merge Request: !2');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-mr'))).toEqual('Merge Request: !2');
- expect(
- vm.$el.querySelector('.js-job-mr a').getAttribute('href'),
- ).toEqual(job.merge_request.path);
+ expect(vm.$el.querySelector('.js-job-mr a').getAttribute('href')).toEqual(
+ job.merge_request.path,
+ );
});
it('should render job duration', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-duration')),
- ).toEqual('Duration: 6 seconds');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-duration'))).toEqual(
+ 'Duration: 6 seconds',
+ );
});
it('should render erased date', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-erased')),
- ).toEqual('Erased: 3 weeks ago');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-erased'))).toEqual('Erased: 3 weeks ago');
});
it('should render finished date', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-finished')),
- ).toEqual('Finished: 3 weeks ago');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-finished'))).toEqual(
+ 'Finished: 3 weeks ago',
+ );
});
it('should render queued date', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-queued')),
- ).toEqual('Queued: 9 seconds');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-queued'))).toEqual('Queued: 9 seconds');
});
it('should render runner ID', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-runner')),
- ).toEqual('Runner: #1');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual('Runner: local ci runner (#1)');
});
it('should render timeout information', () => {
@@ -103,15 +112,11 @@ describe('Sidebar details block', () => {
});
it('should render coverage', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-coverage')),
- ).toEqual('Coverage: 20%');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-coverage'))).toEqual('Coverage: 20%');
});
it('should render tags', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-tags')),
- ).toEqual('Tags: tag');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-tags'))).toEqual('Tags: tag');
});
});
});
diff --git a/spec/javascripts/lib/utils/csrf_token_spec.js b/spec/javascripts/lib/utils/csrf_token_spec.js
index c484213df8e..81a39a97a84 100644
--- a/spec/javascripts/lib/utils/csrf_token_spec.js
+++ b/spec/javascripts/lib/utils/csrf_token_spec.js
@@ -1,6 +1,6 @@
import csrf from '~/lib/utils/csrf';
-describe('csrf', () => {
+describe('csrf', function () {
beforeEach(() => {
this.tokenKey = 'X-CSRF-Token';
this.token = 'pH1cvjnP9grx2oKlhWEDvUZnJ8x2eXsIs1qzyHkF3DugSG5yTxR76CWeEZRhML2D1IeVB7NEW0t5l/axE4iJpQ==';
diff --git a/spec/javascripts/lib/utils/image_utility_spec.js b/spec/javascripts/lib/utils/image_utility_spec.js
index 75addfcc833..a7eff419fba 100644
--- a/spec/javascripts/lib/utils/image_utility_spec.js
+++ b/spec/javascripts/lib/utils/image_utility_spec.js
@@ -1,4 +1,4 @@
-import * as imageUtility from '~/lib/utils/image_utility';
+import { isImageLoaded } from '~/lib/utils/image_utility';
describe('imageUtility', () => {
describe('isImageLoaded', () => {
@@ -8,7 +8,7 @@ describe('imageUtility', () => {
naturalHeight: 100,
};
- expect(imageUtility.isImageLoaded(element)).toEqual(false);
+ expect(isImageLoaded(element)).toEqual(false);
});
it('should return false when naturalHeight = 0', () => {
@@ -17,7 +17,7 @@ describe('imageUtility', () => {
naturalHeight: 0,
};
- expect(imageUtility.isImageLoaded(element)).toEqual(false);
+ expect(isImageLoaded(element)).toEqual(false);
});
it('should return true when image.complete and naturalHeight != 0', () => {
@@ -26,7 +26,7 @@ describe('imageUtility', () => {
naturalHeight: 100,
};
- expect(imageUtility.isImageLoaded(element)).toEqual(true);
+ expect(isImageLoaded(element)).toEqual(true);
});
});
});
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 79c8cf0ba32..3dbd9756cd2 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -3,7 +3,6 @@
import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import * as urlUtils from '~/lib/utils/url_utility';
import MergeRequestTabs from '~/merge_request_tabs';
import '~/commit/pipelines/pipelines_bundle';
import '~/breakpoints';
@@ -356,7 +355,7 @@ import 'vendor/jquery.scrollTo';
describe('with note fragment hash', () => {
it('should expand and scroll to linked fragment hash #note_xxx', function (done) {
- spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId);
+ spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
setTimeout(() => {
@@ -372,7 +371,7 @@ import 'vendor/jquery.scrollTo';
});
it('should gracefully ignore non-existant fragment hash', function (done) {
- spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
+ spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
setTimeout(() => {
@@ -385,7 +384,7 @@ import 'vendor/jquery.scrollTo';
describe('with line number fragment hash', () => {
it('should gracefully ignore line number fragment hash', function () {
- spyOn(urlUtils, 'getLocationHash').and.returnValue(noteLineNumId);
+ spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteLineNumId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(noteLineNumId.length).toBeGreaterThan(0);
@@ -422,7 +421,7 @@ import 'vendor/jquery.scrollTo';
describe('with note fragment hash', () => {
it('should expand and scroll to linked fragment hash #note_xxx', function (done) {
- spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId);
+ spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
@@ -439,7 +438,7 @@ import 'vendor/jquery.scrollTo';
});
it('should gracefully ignore non-existant fragment hash', function (done) {
- spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
+ spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
setTimeout(() => {
@@ -451,7 +450,7 @@ import 'vendor/jquery.scrollTo';
describe('with line number fragment hash', () => {
it('should gracefully ignore line number fragment hash', function () {
- spyOn(urlUtils, 'getLocationHash').and.returnValue(noteLineNumId);
+ spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteLineNumId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(noteLineNumId.length).toBeGreaterThan(0);
diff --git a/spec/javascripts/monitoring/monitoring_store_spec.js b/spec/javascripts/monitoring/monitoring_store_spec.js
index 88aa7659275..08d54946787 100644
--- a/spec/javascripts/monitoring/monitoring_store_spec.js
+++ b/spec/javascripts/monitoring/monitoring_store_spec.js
@@ -1,7 +1,7 @@
import MonitoringStore from '~/monitoring/stores/monitoring_store';
import MonitoringMock, { deploymentData } from './mock_data';
-describe('MonitoringStore', () => {
+describe('MonitoringStore', function () {
this.store = new MonitoringStore();
this.store.storeMetrics(MonitoringMock.data);
diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js
index ab81aabb992..1dfe890e05e 100644
--- a/spec/javascripts/notes/components/note_actions_spec.js
+++ b/spec/javascripts/notes/components/note_actions_spec.js
@@ -3,7 +3,7 @@ import store from '~/notes/stores';
import noteActions from '~/notes/components/note_actions.vue';
import { userDataMock } from '../mock_data';
-describe('issse_note_actions component', () => {
+describe('issue_note_actions component', () => {
let vm;
let Component;
@@ -24,6 +24,7 @@ describe('issse_note_actions component', () => {
authorId: 26,
canDelete: true,
canEdit: true,
+ canAwardEmoji: true,
canReportAsAbuse: true,
noteId: 539,
reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26',
@@ -70,6 +71,7 @@ describe('issse_note_actions component', () => {
authorId: 26,
canDelete: false,
canEdit: false,
+ canAwardEmoji: false,
canReportAsAbuse: false,
noteId: 539,
reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26',
diff --git a/spec/javascripts/notes/components/note_awards_list_spec.js b/spec/javascripts/notes/components/note_awards_list_spec.js
index 15995ec5a05..1c30d8691b1 100644
--- a/spec/javascripts/notes/components/note_awards_list_spec.js
+++ b/spec/javascripts/notes/components/note_awards_list_spec.js
@@ -29,6 +29,7 @@ describe('note_awards_list component', () => {
awards: awardsMock,
noteAuthorId: 2,
noteId: 545,
+ canAwardEmoji: true,
toggleAwardPath: '/gitlab-org/gitlab-ce/notes/545/toggle_award_emoji',
},
}).$mount();
@@ -43,14 +44,45 @@ describe('note_awards_list component', () => {
expect(vm.$el.querySelector('.js-awards-block button [data-name="cartwheel_tone3"]')).toBeDefined();
});
- it('should be possible to remove awareded emoji', () => {
+ it('should be possible to remove awarded emoji', () => {
spyOn(vm, 'handleAward').and.callThrough();
+ spyOn(vm, 'toggleAwardRequest').and.callThrough();
vm.$el.querySelector('.js-awards-block button').click();
expect(vm.handleAward).toHaveBeenCalledWith('flag_tz');
+ expect(vm.toggleAwardRequest).toHaveBeenCalled();
});
it('should be possible to add new emoji', () => {
expect(vm.$el.querySelector('.js-add-award')).toBeDefined();
});
+
+ describe('when the user cannot award emoji', () => {
+ beforeEach(() => {
+ const Component = Vue.extend(awardsNote);
+
+ vm = new Component({
+ store,
+ propsData: {
+ awards: awardsMock,
+ noteAuthorId: 2,
+ noteId: 545,
+ canAwardEmoji: false,
+ toggleAwardPath: '/gitlab-org/gitlab-ce/notes/545/toggle_award_emoji',
+ },
+ }).$mount();
+ });
+
+ it('should not be possible to remove awarded emoji', () => {
+ spyOn(vm, 'toggleAwardRequest').and.callThrough();
+
+ vm.$el.querySelector('.js-awards-block button').click();
+
+ expect(vm.toggleAwardRequest).not.toHaveBeenCalled();
+ });
+
+ it('should not be possible to add new emoji', () => {
+ expect(vm.$el.querySelector('.js-add-award')).toBeNull();
+ });
+ });
});
diff --git a/spec/javascripts/notes/components/note_body_spec.js b/spec/javascripts/notes/components/note_body_spec.js
index 0ff804f0e55..4e551496ff0 100644
--- a/spec/javascripts/notes/components/note_body_spec.js
+++ b/spec/javascripts/notes/components/note_body_spec.js
@@ -18,6 +18,7 @@ describe('issue_note_body component', () => {
propsData: {
note,
canEdit: true,
+ canAwardEmoji: true,
},
}).$mount();
});
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index 24388fba219..bfe3a65feee 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -9,6 +9,7 @@ export const notesDataMock = {
totalNotes: 1,
closePath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=close',
reopenPath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=reopen',
+ canAwardEmoji: true,
};
export const userDataMock = {
@@ -30,6 +31,7 @@ export const noteableDataMock = {
current_user: {
can_create_note: true,
can_update: true,
+ can_award_emoji: true,
},
description: '',
due_date: null,
@@ -86,7 +88,10 @@ export const individualNote = {
human_access: 'Owner',
note: 'sdfdsaf',
note_html: "<p dir='auto'>sdfdsaf</p>",
- current_user: { can_edit: true },
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
emoji_awardable: true,
award_emoji: [
@@ -129,6 +134,7 @@ export const note = {
note_html: '<p dir="auto">Vel id placeat reprehenderit sit numquam.</p>',
current_user: {
can_edit: true,
+ can_award_emoji: true,
},
discussion_id: 'd3842a451b7f3d9a5dfce329515127b2d29a4cd0',
emoji_awardable: true,
@@ -187,6 +193,7 @@ export const discussionMock = {
note_html: "<p dir='auto'>THIS IS A DICUSSSION!</p>",
current_user: {
can_edit: true,
+ can_award_emoji: true,
},
discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
emoji_awardable: true,
@@ -231,6 +238,7 @@ export const discussionMock = {
},
current_user: {
can_edit: true,
+ can_award_emoji: true,
},
discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
emoji_awardable: true,
@@ -275,6 +283,7 @@ export const discussionMock = {
},
current_user: {
can_edit: true,
+ can_award_emoji: true,
},
discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
emoji_awardable: true,
@@ -365,6 +374,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = {
note_html: '\u003cp dir="auto"\u003esdfdsaf\u003c/p\u003e',
current_user: {
can_edit: true,
+ can_award_emoji: true,
},
discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
emoji_awardable: true,
@@ -425,6 +435,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = {
note_html: '\u003cp dir="auto"\u003eNew note!\u003c/p\u003e',
current_user: {
can_edit: true,
+ can_award_emoji: true,
},
discussion_id: '70d5c92a4039a36c70100c6691c18c27e4b0a790',
emoji_awardable: true,
@@ -478,6 +489,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = {
},
current_user: {
can_edit: true,
+ can_award_emoji: true,
},
discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
emoji_awardable: true,
@@ -527,6 +539,7 @@ export const DISCUSSION_NOTE_RESPONSE_MAP = {
note_html: '\u003cp dir="auto"\u003eAdding a comment\u003c/p\u003e',
current_user: {
can_edit: true,
+ can_award_emoji: true,
},
discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
emoji_awardable: true,
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index ec56ab0e2f0..0952356c2f4 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -3,7 +3,6 @@ import $ from 'jquery';
import _ from 'underscore';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import * as urlUtils from '~/lib/utils/url_utility';
import 'autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
@@ -222,7 +221,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('sets target when hash matches', () => {
- spyOn(urlUtils, 'getLocationHash').and.returnValue(hash);
+ spyOnDependency(Notes, 'getLocationHash').and.returnValue(hash);
Notes.updateNoteTargetSelector($note);
@@ -231,7 +230,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('unsets target when hash does not match', () => {
- spyOn(urlUtils, 'getLocationHash').and.returnValue('note_doesnotexist');
+ spyOnDependency(Notes, 'getLocationHash').and.returnValue('note_doesnotexist');
Notes.updateNoteTargetSelector($note);
@@ -239,7 +238,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('unsets target when there is not a hash fragment anymore', () => {
- spyOn(urlUtils, 'getLocationHash').and.returnValue(null);
+ spyOnDependency(Notes, 'getLocationHash').and.returnValue(null);
Notes.updateNoteTargetSelector($note);
diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js
index b09494f0b77..04f2e7ef4f9 100644
--- a/spec/javascripts/pager_spec.js
+++ b/spec/javascripts/pager_spec.js
@@ -1,15 +1,25 @@
-/* global fixture */
+import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import * as utils from '~/lib/utils/url_utility';
import Pager from '~/pager';
describe('pager', () => {
+ let axiosMock;
+
+ beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
describe('init', () => {
const originalHref = window.location.href;
beforeEach(() => {
setFixtures('<div class="content_list"></div><div class="loading"></div>');
+ spyOn($.fn, 'endlessScroll').and.stub();
});
afterEach(() => {
@@ -25,7 +35,7 @@ describe('pager', () => {
it('should use current url if data-href attribute not provided', () => {
const href = `${gl.TEST_HOST}/some_list`;
- spyOn(utils, 'removeParams').and.returnValue(href);
+ spyOnDependency(Pager, 'removeParams').and.returnValue(href);
Pager.init();
expect(Pager.url).toBe(href);
});
@@ -39,42 +49,37 @@ describe('pager', () => {
it('keeps extra query parameters from url', () => {
window.history.replaceState({}, null, '?filter=test&offset=100');
const href = `${gl.TEST_HOST}/some_list?filter=test`;
- spyOn(utils, 'removeParams').and.returnValue(href);
+ const removeParams = spyOnDependency(Pager, 'removeParams').and.returnValue(href);
Pager.init();
- expect(utils.removeParams).toHaveBeenCalledWith(['limit', 'offset']);
+ expect(removeParams).toHaveBeenCalledWith(['limit', 'offset']);
expect(Pager.url).toEqual(href);
});
});
describe('getOld', () => {
const urlRegex = /(.*)some_list(.*)$/;
- let mock;
function mockSuccess() {
- mock.onGet(urlRegex).reply(200, {
+ axiosMock.onGet(urlRegex).reply(200, {
count: 0,
html: '',
});
}
function mockError() {
- mock.onGet(urlRegex).networkError();
+ axiosMock.onGet(urlRegex).networkError();
}
beforeEach(() => {
- setFixtures('<div class="content_list" data-href="/some_list"></div><div class="loading"></div>');
+ setFixtures(
+ '<div class="content_list" data-href="/some_list"></div><div class="loading"></div>',
+ );
spyOn(axios, 'get').and.callThrough();
- mock = new MockAdapter(axios);
-
Pager.init();
});
- afterEach(() => {
- mock.restore();
- });
-
- it('shows loader while loading next page', (done) => {
+ it('shows loader while loading next page', done => {
mockSuccess();
spyOn(Pager.loading, 'show');
@@ -87,7 +92,7 @@ describe('pager', () => {
});
});
- it('hides loader on success', (done) => {
+ it('hides loader on success', done => {
mockSuccess();
spyOn(Pager.loading, 'hide');
@@ -100,7 +105,7 @@ describe('pager', () => {
});
});
- it('hides loader on error', (done) => {
+ it('hides loader on error', done => {
mockError();
spyOn(Pager.loading, 'hide');
@@ -113,7 +118,7 @@ describe('pager', () => {
});
});
- it('sends request to url with offset and limit params', (done) => {
+ it('sends request to url with offset and limit params', done => {
Pager.offset = 100;
Pager.limit = 20;
Pager.getOld();
diff --git a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
index a6fe9fb65e9..b69e5f9a3a0 100644
--- a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
+++ b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
@@ -2,7 +2,6 @@ import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import stopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue';
-import * as urlUtility from '~/lib/utils/url_utility';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
@@ -24,7 +23,7 @@ describe('stop_jobs_modal.vue', () => {
describe('onSubmit', () => {
it('stops jobs and redirects to overview page', (done) => {
const responseURL = `${gl.TEST_HOST}/stop_jobs_modal.vue/jobs`;
- const redirectSpy = spyOn(urlUtility, 'redirectTo');
+ const redirectSpy = spyOnDependency(stopJobsModal, 'redirectTo');
spyOn(axios, 'post').and.callFake((url) => {
expect(url).toBe(props.url);
return Promise.resolve({
@@ -44,7 +43,7 @@ describe('stop_jobs_modal.vue', () => {
it('displays error if stopping jobs failed', (done) => {
const dummyError = new Error('stopping jobs failed');
- const redirectSpy = spyOn(urlUtility, 'redirectTo');
+ const redirectSpy = spyOnDependency(stopJobsModal, 'redirectTo');
spyOn(axios, 'post').and.callFake((url) => {
expect(url).toBe(props.url);
return Promise.reject(dummyError);
diff --git a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
index 6074e06fcec..94401beb5c9 100644
--- a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
+++ b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
@@ -3,7 +3,6 @@ import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import deleteMilestoneModal from '~/pages/milestones/shared/components/delete_milestone_modal.vue';
import eventHub from '~/pages/milestones/shared/event_hub';
-import * as urlUtility from '~/lib/utils/url_utility';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
@@ -40,7 +39,7 @@ describe('delete_milestone_modal.vue', () => {
},
});
});
- const redirectSpy = spyOn(urlUtility, 'redirectTo');
+ const redirectSpy = spyOnDependency(deleteMilestoneModal, 'redirectTo');
vm.onSubmit()
.then(() => {
@@ -60,7 +59,7 @@ describe('delete_milestone_modal.vue', () => {
eventHub.$emit.calls.reset();
return Promise.reject(dummyError);
});
- const redirectSpy = spyOn(urlUtility, 'redirectTo');
+ const redirectSpy = spyOnDependency(deleteMilestoneModal, 'redirectTo');
vm.onSubmit()
.catch((error) => {
diff --git a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
index f95a7cef18a..fb7d2763b49 100644
--- a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
+++ b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
@@ -6,7 +6,7 @@ const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
const cookieKey = 'pipeline_schedules_callout_dismissed';
const docsUrl = 'help/ci/scheduled_pipelines';
-describe('Pipeline Schedule Callout', () => {
+describe('Pipeline Schedule Callout', function () {
beforeEach(() => {
setFixtures(`
<div id='pipeline-schedules-callout' data-docs-url=${docsUrl}></div>
diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js
index 581209f215d..3de10392472 100644
--- a/spec/javascripts/pipelines/graph/action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/action_component_spec.js
@@ -6,7 +6,7 @@ import mountComponent from '../../helpers/vue_mount_component_helper';
describe('pipeline graph action component', () => {
let component;
- beforeEach((done) => {
+ beforeEach(done => {
const ActionComponent = Vue.extend(actionComponent);
component = mountComponent(ActionComponent, {
tooltipText: 'bar',
@@ -22,7 +22,7 @@ describe('pipeline graph action component', () => {
});
it('should emit an event with the provided link', () => {
- eventHub.$on('graphAction', (link) => {
+ eventHub.$on('graphAction', link => {
expect(link).toEqual('foo');
});
});
@@ -31,7 +31,7 @@ describe('pipeline graph action component', () => {
expect(component.$el.getAttribute('data-original-title')).toEqual('bar');
});
- it('should update bootstrap tooltip when title changes', (done) => {
+ it('should update bootstrap tooltip when title changes', done => {
component.tooltipText = 'changed';
setTimeout(() => {
@@ -44,4 +44,45 @@ describe('pipeline graph action component', () => {
expect(component.$el.querySelector('.ci-action-icon-wrapper')).toBeDefined();
expect(component.$el.querySelector('svg')).toBeDefined();
});
+
+ it('disables the button when clicked', done => {
+ component.$el.click();
+
+ component.$nextTick(() => {
+ expect(component.$el.getAttribute('disabled')).toEqual('disabled');
+ done();
+ });
+ });
+
+ it('re-enabled the button when `requestFinishedFor` matches `linkRequested`', done => {
+ component.$el.click();
+
+ component
+ .$nextTick()
+ .then(() => {
+ expect(component.$el.getAttribute('disabled')).toEqual('disabled');
+ component.requestFinishedFor = 'foo';
+ })
+ .then(() => {
+ expect(component.$el.getAttribute('disabled')).toBeNull();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not re-enable the button when `requestFinishedFor` does not matches `linkRequested`', done => {
+ component.$el.click();
+
+ component
+ .$nextTick()
+ .then(() => {
+ expect(component.$el.getAttribute('disabled')).toEqual('disabled');
+ component.requestFinishedFor = 'bar';
+ })
+ .then(() => {
+ expect(component.$el.getAttribute('disabled')).toEqual('disabled');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
diff --git a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
deleted file mode 100644
index ba721bc53c6..00000000000
--- a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import Vue from 'vue';
-import dropdownActionComponent from '~/pipelines/components/graph/dropdown_action_component.vue';
-
-describe('action component', () => {
- let component;
-
- beforeEach((done) => {
- const DropdownActionComponent = Vue.extend(dropdownActionComponent);
- component = new DropdownActionComponent({
- propsData: {
- tooltipText: 'bar',
- link: 'foo',
- actionMethod: 'post',
- actionIcon: 'cancel',
- },
- }).$mount();
-
- Vue.nextTick(done);
- });
-
- it('should render a link', () => {
- expect(component.$el.getAttribute('href')).toEqual('foo');
- });
-
- it('should render the provided title as a bootstrap tooltip', () => {
- expect(component.$el.getAttribute('data-original-title')).toEqual('bar');
- });
-
- it('should render an svg', () => {
- expect(component.$el.querySelector('svg')).toBeDefined();
- });
-});
diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js
index c9677ae209a..073dae56c25 100644
--- a/spec/javascripts/pipelines/graph/job_component_spec.js
+++ b/spec/javascripts/pipelines/graph/job_component_spec.js
@@ -93,17 +93,6 @@ describe('pipeline graph job component', () => {
});
});
- describe('dropdown', () => {
- it('should render the dropdown action icon', () => {
- component = mountComponent(JobComponent, {
- job: mockJob,
- isDropdown: true,
- });
-
- expect(component.$el.querySelector('a.ci-action-icon-wrapper')).toBeDefined();
- });
- });
-
it('should render provided class name', () => {
component = mountComponent(JobComponent, {
job: mockJob,
diff --git a/spec/javascripts/pipelines/mock_data.js b/spec/javascripts/pipelines/mock_data.js
new file mode 100644
index 00000000000..59092e0f041
--- /dev/null
+++ b/spec/javascripts/pipelines/mock_data.js
@@ -0,0 +1,326 @@
+export const pipelineWithStages = {
+ id: 20333396,
+ user: {
+ id: 128633,
+ name: 'Rémy Coutable',
+ username: 'rymai',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon',
+ web_url: 'https://gitlab.com/rymai',
+ path: '/rymai',
+ },
+ active: true,
+ coverage: '58.24',
+ source: 'push',
+ created_at: '2018-04-11T14:04:53.881Z',
+ updated_at: '2018-04-11T14:05:00.792Z',
+ path: '/gitlab-org/gitlab-ee/pipelines/20333396',
+ flags: {
+ latest: true,
+ stuck: false,
+ auto_devops: false,
+ yaml_errors: false,
+ retryable: false,
+ cancelable: true,
+ failure_reason: false,
+ },
+ details: {
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-ee/pipelines/20333396',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_running-2eb56be2871937954b2ba6d6f4ee9fdf7e5e1c146ac45f7be98119ccaca1aca9.ico',
+ },
+ duration: null,
+ finished_at: null,
+ stages: [
+ {
+ name: 'build',
+ title: 'build: skipped',
+ status: {
+ icon: 'status_skipped',
+ text: 'skipped',
+ label: 'skipped',
+ group: 'skipped',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-ee/pipelines/20333396#build',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_skipped-a2eee568a5bffdb494050c7b62dde241de9189280836288ac8923d369f16222d.ico',
+ },
+ path: '/gitlab-org/gitlab-ee/pipelines/20333396#build',
+ dropdown_path: '/gitlab-org/gitlab-ee/pipelines/20333396/stage.json?stage=build',
+ },
+ {
+ name: 'prepare',
+ title: 'prepare: passed',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-ee/pipelines/20333396#prepare',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_success-26f59841becbef8c6fe414e9e74471d8bfd6a91b5855c19fe7f5923a40a7da47.ico',
+ },
+ path: '/gitlab-org/gitlab-ee/pipelines/20333396#prepare',
+ dropdown_path: '/gitlab-org/gitlab-ee/pipelines/20333396/stage.json?stage=prepare',
+ },
+ {
+ name: 'test',
+ title: 'test: running',
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-ee/pipelines/20333396#test',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_running-2eb56be2871937954b2ba6d6f4ee9fdf7e5e1c146ac45f7be98119ccaca1aca9.ico',
+ },
+ path: '/gitlab-org/gitlab-ee/pipelines/20333396#test',
+ dropdown_path: '/gitlab-org/gitlab-ee/pipelines/20333396/stage.json?stage=test',
+ },
+ {
+ name: 'post-test',
+ title: 'post-test: created',
+ status: {
+ icon: 'status_created',
+ text: 'created',
+ label: 'created',
+ group: 'created',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-ee/pipelines/20333396#post-test',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico',
+ },
+ path: '/gitlab-org/gitlab-ee/pipelines/20333396#post-test',
+ dropdown_path: '/gitlab-org/gitlab-ee/pipelines/20333396/stage.json?stage=post-test',
+ },
+ {
+ name: 'pages',
+ title: 'pages: created',
+ status: {
+ icon: 'status_created',
+ text: 'created',
+ label: 'created',
+ group: 'created',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-ee/pipelines/20333396#pages',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico',
+ },
+ path: '/gitlab-org/gitlab-ee/pipelines/20333396#pages',
+ dropdown_path: '/gitlab-org/gitlab-ee/pipelines/20333396/stage.json?stage=pages',
+ },
+ {
+ name: 'post-cleanup',
+ title: 'post-cleanup: created',
+ status: {
+ icon: 'status_created',
+ text: 'created',
+ label: 'created',
+ group: 'created',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-ee/pipelines/20333396#post-cleanup',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico',
+ },
+ path: '/gitlab-org/gitlab-ee/pipelines/20333396#post-cleanup',
+ dropdown_path: '/gitlab-org/gitlab-ee/pipelines/20333396/stage.json?stage=post-cleanup',
+ },
+ ],
+ artifacts: [
+ {
+ name: 'gitlab:assets:compile',
+ expired: false,
+ expire_at: '2018-05-12T14:22:54.730Z',
+ path: '/gitlab-org/gitlab-ee/-/jobs/62411438/artifacts/download',
+ keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411438/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411438/artifacts/browse',
+ },
+ {
+ name: 'rspec-mysql 12 28',
+ expired: false,
+ expire_at: '2018-05-12T14:22:45.136Z',
+ path: '/gitlab-org/gitlab-ee/-/jobs/62411397/artifacts/download',
+ keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411397/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411397/artifacts/browse',
+ },
+ {
+ name: 'rspec-mysql 6 28',
+ expired: false,
+ expire_at: '2018-05-12T14:22:41.523Z',
+ path: '/gitlab-org/gitlab-ee/-/jobs/62411391/artifacts/download',
+ keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411391/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411391/artifacts/browse',
+ },
+ {
+ name: 'rspec-pg geo 0 1',
+ expired: false,
+ expire_at: '2018-05-12T14:22:13.287Z',
+ path: '/gitlab-org/gitlab-ee/-/jobs/62411353/artifacts/download',
+ keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411353/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411353/artifacts/browse',
+ },
+ {
+ name: 'rspec-mysql 0 28',
+ expired: false,
+ expire_at: '2018-05-12T14:22:06.834Z',
+ path: '/gitlab-org/gitlab-ee/-/jobs/62411385/artifacts/download',
+ keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411385/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411385/artifacts/browse',
+ },
+ {
+ name: 'spinach-mysql 0 2',
+ expired: false,
+ expire_at: '2018-05-12T14:21:51.409Z',
+ path: '/gitlab-org/gitlab-ee/-/jobs/62411423/artifacts/download',
+ keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411423/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411423/artifacts/browse',
+ },
+ {
+ name: 'karma',
+ expired: false,
+ expire_at: '2018-05-12T14:21:20.934Z',
+ path: '/gitlab-org/gitlab-ee/-/jobs/62411440/artifacts/download',
+ keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411440/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411440/artifacts/browse',
+ },
+ {
+ name: 'spinach-pg 0 2',
+ expired: false,
+ expire_at: '2018-05-12T14:20:01.028Z',
+ path: '/gitlab-org/gitlab-ee/-/jobs/62411419/artifacts/download',
+ keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411419/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411419/artifacts/browse',
+ },
+ {
+ name: 'spinach-pg 1 2',
+ expired: false,
+ expire_at: '2018-05-12T14:19:04.336Z',
+ path: '/gitlab-org/gitlab-ee/-/jobs/62411421/artifacts/download',
+ keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411421/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411421/artifacts/browse',
+ },
+ {
+ name: 'sast',
+ expired: null,
+ expire_at: null,
+ path: '/gitlab-org/gitlab-ee/-/jobs/62411442/artifacts/download',
+ browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411442/artifacts/browse',
+ },
+ {
+ name: 'codequality',
+ expired: false,
+ expire_at: '2018-04-18T14:16:24.484Z',
+ path: '/gitlab-org/gitlab-ee/-/jobs/62411441/artifacts/download',
+ keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411441/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411441/artifacts/browse',
+ },
+ {
+ name: 'cache gems',
+ expired: null,
+ expire_at: null,
+ path: '/gitlab-org/gitlab-ee/-/jobs/62411447/artifacts/download',
+ browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411447/artifacts/browse',
+ },
+ {
+ name: 'dependency_scanning',
+ expired: null,
+ expire_at: null,
+ path: '/gitlab-org/gitlab-ee/-/jobs/62411443/artifacts/download',
+ browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411443/artifacts/browse',
+ },
+ {
+ name: 'compile-assets',
+ expired: false,
+ expire_at: '2018-04-18T14:12:07.638Z',
+ path: '/gitlab-org/gitlab-ee/-/jobs/62411334/artifacts/download',
+ keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411334/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411334/artifacts/browse',
+ },
+ {
+ name: 'setup-test-env',
+ expired: false,
+ expire_at: '2018-04-18T14:10:27.024Z',
+ path: '/gitlab-org/gitlab-ee/-/jobs/62411336/artifacts/download',
+ keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411336/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411336/artifacts/browse',
+ },
+ {
+ name: 'retrieve-tests-metadata',
+ expired: false,
+ expire_at: '2018-05-12T14:06:35.926Z',
+ path: '/gitlab-org/gitlab-ee/-/jobs/62411333/artifacts/download',
+ keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411333/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411333/artifacts/browse',
+ },
+ ],
+ manual_actions: [
+ {
+ name: 'package-and-qa',
+ path: '/gitlab-org/gitlab-ee/-/jobs/62411330/play',
+ playable: true,
+ },
+ {
+ name: 'review-docs-deploy',
+ path: '/gitlab-org/gitlab-ee/-/jobs/62411332/play',
+ playable: true,
+ },
+ ],
+ },
+ ref: {
+ name: 'master',
+ path: '/gitlab-org/gitlab-ee/commits/master',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'e6a2885c503825792cb8a84a8731295e361bd059',
+ short_id: 'e6a2885c',
+ title: "Merge branch 'ce-to-ee-2018-04-11' into 'master'",
+ created_at: '2018-04-11T14:04:39.000Z',
+ parent_ids: [
+ '5d9b5118f6055f72cff1a82b88133609912f2c1d',
+ '6fdc6ee76a8062fe41b1a33f7c503334a6ebdc02',
+ ],
+ message:
+ "Merge branch 'ce-to-ee-2018-04-11' into 'master'\n\nCE upstream - 2018-04-11 12:26 UTC\n\nSee merge request gitlab-org/gitlab-ee!5326",
+ author_name: 'Rémy Coutable',
+ author_email: 'remy@rymai.me',
+ authored_date: '2018-04-11T14:04:39.000Z',
+ committer_name: 'Rémy Coutable',
+ committer_email: 'remy@rymai.me',
+ committed_date: '2018-04-11T14:04:39.000Z',
+ author: {
+ id: 128633,
+ name: 'Rémy Coutable',
+ username: 'rymai',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon',
+ web_url: 'https://gitlab.com/rymai',
+ path: '/rymai',
+ },
+ author_gravatar_url:
+ 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon',
+ commit_url:
+ 'https://gitlab.com/gitlab-org/gitlab-ee/commit/e6a2885c503825792cb8a84a8731295e361bd059',
+ commit_path: '/gitlab-org/gitlab-ee/commit/e6a2885c503825792cb8a84a8731295e361bd059',
+ },
+ cancel_path: '/gitlab-org/gitlab-ee/pipelines/20333396/cancel',
+ triggered_by: null,
+ triggered: [],
+};
+
+export const stageReply = {
+ html:
+ '\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="karma - failed \u0026lt;br\u0026gt; (script failure)" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62402048"\u003e\u003cspan class="ci-status-icon ci-status-icon-failed"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_failed"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003ekarma\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62402048/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="codequality - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398081"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003ecodequality\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398081/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:check-schema-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398066"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:check-schema-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398066/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:migrate:reset-mysql - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398065"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:migrate:reset-mysql\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398065/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:migrate:reset-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398064"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:migrate:reset-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398064/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:rollback-mysql - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398070"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:rollback-mysql\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398070/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:rollback-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398069"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:rollback-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398069/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="dependency_scanning - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398083"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edependency_scanning\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398083/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="docs lint - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398061"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edocs lint\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398061/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="downtime_check - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398062"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edowntime_check\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398062/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="ee_compat_check - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398063"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003eee_compat_check\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398063/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="gitlab:assets:compile - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398075"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003egitlab:assets:compile\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398075/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="gitlab:setup-mysql - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398073"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003egitlab:setup-mysql\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398073/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="gitlab:setup-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398071"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003egitlab:setup-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398071/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="gitlab_git_test - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398086"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003egitlab_git_test\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398086/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="migration:path-mysql - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398068"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003emigration:path-mysql\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398068/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="migration:path-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398067"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003emigration:path-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398067/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="qa:internal - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398084"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003eqa:internal\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398084/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="qa:selectors - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398085"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003eqa:selectors\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398085/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 0 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398020"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 0 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398020/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 1 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398022"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 1 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398022/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 10 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398033"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 10 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398033/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 11 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398034"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 11 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398034/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 12 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398035"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 12 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398035/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 13 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398036"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 13 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398036/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 14 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398037"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 14 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398037/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 15 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398038"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 15 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398038/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 16 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398039"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 16 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398039/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 17 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398040"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 17 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398040/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 18 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398041"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 18 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398041/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 19 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398042"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 19 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398042/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 2 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398024"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 2 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398024/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 20 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398043"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 20 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398043/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 21 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398044"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 21 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398044/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 22 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398046"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 22 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398046/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 23 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398047"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 23 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398047/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 24 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398048"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 24 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398048/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 25 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398049"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 25 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398049/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 26 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398050"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 26 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398050/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 27 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398051"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 27 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398051/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 3 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398025"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 3 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398025/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 4 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398027"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 4 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398027/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 5 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398028"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 5 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398028/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 6 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398029"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 6 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398029/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 7 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398030"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 7 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398030/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 8 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398031"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 8 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398031/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 9 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398032"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 9 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398032/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 0 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397981"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 0 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397981/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 1 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397985"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 1 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397985/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 10 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398000"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 10 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398000/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 11 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398001"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 11 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398001/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 12 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398002"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 12 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398002/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 13 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398003"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 13 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398003/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 14 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398004"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 14 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398004/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 15 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398006"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 15 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398006/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 16 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398007"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 16 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398007/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 17 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398008"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 17 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398008/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 18 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398009"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 18 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398009/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 19 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398010"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 19 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398010/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 2 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397986"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 2 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397986/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 20 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398012"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 20 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398012/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 21 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398013"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 21 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398013/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 22 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398014"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 22 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398014/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 23 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398015"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 23 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398015/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 24 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398016"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 24 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398016/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 25 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398017"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 25 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398017/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 26 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398018"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 26 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398018/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 27 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398019"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 27 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398019/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 3 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397988"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 3 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397988/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 4 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397989"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 4 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397989/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 5 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397991"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 5 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397991/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 6 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397993"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 6 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397993/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 7 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397994"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 7 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397994/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 8 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397995"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 8 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397995/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 9 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397996"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 9 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397996/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="sast - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398082"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003esast\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398082/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="spinach-mysql 0 2 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398058"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003espinach-mysql 0 2\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398058/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="spinach-mysql 1 2 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398059"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003espinach-mysql 1 2\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398059/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="spinach-pg 0 2 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398053"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003espinach-pg 0 2\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398053/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="spinach-pg 1 2 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398056"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003espinach-pg 1 2\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398056/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="static-analysis - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398060"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003estatic-analysis\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398060/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n',
+};
diff --git a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js
index e58a8018ed5..61ee2dc13ca 100644
--- a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js
+++ b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js
@@ -1,42 +1,36 @@
-import _ from 'underscore';
-import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import PipelineMediator from '~/pipelines/pipeline_details_mediator';
describe('PipelineMdediator', () => {
let mediator;
+ let mock;
+
beforeEach(() => {
- mediator = new PipelineMediator({ endpoint: 'foo' });
+ mock = new MockAdapter(axios);
+ mediator = new PipelineMediator({ endpoint: 'foo.json' });
+ });
+
+ afterEach(() => {
+ mock.restore();
});
it('should set defaults', () => {
- expect(mediator.options).toEqual({ endpoint: 'foo' });
+ expect(mediator.options).toEqual({ endpoint: 'foo.json' });
expect(mediator.state.isLoading).toEqual(false);
expect(mediator.store).toBeDefined();
expect(mediator.service).toBeDefined();
});
describe('request and store data', () => {
- const interceptor = (request, next) => {
- next(request.respondWith(JSON.stringify({ foo: 'bar' }), {
- status: 200,
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(interceptor);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptor, interceptor);
- });
-
- it('should store received data', (done) => {
+ it('should store received data', done => {
+ mock.onGet('foo.json').reply(200, { id: '121123' });
mediator.fetchPipeline();
setTimeout(() => {
- expect(mediator.store.state.pipeline).toEqual({ foo: 'bar' });
+ expect(mediator.store.state.pipeline).toEqual({ id: '121123' });
done();
- });
+ }, 0);
});
});
});
diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js
index d79544f83ad..ff17602da2b 100644
--- a/spec/javascripts/pipelines/pipelines_spec.js
+++ b/spec/javascripts/pipelines/pipelines_spec.js
@@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import pipelinesComp from '~/pipelines/components/pipelines.vue';
import Store from '~/pipelines/stores/pipelines_store';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { pipelineWithStages, stageReply } from './mock_data';
describe('Pipelines', () => {
const jsonFixtureName = 'pipelines/pipelines.json';
@@ -668,4 +669,79 @@ describe('Pipelines', () => {
});
});
});
+
+ describe('updates results when a staged is clicked', () => {
+ beforeEach(() => {
+ const copyPipeline = Object.assign({}, pipelineWithStages);
+ copyPipeline.id += 1;
+ mock
+ .onGet('twitter/flight/pipelines.json').reply(200, {
+ pipelines: [pipelineWithStages],
+ count: {
+ all: 1,
+ finished: 1,
+ pending: 0,
+ running: 0,
+ },
+ }, {
+ 'POLL-INTERVAL': 100,
+ })
+ .onGet(pipelineWithStages.details.stages[0].dropdown_path)
+ .reply(200, stageReply);
+
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: true,
+ canCreatePipeline: true,
+ ...paths,
+ });
+ });
+
+ describe('when a request is being made', () => {
+ it('stops polling, cancels the request, fetches pipelines & restarts polling', (done) => {
+ spyOn(vm.poll, 'stop');
+ spyOn(vm.poll, 'restart');
+ spyOn(vm, 'getPipelines').and.returnValue(Promise.resolve());
+ spyOn(vm.service.cancelationSource, 'cancel').and.callThrough();
+
+ setTimeout(() => {
+ vm.isMakingRequest = true;
+ return vm.$nextTick()
+ .then(() => {
+ vm.$el.querySelector('.js-builds-dropdown-button').click();
+ })
+ .then(() => {
+ expect(vm.service.cancelationSource.cancel).toHaveBeenCalled();
+ expect(vm.poll.stop).toHaveBeenCalled();
+
+ setTimeout(() => {
+ expect(vm.getPipelines).toHaveBeenCalled();
+ expect(vm.poll.restart).toHaveBeenCalled();
+ done();
+ }, 0);
+ });
+ }, 0);
+ });
+ });
+
+ describe('when no request is being made', () => {
+ it('stops polling, fetches pipelines & restarts polling', (done) => {
+ spyOn(vm.poll, 'stop');
+ spyOn(vm.poll, 'restart');
+ spyOn(vm, 'getPipelines').and.returnValue(Promise.resolve());
+
+ setTimeout(() => {
+ vm.$el.querySelector('.js-builds-dropdown-button').click();
+
+ expect(vm.poll.stop).toHaveBeenCalled();
+
+ setTimeout(() => {
+ expect(vm.getPipelines).toHaveBeenCalled();
+ expect(vm.poll.restart).toHaveBeenCalled();
+ done();
+ }, 0);
+ }, 0);
+ });
+ });
+ });
});
diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js
index 61c2f783acc..be1632e7206 100644
--- a/spec/javascripts/pipelines/stage_spec.js
+++ b/spec/javascripts/pipelines/stage_spec.js
@@ -1,27 +1,36 @@
-import _ from 'underscore';
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import stage from '~/pipelines/components/stage.vue';
+import eventHub from '~/pipelines/event_hub';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Pipelines stage component', () => {
let StageComponent;
let component;
+ let mock;
beforeEach(() => {
+ mock = new MockAdapter(axios);
+
StageComponent = Vue.extend(stage);
- component = new StageComponent({
- propsData: {
- stage: {
- status: {
- group: 'success',
- icon: 'icon_status_success',
- title: 'success',
- },
- dropdown_path: 'foo',
+ component = mountComponent(StageComponent, {
+ stage: {
+ status: {
+ group: 'success',
+ icon: 'icon_status_success',
+ title: 'success',
},
- updateDropdown: false,
+ dropdown_path: 'path.json',
},
- }).$mount();
+ updateDropdown: false,
+ });
+ });
+
+ afterEach(() => {
+ component.$destroy();
+ mock.restore();
});
it('should render a dropdown with the status icon', () => {
@@ -31,49 +40,27 @@ describe('Pipelines stage component', () => {
});
describe('with successfull request', () => {
- const interceptor = (request, next) => {
- next(request.respondWith(JSON.stringify({ html: 'foo' }), {
- status: 200,
- }));
- };
-
beforeEach(() => {
- Vue.http.interceptors.push(interceptor);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, interceptor,
- );
+ mock.onGet('path.json').reply(200, { html: 'foo' });
});
- it('should render the received data', (done) => {
+ it('should render the received data and emit `clickedDropdown` event', done => {
+ spyOn(eventHub, '$emit');
component.$el.querySelector('button').click();
setTimeout(() => {
expect(
component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(),
).toEqual('foo');
+ expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
done();
}, 0);
});
});
describe('when request fails', () => {
- const interceptor = (request, next) => {
- next(request.respondWith(JSON.stringify({}), {
- status: 500,
- }));
- };
-
beforeEach(() => {
- Vue.http.interceptors.push(interceptor);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, interceptor,
- );
+ mock.onGet('path.json').reply(500);
});
it('should close the dropdown', () => {
@@ -86,33 +73,18 @@ describe('Pipelines stage component', () => {
});
describe('update endpoint correctly', () => {
- const updatedInterceptor = (request, next) => {
- if (request.url === 'bar') {
- next(request.respondWith(JSON.stringify({ html: 'this is the updated content' }), {
- status: 200,
- }));
- }
- next();
- };
-
beforeEach(() => {
- Vue.http.interceptors.push(updatedInterceptor);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, updatedInterceptor,
- );
+ mock.onGet('bar.json').reply(200, { html: 'this is the updated content' });
});
- it('should update the stage to request the new endpoint provided', (done) => {
+ it('should update the stage to request the new endpoint provided', done => {
component.stage = {
status: {
group: 'running',
icon: 'running',
title: 'running',
},
- dropdown_path: 'bar',
+ dropdown_path: 'bar.json',
};
Vue.nextTick(() => {
@@ -121,7 +93,7 @@ describe('Pipelines stage component', () => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(),
- ).toEqual('this is the updated content');
+ ).toEqual('this is the updated content');
done();
});
});
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 80770a61011..e264b16335f 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -9,8 +9,6 @@ import Sidebar from '~/right_sidebar';
(function() {
var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState;
- this.sidebar = null;
-
$aside = null;
$toggle = null;
@@ -43,7 +41,7 @@ import Sidebar from '~/right_sidebar';
beforeEach(function() {
loadFixtures(fixtureName);
mock = new MockAdapter(axios);
- this.sidebar = new Sidebar();
+ new Sidebar(); // eslint-disable-line no-new
$aside = $('.right-sidebar');
$page = $('.layout-page');
$icon = $aside.find('i');
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 1a27955983d..4f515f98a7e 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -4,7 +4,6 @@ import $ from 'jquery';
import '~/gl_dropdown';
import SearchAutocomplete from '~/search_autocomplete';
import '~/lib/utils/common_utils';
-import * as urlUtils from '~/lib/utils/url_utility';
describe('Search autocomplete dropdown', () => {
var assertLinks,
@@ -129,9 +128,6 @@ describe('Search autocomplete dropdown', () => {
beforeEach(function() {
loadFixtures('static/search_autocomplete.html.raw');
- // Prevent turbolinks from triggering within gl_dropdown
- spyOn(urlUtils, 'visitUrl').and.returnValue(true);
-
window.gon = {};
window.gon.current_user_id = userId;
window.gon.current_username = userName;
diff --git a/spec/javascripts/settings_panels_spec.js b/spec/javascripts/settings_panels_spec.js
index d433f8c3e07..4fba36bd4de 100644
--- a/spec/javascripts/settings_panels_spec.js
+++ b/spec/javascripts/settings_panels_spec.js
@@ -13,9 +13,9 @@ describe('Settings Panels', () => {
});
it('should expand linked hash fragment panel', () => {
- location.hash = '#js-general-pipeline-settings';
+ location.hash = '#autodevops-settings';
- const pipelineSettingsPanel = document.querySelector('#js-general-pipeline-settings');
+ const pipelineSettingsPanel = document.querySelector('#autodevops-settings');
// Our test environment automatically expands everything so we need to clear that out first
pipelineSettingsPanel.classList.remove('expanded');
diff --git a/spec/javascripts/shared/popover_spec.js b/spec/javascripts/shared/popover_spec.js
new file mode 100644
index 00000000000..1d574c9424b
--- /dev/null
+++ b/spec/javascripts/shared/popover_spec.js
@@ -0,0 +1,162 @@
+import $ from 'jquery';
+import {
+ togglePopover,
+ mouseleave,
+ mouseenter,
+} from '~/shared/popover';
+
+describe('popover', () => {
+ describe('togglePopover', () => {
+ describe('togglePopover(true)', () => {
+ it('returns true when popover is shown', () => {
+ const context = {
+ hasClass: () => false,
+ popover: () => {},
+ toggleClass: () => {},
+ };
+
+ expect(togglePopover.call(context, true)).toEqual(true);
+ });
+
+ it('returns false when popover is already shown', () => {
+ const context = {
+ hasClass: () => true,
+ };
+
+ expect(togglePopover.call(context, true)).toEqual(false);
+ });
+
+ it('shows popover', (done) => {
+ const context = {
+ hasClass: () => false,
+ popover: () => {},
+ toggleClass: () => {},
+ };
+
+ spyOn(context, 'popover').and.callFake((method) => {
+ expect(method).toEqual('show');
+ done();
+ });
+
+ togglePopover.call(context, true);
+ });
+
+ it('adds disable-animation and js-popover-show class', (done) => {
+ const context = {
+ hasClass: () => false,
+ popover: () => {},
+ toggleClass: () => {},
+ };
+
+ spyOn(context, 'toggleClass').and.callFake((classNames, show) => {
+ expect(classNames).toEqual('disable-animation js-popover-show');
+ expect(show).toEqual(true);
+ done();
+ });
+
+ togglePopover.call(context, true);
+ });
+ });
+
+ describe('togglePopover(false)', () => {
+ it('returns true when popover is hidden', () => {
+ const context = {
+ hasClass: () => true,
+ popover: () => {},
+ toggleClass: () => {},
+ };
+
+ expect(togglePopover.call(context, false)).toEqual(true);
+ });
+
+ it('returns false when popover is already hidden', () => {
+ const context = {
+ hasClass: () => false,
+ };
+
+ expect(togglePopover.call(context, false)).toEqual(false);
+ });
+
+ it('hides popover', (done) => {
+ const context = {
+ hasClass: () => true,
+ popover: () => {},
+ toggleClass: () => {},
+ };
+
+ spyOn(context, 'popover').and.callFake((method) => {
+ expect(method).toEqual('hide');
+ done();
+ });
+
+ togglePopover.call(context, false);
+ });
+
+ it('removes disable-animation and js-popover-show class', (done) => {
+ const context = {
+ hasClass: () => true,
+ popover: () => {},
+ toggleClass: () => {},
+ };
+
+ spyOn(context, 'toggleClass').and.callFake((classNames, show) => {
+ expect(classNames).toEqual('disable-animation js-popover-show');
+ expect(show).toEqual(false);
+ done();
+ });
+
+ togglePopover.call(context, false);
+ });
+ });
+ });
+
+ describe('mouseleave', () => {
+ it('calls hide popover if .popover:hover is false', () => {
+ const fakeJquery = {
+ length: 0,
+ };
+
+ spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
+ spyOn(togglePopover, 'call');
+ mouseleave();
+ expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), false);
+ });
+
+ it('does not call hide popover if .popover:hover is true', () => {
+ const fakeJquery = {
+ length: 1,
+ };
+
+ spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
+ spyOn(togglePopover, 'call');
+ mouseleave();
+ expect(togglePopover.call).not.toHaveBeenCalledWith(false);
+ });
+ });
+
+ describe('mouseenter', () => {
+ const context = {};
+
+ it('shows popover', () => {
+ spyOn(togglePopover, 'call').and.returnValue(false);
+ mouseenter.call(context);
+ expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), true);
+ });
+
+ it('registers mouseleave event if popover is showed', (done) => {
+ spyOn(togglePopover, 'call').and.returnValue(true);
+ spyOn($.fn, 'on').and.callFake((eventName) => {
+ expect(eventName).toEqual('mouseleave');
+ done();
+ });
+ mouseenter.call(context);
+ });
+
+ it('does not register mouseleave event if popover is not showed', () => {
+ spyOn(togglePopover, 'call').and.returnValue(false);
+ const spy = spyOn($.fn, 'on').and.callFake(() => {});
+ mouseenter.call(context);
+ expect(spy).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/shortcuts_dashboard_navigation_spec.js b/spec/javascripts/shortcuts_dashboard_navigation_spec.js
new file mode 100644
index 00000000000..7cb201e01d8
--- /dev/null
+++ b/spec/javascripts/shortcuts_dashboard_navigation_spec.js
@@ -0,0 +1,23 @@
+import findAndFollowLink from '~/shortcuts_dashboard_navigation';
+
+describe('findAndFollowLink', () => {
+ it('visits a link when the selector exists', () => {
+ const href = '/some/path';
+ const visitUrl = spyOnDependency(findAndFollowLink, 'visitUrl');
+
+ setFixtures(`<a class="my-shortcut" href="${href}">link</a>`);
+
+ findAndFollowLink('.my-shortcut');
+
+ expect(visitUrl).toHaveBeenCalledWith(href);
+ });
+
+ it('does not throw an exception when the selector does not exist', () => {
+ const visitUrl = spyOnDependency(findAndFollowLink, 'visitUrl');
+
+ // this should not throw an exception
+ findAndFollowLink('.this-selector-does-not-exist');
+
+ expect(visitUrl).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index b0d714cbefb..d73608ed0ed 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -4,7 +4,7 @@ import ShortcutsIssuable from '~/shortcuts_issuable';
initCopyAsGFM();
-describe('ShortcutsIssuable', () => {
+describe('ShortcutsIssuable', function () {
const fixtureName = 'merge_requests/diff_comment.html.raw';
preloadFixtures(fixtureName);
beforeEach(() => {
diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js
index afa18cc127e..da950258a94 100644
--- a/spec/javascripts/sidebar/sidebar_mediator_spec.js
+++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js
@@ -1,12 +1,11 @@
import _ from 'underscore';
import Vue from 'vue';
-import * as urlUtils from '~/lib/utils/url_utility';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import SidebarService from '~/sidebar/services/sidebar_service';
import Mock from './mock_data';
-describe('Sidebar mediator', () => {
+describe('Sidebar mediator', function() {
beforeEach(() => {
Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
this.mediator = new SidebarMediator(Mock.mediator);
@@ -87,12 +86,12 @@ describe('Sidebar mediator', () => {
const moveToProjectId = 7;
this.mediator.store.setMoveToProjectId(moveToProjectId);
spyOn(this.mediator.service, 'moveIssue').and.callThrough();
- spyOn(urlUtils, 'visitUrl');
+ const visitUrl = spyOnDependency(SidebarMediator, 'visitUrl');
this.mediator.moveIssue()
.then(() => {
expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId);
- expect(urlUtils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5');
+ expect(visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5');
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js
index d8e636cbdf0..a3fb965fbab 100644
--- a/spec/javascripts/sidebar/sidebar_move_issue_spec.js
+++ b/spec/javascripts/sidebar/sidebar_move_issue_spec.js
@@ -7,7 +7,7 @@ import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
import Mock from './mock_data';
-describe('SidebarMoveIssue', () => {
+describe('SidebarMoveIssue', function () {
beforeEach(() => {
Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
this.mediator = new SidebarMediator(Mock.mediator);
diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js
index 3591f96ff87..08b112a54ba 100644
--- a/spec/javascripts/sidebar/sidebar_store_spec.js
+++ b/spec/javascripts/sidebar/sidebar_store_spec.js
@@ -31,7 +31,7 @@ const PARTICIPANT_LIST = [
{ ...PARTICIPANT, id: 3 },
];
-describe('Sidebar store', () => {
+describe('Sidebar store', function () {
beforeEach(() => {
this.store = new SidebarStore({
currentUser: {
diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js b/spec/javascripts/signin_tabs_memoizer_spec.js
index b1b03ef1e09..423432c9e5d 100644
--- a/spec/javascripts/signin_tabs_memoizer_spec.js
+++ b/spec/javascripts/signin_tabs_memoizer_spec.js
@@ -4,7 +4,7 @@ import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer';
(() => {
describe('SigninTabsMemoizer', () => {
const fixtureTemplate = 'static/signin_tabs.html.raw';
- const tabSelector = 'ul.nav-tabs';
+ const tabSelector = 'ul.new-session-tabs';
const currentTabKey = 'current_signin_tab';
let memo;
@@ -27,7 +27,7 @@ import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer';
it('does nothing if no tab was previously selected', () => {
createMemoizer();
- expect(document.querySelector('li a.active').getAttribute('id')).toEqual('standard');
+ expect(document.querySelector(`${tabSelector} > li.active a`).getAttribute('href')).toEqual('#ldap');
});
it('shows last selected tab on boot', () => {
@@ -48,9 +48,9 @@ import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer';
it('saves last selected tab on change', () => {
createMemoizer();
- document.getElementById('standard').click();
+ document.querySelector('a[href="#login-pane"]').click();
- expect(memo.readData()).toEqual('#standard');
+ expect(memo.readData()).toEqual('#login-pane');
});
it('overrides last selected tab with hash tag when given', () => {
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index d158786e484..bcd15f5eae2 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -1,10 +1,12 @@
-/* eslint-disable jasmine/no-global-setup */
+/* eslint-disable jasmine/no-global-setup, jasmine/no-unsafe-spy, no-underscore-dangle */
+
import $ from 'jquery';
import 'vendor/jasmine-jquery';
import '~/commons';
import Vue from 'vue';
import VueResource from 'vue-resource';
+import Translate from '~/vue_shared/translate';
import { getDefaultAdapter } from '~/lib/utils/axios_utils';
import { FIXTURES_PATH, TEST_HOST } from './test_constants';
@@ -22,12 +24,13 @@ Vue.config.warnHandler = (msg, vm, trace) => {
};
let hasVueErrors = false;
-Vue.config.errorHandler = function (err) {
+Vue.config.errorHandler = function(err) {
hasVueErrors = true;
fail(err);
};
Vue.use(VueResource);
+Vue.use(Translate);
// enable test fixtures
jasmine.getFixtures().fixturesPath = FIXTURES_PATH;
@@ -43,15 +46,27 @@ window.gl = window.gl || {};
window.gl.TEST_HOST = TEST_HOST;
window.gon = window.gon || {};
window.gon.test_env = true;
+gon.relative_url_root = '';
let hasUnhandledPromiseRejections = false;
-window.addEventListener('unhandledrejection', (event) => {
+window.addEventListener('unhandledrejection', event => {
hasUnhandledPromiseRejections = true;
console.error('Unhandled promise rejection:');
console.error(event.reason.stack || event.reason);
});
+// Add global function to spy on a module's dependencies via rewire
+window.spyOnDependency = (module, name) => {
+ const dependency = module.__GetDependency__(name);
+ const spy = jasmine.createSpy(name, dependency);
+ module.__Rewire__(name, spy);
+ return spy;
+};
+
+// Reset any rewired modules after each test (see babel-plugin-rewire)
+afterEach(__rewire_reset_all__); // eslint-disable-line
+
// 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
@@ -69,15 +84,25 @@ beforeEach(() => {
const axiosDefaultAdapter = getDefaultAdapter();
+let testFiles = process.env.TEST_FILES || [];
+if (testFiles.length > 0) {
+ testFiles = testFiles.map(path => path.replace(/^spec\/javascripts\//, '').replace(/\.js$/, ''));
+ console.log(`Running only tests matching: ${testFiles}`);
+} else {
+ console.log('Running all tests');
+}
+
// render all of our tests
const testsContext = require.context('.', true, /_spec$/);
-testsContext.keys().forEach(function (path) {
+testsContext.keys().forEach(function(path) {
try {
- testsContext(path);
+ if (testFiles.length === 0 || testFiles.some(p => path.includes(p))) {
+ testsContext(path);
+ }
} catch (err) {
console.error('[ERROR] Unable to load spec: ', path);
- describe('Test bundle', function () {
- it(`includes '${path}'`, function () {
+ describe('Test bundle', function() {
+ it(`includes '${path}'`, function() {
expect(err).toBeNull();
});
});
@@ -85,7 +110,7 @@ testsContext.keys().forEach(function (path) {
});
describe('test errors', () => {
- beforeAll((done) => {
+ beforeAll(done => {
if (hasUnhandledPromiseRejections || hasVueWarnings || hasVueErrors) {
setTimeout(done, 1000);
} else {
@@ -149,18 +174,18 @@ if (process.env.BABEL_ENV === 'coverage') {
'./issue_show/index.js',
];
- describe('Uncovered files', function () {
+ describe('Uncovered files', function() {
const sourceFiles = require.context('~', true, /\.js$/);
$.holdReady(true);
- sourceFiles.keys().forEach(function (path) {
+ sourceFiles.keys().forEach(function(path) {
// ignore if there is a matching spec file
if (testsContext.keys().indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) {
return;
}
- it(`includes '${path}'`, function () {
+ it(`includes '${path}'`, function() {
try {
sourceFiles(path);
} catch (err) {
diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js
index 898bbb3819b..e74f4bdef7e 100644
--- a/spec/javascripts/todos_spec.js
+++ b/spec/javascripts/todos_spec.js
@@ -1,5 +1,4 @@
import $ from 'jquery';
-import * as urlUtils from '~/lib/utils/url_utility';
import Todos from '~/pages/dashboard/todos/index/todos';
import '~/lib/utils/common_utils';
@@ -18,7 +17,7 @@ describe('Todos', () => {
it('opens the todo url', (done) => {
const todoLink = todoItem.dataset.url;
- spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
+ spyOnDependency(Todos, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(todoLink);
done();
});
@@ -33,7 +32,7 @@ describe('Todos', () => {
beforeEach(() => {
metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true });
- visitUrlSpy = spyOn(urlUtils, 'visitUrl').and.callFake(() => {});
+ visitUrlSpy = spyOnDependency(Todos, 'visitUrl').and.callFake(() => {});
windowOpenSpy = spyOn(window, 'open').and.callFake(() => {});
});
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index 39c47a5c06d..d84b13b07c4 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -3,7 +3,7 @@ import U2FAuthenticate from '~/u2f/authenticate';
import 'vendor/u2f';
import MockU2FDevice from './mock_u2f_device';
-describe('U2FAuthenticate', () => {
+describe('U2FAuthenticate', function () {
preloadFixtures('u2f/authenticate.html.raw');
beforeEach((done) => {
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index 136b4cad737..d9383314891 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -3,7 +3,7 @@ import U2FRegister from '~/u2f/register';
import 'vendor/u2f';
import MockU2FDevice from './mock_u2f_device';
-describe('U2FRegister', () => {
+describe('U2FRegister', function () {
preloadFixtures('u2f/register.html.raw');
beforeEach((done) => {
diff --git a/spec/javascripts/visibility_select_spec.js b/spec/javascripts/visibility_select_spec.js
deleted file mode 100644
index 82714cb69bd..00000000000
--- a/spec/javascripts/visibility_select_spec.js
+++ /dev/null
@@ -1,98 +0,0 @@
-import VisibilitySelect from '~/visibility_select';
-
-(() => {
- describe('VisibilitySelect', function () {
- const lockedElement = document.createElement('div');
- lockedElement.dataset.helpBlock = 'lockedHelpBlock';
-
- const checkedElement = document.createElement('div');
- checkedElement.dataset.description = 'checkedDescription';
-
- const mockElements = {
- container: document.createElement('div'),
- select: document.createElement('div'),
- '.help-block': document.createElement('div'),
- '.js-locked': lockedElement,
- 'option:checked': checkedElement,
- };
-
- beforeEach(function () {
- spyOn(Element.prototype, 'querySelector').and.callFake(selector => mockElements[selector]);
- });
-
- describe('constructor', function () {
- beforeEach(function () {
- this.visibilitySelect = new VisibilitySelect(mockElements.container);
- });
-
- it('sets the container member', function () {
- expect(this.visibilitySelect.container).toEqual(mockElements.container);
- });
-
- it('queries and sets the helpBlock member', function () {
- expect(Element.prototype.querySelector).toHaveBeenCalledWith('.help-block');
- expect(this.visibilitySelect.helpBlock).toEqual(mockElements['.help-block']);
- });
-
- it('queries and sets the select member', function () {
- expect(Element.prototype.querySelector).toHaveBeenCalledWith('select');
- expect(this.visibilitySelect.select).toEqual(mockElements.select);
- });
-
- describe('if there is no container element provided', function () {
- it('throws an error', function () {
- expect(() => new VisibilitySelect()).toThrowError('VisibilitySelect requires a container element as argument 1');
- });
- });
- });
-
- describe('init', function () {
- describe('if there is a select', function () {
- beforeEach(function () {
- this.visibilitySelect = new VisibilitySelect(mockElements.container);
- });
-
- it('calls updateHelpText', function () {
- spyOn(VisibilitySelect.prototype, 'updateHelpText');
- this.visibilitySelect.init();
- expect(this.visibilitySelect.updateHelpText).toHaveBeenCalled();
- });
-
- it('adds a change event listener', function () {
- spyOn(this.visibilitySelect.select, 'addEventListener');
- this.visibilitySelect.init();
- expect(this.visibilitySelect.select.addEventListener.calls.argsFor(0)).toContain('change');
- });
- });
-
- describe('if there is no select', function () {
- beforeEach(function () {
- mockElements.select = undefined;
- this.visibilitySelect = new VisibilitySelect(mockElements.container);
- this.visibilitySelect.init();
- });
-
- it('updates the helpBlock text to the locked `data-help-block` messaged', function () {
- expect(this.visibilitySelect.helpBlock.textContent)
- .toEqual(lockedElement.dataset.helpBlock);
- });
-
- afterEach(function () {
- mockElements.select = document.createElement('div');
- });
- });
- });
-
- describe('updateHelpText', function () {
- beforeEach(function () {
- this.visibilitySelect = new VisibilitySelect(mockElements.container);
- this.visibilitySelect.init();
- });
-
- it('updates the helpBlock text to the selected options `data-description`', function () {
- expect(this.visibilitySelect.helpBlock.textContent)
- .toEqual(checkedElement.dataset.description);
- });
- });
- });
-})();
diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
index ff8d54c029f..c82ba61a5b1 100644
--- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
@@ -1,5 +1,4 @@
import Vue from 'vue';
-import * as urlUtils from '~/lib/utils/url_utility';
import deploymentComponent from '~/vue_merge_request_widget/components/deployment.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import { getTimeago } from '~/lib/utils/datetime_utility';
@@ -117,13 +116,13 @@ describe('Deployment component', () => {
it('should show a confirm dialog and call service.stopEnvironment when confirmed', (done) => {
spyOn(window, 'confirm').and.returnValue(true);
spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(true));
- spyOn(urlUtils, 'visitUrl').and.returnValue(true);
+ const visitUrl = spyOnDependency(deploymentComponent, 'visitUrl').and.returnValue(true);
vm = mockStopEnvironment();
expect(window.confirm).toHaveBeenCalled();
expect(MRWidgetService.stopEnvironment).toHaveBeenCalledWith(deploymentMockData.stop_url);
setTimeout(() => {
- expect(urlUtils.visitUrl).toHaveBeenCalledWith(url);
+ expect(visitUrl).toHaveBeenCalledWith(url);
done();
}, 333);
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
index d9c03296857..91e81a0675a 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
@@ -51,8 +51,7 @@ const createComponent = () => {
const messages = {
loadingMetrics: 'Loading deployment statistics',
- hasMetrics:
- '<a href="/root/acets-review-apps/environments/15/metrics"> Memory </a> usage is <b> unchanged </b> at 0MB',
+ hasMetrics: 'Memory usage is unchanged at 0MB',
loadFailed: 'Failed to load deployment statistics',
metricsUnavailable: 'Deployment statistics are not available currently',
};
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
index fcbd8169bc7..3d05dbfa305 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import removeBreakLine from 'spec/helpers/vue_component_helper';
+import { removeBreakLine } from 'spec/helpers/vue_component_helper';
describe('MRWidgetConflicts', () => {
let Component;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
index 894dbe3382f..ab096a56918 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import removeBreakLine from 'spec/helpers/vue_component_helper';
+import { removeBreakLine } from 'spec/helpers/vue_component_helper';
describe('MRWidgetPipelineBlocked', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
index 78bac1c61a5..5573d7c5c93 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
@@ -1,16 +1,19 @@
import Vue from 'vue';
-import pipelineFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_failed';
+import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue';
+import { removeBreakLine } from 'spec/helpers/vue_component_helper';
-describe('MRWidgetPipelineFailed', () => {
+describe('PipelineFailed', () => {
describe('template', () => {
- const Component = Vue.extend(pipelineFailedComponent);
+ const Component = Vue.extend(PipelineFailed);
const vm = new Component({
el: document.createElement('div'),
});
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
- expect(vm.$el.innerText).toContain('The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure');
+ expect(
+ removeBreakLine(vm.$el.innerText).trim(),
+ ).toContain('The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure');
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index 58f683fb3e6..81c16593eb4 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -1,12 +1,11 @@
import Vue from 'vue';
-import readyToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_ready_to_merge';
+import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
-import * as simplePoll from '~/lib/utils/simple_poll';
const commitMessage = 'This is the commit message';
const commitMessageWithDescription = 'This is the commit message description';
const createComponent = (customConfig = {}) => {
- const Component = Vue.extend(readyToMergeComponent);
+ const Component = Vue.extend(ReadyToMerge);
const mr = {
isPipelineActive: false,
pipeline: null,
@@ -36,7 +35,7 @@ const createComponent = (customConfig = {}) => {
});
};
-describe('MRWidgetReadyToMerge', () => {
+describe('ReadyToMerge', () => {
let vm;
beforeEach(() => {
@@ -49,7 +48,7 @@ describe('MRWidgetReadyToMerge', () => {
describe('props', () => {
it('should have props', () => {
- const { mr, service } = readyToMergeComponent.props;
+ const { mr, service } = ReadyToMerge.props;
expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy();
@@ -355,9 +354,9 @@ describe('MRWidgetReadyToMerge', () => {
describe('initiateMergePolling', () => {
it('should call simplePoll', () => {
- spyOn(simplePoll, 'default');
+ const simplePoll = spyOnDependency(ReadyToMerge, 'simplePoll');
vm.initiateMergePolling();
- expect(simplePoll.default).toHaveBeenCalled();
+ expect(simplePoll).toHaveBeenCalled();
});
});
@@ -457,11 +456,11 @@ describe('MRWidgetReadyToMerge', () => {
describe('initiateRemoveSourceBranchPolling', () => {
it('should emit event and call simplePoll', () => {
spyOn(eventHub, '$emit');
- spyOn(simplePoll, 'default');
+ const simplePoll = spyOnDependency(ReadyToMerge, 'simplePoll');
vm.initiateRemoveSourceBranchPolling();
expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [true]);
- expect(simplePoll.default).toHaveBeenCalled();
+ expect(simplePoll).toHaveBeenCalled();
});
});
@@ -524,18 +523,20 @@ describe('MRWidgetReadyToMerge', () => {
});
describe('when user can merge and can delete branch', () => {
+ let customVm;
+
beforeEach(() => {
- this.customVm = createComponent({
+ customVm = createComponent({
mr: { canRemoveSourceBranch: true },
});
});
it('isRemoveSourceBranchButtonDisabled should be false', () => {
- expect(this.customVm.isRemoveSourceBranchButtonDisabled).toBe(false);
+ expect(customVm.isRemoveSourceBranchButtonDisabled).toBe(false);
});
it('should be enabled in rendered output', () => {
- const checkboxElement = this.customVm.$el.querySelector('#remove-source-branch-input');
+ const checkboxElement = customVm.$el.querySelector('#remove-source-branch-input');
expect(checkboxElement).not.toBeNull();
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
index b02af94d03a..abf642c166a 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import ShaMismatch from '~/vue_merge_request_widget/components/states/sha_mismatch.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import removeBreakLine from 'spec/helpers/vue_component_helper';
+import { removeBreakLine } from 'spec/helpers/vue_component_helper';
describe('ShaMismatch', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
index 046968fbc1f..d797f1266df 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
@@ -1,47 +1,37 @@
import Vue from 'vue';
import UnresolvedDiscussions from '~/vue_merge_request_widget/components/states/unresolved_discussions.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('UnresolvedDiscussions', () => {
- describe('props', () => {
- it('should have props', () => {
- const { mr } = UnresolvedDiscussions.props;
+ const Component = Vue.extend(UnresolvedDiscussions);
+ let vm;
- expect(mr.type instanceof Object).toBeTruthy();
- expect(mr.required).toBeTruthy();
- });
+ afterEach(() => {
+ vm.$destroy();
});
- describe('template', () => {
- let el;
- let vm;
- const path = 'foo/bar';
-
+ describe('with discussions path', () => {
beforeEach(() => {
- const Component = Vue.extend(UnresolvedDiscussions);
- const mr = {
- createIssueToResolveDiscussionsPath: path,
- };
- vm = new Component({
- el: document.createElement('div'),
- propsData: { mr },
- });
- el = vm.$el;
+ vm = mountComponent(Component, { mr: {
+ createIssueToResolveDiscussionsPath: gl.TEST_HOST,
+ } });
});
it('should have correct elements', () => {
- expect(el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(el.innerText).toContain('There are unresolved discussions. Please resolve these discussions');
- expect(el.innerText).toContain('Create an issue to resolve them later');
- expect(el.querySelector('.js-create-issue').getAttribute('href')).toEqual(path);
+ expect(vm.$el.innerText).toContain('There are unresolved discussions. Please resolve these discussions');
+ expect(vm.$el.innerText).toContain('Create an issue to resolve them later');
+ expect(vm.$el.querySelector('.js-create-issue').getAttribute('href')).toEqual(gl.TEST_HOST);
});
+ });
- it('should not show create issue button if user cannot create issue', (done) => {
- vm.mr.createIssueToResolveDiscussionsPath = '';
+ describe('without discussions path', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, { mr: {} });
+ });
- Vue.nextTick(() => {
- expect(el.querySelector('.js-create-issue')).toEqual(null);
- done();
- });
+ it('should not show create issue link if user cannot create issue', () => {
+ expect(vm.$el.innerText).toContain('There are unresolved discussions. Please resolve these discussions');
+ expect(vm.$el.querySelector('.js-create-issue')).toEqual(null);
});
});
});
diff --git a/spec/javascripts/vue_shared/components/callout_spec.js b/spec/javascripts/vue_shared/components/callout_spec.js
new file mode 100644
index 00000000000..e62bd86f4ca
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/callout_spec.js
@@ -0,0 +1,45 @@
+import Vue from 'vue';
+import callout from '~/vue_shared/components/callout.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('Callout Component', () => {
+ let CalloutComponent;
+ let vm;
+ const exampleMessage = 'This is a callout message!';
+
+ beforeEach(() => {
+ CalloutComponent = Vue.extend(callout);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render the appropriate variant of callout', () => {
+ vm = createComponent(CalloutComponent, {
+ category: 'info',
+ message: exampleMessage,
+ });
+
+ expect(vm.$el.getAttribute('class')).toEqual('bs-callout bs-callout-info');
+
+ expect(vm.$el.tagName).toEqual('DIV');
+ });
+
+ it('should render accessibility attributes', () => {
+ vm = createComponent(CalloutComponent, {
+ message: exampleMessage,
+ });
+
+ expect(vm.$el.getAttribute('role')).toEqual('alert');
+ expect(vm.$el.getAttribute('aria-live')).toEqual('assertive');
+ });
+
+ it('should render the provided message', () => {
+ vm = createComponent(CalloutComponent, {
+ message: exampleMessage,
+ });
+
+ expect(vm.$el.innerHTML.trim()).toEqual(exampleMessage);
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/ci_icon_spec.js b/spec/javascripts/vue_shared/components/ci_icon_spec.js
index d8664408595..423bc746a22 100644
--- a/spec/javascripts/vue_shared/components/ci_icon_spec.js
+++ b/spec/javascripts/vue_shared/components/ci_icon_spec.js
@@ -1,139 +1,122 @@
import Vue from 'vue';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('CI Icon component', () => {
- let CiIcon;
- beforeEach(() => {
- CiIcon = Vue.extend(ciIcon);
+ const Component = Vue.extend(ciIcon);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
});
it('should render a span element with an svg', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_success',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_success',
},
- }).$mount();
+ });
- expect(component.$el.tagName).toEqual('SPAN');
- expect(component.$el.querySelector('span > svg')).toBeDefined();
+ expect(vm.$el.tagName).toEqual('SPAN');
+ expect(vm.$el.querySelector('span > svg')).toBeDefined();
});
it('should render a success status', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_success',
- group: 'success',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_success',
+ group: 'success',
},
- }).$mount();
+ });
- expect(component.$el.classList.contains('ci-status-icon-success')).toEqual(true);
+ expect(vm.$el.classList.contains('ci-status-icon-success')).toEqual(true);
});
it('should render a failed status', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_failed',
- group: 'failed',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_failed',
+ group: 'failed',
},
- }).$mount();
+ });
- expect(component.$el.classList.contains('ci-status-icon-failed')).toEqual(true);
+ expect(vm.$el.classList.contains('ci-status-icon-failed')).toEqual(true);
});
it('should render success with warnings status', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_warning',
- group: 'warning',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_warning',
+ group: 'warning',
},
- }).$mount();
+ });
- expect(component.$el.classList.contains('ci-status-icon-warning')).toEqual(true);
+ expect(vm.$el.classList.contains('ci-status-icon-warning')).toEqual(true);
});
it('should render pending status', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_pending',
- group: 'pending',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_pending',
+ group: 'pending',
},
- }).$mount();
+ });
- expect(component.$el.classList.contains('ci-status-icon-pending')).toEqual(true);
+ expect(vm.$el.classList.contains('ci-status-icon-pending')).toEqual(true);
});
it('should render running status', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_running',
- group: 'running',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_running',
+ group: 'running',
},
- }).$mount();
+ });
- expect(component.$el.classList.contains('ci-status-icon-running')).toEqual(true);
+ expect(vm.$el.classList.contains('ci-status-icon-running')).toEqual(true);
});
it('should render created status', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_created',
- group: 'created',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_created',
+ group: 'created',
},
- }).$mount();
+ });
- expect(component.$el.classList.contains('ci-status-icon-created')).toEqual(true);
+ expect(vm.$el.classList.contains('ci-status-icon-created')).toEqual(true);
});
it('should render skipped status', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_skipped',
- group: 'skipped',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_skipped',
+ group: 'skipped',
},
- }).$mount();
+ });
- expect(component.$el.classList.contains('ci-status-icon-skipped')).toEqual(true);
+ expect(vm.$el.classList.contains('ci-status-icon-skipped')).toEqual(true);
});
it('should render canceled status', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_canceled',
- group: 'canceled',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_canceled',
+ group: 'canceled',
},
- }).$mount();
+ });
- expect(component.$el.classList.contains('ci-status-icon-canceled')).toEqual(true);
+ expect(vm.$el.classList.contains('ci-status-icon-canceled')).toEqual(true);
});
it('should render status for manual action', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_manual',
- group: 'manual',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_manual',
+ group: 'manual',
},
- }).$mount();
+ });
- expect(component.$el.classList.contains('ci-status-icon-manual')).toEqual(true);
+ expect(vm.$el.classList.contains('ci-status-icon-manual')).toEqual(true);
});
});
diff --git a/spec/javascripts/vue_shared/components/clipboard_button_spec.js b/spec/javascripts/vue_shared/components/clipboard_button_spec.js
index f598b1afa74..97f0fbb04db 100644
--- a/spec/javascripts/vue_shared/components/clipboard_button_spec.js
+++ b/spec/javascripts/vue_shared/components/clipboard_button_spec.js
@@ -3,10 +3,10 @@ import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('clipboard button', () => {
+ const Component = Vue.extend(clipboardButton);
let vm;
beforeEach(() => {
- const Component = Vue.extend(clipboardButton);
vm = mountComponent(Component, {
text: 'copy me',
title: 'Copy this value into Clipboard!',
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js
index fdead874209..7189e8cfcfa 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import commitComp from '~/vue_shared/components/commit.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Commit component', () => {
let props;
@@ -10,25 +11,28 @@ describe('Commit component', () => {
CommitComponent = Vue.extend(commitComp);
});
+ afterEach(() => {
+ component.$destroy();
+ });
+
it('should render a fork icon if it does not represent a tag', () => {
- component = new CommitComponent({
- propsData: {
- tag: false,
- commitRef: {
- name: 'master',
- ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
- },
- commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
- shortSha: 'b7836edd',
- title: 'Commit message',
- author: {
- avatar_url: 'https://gitlab.com/uploads/-/system/user/avatar/300478/avatar.png',
- web_url: 'https://gitlab.com/jschatz1',
- path: '/jschatz1',
- username: 'jschatz1',
- },
+ component = mountComponent(CommitComponent, {
+ tag: false,
+ commitRef: {
+ name: 'master',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ },
+ commitUrl:
+ 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ shortSha: 'b7836edd',
+ title: 'Commit message',
+ author: {
+ avatar_url: 'https://gitlab.com/uploads/-/system/user/avatar/300478/avatar.png',
+ web_url: 'https://gitlab.com/jschatz1',
+ path: '/jschatz1',
+ username: 'jschatz1',
},
- }).$mount();
+ });
expect(component.$el.querySelector('.icon-container').children).toContain('svg');
});
@@ -41,7 +45,8 @@ describe('Commit component', () => {
name: 'master',
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
},
- commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ commitUrl:
+ 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
shortSha: 'b7836edd',
title: 'Commit message',
author: {
@@ -50,12 +55,9 @@ describe('Commit component', () => {
path: '/jschatz1',
username: 'jschatz1',
},
- commitIconSvg: '<svg></svg>',
};
- component = new CommitComponent({
- propsData: props,
- }).$mount();
+ component = mountComponent(CommitComponent, props);
});
it('should render a tag icon if it represents a tag', () => {
@@ -63,7 +65,9 @@ describe('Commit component', () => {
});
it('should render a link to the ref url', () => {
- expect(component.$el.querySelector('.ref-name').getAttribute('href')).toEqual(props.commitRef.ref_url);
+ expect(component.$el.querySelector('.ref-name').getAttribute('href')).toEqual(
+ props.commitRef.ref_url,
+ );
});
it('should render the ref name', () => {
@@ -71,12 +75,16 @@ describe('Commit component', () => {
});
it('should render the commit short sha with a link to the commit url', () => {
- expect(component.$el.querySelector('.commit-sha').getAttribute('href')).toEqual(props.commitUrl);
+ expect(component.$el.querySelector('.commit-sha').getAttribute('href')).toEqual(
+ props.commitUrl,
+ );
expect(component.$el.querySelector('.commit-sha').textContent).toContain(props.shortSha);
});
- it('should render the given commitIconSvg', () => {
- expect(component.$el.querySelector('.js-commit-icon').children).toContain('svg');
+ it('should render icon for commit', () => {
+ expect(
+ component.$el.querySelector('.js-commit-icon use').getAttribute('xlink:href'),
+ ).toContain('commit');
});
describe('Given commit title and author props', () => {
@@ -88,21 +96,25 @@ describe('Commit component', () => {
it('Should render the author avatar with title and alt attributes', () => {
expect(
- component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('data-original-title'),
+ component.$el
+ .querySelector('.commit-title .avatar-image-container img')
+ .getAttribute('data-original-title'),
).toContain(props.author.username);
expect(
- component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt'),
+ component.$el
+ .querySelector('.commit-title .avatar-image-container img')
+ .getAttribute('alt'),
).toContain(`${props.author.username}'s avatar`);
});
});
it('should render the commit title', () => {
- expect(
- component.$el.querySelector('a.commit-row-message').getAttribute('href'),
- ).toEqual(props.commitUrl);
- expect(
- component.$el.querySelector('a.commit-row-message').textContent,
- ).toContain(props.title);
+ expect(component.$el.querySelector('a.commit-row-message').getAttribute('href')).toEqual(
+ props.commitUrl,
+ );
+ expect(component.$el.querySelector('a.commit-row-message').textContent).toContain(
+ props.title,
+ );
});
});
@@ -114,19 +126,18 @@ describe('Commit component', () => {
name: 'master',
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
},
- commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ commitUrl:
+ 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
shortSha: 'b7836edd',
title: null,
author: {},
};
- component = new CommitComponent({
- propsData: props,
- }).$mount();
+ component = mountComponent(CommitComponent, props);
- expect(
- component.$el.querySelector('.commit-title span').textContent,
- ).toContain('Cant find HEAD commit for this branch');
+ expect(component.$el.querySelector('.commit-title span').textContent).toContain(
+ "Can't find HEAD commit for this branch",
+ );
});
});
});
diff --git a/spec/javascripts/vue_shared/components/expand_button_spec.js b/spec/javascripts/vue_shared/components/expand_button_spec.js
index f19589d3b75..af9693c48fd 100644
--- a/spec/javascripts/vue_shared/components/expand_button_spec.js
+++ b/spec/javascripts/vue_shared/components/expand_button_spec.js
@@ -3,10 +3,10 @@ import expandButton from '~/vue_shared/components/expand_button.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('expand button', () => {
+ const Component = Vue.extend(expandButton);
let vm;
beforeEach(() => {
- const Component = Vue.extend(expandButton);
vm = mountComponent(Component, {
slots: {
expanded: '<p>Expanded!</p>',
@@ -22,7 +22,7 @@ describe('expand button', () => {
expect(vm.$el.textContent.trim()).toEqual('...');
});
- it('hides expander on click', (done) => {
+ it('hides expander on click', done => {
vm.$el.querySelector('button').click();
vm.$nextTick(() => {
expect(vm.$el.querySelector('button').getAttribute('style')).toEqual('display: none;');
diff --git a/spec/javascripts/vue_shared/components/markdown/header_spec.js b/spec/javascripts/vue_shared/components/markdown/header_spec.js
index edebd822295..02117638b63 100644
--- a/spec/javascripts/vue_shared/components/markdown/header_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/header_spec.js
@@ -1,10 +1,11 @@
import Vue from 'vue';
+import $ from 'jquery';
import headerComponent from '~/vue_shared/components/markdown/header.vue';
describe('Markdown field header component', () => {
let vm;
- beforeEach((done) => {
+ beforeEach(done => {
const Component = Vue.extend(headerComponent);
vm = new Component({
@@ -17,24 +18,18 @@ describe('Markdown field header component', () => {
});
it('renders markdown buttons', () => {
- expect(
- vm.$el.querySelectorAll('.js-md').length,
- ).toBe(7);
+ expect(vm.$el.querySelectorAll('.js-md').length).toBe(7);
});
it('renders `write` link as active when previewMarkdown is false', () => {
- expect(
- vm.$el.querySelector('li:nth-child(1)').classList.contains('active'),
- ).toBeTruthy();
+ expect(vm.$el.querySelector('li:nth-child(1)').classList.contains('active')).toBeTruthy();
});
- it('renders `preview` link as active when previewMarkdown is true', (done) => {
+ it('renders `preview` link as active when previewMarkdown is true', done => {
vm.previewMarkdown = true;
Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('li:nth-child(2)').classList.contains('active'),
- ).toBeTruthy();
+ expect(vm.$el.querySelector('li:nth-child(2)').classList.contains('active')).toBeTruthy();
done();
});
@@ -52,16 +47,24 @@ describe('Markdown field header component', () => {
expect(vm.$emit).toHaveBeenCalledWith('write-markdown');
});
- it('blurs preview link after click', (done) => {
+ it('does not emit toggle markdown event when triggered from another form', () => {
+ spyOn(vm, '$emit');
+
+ $(document).triggerHandler('markdown-preview:show', [
+ $('<form><textarea class="markdown-area"></textarea></textarea></form>'),
+ ]);
+
+ expect(vm.$emit).not.toHaveBeenCalled();
+ });
+
+ it('blurs preview link after click', done => {
const link = vm.$el.querySelector('li:nth-child(2) a');
spyOn(HTMLElement.prototype, 'blur');
link.click();
setTimeout(() => {
- expect(
- link.blur,
- ).toHaveBeenCalled();
+ expect(link.blur).toHaveBeenCalled();
done();
});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
index 6fe95153204..e8685ab48be 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
@@ -73,6 +73,22 @@ describe('BaseComponent', () => {
expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]);
});
});
+
+ describe('handleCollapsedValueClick', () => {
+ it('emits toggleCollapse event on component', () => {
+ spyOn(vm, '$emit');
+ vm.handleCollapsedValueClick();
+ expect(vm.$emit).toHaveBeenCalledWith('toggleCollapse');
+ });
+ });
+
+ describe('handleDropdownHidden', () => {
+ it('emits onDropdownClose event on component', () => {
+ spyOn(vm, '$emit');
+ vm.handleDropdownHidden();
+ expect(vm.$emit).toHaveBeenCalledWith('onDropdownClose');
+ });
+ });
});
describe('mounted', () => {
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
index 39040670a87..da74595bcdc 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
@@ -56,6 +56,16 @@ describe('DropdownValueCollapsedComponent', () => {
});
});
+ describe('methods', () => {
+ describe('handleClick', () => {
+ it('emits onValueClick event on component', () => {
+ spyOn(vm, '$emit');
+ vm.handleClick();
+ expect(vm.$emit).toHaveBeenCalledWith('onValueClick');
+ });
+ });
+ });
+
describe('template', () => {
it('renders component container element with tooltip`', () => {
expect(vm.$el.dataset.placement).toBe('left');
diff --git a/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js b/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js
index bbd50863069..34487885cf0 100644
--- a/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js
+++ b/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js
@@ -14,8 +14,8 @@ describe('Skeleton loading container', () => {
vm.$destroy();
});
- it('renders 6 skeleton lines by default', () => {
- expect(vm.$el.querySelector('.skeleton-line-6')).not.toBeNull();
+ it('renders 3 skeleton lines by default', () => {
+ expect(vm.$el.querySelector('.skeleton-line-3')).not.toBeNull();
});
it('renders in full mode by default', () => {
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index 3c4deba4712..58a49124ce6 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -3,6 +3,48 @@ require 'spec_helper'
describe API::Helpers do
subject { Class.new.include(described_class).new }
+ describe '#find_project' do
+ let(:project) { create(:project) }
+
+ shared_examples 'project finder' do
+ context 'when project exists' do
+ it 'returns requested project' do
+ expect(subject.find_project(existing_id)).to eq(project)
+ end
+
+ it 'returns nil' do
+ expect(subject.find_project(non_existing_id)).to be_nil
+ end
+ end
+ end
+
+ context 'when ID is used as an argument' do
+ let(:existing_id) { project.id }
+ let(:non_existing_id) { (Project.maximum(:id) || 0) + 1 }
+
+ it_behaves_like 'project finder'
+ end
+
+ context 'when PATH is used as an argument' do
+ let(:existing_id) { project.full_path }
+ let(:non_existing_id) { 'something/else' }
+
+ it_behaves_like 'project finder'
+
+ context 'with an invalid PATH' do
+ let(:non_existing_id) { 'undefined' } # path without slash
+
+ it_behaves_like 'project finder'
+
+ it 'does not hit the database' do
+ expect(Project).not_to receive(:find_by_full_path)
+
+ subject.find_project(non_existing_id)
+ end
+ end
+ end
+ end
+
describe '#find_namespace' do
let(:namespace) { create(:namespace) }
diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb
index 14d055cbcc1..99872211a4e 100644
--- a/spec/lib/backup/files_spec.rb
+++ b/spec/lib/backup/files_spec.rb
@@ -62,5 +62,19 @@ describe Backup::Files do
subject.restore
end
end
+
+ describe 'folders that are a mountpoint' do
+ before do
+ allow(FileUtils).to receive(:mv).and_raise(Errno::EBUSY)
+ allow(subject).to receive(:run_pipeline!).and_return(true)
+ end
+
+ it 'shows error message' do
+ expect(subject).to receive(:resource_busy_error).with("/var/gitlab-registry")
+ .and_call_original
+
+ expect { subject.restore }.to raise_error(/is a mountpoint/)
+ end
+ end
end
end
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb
index e4c1c9bafc0..b3777be312b 100644
--- a/spec/lib/backup/repository_spec.rb
+++ b/spec/lib/backup/repository_spec.rb
@@ -81,6 +81,18 @@ describe Backup::Repository do
subject.restore
end
end
+
+ describe 'folder that is a mountpoint' do
+ before do
+ allow(FileUtils).to receive(:mv).and_raise(Errno::EBUSY)
+ end
+
+ it 'shows error message' do
+ expect(subject).to receive(:resource_busy_error).and_call_original
+
+ expect { subject.restore }.to raise_error(/is a mountpoint/)
+ end
+ end
end
describe '#empty_repo?' do
diff --git a/spec/lib/banzai/commit_renderer_spec.rb b/spec/lib/banzai/commit_renderer_spec.rb
index e7ebb2a332f..1f53657c59c 100644
--- a/spec/lib/banzai/commit_renderer_spec.rb
+++ b/spec/lib/banzai/commit_renderer_spec.rb
@@ -6,7 +6,10 @@ describe Banzai::CommitRenderer do
user = build(:user)
project = create(:project, :repository)
- expect(Banzai::ObjectRenderer).to receive(:new).with(project, user).and_call_original
+ expect(Banzai::ObjectRenderer)
+ .to receive(:new)
+ .with(user: user, default_project: project)
+ .and_call_original
described_class::ATTRIBUTES.each do |attr|
expect_any_instance_of(Banzai::ObjectRenderer).to receive(:render).with([project.commit], attr).once.and_call_original
diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
index a41a28a56f1..e1af5a15371 100644
--- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
@@ -233,4 +233,20 @@ describe Banzai::Filter::CommitRangeReferenceFilter do
expect(reference_filter(act).to_html).to eq exp
end
end
+
+ context 'group context' do
+ let(:context) { { project: nil, group: create(:group) } }
+
+ it 'ignores internal references' do
+ exp = act = "See #{range.to_reference}"
+
+ expect(reference_filter(act, context).to_html).to eq exp
+ end
+
+ it 'links to a full-path reference' do
+ reference = "#{project.full_path}@#{commit1.short_id}...#{commit2.short_id}"
+
+ expect(reference_filter("See #{reference}", context).css('a').first.text).to eql(reference)
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
index b18af806118..d6c9e9e4b19 100644
--- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
@@ -238,4 +238,20 @@ describe Banzai::Filter::CommitReferenceFilter do
expect(doc.text).to eq("See (#{commit.reference_link_text(project)} (builds).patch)")
end
end
+
+ context 'group context' do
+ let(:context) { { project: nil, group: create(:group) } }
+
+ it 'ignores internal references' do
+ exp = act = "See #{commit.id}"
+
+ expect(reference_filter(act, context).to_html).to eq exp
+ end
+
+ it 'links to a valid reference' do
+ act = "See #{project.full_path}@#{commit.id}"
+
+ expect(reference_filter(act, context).css('a').first.text).to eql("#{project.full_path}@#{commit.short_id}")
+ end
+ 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 6a9087d2e59..f8fa9b2d13d 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -343,14 +343,22 @@ describe Banzai::Filter::MilestoneReferenceFilter do
end
context 'group context' do
+ let(:context) { { project: nil, group: create(:group) } }
+ let(:milestone) { create(:milestone, project: project) }
+
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) } )
+ result = reference_filter("See #{reference}", context)
expect(result.css('a').first.attr('href')).to eq(urls.milestone_url(milestone))
end
+
+ it 'ignores internal references' do
+ exp = act = "See %#{milestone.iid}"
+
+ expect(reference_filter(act, context).to_html).to eq exp
+ end
end
context 'when milestone is open' do
diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
index e068e02d4fc..21cf092428d 100644
--- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
@@ -210,5 +210,11 @@ describe Banzai::Filter::SnippetReferenceFilter do
expect(result.css('a').first.attr('href')).to eq(urls.project_snippet_url(project, snippet))
end
+
+ it 'ignores internal references' do
+ exp = act = "See $#{snippet.id}"
+
+ expect(reference_filter(act, project: nil, group: create(:group)).to_html).to eq exp
+ end
end
end
diff --git a/spec/lib/banzai/issuable_extractor_spec.rb b/spec/lib/banzai/issuable_extractor_spec.rb
index 69763476dac..f42951d9781 100644
--- a/spec/lib/banzai/issuable_extractor_spec.rb
+++ b/spec/lib/banzai/issuable_extractor_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Banzai::IssuableExtractor do
let(:project) { create(:project) }
let(:user) { create(:user) }
- let(:extractor) { described_class.new(project, user) }
+ let(:extractor) { described_class.new(Banzai::RenderContext.new(project, user)) }
let(:issue) { create(:issue, project: project) }
let(:merge_request) { create(:merge_request, source_project: project) }
let(:issue_link) do
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
index 074d521a5c6..209a547c3b3 100644
--- a/spec/lib/banzai/object_renderer_spec.rb
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -3,8 +3,15 @@ require 'spec_helper'
describe Banzai::ObjectRenderer do
let(:project) { create(:project, :repository) }
let(:user) { project.owner }
- let(:renderer) { described_class.new(project, user, custom_value: 'value') }
- let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_VERSION) }
+ let(:renderer) do
+ described_class.new(
+ default_project: project,
+ user: user,
+ redaction_context: { custom_value: 'value' }
+ )
+ end
+
+ let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
describe '#render' do
context 'with cache' do
diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb
index 441f3725985..aaeec953e4b 100644
--- a/spec/lib/banzai/redactor_spec.rb
+++ b/spec/lib/banzai/redactor_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Banzai::Redactor do
let(:user) { create(:user) }
let(:project) { build(:project) }
- let(:redactor) { described_class.new(project, user) }
+ let(:redactor) { described_class.new(Banzai::RenderContext.new(project, user)) }
describe '#redact' do
context 'when reference not visible to user' do
@@ -54,7 +54,7 @@ describe Banzai::Redactor do
context 'when project is in pending delete' do
let!(:issue) { create(:issue, project: project) }
- let(:redactor) { described_class.new(project, user) }
+ let(:redactor) { described_class.new(Banzai::RenderContext.new(project, user)) }
before do
project.update(pending_delete: true)
diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb
index 6175d4c4ca9..4e6e8eca38a 100644
--- a/spec/lib/banzai/reference_parser/base_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb
@@ -5,13 +5,14 @@ describe Banzai::ReferenceParser::BaseParser do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
+ let(:context) { Banzai::RenderContext.new(project, user) }
subject do
klass = Class.new(described_class) do
self.reference_type = :foo
end
- klass.new(project, user)
+ klass.new(context)
end
describe '.reference_type=' do
@@ -23,6 +24,19 @@ describe Banzai::ReferenceParser::BaseParser do
end
end
+ describe '#project_for_node' do
+ it 'returns the Project for a node' do
+ document = instance_double('document', fragment?: false)
+ project = instance_double('project')
+ object = instance_double('object', project: project)
+ node = instance_double('node', document: document)
+
+ context.associate_document(document, object)
+
+ expect(subject.project_for_node(node)).to eq(project)
+ end
+ end
+
describe '#nodes_visible_to_user' do
let(:link) { empty_html_link }
@@ -164,7 +178,7 @@ describe Banzai::ReferenceParser::BaseParser do
self.reference_type = :test
end
- instance = dummy.new(project, user)
+ instance = dummy.new(Banzai::RenderContext.new(project, user))
document = Nokogiri::HTML.fragment('<a class="gfm"></a><a class="gfm" data-reference-type="test"></a>')
expect(instance).to receive(:gather_references)
diff --git a/spec/lib/banzai/reference_parser/commit_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
index 3505659c2c3..cca53a8b9b9 100644
--- a/spec/lib/banzai/reference_parser/commit_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
@@ -5,7 +5,7 @@ describe Banzai::ReferenceParser::CommitParser do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
- subject { described_class.new(project, user) }
+ subject { described_class.new(Banzai::RenderContext.new(project, user)) }
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
diff --git a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
index 21813177deb..ff3b82cc482 100644
--- a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
@@ -5,7 +5,7 @@ describe Banzai::ReferenceParser::CommitRangeParser do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
- subject { described_class.new(project, user) }
+ subject { described_class.new(Banzai::RenderContext.new(project, user)) }
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
@@ -107,12 +107,9 @@ describe Banzai::ReferenceParser::CommitRangeParser do
describe '#find_object' do
let(:range) { double(:range) }
- before do
- expect(CommitRange).to receive(:new).and_return(range)
- end
-
context 'when the range has valid commits' do
it 'returns the commit range' do
+ expect(CommitRange).to receive(:new).and_return(range)
expect(range).to receive(:valid_commits?).and_return(true)
expect(subject.find_object(project, '123..456')).to eq(range)
@@ -121,10 +118,19 @@ describe Banzai::ReferenceParser::CommitRangeParser do
context 'when the range does not have any valid commits' do
it 'returns nil' do
+ expect(CommitRange).to receive(:new).and_return(range)
expect(range).to receive(:valid_commits?).and_return(false)
expect(subject.find_object(project, '123..456')).to be_nil
end
end
+
+ context 'group context' do
+ it 'returns nil' do
+ group = create(:group)
+
+ expect(subject.find_object(group, '123..456')).to be_nil
+ end
+ end
end
end
diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
index 25969b65168..1cb31e57114 100644
--- a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
@@ -5,7 +5,7 @@ describe Banzai::ReferenceParser::ExternalIssueParser do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
- subject { described_class.new(project, user) }
+ subject { described_class.new(Banzai::RenderContext.new(project, user)) }
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
index cb7f8b20dda..77c2064caba 100644
--- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
@@ -7,7 +7,7 @@ describe Banzai::ReferenceParser::IssueParser do
let(:user) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:link) { empty_html_link }
- subject { described_class.new(project, user) }
+ subject { described_class.new(Banzai::RenderContext.new(project, user)) }
describe '#nodes_visible_to_user' do
context 'when the link has a data-issue attribute' do
diff --git a/spec/lib/banzai/reference_parser/label_parser_spec.rb b/spec/lib/banzai/reference_parser/label_parser_spec.rb
index b700161d6c2..e4df2533821 100644
--- a/spec/lib/banzai/reference_parser/label_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/label_parser_spec.rb
@@ -6,7 +6,7 @@ describe Banzai::ReferenceParser::LabelParser do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
let(:label) { create(:label, project: project) }
- subject { described_class.new(project, user) }
+ subject { described_class.new(Banzai::RenderContext.new(project, user)) }
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
diff --git a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
index 14542342cf6..5417b1f00be 100644
--- a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
@@ -6,7 +6,7 @@ describe Banzai::ReferenceParser::MergeRequestParser do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request, source_project: project) }
- subject { described_class.new(project, user) }
+ subject { described_class.new(Banzai::RenderContext.new(merge_request.target_project, user)) }
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
diff --git a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
index 7dacdf8d629..751d042ffde 100644
--- a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
@@ -6,7 +6,7 @@ describe Banzai::ReferenceParser::MilestoneParser do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
let(:milestone) { create(:milestone, project: project) }
- subject { described_class.new(project, user) }
+ subject { described_class.new(Banzai::RenderContext.new(project, user)) }
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
diff --git a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
index 69ec3f66aa8..d410bd4c164 100644
--- a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
@@ -9,7 +9,7 @@ describe Banzai::ReferenceParser::SnippetParser do
let(:external_user) { create(:user, :external) }
let(:project_member) { create(:user) }
- subject { described_class.new(project, user) }
+ subject { described_class.new(Banzai::RenderContext.new(project, user)) }
let(:link) { empty_html_link }
def visible_references(snippet_visibility, user = nil)
diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb
index b079a3be029..112447f098e 100644
--- a/spec/lib/banzai/reference_parser/user_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb
@@ -6,7 +6,7 @@ describe Banzai::ReferenceParser::UserParser do
let(:group) { create(:group) }
let(:user) { create(:user) }
let(:project) { create(:project, :public, group: group, creator: user) }
- subject { described_class.new(project, user) }
+ subject { described_class.new(Banzai::RenderContext.new(project, user)) }
let(:link) { empty_html_link }
describe '#referenced_by' do
diff --git a/spec/lib/banzai/render_context_spec.rb b/spec/lib/banzai/render_context_spec.rb
new file mode 100644
index 00000000000..ad17db11613
--- /dev/null
+++ b/spec/lib/banzai/render_context_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::RenderContext do
+ let(:document) { Nokogiri::HTML.fragment('<p>hello</p>') }
+
+ describe '#project_for_node' do
+ it 'returns the default project if no associated project was found' do
+ project = instance_double('project')
+ context = described_class.new(project)
+
+ expect(context.project_for_node(document)).to eq(project)
+ end
+
+ it 'returns the associated project if one was associated explicitly' do
+ project = instance_double('project')
+ obj = instance_double('object', project: project)
+ context = described_class.new
+
+ context.associate_document(document, obj)
+
+ expect(context.project_for_node(document)).to eq(project)
+ end
+
+ it 'returns the project associated with a DocumentFragment when using a node' do
+ project = instance_double('project')
+ obj = instance_double('object', project: project)
+ context = described_class.new
+ node = document.children.first
+
+ context.associate_document(document, obj)
+
+ expect(context.project_for_node(node)).to eq(project)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth/ldap/user_spec.rb b/spec/lib/gitlab/auth/ldap/user_spec.rb
index cab2169593a..653c19942ea 100644
--- a/spec/lib/gitlab/auth/ldap/user_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/user_spec.rb
@@ -25,20 +25,20 @@ describe Gitlab::Auth::LDAP::User do
OmniAuth::AuthHash.new(uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain', info: info_upper_case)
end
- describe '#changed?' do
+ describe '#should_save?' do
it "marks existing ldap user as changed" do
create(:omniauth_user, extern_uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain')
- expect(ldap_user.changed?).to be_truthy
+ expect(ldap_user.should_save?).to be_truthy
end
it "marks existing non-ldap user if the email matches as changed" do
create(:user, email: 'john@example.com')
- expect(ldap_user.changed?).to be_truthy
+ expect(ldap_user.should_save?).to be_truthy
end
it "does not mark existing ldap user as changed" do
create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain')
- expect(ldap_user.changed?).to be_falsey
+ expect(ldap_user.should_save?).to be_falsey
end
end
diff --git a/spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb b/spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb
new file mode 100644
index 00000000000..528f1b4ec57
--- /dev/null
+++ b/spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::OAuth::IdentityLinker do
+ let(:user) { create(:user) }
+ let(:provider) { 'twitter' }
+ let(:uid) { user.email }
+ let(:oauth) { { 'provider' => provider, 'uid' => uid } }
+
+ subject { described_class.new(user, oauth) }
+
+ context 'linked identity exists' do
+ let!(:identity) { user.identities.create!(provider: provider, extern_uid: uid) }
+
+ it "doesn't create new identity" do
+ expect { subject.link }.not_to change { Identity.count }
+ end
+
+ it "sets #changed? to false" do
+ subject.link
+
+ expect(subject).not_to be_changed
+ end
+ end
+
+ context 'identity already linked to different user' do
+ let!(:identity) { create(:identity, provider: provider, extern_uid: uid) }
+
+ it "#changed? returns false" do
+ subject.link
+
+ expect(subject).not_to be_changed
+ end
+
+ it 'exposes error message' do
+ expect(subject.error_message).to eq 'Extern uid has already been taken'
+ end
+ end
+
+ context 'identity needs to be created' do
+ it 'creates linked identity' do
+ expect { subject.link }.to change { user.identities.count }
+ end
+
+ it 'sets identity provider' do
+ subject.link
+
+ expect(user.identities.last.provider).to eq provider
+ end
+
+ it 'sets identity extern_uid' do
+ subject.link
+
+ expect(user.identities.last.extern_uid).to eq uid
+ end
+
+ it 'sets #changed? to true' do
+ subject.link
+
+ expect(subject).to be_changed
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth/saml/identity_linker_spec.rb b/spec/lib/gitlab/auth/saml/identity_linker_spec.rb
new file mode 100644
index 00000000000..f3305d574cc
--- /dev/null
+++ b/spec/lib/gitlab/auth/saml/identity_linker_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::Saml::IdentityLinker do
+ let(:user) { create(:user) }
+ let(:provider) { 'saml' }
+ let(:uid) { user.email }
+ let(:oauth) { { 'provider' => provider, 'uid' => uid } }
+
+ subject { described_class.new(user, oauth) }
+
+ context 'linked identity exists' do
+ let!(:identity) { user.identities.create!(provider: provider, extern_uid: uid) }
+
+ it "doesn't create new identity" do
+ expect { subject.link }.not_to change { Identity.count }
+ end
+
+ it "sets #changed? to false" do
+ subject.link
+
+ expect(subject).not_to be_changed
+ end
+ end
+
+ context 'identity needs to be created' do
+ it 'creates linked identity' do
+ expect { subject.link }.to change { user.identities.count }
+ end
+
+ it 'sets identity provider' do
+ subject.link
+
+ expect(user.identities.last.provider).to eq provider
+ end
+
+ it 'sets identity extern_uid' do
+ subject.link
+
+ expect(user.identities.last.extern_uid).to eq uid
+ end
+
+ it 'sets #changed? to true' do
+ subject.link
+
+ expect(subject).to be_changed
+ end
+ end
+end
diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
index eb4b9d8b12f..5c8a19a53bc 100644
--- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb
+++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
@@ -4,6 +4,7 @@ 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")) }
+ let(:gitlab_shell) { Gitlab::Shell.new }
subject(:importer) { described_class.new(admin, bare_repository) }
@@ -84,12 +85,14 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do
importer.create_project_if_needed
project = Project.find_by_full_path(project_path)
- repo_path = File.join(project.repository_storage_path, project.disk_path + '.git')
+ repo_path = "#{project.disk_path}.git"
hook_path = File.join(repo_path, 'hooks')
- expect(File).to exist(repo_path)
- expect(File.symlink?(hook_path)).to be true
- expect(File.readlink(hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
+ expect(gitlab_shell.exists?(project.repository_storage, repo_path)).to be(true)
+ expect(gitlab_shell.exists?(project.repository_storage, hook_path)).to be(true)
+
+ full_hook_path = File.join(project.repository.path_to_repo, 'hooks')
+ expect(File.readlink(full_hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
end
context 'hashed storage enabled' do
@@ -144,8 +147,8 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do
project = Project.find_by_full_path("#{admin.full_path}/#{project_path}")
- expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.git'))
- expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.wiki.git'))
+ expect(gitlab_shell.exists?(project.repository_storage, project.disk_path + '.git')).to be(true)
+ expect(gitlab_shell.exists?(project.repository_storage, project.disk_path + '.wiki.git')).to be(true)
end
it 'moves an existing project to the correct path' do
@@ -155,7 +158,9 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do
project = build(:project, :legacy_storage, :repository)
original_commit_count = project.repository.commit_count
- bare_repo = Gitlab::BareRepositoryImport::Repository.new(project.repository_storage_path, project.repository.path)
+ legacy_path = Gitlab.config.repositories.storages[project.repository_storage].legacy_disk_path
+
+ bare_repo = Gitlab::BareRepositoryImport::Repository.new(legacy_path, project.repository.path)
gitlab_importer = described_class.new(admin, bare_repo)
expect(gitlab_importer).to receive(:create_project).and_call_original
@@ -183,7 +188,7 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do
project = Project.find_by_full_path(project_path)
- expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.wiki.git'))
+ expect(gitlab_shell.exists?(project.repository_storage, project.disk_path + '.wiki.git')).to be(true)
end
end
diff --git a/spec/lib/gitlab/bare_repository_import/repository_spec.rb b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
index 0dc3705825d..1504826c7a5 100644
--- a/spec/lib/gitlab/bare_repository_import/repository_spec.rb
+++ b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
@@ -67,7 +67,7 @@ describe ::Gitlab::BareRepositoryImport::Repository do
end
after do
- gitlab_shell.remove_repository(root_path, hashed_path)
+ gitlab_shell.remove_repository(repository_storage, hashed_path)
end
subject { described_class.new(root_path, repo_path) }
diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
index ae5b31dc12d..c3f528dd6fc 100644
--- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
@@ -15,7 +15,7 @@ describe Gitlab::BitbucketImport::ProjectCreator do
has_wiki?: false)
end
- let(:namespace) { create(:group, owner: user) }
+ let(:namespace) { create(:group) }
let(:token) { "asdasd12345" }
let(:secret) { "sekrettt" }
let(:access_params) { { bitbucket_access_token: token, bitbucket_access_token_secret: secret } }
diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
index 16704ff5e77..18658588a40 100644
--- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
+++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do
let!(:project) { create(:project, :repository) }
let(:pipeline_status) { described_class.new(project) }
- let(:cache_key) { "projects/#{project.id}/pipeline_status" }
+ let(:cache_key) { described_class.cache_key_for_project(project) }
describe '.load_for_project' do
it "loads the status" do
diff --git a/spec/lib/gitlab/ci/status/build/common_spec.rb b/spec/lib/gitlab/ci/status/build/common_spec.rb
index 2cce7a23ea7..ca3c66f0152 100644
--- a/spec/lib/gitlab/ci/status/build/common_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/common_spec.rb
@@ -38,4 +38,10 @@ describe Gitlab::Ci::Status::Build::Common do
expect(subject.details_path).to include "jobs/#{build.id}"
end
end
+
+ describe '#illustration' do
+ it 'provides a fallback empty state illustration' do
+ expect(subject.illustration).not_to be_empty
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index 6d5b73bb01b..d53a7d468e3 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -75,7 +75,8 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
- .to eq [Gitlab::Ci::Status::Build::Retryable, Gitlab::Ci::Status::Build::Failed]
+ .to eq [Gitlab::Ci::Status::Build::Retryable,
+ Gitlab::Ci::Status::Build::Failed]
end
it 'fabricates a failed build status' do
@@ -94,7 +95,7 @@ describe Gitlab::Ci::Status::Build::Factory do
end
context 'when build is allowed to fail' do
- let(:build) { create(:ci_build, :failed, :allowed_to_fail) }
+ let(:build) { create(:ci_build, :failed, :allowed_to_fail, :trace_artifact) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed
diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb
index f128c1d4ca4..e2bb378f663 100644
--- a/spec/lib/gitlab/ci/status/build/play_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/play_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe Gitlab::Ci::Status::Build::Play do
let(:user) { create(:user) }
- let(:project) { build.project }
- let(:build) { create(:ci_build, :manual) }
+ let(:project) { create(:project, :stubbed_repository) }
+ let(:build) { create(:ci_build, :manual, project: project) }
let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
subject { described_class.new(status) }
@@ -46,6 +46,8 @@ describe Gitlab::Ci::Status::Build::Play do
context 'when user can not push to the branch' do
before do
build.project.add_developer(user)
+ create(:protected_branch, :masters_can_push,
+ name: build.ref, project: project)
end
it { is_expected.not_to have_action }
diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb
index 3a9371ed2e8..6a9c6442282 100644
--- a/spec/lib/gitlab/ci/trace_spec.rb
+++ b/spec/lib/gitlab/ci/trace_spec.rb
@@ -458,7 +458,7 @@ describe Gitlab::Ci::Trace do
context 'when job does not have trace artifact' do
context 'when trace file stored in default path' do
let!(:build) { create(:ci_build, :success, :trace_live) }
- let!(:src_path) { trace.read { |s| return s.path } }
+ let!(:src_path) { trace.read { |s| s.path } }
let!(:src_checksum) { Digest::SHA256.file(src_path).hexdigest }
it_behaves_like 'archive trace file'
diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
index bf9208f1ff4..e79f0a7f257 100644
--- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
@@ -5,6 +5,18 @@ describe Gitlab::Ci::Variables::Collection::Item do
{ key: 'VAR', value: 'something', public: true }
end
+ describe '.new' do
+ it 'raises error if unknown key i specified' do
+ expect { described_class.new(key: 'VAR', value: 'abc', files: true) }
+ .to raise_error ArgumentError, 'unknown keyword: files'
+ end
+
+ it 'raises error when required keywords are not specified' do
+ expect { described_class.new(key: 'VAR') }
+ .to raise_error ArgumentError, 'missing keyword: value'
+ end
+ end
+
describe '.fabricate' do
it 'supports using a hash' do
resource = described_class.fabricate(variable)
@@ -47,12 +59,25 @@ describe Gitlab::Ci::Variables::Collection::Item do
end
describe '#to_runner_variable' do
- it 'returns a runner-compatible hash representation' do
- runner_variable = described_class
- .new(**variable)
- .to_runner_variable
+ context 'when variable is not a file-related' do
+ it 'returns a runner-compatible hash representation' do
+ runner_variable = described_class
+ .new(**variable)
+ .to_runner_variable
+
+ expect(runner_variable).to eq variable
+ end
+ end
+
+ context 'when variable is file-related' do
+ it 'appends file description component' do
+ runner_variable = described_class
+ .new(key: 'VAR', value: 'value', file: true)
+ .to_runner_variable
- expect(runner_variable).to eq variable
+ expect(runner_variable)
+ .to eq(key: 'VAR', value: 'value', public: true, file: true)
+ end
end
end
end
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index 73d60c021c8..7c9e8c8d04e 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -79,6 +79,8 @@ describe Gitlab::Diff::Highlight do
end
it 'keeps the original rich line' do
+ allow(Gitlab::Sentry).to receive(:track_exception)
+
code = %q{+ raise RuntimeError, "System commands must be given as an array of strings"}
expect(subject[5].text).to eq(code)
@@ -86,12 +88,9 @@ describe Gitlab::Diff::Highlight do
end
it 'reports to Sentry if configured' do
- allow(Gitlab::Sentry).to receive(:enabled?).and_return(true)
-
- expect(Gitlab::Sentry).to receive(:context)
- expect(Raven).to receive(:capture_exception)
+ expect(Gitlab::Sentry).to receive(:track_exception).and_call_original
- subject
+ expect { subject }. to raise_exception(RangeError)
end
end
end
diff --git a/spec/lib/gitlab/email/handler_spec.rb b/spec/lib/gitlab/email/handler_spec.rb
index 386d73e6115..cedbfcc0d18 100644
--- a/spec/lib/gitlab/email/handler_spec.rb
+++ b/spec/lib/gitlab/email/handler_spec.rb
@@ -25,12 +25,12 @@ describe Gitlab::Email::Handler do
described_class.for('email', address).class
end
- expect(matched_handlers.uniq).to match_array(Gitlab::Email::Handler::HANDLERS)
+ expect(matched_handlers.uniq).to match_array(ce_handlers)
end
it 'can pick exactly one handler for each address' do
addresses.each do |address|
- matched_handlers = Gitlab::Email::Handler::HANDLERS.select do |handler|
+ matched_handlers = ce_handlers.select do |handler|
handler.new('email', address).can_handle?
end
@@ -38,4 +38,10 @@ describe Gitlab::Email::Handler do
end
end
end
+
+ def ce_handlers
+ @ce_handlers ||= Gitlab::Email::Handler::HANDLERS.reject do |handler|
+ handler.name.start_with?('Gitlab::Email::Handler::EE::')
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/attributes_parser_spec.rb b/spec/lib/gitlab/git/attributes_parser_spec.rb
index 323334e99a5..2d103123998 100644
--- a/spec/lib/gitlab/git/attributes_parser_spec.rb
+++ b/spec/lib/gitlab/git/attributes_parser_spec.rb
@@ -66,18 +66,6 @@ describe Gitlab::Git::AttributesParser, seed_helper: true do
end
end
- context 'when attributes data is a file handle' do
- subject do
- File.open(attributes_path, 'r') do |file_handle|
- described_class.new(file_handle)
- end
- end
-
- it 'returns the attributes as a Hash' do
- expect(subject.attributes('test.txt')).to eq({ 'text' => true })
- end
- end
-
context 'when attributes data is nil' do
let(:data) { nil }
diff --git a/spec/lib/gitlab/git/committer_with_hooks_spec.rb b/spec/lib/gitlab/git/committer_with_hooks_spec.rb
new file mode 100644
index 00000000000..267056b96e6
--- /dev/null
+++ b/spec/lib/gitlab/git/committer_with_hooks_spec.rb
@@ -0,0 +1,154 @@
+require 'spec_helper'
+
+describe Gitlab::Git::CommitterWithHooks, seed_helper: true do
+ shared_examples 'calling wiki hooks' do
+ let(:project) { create(:project) }
+ let(:user) { project.owner }
+ let(:project_wiki) { ProjectWiki.new(project, user) }
+ let(:wiki) { project_wiki.wiki }
+ let(:options) do
+ {
+ id: user.id,
+ username: user.username,
+ name: user.name,
+ email: user.email,
+ message: 'commit message'
+ }
+ end
+
+ subject { described_class.new(wiki, options) }
+
+ before do
+ project_wiki.create_page('home', 'test content')
+ end
+
+ shared_examples 'failing pre-receive hook' do
+ before do
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([false, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('update')
+ expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive')
+ end
+
+ it 'raises exception' do
+ expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
+ end
+
+ it 'does not create a new commit inside the repository' do
+ current_rev = find_current_rev
+
+ expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
+
+ expect(current_rev).to eq find_current_rev
+ end
+ end
+
+ shared_examples 'failing update hook' do
+ before do
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([false, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive')
+ end
+
+ it 'raises exception' do
+ expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
+ end
+
+ it 'does not create a new commit inside the repository' do
+ current_rev = find_current_rev
+
+ expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
+
+ expect(current_rev).to eq find_current_rev
+ end
+ end
+
+ shared_examples 'failing post-receive hook' do
+ before do
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([true, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('post-receive').and_return([false, ''])
+ end
+
+ it 'does not raise exception' do
+ expect { subject.commit }.not_to raise_error
+ end
+
+ it 'creates the commit' do
+ current_rev = find_current_rev
+
+ subject.commit
+
+ expect(current_rev).not_to eq find_current_rev
+ end
+ end
+
+ shared_examples 'when hooks call succceeds' do
+ let(:hook) { double(:hook) }
+
+ it 'calls the three hooks' do
+ expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
+ expect(hook).to receive(:trigger).exactly(3).times.and_return([true, nil])
+
+ subject.commit
+ end
+
+ it 'creates the commit' do
+ current_rev = find_current_rev
+
+ subject.commit
+
+ expect(current_rev).not_to eq find_current_rev
+ end
+ end
+
+ context 'when creating a page' do
+ before do
+ project_wiki.create_page('index', 'test content')
+ end
+
+ it_behaves_like 'failing pre-receive hook'
+ it_behaves_like 'failing update hook'
+ it_behaves_like 'failing post-receive hook'
+ it_behaves_like 'when hooks call succceeds'
+ end
+
+ context 'when updating a page' do
+ before do
+ project_wiki.update_page(find_page('home'), content: 'some other content', format: :markdown)
+ end
+
+ it_behaves_like 'failing pre-receive hook'
+ it_behaves_like 'failing update hook'
+ it_behaves_like 'failing post-receive hook'
+ it_behaves_like 'when hooks call succceeds'
+ end
+
+ context 'when deleting a page' do
+ before do
+ project_wiki.delete_page(find_page('home'))
+ end
+
+ it_behaves_like 'failing pre-receive hook'
+ it_behaves_like 'failing update hook'
+ it_behaves_like 'failing post-receive hook'
+ it_behaves_like 'when hooks call succceeds'
+ end
+
+ def find_current_rev
+ wiki.gollum_wiki.repo.commits.first&.sha
+ end
+
+ def find_page(name)
+ wiki.page(title: name)
+ end
+ end
+
+ # TODO: Uncomment once Gitaly updates the ruby vendor code
+ # context 'when Gitaly is enabled' do
+ # it_behaves_like 'calling wiki hooks'
+ # end
+
+ context 'when Gitaly is disabled', :skip_gitaly_mock do
+ it_behaves_like 'calling wiki hooks'
+ end
+end
diff --git a/spec/lib/gitlab/git/info_attributes_spec.rb b/spec/lib/gitlab/git/info_attributes_spec.rb
deleted file mode 100644
index ea84909c3e0..00000000000
--- a/spec/lib/gitlab/git/info_attributes_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Git::InfoAttributes, seed_helper: true do
- let(:path) do
- File.join(SEED_STORAGE_PATH, 'with-git-attributes.git')
- end
-
- subject { described_class.new(path) }
-
- describe '#attributes' do
- context 'using a path with attributes' do
- it 'returns the attributes as a Hash' do
- expect(subject.attributes('test.txt')).to eq({ 'text' => true })
- end
-
- it 'returns an empty Hash for a defined path without attributes' do
- expect(subject.attributes('bla/bla.txt')).to eq({})
- end
- end
- end
-
- describe '#parser' do
- it 'parses a file with entries' do
- expect(subject.patterns).to be_an_instance_of(Hash)
- expect(subject.patterns["/*.txt"]).to eq({ 'text' => true })
- end
-
- it 'does not parse anything when the attributes file does not exist' do
- expect(File).to receive(:exist?)
- .with(File.join(path, 'info/attributes'))
- .and_return(false)
-
- expect(subject.patterns).to eq({})
- end
-
- it 'does not parse attributes files with unsupported encoding' do
- path = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git')
- subject = described_class.new(path)
-
- expect(subject.patterns).to eq({})
- end
- end
-end
diff --git a/spec/lib/gitlab/git/raw_diff_change_spec.rb b/spec/lib/gitlab/git/raw_diff_change_spec.rb
new file mode 100644
index 00000000000..eedde34534f
--- /dev/null
+++ b/spec/lib/gitlab/git/raw_diff_change_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Gitlab::Git::RawDiffChange do
+ let(:raw_change) { }
+ let(:change) { described_class.new(raw_change) }
+
+ context 'bad input' do
+ let(:raw_change) { 'foo' }
+
+ it 'does not set most of the attrs' do
+ expect(change.blob_id).to eq('foo')
+ expect(change.operation).to eq(:unknown)
+ expect(change.old_path).to be_blank
+ expect(change.new_path).to be_blank
+ expect(change.blob_size).to be_blank
+ end
+ end
+
+ context 'adding a file' do
+ let(:raw_change) { '93e123ac8a3e6a0b600953d7598af629dec7b735 59 A bar/branch-test.txt' }
+
+ it 'initialize the proper attrs' do
+ expect(change.operation).to eq(:added)
+ expect(change.old_path).to be_blank
+ expect(change.new_path).to eq('bar/branch-test.txt')
+ expect(change.blob_id).to be_present
+ expect(change.blob_size).to be_present
+ end
+ end
+
+ context 'renaming a file' do
+ let(:raw_change) { "85bc2f9753afd5f4fc5d7c75f74f8d526f26b4f3 107 R060\tfiles/js/commit.js.coffee\tfiles/js/commit.coffee" }
+
+ it 'initialize the proper attrs' do
+ expect(change.operation).to eq(:renamed)
+ expect(change.old_path).to eq('files/js/commit.js.coffee')
+ expect(change.new_path).to eq('files/js/commit.coffee')
+ expect(change.blob_id).to be_present
+ expect(change.blob_size).to be_present
+ end
+ end
+
+ context 'modifying a file' do
+ let(:raw_change) { 'c60514b6d3d6bf4bec1030f70026e34dfbd69ad5 824 M README.md' }
+
+ it 'initialize the proper attrs' do
+ expect(change.operation).to eq(:modified)
+ expect(change.old_path).to eq('README.md')
+ expect(change.new_path).to eq('README.md')
+ expect(change.blob_id).to be_present
+ expect(change.blob_size).to be_present
+ end
+ end
+
+ context 'deleting a file' do
+ let(:raw_change) { '60d7a906c2fd9e4509aeb1187b98d0ea7ce827c9 15364 D files/.DS_Store' }
+
+ it 'initialize the proper attrs' do
+ expect(change.operation).to eq(:deleted)
+ expect(change.old_path).to eq('files/.DS_Store')
+ expect(change.new_path).to be_nil
+ expect(change.blob_id).to be_present
+ expect(change.blob_size).to be_present
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index d3ab61746f4..da1a6229ccf 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -470,9 +470,20 @@ describe Gitlab::Git::Repository, seed_helper: true do
FileUtils.rm_rf(heads_dir)
FileUtils.mkdir_p(heads_dir)
+ repository.expire_has_local_branches_cache
expect(repository.has_local_branches?).to eq(false)
end
end
+
+ context 'memoizes the value' do
+ it 'returns true' do
+ expect(repository).to receive(:uncached_has_local_branches?).once.and_call_original
+
+ 2.times do
+ expect(repository.has_local_branches?).to eq(true)
+ end
+ end
+ end
end
context 'with gitaly' do
@@ -678,7 +689,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
after do
- Gitlab::Shell.new.remove_repository(storage_path, 'my_project')
+ Gitlab::Shell.new.remove_repository('default', 'my_project')
end
shared_examples 'repository mirror fecthing' do
@@ -1043,6 +1054,44 @@ describe Gitlab::Git::Repository, seed_helper: true do
it { is_expected.to eq(17) }
end
+ describe '#raw_changes_between' do
+ let(:old_rev) { }
+ let(:new_rev) { }
+ let(:changes) { repository.raw_changes_between(old_rev, new_rev) }
+
+ context 'initial commit' do
+ let(:old_rev) { Gitlab::Git::BLANK_SHA }
+ let(:new_rev) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' }
+
+ it 'returns the changes' do
+ expect(changes).to be_present
+ expect(changes.size).to eq(3)
+ end
+ end
+
+ context 'with an invalid rev' do
+ let(:old_rev) { 'foo' }
+ let(:new_rev) { 'bar' }
+
+ it 'returns an error' do
+ expect { changes }.to raise_error(Gitlab::Git::Repository::GitError)
+ end
+ end
+
+ context 'with valid revs' do
+ let(:old_rev) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' }
+ let(:new_rev) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
+
+ it 'returns the changes' do
+ expect(changes.size).to eq(9)
+ expect(changes.first.operation).to eq(:modified)
+ expect(changes.first.new_path).to eq('.gitmodules')
+ expect(changes.last.operation).to eq(:added)
+ expect(changes.last.new_path).to eq('files/lfs/picture-invalid.png')
+ end
+ end
+ end
+
describe '#merge_base' do
shared_examples '#merge_base' do
where(:from, :to, :result) do
diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb
index 761f7732036..722d697c28e 100644
--- a/spec/lib/gitlab/git/wiki_spec.rb
+++ b/spec/lib/gitlab/git/wiki_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::Git::Wiki do
end
def commit_details(name)
- Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "created page #{name}")
+ Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "created page #{name}")
end
def destroy_page(title, dir = '')
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index 9be3fa633a7..7951cbe7b1d 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::GitalyClient::CommitService do
initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863').raw
request = Gitaly::CommitDiffRequest.new(
repository: repository_message,
- left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
+ left_commit_id: Gitlab::Git::EMPTY_TREE_ID,
right_commit_id: initial_commit.id,
collapse_diffs: true,
enforce_limits: true,
@@ -77,7 +77,7 @@ describe Gitlab::GitalyClient::CommitService do
initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863')
request = Gitaly::CommitDeltaRequest.new(
repository: repository_message,
- left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
+ left_commit_id: Gitlab::Git::EMPTY_TREE_ID,
right_commit_id: initial_commit.id
)
@@ -90,7 +90,7 @@ describe Gitlab::GitalyClient::CommitService do
describe '#between' do
let(:from) { 'master' }
- let(:to) { '4b825dc642cb6eb9a060e54bf8d69288fbee4904' }
+ let(:to) { Gitlab::Git::EMPTY_TREE_ID }
it 'sends an RPC request' do
request = Gitaly::CommitsBetweenRequest.new(
@@ -155,7 +155,7 @@ describe Gitlab::GitalyClient::CommitService do
end
describe '#find_commit' do
- let(:revision) { '4b825dc642cb6eb9a060e54bf8d69288fbee4904' }
+ let(:revision) { Gitlab::Git::EMPTY_TREE_ID }
it 'sends an RPC request' do
request = Gitaly::FindCommitRequest.new(
repository: repository_message, revision: revision
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index 21592688bf0..ecd8657c406 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -84,6 +84,17 @@ describe Gitlab::GitalyClient::RepositoryService do
end
end
+ describe '#info_attributes' do
+ it 'reads the info attributes' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:get_info_attributes)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return([])
+
+ client.info_attributes
+ end
+ end
+
describe '#has_local_branches?' do
it 'sends a has_local_branches message' do
expect_any_instance_of(Gitaly::RepositoryService::Stub)
@@ -145,4 +156,15 @@ describe Gitlab::GitalyClient::RepositoryService do
client.calculate_checksum
end
end
+
+ describe '#create_from_snapshot' do
+ it 'sends a create_repository_from_snapshot message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:create_repository_from_snapshot)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(double)
+
+ client.create_from_snapshot('http://example.com?wiki=1', 'Custom xyz')
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
index 82548c7fd31..5ea086e4abd 100644
--- a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
@@ -12,7 +12,7 @@ describe Gitlab::GitlabImport::ProjectCreator do
owner: { name: "john" }
}.with_indifferent_access
end
- let(:namespace) { create(:group, owner: user) }
+ let(:namespace) { create(:group) }
let(:token) { "asdffg" }
let(:access_params) { { gitlab_access_token: token } }
diff --git a/spec/lib/gitlab/google_code_import/project_creator_spec.rb b/spec/lib/gitlab/google_code_import/project_creator_spec.rb
index 8d5b60d50de..24cd518c77b 100644
--- a/spec/lib/gitlab/google_code_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/google_code_import/project_creator_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::GoogleCodeImport::ProjectCreator do
"repositoryUrls" => ["https://vim.googlecode.com/git/"]
)
end
- let(:namespace) { create(:group, owner: user) }
+ let(:namespace) { create(:group) }
before do
namespace.add_owner(user)
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 897a5984782..e7f20f81fe0 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -286,6 +286,7 @@ project:
- internal_ids
- project_deploy_tokens
- deploy_tokens
+- ci_cd_settings
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index f84a777a27f..31141807cb2 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -537,12 +537,6 @@ ProjectCustomAttribute:
- project_id
- key
- value
-LfsFileLock:
-- id
-- path
-- user_id
-- project_id
-- created_at
Badge:
- id
- link_url
@@ -552,3 +546,5 @@ Badge:
- created_at
- updated_at
- type
+ProjectCiCdSetting:
+- group_runners_enabled
diff --git a/spec/lib/gitlab/import_export/wiki_restorer_spec.rb b/spec/lib/gitlab/import_export/wiki_restorer_spec.rb
index 5c01ee0ebb8..f99f198da33 100644
--- a/spec/lib/gitlab/import_export/wiki_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/wiki_restorer_spec.rb
@@ -24,8 +24,8 @@ describe Gitlab::ImportExport::WikiRestorer do
after do
FileUtils.rm_rf(export_path)
- Gitlab::Shell.new.remove_repository(project_with_wiki.wiki.repository_storage_path, project_with_wiki.wiki.disk_path)
- Gitlab::Shell.new.remove_repository(project.wiki.repository_storage_path, project.wiki.disk_path)
+ Gitlab::Shell.new.remove_repository(project_with_wiki.wiki.repository_storage, project_with_wiki.wiki.disk_path)
+ Gitlab::Shell.new.remove_repository(project.wiki.repository_storage, project.wiki.disk_path)
end
it 'restores the wiki repo successfully' do
diff --git a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
index 737c9a624e0..eb1b13704ea 100644
--- a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::LegacyGithubImport::ProjectCreator do
let(:user) { create(:user) }
- let(:namespace) { create(:group, owner: user) }
+ let(:namespace) { create(:group) }
let(:repo) do
OpenStruct.new(
diff --git a/spec/lib/gitlab/sentry_spec.rb b/spec/lib/gitlab/sentry_spec.rb
index 8c211d1c63f..499757da061 100644
--- a/spec/lib/gitlab/sentry_spec.rb
+++ b/spec/lib/gitlab/sentry_spec.rb
@@ -7,7 +7,49 @@ describe Gitlab::Sentry do
described_class.context(nil)
- expect(Raven.tags_context[:locale]).to eq(I18n.locale.to_s)
+ expect(Raven.tags_context[:locale].to_s).to eq(I18n.locale.to_s)
+ end
+ end
+
+ describe '.track_exception' do
+ let(:exception) { RuntimeError.new('boom') }
+
+ before do
+ allow(described_class).to receive(:enabled?).and_return(true)
+ end
+
+ it 'raises the exception if it should' do
+ expect(described_class).to receive(:should_raise?).and_return(true)
+ expect { described_class.track_exception(exception) }
+ .to raise_error(RuntimeError)
+ end
+
+ context 'when exceptions should not be raised' do
+ before do
+ allow(described_class).to receive(:should_raise?).and_return(false)
+ end
+
+ it 'logs the exception with all attributes passed' do
+ expected_extras = {
+ some_other_info: 'info',
+ issue_url: 'http://gitlab.com/gitlab-org/gitlab-ce/issues/1'
+ }
+
+ expect(Raven).to receive(:capture_exception)
+ .with(exception, extra: a_hash_including(expected_extras))
+
+ described_class.track_exception(
+ exception,
+ issue_url: 'http://gitlab.com/gitlab-org/gitlab-ce/issues/1',
+ extra: { some_other_info: 'info' }
+ )
+ end
+
+ it 'sets the context' do
+ expect(described_class).to receive(:context)
+
+ described_class.track_exception(exception)
+ end
end
end
end
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index 7ff2c0639ec..bf6ee4b0b59 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -447,18 +447,18 @@ describe Gitlab::Shell do
let(:disk_path) { "#{project.disk_path}.git" }
it 'returns true when the command succeeds' do
- expect(gitlab_shell.exists?(project.repository_storage_path, disk_path)).to be(true)
+ expect(gitlab_shell.exists?(project.repository_storage, disk_path)).to be(true)
- expect(gitlab_shell.remove_repository(project.repository_storage_path, project.disk_path)).to be(true)
+ expect(gitlab_shell.remove_repository(project.repository_storage, project.disk_path)).to be(true)
- expect(gitlab_shell.exists?(project.repository_storage_path, disk_path)).to be(false)
+ expect(gitlab_shell.exists?(project.repository_storage, disk_path)).to be(false)
end
it 'keeps the namespace directory' do
- gitlab_shell.remove_repository(project.repository_storage_path, project.disk_path)
+ gitlab_shell.remove_repository(project.repository_storage, project.disk_path)
- expect(gitlab_shell.exists?(project.repository_storage_path, disk_path)).to be(false)
- expect(gitlab_shell.exists?(project.repository_storage_path, project.disk_path.gsub(project.name, ''))).to be(true)
+ expect(gitlab_shell.exists?(project.repository_storage, disk_path)).to be(false)
+ expect(gitlab_shell.exists?(project.repository_storage, project.disk_path.gsub(project.name, ''))).to be(true)
end
end
@@ -469,18 +469,18 @@ describe Gitlab::Shell do
old_path = project2.disk_path
new_path = "project/new_path"
- expect(gitlab_shell.exists?(project2.repository_storage_path, "#{old_path}.git")).to be(true)
- expect(gitlab_shell.exists?(project2.repository_storage_path, "#{new_path}.git")).to be(false)
+ expect(gitlab_shell.exists?(project2.repository_storage, "#{old_path}.git")).to be(true)
+ expect(gitlab_shell.exists?(project2.repository_storage, "#{new_path}.git")).to be(false)
- expect(gitlab_shell.mv_repository(project2.repository_storage_path, old_path, new_path)).to be_truthy
+ expect(gitlab_shell.mv_repository(project2.repository_storage, old_path, new_path)).to be_truthy
- expect(gitlab_shell.exists?(project2.repository_storage_path, "#{old_path}.git")).to be(false)
- expect(gitlab_shell.exists?(project2.repository_storage_path, "#{new_path}.git")).to be(true)
+ expect(gitlab_shell.exists?(project2.repository_storage, "#{old_path}.git")).to be(false)
+ expect(gitlab_shell.exists?(project2.repository_storage, "#{new_path}.git")).to be(true)
end
it 'returns false when the command fails' do
- expect(gitlab_shell.mv_repository(project2.repository_storage_path, project2.disk_path, '')).to be_falsy
- expect(gitlab_shell.exists?(project2.repository_storage_path, "#{project2.disk_path}.git")).to be(true)
+ expect(gitlab_shell.mv_repository(project2.repository_storage, project2.disk_path, '')).to be_falsy
+ expect(gitlab_shell.exists?(project2.repository_storage, "#{project2.disk_path}.git")).to be(true)
end
end
@@ -679,55 +679,55 @@ describe Gitlab::Shell do
describe 'namespace actions' do
subject { described_class.new }
- let(:storage_path) { Gitlab.config.repositories.storages.default.legacy_disk_path }
+ let(:storage) { Gitlab.config.repositories.storages.keys.first }
describe '#add_namespace' do
it 'creates a namespace' do
- subject.add_namespace(storage_path, "mepmep")
+ subject.add_namespace(storage, "mepmep")
- expect(subject.exists?(storage_path, "mepmep")).to be(true)
+ expect(subject.exists?(storage, "mepmep")).to be(true)
end
end
describe '#exists?' do
context 'when the namespace does not exist' do
it 'returns false' do
- expect(subject.exists?(storage_path, "non-existing")).to be(false)
+ expect(subject.exists?(storage, "non-existing")).to be(false)
end
end
context 'when the namespace exists' do
it 'returns true' do
- subject.add_namespace(storage_path, "mepmep")
+ subject.add_namespace(storage, "mepmep")
- expect(subject.exists?(storage_path, "mepmep")).to be(true)
+ expect(subject.exists?(storage, "mepmep")).to be(true)
end
end
end
describe '#remove' do
it 'removes the namespace' do
- subject.add_namespace(storage_path, "mepmep")
- subject.rm_namespace(storage_path, "mepmep")
+ subject.add_namespace(storage, "mepmep")
+ subject.rm_namespace(storage, "mepmep")
- expect(subject.exists?(storage_path, "mepmep")).to be(false)
+ expect(subject.exists?(storage, "mepmep")).to be(false)
end
end
describe '#mv_namespace' do
it 'renames the namespace' do
- subject.add_namespace(storage_path, "mepmep")
- subject.mv_namespace(storage_path, "mepmep", "2mep")
+ subject.add_namespace(storage, "mepmep")
+ subject.mv_namespace(storage, "mepmep", "2mep")
- expect(subject.exists?(storage_path, "mepmep")).to be(false)
- expect(subject.exists?(storage_path, "2mep")).to be(true)
+ expect(subject.exists?(storage, "mepmep")).to be(false)
+ expect(subject.exists?(storage, "2mep")).to be(true)
end
end
end
def find_in_authorized_keys_file(key_id)
gitlab_shell.batch_read_key_ids do |ids|
- return true if ids.include?(key_id)
+ return true if ids.include?(key_id) # rubocop:disable Cop/AvoidReturnFromBlocks
end
false
diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb
index 40c8286b1b9..97b6069f64d 100644
--- a/spec/lib/gitlab/user_access_spec.rb
+++ b/spec/lib/gitlab/user_access_spec.rb
@@ -32,6 +32,12 @@ describe Gitlab::UserAccess do
let(:empty_project) { create(:project_empty_repo) }
let(:project_access) { described_class.new(user, project: empty_project) }
+ it 'returns true for admins' do
+ user.update!(admin: true)
+
+ expect(access.can_push_to_branch?('master')).to be_truthy
+ end
+
it 'returns true if user is master' do
empty_project.add_master(user)
@@ -71,6 +77,12 @@ describe Gitlab::UserAccess do
let(:branch) { create :protected_branch, project: project, name: "test" }
let(:not_existing_branch) { create :protected_branch, :developers_can_merge, project: project }
+ it 'returns true for admins' do
+ user.update!(admin: true)
+
+ expect(access.can_push_to_branch?(branch.name)).to be_truthy
+ end
+
it 'returns true if user is a master' do
project.add_master(user)
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index 71a743495a2..4ba99009855 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe Gitlab::Utils do
- delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, :which, :ensure_array_from_string, to: :described_class
+ delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, :which, :ensure_array_from_string,
+ :bytes_to_megabytes, to: :described_class
describe '.slugify' do
{
@@ -97,4 +98,12 @@ describe Gitlab::Utils do
expect(ensure_array_from_string(str)).to eq(%w[seven eight 9 10])
end
end
+
+ describe '.bytes_to_megabytes' do
+ it 'converts bytes to megabytes' do
+ bytes = 1.megabyte
+
+ expect(bytes_to_megabytes(bytes)).to eq(1)
+ end
+ end
end
diff --git a/spec/lib/gitlab/view/presenter/base_spec.rb b/spec/lib/gitlab/view/presenter/base_spec.rb
index 32a946ca034..4eca53032a2 100644
--- a/spec/lib/gitlab/view/presenter/base_spec.rb
+++ b/spec/lib/gitlab/view/presenter/base_spec.rb
@@ -48,4 +48,11 @@ describe Gitlab::View::Presenter::Base do
end
end
end
+
+ describe '#present' do
+ it 'returns self' do
+ presenter = presenter_class.new(build_stubbed(:project))
+ expect(presenter.present).to eq(presenter)
+ end
+ end
end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index d64ea72e346..e732b089d44 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -482,4 +482,26 @@ describe Gitlab::Workhorse do
}.deep_stringify_keys)
end
end
+
+ describe '.send_git_snapshot' do
+ let(:url) { 'http://example.com' }
+
+ subject(:request) { described_class.send_git_snapshot(repository) }
+
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(request)
+
+ expect(key).to eq("Gitlab-Workhorse-Send-Data")
+ expect(command).to eq('git-snapshot')
+ expect(params).to eq(
+ 'GitalyServer' => {
+ 'address' => Gitlab::GitalyClient.address(project.repository_storage),
+ 'token' => Gitlab::GitalyClient.token(project.repository_storage)
+ },
+ 'GetSnapshotRequest' => Gitaly::GetSnapshotRequest.new(
+ repository: repository.gitaly_repository
+ ).to_json
+ )
+ end
+ end
end
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index f97136f0191..da146e24893 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -1,6 +1,14 @@
-require 'rails_helper'
+require 'fast_spec_helper'
+
+require_dependency 'gitlab'
describe Gitlab do
+ describe '.root' do
+ it 'returns the root path of the app' do
+ expect(described_class.root).to eq(Pathname.new(File.expand_path('../..', __dir__)))
+ end
+ end
+
describe '.com?' do
it 'is true when on GitLab.com' do
stub_config_setting(url: 'https://gitlab.com')
@@ -14,6 +22,12 @@ describe Gitlab do
expect(described_class.com?).to eq true
end
+ it 'is true when on other gitlab subdomain' do
+ stub_config_setting(url: 'https://example.gitlab.com')
+
+ expect(described_class.com?).to eq true
+ end
+
it 'is false when not on GitLab.com' do
stub_config_setting(url: 'http://example.com')
diff --git a/spec/lib/rspec_flaky/config_spec.rb b/spec/lib/rspec_flaky/config_spec.rb
index 83556787e85..4a71b1feebd 100644
--- a/spec/lib/rspec_flaky/config_spec.rb
+++ b/spec/lib/rspec_flaky/config_spec.rb
@@ -16,23 +16,25 @@ describe RspecFlaky::Config, :aggregate_failures do
end
end
- context "when ENV['FLAKY_RSPEC_GENERATE_REPORT'] is set to 'false'" do
- before do
- stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'false')
- end
-
- it 'returns false' do
- expect(described_class).not_to be_generate_report
+ context "when ENV['FLAKY_RSPEC_GENERATE_REPORT'] is set" do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:env_value, :result) do
+ '1' | true
+ 'true' | true
+ 'foo' | false
+ '0' | false
+ 'false' | false
end
- end
- context "when ENV['FLAKY_RSPEC_GENERATE_REPORT'] is set to 'true'" do
- before do
- stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'true')
- end
+ with_them do
+ before do
+ stub_env('FLAKY_RSPEC_GENERATE_REPORT', env_value)
+ end
- it 'returns true' do
- expect(described_class).to be_generate_report
+ it 'returns false' do
+ expect(described_class.generate_report?).to be(result)
+ end
end
end
end
diff --git a/spec/lib/rspec_flaky/flaky_examples_collection_spec.rb b/spec/lib/rspec_flaky/flaky_examples_collection_spec.rb
index 06a8ba0d02e..6731a27ed17 100644
--- a/spec/lib/rspec_flaky/flaky_examples_collection_spec.rb
+++ b/spec/lib/rspec_flaky/flaky_examples_collection_spec.rb
@@ -24,14 +24,6 @@ describe RspecFlaky::FlakyExamplesCollection, :aggregate_failures do
}
end
- describe '.from_json' do
- it 'accepts a JSON' do
- collection = described_class.from_json(JSON.pretty_generate(collection_hash))
-
- expect(collection.to_report).to eq(described_class.new(collection_hash).to_report)
- end
- end
-
describe '#initialize' do
it 'accepts no argument' do
expect { described_class.new }.not_to raise_error
@@ -46,11 +38,11 @@ describe RspecFlaky::FlakyExamplesCollection, :aggregate_failures do
end
end
- describe '#to_report' do
+ describe '#to_h' do
it 'calls #to_h on the values' do
collection = described_class.new(collection_hash)
- expect(collection.to_report).to eq(collection_report)
+ expect(collection.to_h).to eq(collection_report)
end
end
@@ -61,7 +53,7 @@ describe RspecFlaky::FlakyExamplesCollection, :aggregate_failures do
a: { example_id: 'spec/foo/bar_spec.rb:2' },
c: { example_id: 'spec/bar/baz_spec.rb:4' })
- expect((collection2 - collection1).to_report).to eq(
+ expect((collection2 - collection1).to_h).to eq(
c: {
example_id: 'spec/bar/baz_spec.rb:4',
first_flaky_at: nil,
diff --git a/spec/lib/rspec_flaky/listener_spec.rb b/spec/lib/rspec_flaky/listener_spec.rb
index bfb7648b486..ef085445081 100644
--- a/spec/lib/rspec_flaky/listener_spec.rb
+++ b/spec/lib/rspec_flaky/listener_spec.rb
@@ -4,7 +4,7 @@ describe RspecFlaky::Listener, :aggregate_failures do
let(:already_flaky_example_uid) { '6e869794f4cfd2badd93eb68719371d1' }
let(:suite_flaky_example_report) do
{
- already_flaky_example_uid => {
+ "#{already_flaky_example_uid}": {
example_id: 'spec/foo/bar_spec.rb:2',
file: 'spec/foo/bar_spec.rb',
line: 2,
@@ -55,8 +55,7 @@ describe RspecFlaky::Listener, :aggregate_failures do
it 'returns a valid Listener instance' do
listener = described_class.new
- expect(listener.to_report(listener.suite_flaky_examples))
- .to eq(expected_suite_flaky_examples)
+ expect(listener.suite_flaky_examples.to_h).to eq(expected_suite_flaky_examples)
expect(listener.flaky_examples).to eq({})
end
end
@@ -65,25 +64,35 @@ describe RspecFlaky::Listener, :aggregate_failures do
it_behaves_like 'a valid Listener instance'
end
- context 'when a report file exists and set by SUITE_FLAKY_RSPEC_REPORT_PATH' do
- let(:report_file) do
- Tempfile.new(%w[rspec_flaky_report .json]).tap do |f|
- f.write(JSON.pretty_generate(suite_flaky_example_report))
- f.rewind
- end
- end
+ context 'when SUITE_FLAKY_RSPEC_REPORT_PATH is set' do
+ let(:report_file_path) { 'foo/report.json' }
before do
- stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', report_file.path)
+ stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', report_file_path)
end
- after do
- report_file.close
- report_file.unlink
+ context 'and report file exists' do
+ before do
+ expect(File).to receive(:exist?).with(report_file_path).and_return(true)
+ end
+
+ it 'delegates the load to RspecFlaky::Report' do
+ report = RspecFlaky::Report.new(RspecFlaky::FlakyExamplesCollection.new(suite_flaky_example_report))
+
+ expect(RspecFlaky::Report).to receive(:load).with(report_file_path).and_return(report)
+ expect(described_class.new.suite_flaky_examples.to_h).to eq(report.flaky_examples.to_h)
+ end
end
- it_behaves_like 'a valid Listener instance' do
- let(:expected_suite_flaky_examples) { suite_flaky_example_report }
+ context 'and report file does not exist' do
+ before do
+ expect(File).to receive(:exist?).with(report_file_path).and_return(false)
+ end
+
+ it 'return an empty hash' do
+ expect(RspecFlaky::Report).not_to receive(:load)
+ expect(described_class.new.suite_flaky_examples.to_h).to eq({})
+ end
end
end
end
@@ -186,74 +195,21 @@ describe RspecFlaky::Listener, :aggregate_failures do
let(:notification_already_flaky_rspec_example) { double(example: already_flaky_rspec_example) }
context 'when a report file path is set by FLAKY_RSPEC_REPORT_PATH' do
- let(:report_file_path) { Rails.root.join('tmp', 'rspec_flaky_report.json') }
- let(:new_report_file_path) { Rails.root.join('tmp', 'rspec_flaky_new_report.json') }
+ it 'delegates the writes to RspecFlaky::Report' do
+ listener.example_passed(notification_new_flaky_rspec_example)
+ listener.example_passed(notification_already_flaky_rspec_example)
- before do
- stub_env('FLAKY_RSPEC_REPORT_PATH', report_file_path)
- stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', new_report_file_path)
- FileUtils.rm(report_file_path) if File.exist?(report_file_path)
- FileUtils.rm(new_report_file_path) if File.exist?(new_report_file_path)
- end
+ report1 = double
+ report2 = double
- after do
- FileUtils.rm(report_file_path) if File.exist?(report_file_path)
- FileUtils.rm(new_report_file_path) if File.exist?(new_report_file_path)
- end
+ expect(RspecFlaky::Report).to receive(:new).with(listener.flaky_examples).and_return(report1)
+ expect(report1).to receive(:write).with(RspecFlaky::Config.flaky_examples_report_path)
- context 'when FLAKY_RSPEC_GENERATE_REPORT == "false"' do
- before do
- stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'false')
- end
-
- it 'does not write any report file' do
- listener.example_passed(notification_new_flaky_rspec_example)
+ expect(RspecFlaky::Report).to receive(:new).with(listener.flaky_examples - listener.suite_flaky_examples).and_return(report2)
+ expect(report2).to receive(:write).with(RspecFlaky::Config.new_flaky_examples_report_path)
- listener.dump_summary(nil)
-
- expect(File.exist?(report_file_path)).to be(false)
- expect(File.exist?(new_report_file_path)).to be(false)
- end
+ listener.dump_summary(nil)
end
-
- context 'when FLAKY_RSPEC_GENERATE_REPORT == "true"' do
- before do
- stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'true')
- end
-
- around do |example|
- Timecop.freeze { example.run }
- end
-
- it 'writes the report files' do
- listener.example_passed(notification_new_flaky_rspec_example)
- listener.example_passed(notification_already_flaky_rspec_example)
-
- listener.dump_summary(nil)
-
- expect(File.exist?(report_file_path)).to be(true)
- expect(File.exist?(new_report_file_path)).to be(true)
-
- expect(File.read(report_file_path))
- .to eq(JSON.pretty_generate(listener.to_report(listener.flaky_examples)))
-
- new_example = RspecFlaky::Example.new(notification_new_flaky_rspec_example)
- new_flaky_example = RspecFlaky::FlakyExample.new(new_example)
- new_flaky_example.update_flakiness!
-
- expect(File.read(new_report_file_path))
- .to eq(JSON.pretty_generate(listener.to_report(new_example.uid => new_flaky_example)))
- end
- end
- end
- end
-
- describe '#to_report' do
- let(:listener) { described_class.new(suite_flaky_example_report.to_json) }
-
- it 'transforms the internal hash to a JSON-ready hash' do
- expect(listener.to_report(already_flaky_example_uid => already_flaky_example))
- .to match(hash_including(suite_flaky_example_report))
end
end
end
diff --git a/spec/lib/rspec_flaky/report_spec.rb b/spec/lib/rspec_flaky/report_spec.rb
new file mode 100644
index 00000000000..7d57d99f7e5
--- /dev/null
+++ b/spec/lib/rspec_flaky/report_spec.rb
@@ -0,0 +1,125 @@
+require 'spec_helper'
+
+describe RspecFlaky::Report, :aggregate_failures do
+ let(:a_hundred_days) { 3600 * 24 * 100 }
+ let(:collection_hash) do
+ {
+ a: { example_id: 'spec/foo/bar_spec.rb:2' },
+ b: { example_id: 'spec/foo/baz_spec.rb:3', first_flaky_at: (Time.now - a_hundred_days).to_s, last_flaky_at: (Time.now - a_hundred_days).to_s }
+ }
+ end
+ let(:suite_flaky_example_report) do
+ {
+ '6e869794f4cfd2badd93eb68719371d1': {
+ example_id: 'spec/foo/bar_spec.rb:2',
+ file: 'spec/foo/bar_spec.rb',
+ line: 2,
+ description: 'hello world',
+ first_flaky_at: 1234,
+ last_flaky_at: 4321,
+ last_attempts_count: 3,
+ flaky_reports: 1,
+ last_flaky_job: nil
+ }
+ }
+ end
+ let(:flaky_examples) { RspecFlaky::FlakyExamplesCollection.new(collection_hash) }
+ let(:report) { described_class.new(flaky_examples) }
+
+ describe '.load' do
+ let!(:report_file) do
+ Tempfile.new(%w[rspec_flaky_report .json]).tap do |f|
+ f.write(JSON.pretty_generate(suite_flaky_example_report))
+ f.rewind
+ end
+ end
+
+ after do
+ report_file.close
+ report_file.unlink
+ end
+
+ it 'loads the report file' do
+ expect(described_class.load(report_file.path).flaky_examples.to_h).to eq(suite_flaky_example_report)
+ end
+ end
+
+ describe '.load_json' do
+ let(:report_json) do
+ JSON.pretty_generate(suite_flaky_example_report)
+ end
+
+ it 'loads the report file' do
+ expect(described_class.load_json(report_json).flaky_examples.to_h).to eq(suite_flaky_example_report)
+ end
+ end
+
+ describe '#initialize' do
+ it 'accepts a RspecFlaky::FlakyExamplesCollection' do
+ expect { report }.not_to raise_error
+ end
+
+ it 'does not accept anything else' do
+ expect { described_class.new([1, 2, 3]) }.to raise_error(ArgumentError, "`flaky_examples` must be a RspecFlaky::FlakyExamplesCollection, Array given!")
+ end
+ end
+
+ it 'delegates to #flaky_examples using SimpleDelegator' do
+ expect(report.__getobj__).to eq(flaky_examples)
+ end
+
+ describe '#write' do
+ let(:report_file_path) { Rails.root.join('tmp', 'rspec_flaky_report.json') }
+
+ before do
+ FileUtils.rm(report_file_path) if File.exist?(report_file_path)
+ end
+
+ after do
+ FileUtils.rm(report_file_path) if File.exist?(report_file_path)
+ end
+
+ context 'when RspecFlaky::Config.generate_report? is false' do
+ before do
+ allow(RspecFlaky::Config).to receive(:generate_report?).and_return(false)
+ end
+
+ it 'does not write any report file' do
+ report.write(report_file_path)
+
+ expect(File.exist?(report_file_path)).to be(false)
+ end
+ end
+
+ context 'when RspecFlaky::Config.generate_report? is true' do
+ before do
+ allow(RspecFlaky::Config).to receive(:generate_report?).and_return(true)
+ end
+
+ it 'delegates the writes to RspecFlaky::Report' do
+ report.write(report_file_path)
+
+ expect(File.exist?(report_file_path)).to be(true)
+ expect(File.read(report_file_path))
+ .to eq(JSON.pretty_generate(report.flaky_examples.to_h))
+ end
+ end
+ end
+
+ describe '#prune_outdated' do
+ it 'returns a new collection without the examples older than 90 days by default' do
+ new_report = flaky_examples.to_h.dup.tap { |r| r.delete(:b) }
+ new_flaky_examples = report.prune_outdated
+
+ expect(new_flaky_examples).to be_a(described_class)
+ expect(new_flaky_examples.to_h).to eq(new_report)
+ expect(flaky_examples).to have_key(:b)
+ end
+
+ it 'accepts a given number of days' do
+ new_flaky_examples = report.prune_outdated(days: 200)
+
+ expect(new_flaky_examples.to_h).to eq(report.to_h)
+ end
+ end
+end
diff --git a/spec/migrations/assure_commits_count_for_merge_request_diff_spec.rb b/spec/migrations/assure_commits_count_for_merge_request_diff_spec.rb
new file mode 100644
index 00000000000..b8c3a3eda4e
--- /dev/null
+++ b/spec/migrations/assure_commits_count_for_merge_request_diff_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20180425131009_assure_commits_count_for_merge_request_diff.rb')
+
+describe AssureCommitsCountForMergeRequestDiff, :migration, :sidekiq, :redis do
+ let(:migration) { spy('migration') }
+
+ before do
+ allow(Gitlab::BackgroundMigration::AddMergeRequestDiffCommitsCount)
+ .to receive(:new).and_return(migration)
+ end
+
+ context 'when there are still unmigrated commit_counts afterwards' do
+ let(:namespaces) { table('namespaces') }
+ let(:projects) { table('projects') }
+ let(:merge_requests) { table('merge_requests') }
+ let(:diffs) { table('merge_request_diffs') }
+
+ before do
+ namespace = namespaces.create(name: 'foo', path: 'foo')
+ project = projects.create!(namespace_id: namespace.id)
+ merge_request = merge_requests.create!(source_branch: 'x', target_branch: 'y', target_project_id: project.id)
+ diffs.create!(commits_count: nil, merge_request_id: merge_request.id)
+ diffs.create!(commits_count: nil, merge_request_id: merge_request.id)
+ end
+
+ it 'migrates commit_counts sequentially in batches' do
+ migrate!
+
+ expect(migration).to have_received(:perform).once
+ end
+ end
+end
diff --git a/spec/migrations/create_missing_namespace_for_internal_users_spec.rb b/spec/migrations/create_missing_namespace_for_internal_users_spec.rb
new file mode 100644
index 00000000000..ac3a4b1f68f
--- /dev/null
+++ b/spec/migrations/create_missing_namespace_for_internal_users_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20180413022611_create_missing_namespace_for_internal_users.rb')
+
+describe CreateMissingNamespaceForInternalUsers, :migration do
+ let(:users) { table(:users) }
+ let(:namespaces) { table(:namespaces) }
+ let(:routes) { table(:routes) }
+
+ internal_user_types = [:ghost]
+ internal_user_types << :support_bot if ActiveRecord::Base.connection.column_exists?(:users, :support_bot)
+
+ internal_user_types.each do |attr|
+ context "for #{attr} user" do
+ let(:internal_user) do
+ users.create!(email: 'test@example.com', projects_limit: 100, username: 'test', attr => true)
+ end
+
+ it 'creates the missing namespace' do
+ expect(namespaces.find_by(owner_id: internal_user.id)).to be_nil
+
+ migrate!
+
+ namespace = Namespace.find_by(type: nil, owner_id: internal_user.id)
+ route = namespace.route
+
+ expect(namespace.path).to eq(route.path)
+ expect(namespace.name).to eq(route.name)
+ end
+
+ it 'sets notification email' do
+ users.update(internal_user.id, notification_email: nil)
+
+ expect(users.find(internal_user.id).notification_email).to be_nil
+
+ migrate!
+
+ user = users.find(internal_user.id)
+ expect(user.notification_email).to eq(user.email)
+ end
+ end
+ end
+end
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index cd175dba6da..199f49d0bf2 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -7,62 +7,6 @@ describe Ability do
end
end
- describe '.can_edit_note?' do
- let(:project) { create(:project) }
- let(:note) { create(:note_on_issue, project: project) }
-
- context 'using an anonymous user' do
- it 'returns false' do
- expect(described_class.can_edit_note?(nil, note)).to be_falsy
- end
- end
-
- context 'using a system note' do
- it 'returns false' do
- system_note = create(:note, system: true)
- user = create(:user)
-
- expect(described_class.can_edit_note?(user, system_note)).to be_falsy
- end
- end
-
- context 'using users with different access levels' do
- let(:user) { create(:user) }
-
- it 'returns true for the author' do
- expect(described_class.can_edit_note?(note.author, note)).to be_truthy
- end
-
- it 'returns false for a guest user' do
- project.add_guest(user)
-
- expect(described_class.can_edit_note?(user, note)).to be_falsy
- end
-
- it 'returns false for a developer' do
- project.add_developer(user)
-
- expect(described_class.can_edit_note?(user, note)).to be_falsy
- end
-
- it 'returns true for a master' do
- project.add_master(user)
-
- expect(described_class.can_edit_note?(user, note)).to be_truthy
- end
-
- it 'returns true for a group owner' do
- group = create(:group)
- project.project_group_links.create(
- group: group,
- group_access: Gitlab::Access::MASTER)
- group.add_owner(user)
-
- expect(described_class.can_edit_note?(user, note)).to be_truthy
- end
- end
- end
-
describe '.users_that_can_read_project' do
context 'using a public project' do
it 'returns all the users' do
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 461e754dc1f..5326f9cb8c0 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -51,7 +51,11 @@ describe BroadcastMessage do
expect(described_class).to receive(:where).and_call_original.once
- 2.times { described_class.current }
+ described_class.current
+
+ Timecop.travel(1.year) do
+ described_class.current
+ end
end
it 'includes messages that need to be displayed in the future' do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index a12717835b0..3158e006720 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1384,29 +1384,51 @@ describe Ci::Build do
end
end
- describe '#update_project_statistics' do
- let!(:build) { create(:ci_build, artifacts_size: 23) }
-
- it 'updates project statistics when the artifact size changes' do
- expect(ProjectCacheWorker).to receive(:perform_async)
- .with(build.project_id, [], [:build_artifacts_size])
+ context 'when updating the build' do
+ let(:build) { create(:ci_build, artifacts_size: 23) }
+ it 'updates project statistics' do
build.artifacts_size = 42
- build.save!
+
+ expect(build).to receive(:update_project_statistics_after_save).and_call_original
+
+ expect { build.save! }
+ .to change { build.project.statistics.reload.build_artifacts_size }
+ .by(19)
end
- it 'does not update project statistics when the artifact size stays the same' do
- expect(ProjectCacheWorker).not_to receive(:perform_async)
+ context 'when the artifact size stays the same' do
+ it 'does not update project statistics' do
+ build.name = 'changed'
- build.name = 'changed'
- build.save!
+ expect(build).not_to receive(:update_project_statistics_after_save)
+
+ build.save!
+ end
end
+ end
- it 'updates project statistics when the build is destroyed' do
- expect(ProjectCacheWorker).to receive(:perform_async)
- .with(build.project_id, [], [:build_artifacts_size])
+ context 'when destroying the build' do
+ let!(:build) { create(:ci_build, artifacts_size: 23) }
- build.destroy
+ it 'updates project statistics' do
+ expect(ProjectStatistics)
+ .to receive(:increment_statistic)
+ .and_call_original
+
+ expect { build.destroy! }
+ .to change { build.project.statistics.reload.build_artifacts_size }
+ .by(-23)
+ end
+
+ context 'when the build is destroyed due to the project being destroyed' do
+ it 'does not update the project statistics' do
+ expect(ProjectStatistics)
+ .not_to receive(:increment_statistic)
+
+ build.project.update_attributes(pending_delete: true)
+ build.project.destroy!
+ end
end
end
@@ -1472,7 +1494,7 @@ describe Ci::Build do
{ key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false },
{ key: 'CI', value: 'true', public: true },
{ key: 'GITLAB_CI', value: 'true', public: true },
- { key: 'GITLAB_FEATURES', value: project.namespace.features.join(','), public: true },
+ { key: 'GITLAB_FEATURES', value: project.licensed_features.join(','), public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
{ key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
{ key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
@@ -2013,6 +2035,34 @@ describe Ci::Build do
expect(build).not_to be_persisted
end
end
+
+ context 'for deploy tokens' do
+ let(:deploy_token) { create(:deploy_token, :gitlab_deploy_token) }
+
+ let(:deploy_token_variables) do
+ [
+ { key: 'CI_DEPLOY_USER', value: deploy_token.name, public: true },
+ { key: 'CI_DEPLOY_PASSWORD', value: deploy_token.token, public: false }
+ ]
+ end
+
+ context 'when gitlab-deploy-token exists' do
+ before do
+ project.deploy_tokens << deploy_token
+ end
+
+ it 'should include deploy token variables' do
+ is_expected.to include(*deploy_token_variables)
+ end
+ end
+
+ context 'when gitlab-deploy-token does not exist' do
+ it 'should not include deploy token variables' do
+ expect(subject.find { |v| v[:key] == 'CI_DEPLOY_USER'}).to be_nil
+ expect(subject.find { |v| v[:key] == 'CI_DEPLOY_PASSWORD'}).to be_nil
+ end
+ end
+ end
end
describe '#scoped_variables' do
@@ -2061,7 +2111,9 @@ describe Ci::Build do
CI_REGISTRY_USER
CI_REGISTRY_PASSWORD
CI_REPOSITORY_URL
- CI_ENVIRONMENT_URL]
+ CI_ENVIRONMENT_URL
+ CI_DEPLOY_USER
+ CI_DEPLOY_PASSWORD]
build.scoped_variables.map { |env| env[:key] }.tap do |names|
expect(names).not_to include(*keys)
@@ -2140,10 +2192,6 @@ describe Ci::Build do
it "doesn't save timeout_source" do
expect { run_job_without_exception }.not_to change { job.reload.ensure_metadata.timeout_source }
end
-
- it 'raises an exception' do
- expect { job.run! }.to raise_error(StateMachines::InvalidTransition)
- end
end
end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index 1aa28434879..a3e119cbc27 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Ci::JobArtifact do
- set(:artifact) { create(:ci_job_artifact, :archive) }
+ let(:artifact) { create(:ci_job_artifact, :archive) }
describe "Associations" do
it { is_expected.to belong_to(:project) }
@@ -59,10 +59,32 @@ describe Ci::JobArtifact do
end
end
- describe '#set_size' do
- it 'sets the size' do
+ context 'creating the artifact' do
+ let(:project) { create(:project) }
+ let(:artifact) { create(:ci_job_artifact, :archive, project: project) }
+
+ it 'sets the size from the file size' do
expect(artifact.size).to eq(106365)
end
+
+ it 'updates the project statistics' do
+ expect { artifact }
+ .to change { project.statistics.reload.build_artifacts_size }
+ .by(106365)
+ end
+ end
+
+ context 'updating the artifact file' do
+ it 'updates the artifact size' do
+ artifact.update!(file: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png')))
+ expect(artifact.size).to eq(1062)
+ end
+
+ it 'updates the project statistics' do
+ expect { artifact.update!(file: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
+ .to change { artifact.project.statistics.reload.build_artifacts_size }
+ .by(1062 - 106365)
+ end
end
describe '#file' do
@@ -118,4 +140,71 @@ describe Ci::JobArtifact do
is_expected.to be_nil
end
end
+
+ context 'when destroying the artifact' do
+ let(:project) { create(:project, :repository) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let!(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ it 'updates the project statistics' do
+ artifact = build.job_artifacts.first
+
+ expect(ProjectStatistics)
+ .to receive(:increment_statistic)
+ .and_call_original
+
+ expect { artifact.destroy }
+ .to change { project.statistics.reload.build_artifacts_size }
+ .by(-106365)
+ end
+
+ context 'when it is destroyed from the project level' do
+ it 'does not update the project statistics' do
+ expect(ProjectStatistics)
+ .not_to receive(:increment_statistic)
+
+ project.update_attributes(pending_delete: true)
+ project.destroy!
+ end
+ end
+ end
+
+ describe 'file is being stored' do
+ subject { create(:ci_job_artifact, :archive) }
+
+ context 'when object has nil store' do
+ before do
+ subject.update_column(:file_store, nil)
+ subject.reload
+ end
+
+ it 'is stored locally' do
+ expect(subject.file_store).to be(nil)
+ expect(subject.file).to be_file_storage
+ expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+
+ context 'when existing object has local store' do
+ it 'is stored locally' do
+ expect(subject.file_store).to be(ObjectStorage::Store::LOCAL)
+ expect(subject.file).to be_file_storage
+ expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+
+ context 'when direct upload is enabled' do
+ before do
+ stub_artifacts_object_storage(direct_upload: true)
+ end
+
+ context 'when file is stored' do
+ it 'is stored remotely' do
+ expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(subject.file).not_to be_file_storage
+ expect(subject.file.object_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 959383ff0b7..4e6b037a720 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -450,6 +450,11 @@ eos
it "returns nil if the path doesn't exists" do
expect(commit.uri_type('this/path/doesnt/exist')).to be_nil
end
+
+ it 'is nil if the path is nil or empty' do
+ expect(commit.uri_type(nil)).to be_nil
+ expect(commit.uri_type("")).to be_nil
+ end
end
context 'when Gitaly commit_tree_entry feature is enabled' do
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index c536dab2681..2ed29052dc1 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -533,4 +533,36 @@ describe CommitStatus do
end
end
end
+
+ describe '#enqueue' do
+ let!(:current_time) { Time.new(2018, 4, 5, 14, 0, 0) }
+
+ before do
+ allow(Time).to receive(:now).and_return(current_time)
+ end
+
+ shared_examples 'commit status enqueued' do
+ it 'sets queued_at value when enqueued' do
+ expect { commit_status.enqueue }.to change { commit_status.reload.queued_at }.from(nil).to(current_time)
+ end
+ end
+
+ context 'when initial state is :created' do
+ let(:commit_status) { create(:commit_status, :created) }
+
+ it_behaves_like 'commit status enqueued'
+ end
+
+ context 'when initial state is :skipped' do
+ let(:commit_status) { create(:commit_status, :skipped) }
+
+ it_behaves_like 'commit status enqueued'
+ end
+
+ context 'when initial state is :manual' do
+ let(:commit_status) { create(:commit_status, :manual) }
+
+ it_behaves_like 'commit status enqueued'
+ end
+ end
end
diff --git a/spec/models/concerns/avatarable_spec.rb b/spec/models/concerns/avatarable_spec.rb
index 3696e6f62fd..9faf21bfbbd 100644
--- a/spec/models/concerns/avatarable_spec.rb
+++ b/spec/models/concerns/avatarable_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Avatarable do
- set(:project) { create(:project, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
+ let(:project) { create(:project, :with_avatar) }
let(:gitlab_host) { "https://gitlab.example.com" }
let(:relative_url_root) { "/gitlab" }
@@ -37,11 +37,23 @@ describe Avatarable do
project.visibility_level = visibility_level
end
- let(:avatar_path) { (avatar_path_prefix + [project.avatar.url]).join }
+ let(:avatar_path) { (avatar_path_prefix + [project.avatar.local_url]).join }
it 'returns the expected avatar path' do
expect(project.avatar_path(only_path: only_path)).to eq(avatar_path)
end
+
+ context "when avatar is stored remotely" do
+ before do
+ stub_uploads_object_storage(AvatarUploader)
+
+ project.avatar.migrate!(ObjectStorage::Store::REMOTE)
+ end
+
+ it 'returns the expected avatar path' do
+ expect(project.avatar_url(only_path: only_path)).to eq(avatar_path)
+ end
+ end
end
end
end
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index 34f923d3f0c..a980cff28fb 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -46,6 +46,31 @@ describe Awardable do
end
end
+ describe '#user_can_award?' do
+ let(:user) { create(:user) }
+
+ before do
+ issue.project.add_guest(user)
+ end
+
+ it 'does not allow upvoting or downvoting your own issue' do
+ issue.update!(author: user)
+
+ expect(issue.user_can_award?(user, AwardEmoji::DOWNVOTE_NAME)).to be_falsy
+ expect(issue.user_can_award?(user, AwardEmoji::UPVOTE_NAME)).to be_falsy
+ end
+
+ it 'is truthy when the user is allowed to award emoji' do
+ expect(issue.user_can_award?(user, AwardEmoji::UPVOTE_NAME)).to be_truthy
+ end
+
+ it 'is falsy when the project is archived' do
+ issue.project.update!(archived: true)
+
+ expect(issue.user_can_award?(user, AwardEmoji::UPVOTE_NAME)).to be_falsy
+ end
+ end
+
describe "#toggle_award_emoji" do
it "adds an emoji if it isn't awarded yet" do
expect { issue.toggle_award_emoji("thumbsup", award_emoji.user) }.to change { AwardEmoji.count }.by(1)
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 3c7f578975b..b3797c1fb46 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -72,7 +72,7 @@ describe CacheMarkdownField do
let(:updated_markdown) { '`Bar`' }
let(:updated_html) { '<p dir="auto"><code>Bar</code></p>' }
- let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: CacheMarkdownField::CACHE_VERSION) }
+ let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
describe '.attributes' do
it 'excludes cache attributes' do
@@ -89,17 +89,24 @@ describe CacheMarkdownField do
it { expect(thing.foo).to eq(markdown) }
it { expect(thing.foo_html).to eq(html) }
it { expect(thing.foo_html_changed?).not_to be_truthy }
- it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
+ it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
end
context 'a changed markdown field' do
- before do
- thing.foo = updated_markdown
- thing.save
+ shared_examples 'with cache version' do |cache_version|
+ let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
+
+ before do
+ thing.foo = updated_markdown
+ thing.save
+ end
+
+ it { expect(thing.foo_html).to eq(updated_html) }
+ it { expect(thing.cached_markdown_version).to eq(cache_version) }
end
- it { expect(thing.foo_html).to eq(updated_html) }
- it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
+ it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
+ it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
end
context 'when a markdown field is set repeatedly to an empty string' do
@@ -123,15 +130,22 @@ describe CacheMarkdownField do
end
context 'a non-markdown field changed' do
- before do
- thing.bar = 'OK'
- thing.save
+ shared_examples 'with cache version' do |cache_version|
+ let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
+
+ before do
+ thing.bar = 'OK'
+ thing.save
+ end
+
+ it { expect(thing.bar).to eq('OK') }
+ it { expect(thing.foo).to eq(markdown) }
+ it { expect(thing.foo_html).to eq(html) }
+ it { expect(thing.cached_markdown_version).to eq(cache_version) }
end
- it { expect(thing.bar).to eq('OK') }
- it { expect(thing.foo).to eq(markdown) }
- it { expect(thing.foo_html).to eq(html) }
- it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
+ it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
+ it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
end
context 'version is out of date' do
@@ -142,59 +156,85 @@ describe CacheMarkdownField do
end
it { expect(thing.foo_html).to eq(updated_html) }
- it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
+ it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION) }
end
describe '#cached_html_up_to_date?' do
- subject { thing.cached_html_up_to_date?(:foo) }
+ shared_examples 'with cache version' do |cache_version|
+ let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
- it 'returns false when the version is absent' do
- thing.cached_markdown_version = nil
+ subject { thing.cached_html_up_to_date?(:foo) }
- is_expected.to be_falsy
- end
+ it 'returns false when the version is absent' do
+ thing.cached_markdown_version = nil
- it 'returns false when the version is too early' do
- thing.cached_markdown_version -= 1
+ is_expected.to be_falsy
+ end
- is_expected.to be_falsy
- end
+ it 'returns false when the version is too early' do
+ thing.cached_markdown_version -= 1
- it 'returns false when the version is too late' do
- thing.cached_markdown_version += 1
+ is_expected.to be_falsy
+ end
- is_expected.to be_falsy
- end
+ it 'returns false when the version is too late' do
+ thing.cached_markdown_version += 1
- it 'returns true when the version is just right' do
- thing.cached_markdown_version = CacheMarkdownField::CACHE_VERSION
+ is_expected.to be_falsy
+ end
- is_expected.to be_truthy
- end
+ it 'returns true when the version is just right' do
+ thing.cached_markdown_version = cache_version
- it 'returns false if markdown has been changed but html has not' do
- thing.foo = updated_html
+ is_expected.to be_truthy
+ end
- is_expected.to be_falsy
- end
+ it 'returns false if markdown has been changed but html has not' do
+ thing.foo = updated_html
- it 'returns true if markdown has not been changed but html has' do
- thing.foo_html = updated_html
+ is_expected.to be_falsy
+ end
+
+ it 'returns true if markdown has not been changed but html has' do
+ thing.foo_html = updated_html
- is_expected.to be_truthy
+ is_expected.to be_truthy
+ end
+
+ it 'returns true if markdown and html have both been changed' do
+ thing.foo = updated_markdown
+ thing.foo_html = updated_html
+
+ is_expected.to be_truthy
+ end
+
+ it 'returns false if the markdown field is set but the html is not' do
+ thing.foo_html = nil
+
+ is_expected.to be_falsy
+ end
end
- it 'returns true if markdown and html have both been changed' do
- thing.foo = updated_markdown
- thing.foo_html = updated_html
+ it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
+ it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
+ end
+
+ describe '#latest_cached_markdown_version' do
+ subject { thing.latest_cached_markdown_version }
- is_expected.to be_truthy
+ it 'returns redcarpet version' do
+ thing.cached_markdown_version = CacheMarkdownField::CACHE_COMMONMARK_VERSION_START - 1
+ is_expected.to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end
- it 'returns false if the markdown field is set but the html is not' do
- thing.foo_html = nil
+ it 'returns commonmark version' do
+ thing.cached_markdown_version = CacheMarkdownField::CACHE_COMMONMARK_VERSION_START + 1
+ is_expected.to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION)
+ end
- is_expected.to be_falsy
+ it 'returns default version when version is nil' do
+ thing.cached_markdown_version = nil
+ is_expected.to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end
end
@@ -221,37 +261,44 @@ describe CacheMarkdownField do
thing.cached_markdown_version = nil
thing.refresh_markdown_cache
- expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
+ expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end
end
describe '#refresh_markdown_cache!' do
- before do
- thing.foo = updated_markdown
- end
+ shared_examples 'with cache version' do |cache_version|
+ let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
- it 'fills all html fields' do
- thing.refresh_markdown_cache!
+ before do
+ thing.foo = updated_markdown
+ end
- expect(thing.foo_html).to eq(updated_html)
- expect(thing.foo_html_changed?).to be_truthy
- expect(thing.baz_html_changed?).to be_truthy
- end
+ it 'fills all html fields' do
+ thing.refresh_markdown_cache!
- it 'skips saving if not persisted' do
- expect(thing).to receive(:persisted?).and_return(false)
- expect(thing).not_to receive(:update_columns)
+ expect(thing.foo_html).to eq(updated_html)
+ expect(thing.foo_html_changed?).to be_truthy
+ expect(thing.baz_html_changed?).to be_truthy
+ end
- thing.refresh_markdown_cache!
- end
+ it 'skips saving if not persisted' do
+ expect(thing).to receive(:persisted?).and_return(false)
+ expect(thing).not_to receive(:update_columns)
- it 'saves the changes using #update_columns' do
- expect(thing).to receive(:persisted?).and_return(true)
- expect(thing).to receive(:update_columns)
- .with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION)
+ thing.refresh_markdown_cache!
+ end
- thing.refresh_markdown_cache!
+ it 'saves the changes using #update_columns' do
+ expect(thing).to receive(:persisted?).and_return(true)
+ expect(thing).to receive(:update_columns)
+ .with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => cache_version)
+
+ thing.refresh_markdown_cache!
+ end
end
+
+ it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
+ it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
end
describe '#banzai_render_context' do
@@ -299,7 +346,7 @@ describe CacheMarkdownField do
expect(thing.foo_html).to eq(updated_html)
expect(thing.baz_html).to eq(updated_html)
- expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
+ expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end
end
@@ -319,7 +366,7 @@ describe CacheMarkdownField do
expect(thing.foo_html).to eq(updated_html)
expect(thing.baz_html).to eq(updated_html)
- expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
+ expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end
end
end
diff --git a/spec/models/concerns/group_descendant_spec.rb b/spec/models/concerns/group_descendant_spec.rb
index c163fb01a81..28352d8c961 100644
--- a/spec/models/concerns/group_descendant_spec.rb
+++ b/spec/models/concerns/group_descendant_spec.rb
@@ -79,9 +79,24 @@ describe GroupDescendant, :nested_groups do
expect(described_class.build_hierarchy(groups)).to eq(expected_hierarchy)
end
+ it 'tracks the exception when a parent was not preloaded' do
+ expect(Gitlab::Sentry).to receive(:track_exception).and_call_original
+
+ expect { GroupDescendant.build_hierarchy([subsub_group]) }.to raise_error(ArgumentError)
+ end
+
+ it 'recovers if a parent was not reloaded by querying for the parent' do
+ expected_hierarchy = { parent => { subgroup => subsub_group } }
+
+ # this does not raise in production, so stubbing it here.
+ allow(Gitlab::Sentry).to receive(:track_exception)
+
+ expect(GroupDescendant.build_hierarchy([subsub_group])).to eq(expected_hierarchy)
+ end
+
it 'raises an error if not all elements were preloaded' do
expect { described_class.build_hierarchy([subsub_group]) }
- .to raise_error('parent was not preloaded')
+ .to raise_error(/was not preloaded/)
end
end
end
diff --git a/spec/models/concerns/uniquify_spec.rb b/spec/models/concerns/uniquify_spec.rb
index 914730718e7..6cd2de6dcce 100644
--- a/spec/models/concerns/uniquify_spec.rb
+++ b/spec/models/concerns/uniquify_spec.rb
@@ -22,6 +22,15 @@ describe Uniquify do
expect(result).to eq('test_string2')
end
+ it 'allows to pass an initial value for the counter' do
+ start_counting_from = 2
+ uniquify = described_class.new(start_counting_from)
+
+ result = uniquify.string('test_string') { |s| s == 'test_string' }
+
+ expect(result).to eq('test_string2')
+ end
+
it 'allows passing in a base function that defines the location of the counter' do
result = uniquify.string(-> (counter) { "test_#{counter}_string" }) do |s|
s == 'test__string'
diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb
index 780b200e837..f8d51a95833 100644
--- a/spec/models/deploy_token_spec.rb
+++ b/spec/models/deploy_token_spec.rb
@@ -142,4 +142,23 @@ describe DeployToken do
end
end
end
+
+ describe '.gitlab_deploy_token' do
+ let(:project) { create(:project ) }
+
+ subject { project.deploy_tokens.gitlab_deploy_token }
+
+ context 'with a gitlab deploy token associated' do
+ it 'should return the gitlab deploy token' do
+ deploy_token = create(:deploy_token, :gitlab_deploy_token, projects: [project])
+ is_expected.to eq(deploy_token)
+ end
+ end
+
+ context 'with no gitlab deploy token associated' do
+ it 'should return nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index ac30cd27e0c..aee70bcfb29 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -16,6 +16,15 @@ describe Deployment do
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to validate_presence_of(:sha) }
+ describe 'modules' do
+ it_behaves_like 'AtomicInternalId' do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(:deployment) }
+ let(:scope_attrs) { { project: instance.project } }
+ let(:usage) { :deployments }
+ end
+ end
+
describe 'after_create callbacks' do
let(:environment) { create(:environment) }
let(:store) { Gitlab::EtagCaching::Store.new }
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 56161bfcc28..25d6597084c 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Environment do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :stubbed_repository) }
subject(:environment) { create(:environment, project: project) }
it { is_expected.to belong_to(:project) }
@@ -201,7 +201,7 @@ describe Environment do
end
describe '#stop_with_action!' do
- let(:user) { create(:admin) }
+ let(:user) { create(:user) }
subject { environment.stop_with_action!(user) }
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
index 581fd0293cc..8ef91e8fab5 100644
--- a/spec/models/internal_id_spec.rb
+++ b/spec/models/internal_id_spec.rb
@@ -5,7 +5,7 @@ describe InternalId do
let(:usage) { :issues }
let(:issue) { build(:issue, project: project) }
let(:scope) { { project: project } }
- let(:init) { ->(s) { s.project.issues.size } }
+ let(:init) { ->(s) { s.project.issues.maximum(:iid) } }
context 'validations' do
it { is_expected.to validate_presence_of(:usage) }
@@ -39,6 +39,29 @@ describe InternalId do
end
end
+ context 'with an InternalId record present and existing issues with a higher internal id' do
+ # This can happen if the old NonatomicInternalId is still in use
+ before do
+ issues = Array.new(rand(1..10)).map { create(:issue, project: project) }
+
+ issue = issues.last
+ issue.iid = issues.map { |i| i.iid }.max + 1
+ issue.save
+ end
+
+ let(:maximum_iid) { project.issues.map { |i| i.iid }.max }
+
+ it 'updates last_value to the maximum internal id present' do
+ subject
+
+ expect(described_class.find_by(project: project, usage: described_class.usages[usage.to_s]).last_value).to eq(maximum_iid + 1)
+ end
+
+ it 'returns next internal id correctly' do
+ expect(subject).to eq(maximum_iid + 1)
+ end
+ end
+
context 'with concurrent inserts on table' do
it 'looks up the record if it was created concurrently' do
args = { **scope, usage: described_class.usages[usage.to_s] }
@@ -81,7 +104,8 @@ describe InternalId do
describe '#increment_and_save!' do
let(:id) { create(:internal_id) }
- subject { id.increment_and_save! }
+ let(:maximum_iid) { nil }
+ subject { id.increment_and_save!(maximum_iid) }
it 'returns incremented iid' do
value = id.last_value
@@ -102,5 +126,14 @@ describe InternalId do
expect(subject).to eq(1)
end
end
+
+ context 'with maximum_iid given' do
+ let(:id) { create(:internal_id, last_value: 1) }
+ let(:maximum_iid) { id.last_value + 10 }
+
+ it 'returns maximum_iid instead' do
+ expect(subject).to eq(12)
+ end
+ end
end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 11154291368..128acf83686 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -376,6 +376,48 @@ describe Issue do
end
end
+ describe '#suggested_branch_name' do
+ let(:repository) { double }
+
+ subject { build(:issue) }
+
+ before do
+ allow(subject.project).to receive(:repository).and_return(repository)
+ end
+
+ context '#to_branch_name does not exists' do
+ before do
+ allow(repository).to receive(:branch_exists?).and_return(false)
+ end
+
+ it 'returns #to_branch_name' do
+ expect(subject.suggested_branch_name).to eq(subject.to_branch_name)
+ end
+ end
+
+ context '#to_branch_name exists not ending with -index' do
+ before do
+ allow(repository).to receive(:branch_exists?).and_return(true)
+ allow(repository).to receive(:branch_exists?).with(/#{subject.to_branch_name}-\d/).and_return(false)
+ end
+
+ it 'returns #to_branch_name ending with -2' do
+ expect(subject.suggested_branch_name).to eq("#{subject.to_branch_name}-2")
+ end
+ end
+
+ context '#to_branch_name exists ending with -index' do
+ before do
+ allow(repository).to receive(:branch_exists?).and_return(true)
+ allow(repository).to receive(:branch_exists?).with("#{subject.to_branch_name}-3").and_return(false)
+ end
+
+ it 'returns #to_branch_name ending with max index + 1' do
+ expect(subject.suggested_branch_name).to eq("#{subject.to_branch_name}-3")
+ end
+ end
+ end
+
describe '#has_related_branch?' do
let(:issue) { create(:issue, title: "Blue Bell Knoll") }
subject { issue.has_related_branch? }
@@ -425,6 +467,27 @@ describe Issue do
end
end
+ describe '#can_be_worked_on?' do
+ let(:project) { build(:project) }
+ subject { build(:issue, :opened, project: project) }
+
+ context 'is closed' do
+ subject { build(:issue, :closed) }
+
+ it { is_expected.not_to be_can_be_worked_on }
+ end
+
+ context 'project is forked' do
+ before do
+ allow(project).to receive(:forked?).and_return(true)
+ end
+
+ it { is_expected.not_to be_can_be_worked_on }
+ end
+
+ it { is_expected.to be_can_be_worked_on }
+ end
+
describe '#participants' do
context 'using a public project' do
let(:project) { create(:project, :public) }
diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb
index a182116d637..ba06ff42d87 100644
--- a/spec/models/lfs_object_spec.rb
+++ b/spec/models/lfs_object_spec.rb
@@ -81,5 +81,44 @@ describe LfsObject do
end
end
end
+
+ describe 'file is being stored' do
+ let(:lfs_object) { create(:lfs_object, :with_file) }
+
+ context 'when object has nil store' do
+ before do
+ lfs_object.update_column(:file_store, nil)
+ lfs_object.reload
+ end
+
+ it 'is stored locally' do
+ expect(lfs_object.file_store).to be(nil)
+ expect(lfs_object.file).to be_file_storage
+ expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+
+ context 'when existing object has local store' do
+ it 'is stored locally' do
+ expect(lfs_object.file_store).to be(ObjectStorage::Store::LOCAL)
+ expect(lfs_object.file).to be_file_storage
+ expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+
+ context 'when direct upload is enabled' do
+ before do
+ stub_lfs_object_storage(direct_upload: true)
+ end
+
+ context 'when file is stored' do
+ it 'is stored remotely' do
+ expect(lfs_object.file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(lfs_object.file).not_to be_file_storage
+ expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index f73f44ca0ad..becb146422e 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -17,11 +17,17 @@ describe MergeRequest do
describe 'modules' do
subject { described_class }
- it { is_expected.to include_module(NonatomicInternalId) }
it { is_expected.to include_module(Issuable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(Taskable) }
+
+ it_behaves_like 'AtomicInternalId' do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(:merge_request) }
+ let(:scope_attrs) { { project: instance.target_project } }
+ let(:usage) { :merge_requests }
+ end
end
describe 'validation' do
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 47f4a792e5c..4bb9717d33e 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -1,6 +1,26 @@
require 'spec_helper'
describe Milestone do
+ describe 'modules' do
+ context 'with a project' do
+ it_behaves_like 'AtomicInternalId' do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(:milestone, project: build(:project), group: nil) }
+ let(:scope_attrs) { { project: instance.project } }
+ let(:usage) { :milestones }
+ end
+ end
+
+ context 'with a group' do
+ it_behaves_like 'AtomicInternalId' do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(:milestone, project: nil, group: build(:group)) }
+ let(:scope_attrs) { { namespace: instance.group } }
+ let(:usage) { :milestones }
+ end
+ end
+ end
+
describe "Validation" do
before do
allow(subject).to receive(:set_iid).and_return(false)
@@ -96,7 +116,9 @@ describe Milestone do
allow(milestone).to receive(:due_date).and_return(Date.today.prev_year)
end
- it { expect(milestone.expired?).to be_truthy }
+ it 'returns true when due_date is in the past' do
+ expect(milestone.expired?).to be_truthy
+ end
end
context "not expired" do
@@ -104,17 +126,19 @@ describe Milestone do
allow(milestone).to receive(:due_date).and_return(Date.today.next_year)
end
- it { expect(milestone.expired?).to be_falsey }
+ it 'returns false when due_date is in the future' do
+ expect(milestone.expired?).to be_falsey
+ end
end
end
describe '#upcoming?' do
- it 'returns true' do
+ it 'returns true when start_date is in the future' do
milestone = build(:milestone, start_date: Time.now + 1.month)
expect(milestone.upcoming?).to be_truthy
end
- it 'returns false' do
+ it 'returns false when start_date is in the past' do
milestone = build(:milestone, start_date: Date.today.prev_year)
expect(milestone.upcoming?).to be_falsey
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 62e95a622eb..506057dce87 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -5,6 +5,7 @@ describe Namespace do
let!(:namespace) { create(:namespace) }
let(:gitlab_shell) { Gitlab::Shell.new }
+ let(:repository_storage) { 'default' }
describe 'associations' do
it { is_expected.to have_many :projects }
@@ -201,7 +202,7 @@ describe Namespace do
it "moves dir if path changed" do
namespace.update_attributes(path: namespace.full_path + '_new')
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{namespace.path}/#{project.path}.git")).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, "#{namespace.path}/#{project.path}.git")).to be_truthy
end
context 'with subgroups', :nested_groups do
@@ -281,7 +282,7 @@ describe Namespace do
namespace.update_attributes(path: namespace.full_path + '_new')
expect(before_disk_path).to eq(project.disk_path)
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_truthy
end
end
@@ -322,7 +323,7 @@ describe Namespace do
end
it 'schedules the namespace for deletion' do
- expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage_path, deleted_path)
+ expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage, deleted_path)
namespace.destroy
end
@@ -344,7 +345,7 @@ describe Namespace do
end
it 'schedules the namespace for deletion' do
- expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage_path, deleted_path)
+ expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage, deleted_path)
child.destroy
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 86962cd8d61..6a6c71e6c82 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -91,6 +91,23 @@ describe Note do
it "keeps the commit around" do
expect(note.project.repository.kept_around?(commit.id)).to be_truthy
end
+
+ it 'does not generate N+1 queries for participants', :request_store do
+ def retrieve_participants
+ commit.notes_with_associations.map(&:participants).to_a
+ end
+
+ # Project authorization checks are cached, establish a baseline
+ retrieve_participants
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ retrieve_participants
+ end
+
+ create(:note_on_commit, project: note.project, note: 'another note', noteable_id: commit.id)
+
+ expect { retrieve_participants }.not_to exceed_query_limit(control_count)
+ end
end
describe 'authorization' do
diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb
new file mode 100644
index 00000000000..4aa62028169
--- /dev/null
+++ b/spec/models/project_ci_cd_setting_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProjectCiCdSetting do
+ describe '.available?' do
+ before do
+ described_class.reset_column_information
+ end
+
+ it 'returns true' do
+ expect(described_class).to be_available
+ end
+
+ it 'memoizes the schema version' do
+ expect(ActiveRecord::Migrator)
+ .to receive(:current_version)
+ .and_call_original
+ .once
+
+ 2.times { described_class.available? }
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 2675c2f52c1..a9587b1005e 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -93,6 +93,15 @@ describe Project do
end
end
+ context 'when creating a new project' do
+ it 'automatically creates a CI/CD settings row' do
+ project = create(:project)
+
+ expect(project.ci_cd_settings).to be_an_instance_of(ProjectCiCdSetting)
+ expect(project.ci_cd_settings).to be_persisted
+ end
+ end
+
describe '#members & #requesters' do
let(:project) { create(:project, :public, :access_requestable) }
let(:requester) { create(:user) }
@@ -325,7 +334,7 @@ describe Project do
let(:owner) { create(:user, name: 'Gitlab') }
let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) }
let(:project) { create(:project, path: 'sample-project', namespace: namespace) }
- let(:group) { create(:group, name: 'Group', path: 'sample-group', owner: owner) }
+ let(:group) { create(:group, name: 'Group', path: 'sample-group') }
context 'when nil argument' do
it 'returns nil' do
@@ -440,14 +449,6 @@ describe Project do
end
end
- describe '#repository_storage_path' do
- let(:project) { create(:project) }
-
- it 'returns the repository storage path' do
- expect(Dir.exist?(project.repository_storage_path)).to be(true)
- end
- end
-
it 'returns valid url to repo' do
project = described_class.new(path: 'somewhere')
expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
@@ -1099,7 +1100,7 @@ describe Project do
end
context 'repository storage by default' do
- let(:project) { create(:project) }
+ let(:project) { build(:project) }
before do
storages = {
@@ -1452,7 +1453,7 @@ describe Project do
.and_return(false)
allow(shell).to receive(:create_repository)
- .with(project.repository_storage_path, project.disk_path)
+ .with(project.repository_storage, project.disk_path)
.and_return(true)
expect(project).to receive(:create_repository).with(force: true)
@@ -1483,52 +1484,6 @@ describe Project do
end
end
- describe '#user_can_push_to_empty_repo?' do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
-
- it 'returns false when default_branch_protection is in full protection and user is developer' do
- project.add_developer(user)
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
-
- expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
- end
-
- it 'returns false when default_branch_protection only lets devs merge and user is dev' do
- project.add_developer(user)
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
-
- expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
- end
-
- it 'returns true when default_branch_protection lets devs push and user is developer' do
- project.add_developer(user)
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
-
- expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
- end
-
- it 'returns true when default_branch_protection is unprotected and user is developer' do
- project.add_developer(user)
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
-
- expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
- end
-
- it 'returns true when user is master' do
- project.add_master(user)
-
- expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
- end
-
- it 'returns false when the repo is not empty' do
- project.add_master(user)
- expect(project).to receive(:empty_repo?).and_return(false)
-
- expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
- end
- end
-
describe '#container_registry_url' do
let(:project) { create(:project) }
@@ -2673,7 +2628,7 @@ describe Project do
describe '#ensure_storage_path_exists' do
it 'delegates to gitlab_shell to ensure namespace is created' do
- expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage_path, project.base_dir)
+ expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage, project.base_dir)
project.ensure_storage_path_exists
end
@@ -2712,12 +2667,12 @@ describe Project do
expect(gitlab_shell).to receive(:mv_repository)
.ordered
- .with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}")
+ .with(project.repository_storage, "#{project.namespace.full_path}/foo", "#{project.full_path}")
.and_return(true)
expect(gitlab_shell).to receive(:mv_repository)
.ordered
- .with(project.repository_storage_path, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki")
+ .with(project.repository_storage, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki")
.and_return(true)
expect_any_instance_of(SystemHooksService)
@@ -2866,7 +2821,7 @@ describe Project do
it 'delegates to gitlab_shell to ensure namespace is created' do
allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
- expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage_path, hashed_prefix)
+ expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage, hashed_prefix)
project.ensure_storage_path_exists
end
@@ -3585,4 +3540,44 @@ describe Project do
it { is_expected.not_to be_valid }
end
end
+
+ describe '#gitlab_deploy_token' do
+ let(:project) { create(:project) }
+
+ subject { project.gitlab_deploy_token }
+
+ context 'when there is a gitlab deploy token associated' do
+ let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, projects: [project]) }
+
+ it { is_expected.to eq(deploy_token) }
+ end
+
+ context 'when there is no a gitlab deploy token associated' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'when there is a gitlab deploy token associated but is has been revoked' do
+ let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, :revoked, projects: [project]) }
+ it { is_expected.to be_nil }
+ end
+
+ context 'when there is a gitlab deploy token associated but it is expired' do
+ let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, :expired, projects: [project]) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when there is a deploy token associated with a different name' do
+ let!(:deploy_token) { create(:deploy_token, projects: [project]) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when there is a deploy token associated to a different project' do
+ let(:project_2) { create(:project) }
+ let!(:deploy_token) { create(:deploy_token, projects: [project_2]) }
+
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index 5cff2af4aca..38a3590ad12 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -4,26 +4,6 @@ describe ProjectStatistics do
let(:project) { create :project }
let(:statistics) { project.statistics }
- describe 'constants' do
- describe 'STORAGE_COLUMNS' do
- it 'is an array of symbols' do
- expect(described_class::STORAGE_COLUMNS).to be_kind_of Array
- expect(described_class::STORAGE_COLUMNS.map(&:class).uniq).to eq [Symbol]
- end
- end
-
- describe 'STATISTICS_COLUMNS' do
- it 'is an array of symbols' do
- expect(described_class::STATISTICS_COLUMNS).to be_kind_of Array
- expect(described_class::STATISTICS_COLUMNS.map(&:class).uniq).to eq [Symbol]
- end
-
- it 'includes all storage columns' do
- expect(described_class::STATISTICS_COLUMNS & described_class::STORAGE_COLUMNS).to eq described_class::STORAGE_COLUMNS
- end
- end
- end
-
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:namespace) }
@@ -63,7 +43,6 @@ describe ProjectStatistics do
allow(statistics).to receive(:update_commit_count)
allow(statistics).to receive(:update_repository_size)
allow(statistics).to receive(:update_lfs_objects_size)
- allow(statistics).to receive(:update_build_artifacts_size)
allow(statistics).to receive(:update_storage_size)
end
@@ -76,7 +55,6 @@ describe ProjectStatistics do
expect(statistics).to have_received(:update_commit_count)
expect(statistics).to have_received(:update_repository_size)
expect(statistics).to have_received(:update_lfs_objects_size)
- expect(statistics).to have_received(:update_build_artifacts_size)
end
end
@@ -89,7 +67,6 @@ describe ProjectStatistics do
expect(statistics).to have_received(:update_lfs_objects_size)
expect(statistics).not_to have_received(:update_commit_count)
expect(statistics).not_to have_received(:update_repository_size)
- expect(statistics).not_to have_received(:update_build_artifacts_size)
end
end
end
@@ -131,40 +108,6 @@ describe ProjectStatistics do
end
end
- describe '#update_build_artifacts_size' do
- let!(:pipeline) { create(:ci_pipeline, project: project) }
-
- context 'when new job artifacts are calculated' do
- let(:ci_build) { create(:ci_build, pipeline: pipeline) }
-
- before do
- create(:ci_job_artifact, :archive, project: pipeline.project, job: ci_build)
- end
-
- it "stores the size of related build artifacts" do
- statistics.update_build_artifacts_size
-
- expect(statistics.build_artifacts_size).to be(106365)
- end
-
- it 'calculates related build artifacts by project' do
- expect(Ci::JobArtifact).to receive(:artifacts_size_for).with(project) { 0 }
-
- statistics.update_build_artifacts_size
- end
- end
-
- context 'when legacy artifacts are used' do
- let!(:ci_build) { create(:ci_build, pipeline: pipeline, artifacts_size: 10.megabytes) }
-
- it "stores the size of related build artifacts" do
- statistics.update_build_artifacts_size
-
- expect(statistics.build_artifacts_size).to eq(10.megabytes)
- end
- end
- end
-
describe '#update_storage_size' do
it "sums all storage counters" do
statistics.update!(
@@ -177,4 +120,27 @@ describe ProjectStatistics do
expect(statistics.storage_size).to eq 5
end
end
+
+ describe '.increment_statistic' do
+ it 'increases the statistic by that amount' do
+ expect { described_class.increment_statistic(project.id, :build_artifacts_size, 13) }
+ .to change { statistics.reload.build_artifacts_size }
+ .by(13)
+ end
+
+ context 'when the amount is 0' do
+ it 'does not execute a query' do
+ project
+ expect { described_class.increment_statistic(project.id, :build_artifacts_size, 0) }
+ .not_to exceed_query_limit(0)
+ end
+ end
+
+ context 'when using an invalid column' do
+ it 'raises an error' do
+ expect { described_class.increment_statistic(project.id, :id, 13) }
+ .to raise_error(ArgumentError, "Cannot increment attribute: id")
+ end
+ end
+ end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index d87c1ca14f0..cbe7d111fcd 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -11,7 +11,7 @@ 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(:repository_storage).to :project }
it { is_expected.to delegate_method(:hashed_storage?).to :project }
describe "#full_path" do
@@ -172,11 +172,12 @@ describe ProjectWiki do
describe '#find_file' do
shared_examples 'finding a wiki file' do
+ let(:image) { File.open(Rails.root.join('spec', 'fixtures', 'big-image.png')) }
+
before do
- file = File.open(Rails.root.join('spec', 'fixtures', 'dk.png'))
subject.wiki # Make sure the wiki repo exists
- BareRepoOperations.new(subject.repository.path_to_repo).commit_file(file, 'image.png')
+ BareRepoOperations.new(subject.repository.path_to_repo).commit_file(image, 'image.png')
end
it 'returns the latest version of the file if it exists' do
@@ -192,6 +193,13 @@ describe ProjectWiki do
file = subject.find_file('image.png')
expect(file).to be_a Gitlab::Git::WikiFile
end
+
+ it 'returns the whole file' do
+ file = subject.find_file('image.png')
+ image.rewind
+
+ expect(file.raw_data.b).to eq(image.read.b)
+ end
end
context 'when Gitaly wiki_find_file is enabled' do
@@ -369,7 +377,7 @@ describe ProjectWiki do
end
def commit_details
- Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit")
+ Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "test commit")
end
def create_page(name, content)
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 60ab52565cb..e45fe7db1e7 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1437,6 +1437,12 @@ describe Repository do
repository.expire_emptiness_caches
end
+
+ it 'expires the memoized repository cache' do
+ allow(repository.raw_repository).to receive(:expire_has_local_branches_cache).and_call_original
+
+ repository.expire_emptiness_caches
+ end
end
describe 'skip_merges option' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 35db7616efb..3f2eb58f009 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1164,8 +1164,12 @@ describe User do
end
context 'with a group route matching the given path' do
+ let!(:group) { create(:group, path: 'group_path') }
+
context 'when the group namespace has an owner_id (legacy data)' do
- let!(:group) { create(:group, path: 'group_path', owner: user) }
+ before do
+ group.update!(owner_id: user.id)
+ end
it 'returns nil' do
expect(described_class.find_by_full_path('group_path')).to eq(nil)
@@ -1173,8 +1177,6 @@ describe User do
end
context 'when the group namespace does not have an owner_id' do
- let!(:group) { create(:group, path: 'group_path') }
-
it 'returns nil' do
expect(described_class.find_by_full_path('group_path')).to eq(nil)
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index b2b7721674c..90b7e7715a8 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -561,7 +561,7 @@ describe WikiPage do
end
def commit_details
- Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit")
+ Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "test commit")
end
def create_page(name, content)
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index b4d25e06d9a..9b5c290b9f9 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -7,9 +7,9 @@ describe GroupPolicy do
let(:master) { create(:user) }
let(:owner) { create(:user) }
let(:admin) { create(:admin) }
- let(:group) { create(:group) }
+ let(:group) { create(:group, :private) }
- let(:guest_permissions) { [:read_group, :upload_file, :read_namespace] }
+ let(:guest_permissions) { [:read_label, :read_group, :upload_file, :read_namespace] }
let(:reporter_permissions) { [:admin_label] }
@@ -50,6 +50,7 @@ describe GroupPolicy do
end
context 'with no user' do
+ let(:group) { create(:group, :public) }
let(:current_user) { nil }
it do
@@ -63,6 +64,28 @@ describe GroupPolicy do
end
end
+ context 'has projects' do
+ let(:current_user) { create(:user) }
+ let(:project) { create(:project, namespace: group) }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it do
+ expect_allowed(:read_group, :read_label)
+ end
+
+ context 'in subgroups', :nested_groups do
+ let(:subgroup) { create(:group, :private, parent: group) }
+ let(:project) { create(:project, namespace: subgroup) }
+
+ it do
+ expect_allowed(:read_group, :read_label)
+ end
+ end
+ end
+
context 'guests' do
let(:current_user) { guest }
diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb
index 58d36a2c84e..e8096358f7d 100644
--- a/spec/policies/note_policy_spec.rb
+++ b/spec/policies/note_policy_spec.rb
@@ -18,7 +18,6 @@ describe NotePolicy, mdoels: true do
context 'when the project is public' do
context 'when the note author is not a project member' do
it 'can edit a note' do
- expect(policies).to be_allowed(:update_note)
expect(policies).to be_allowed(:admin_note)
expect(policies).to be_allowed(:resolve_note)
expect(policies).to be_allowed(:read_note)
@@ -29,7 +28,6 @@ describe NotePolicy, mdoels: true do
it 'can edit note' do
policies = policies(create(:project_snippet, project: project))
- expect(policies).to be_allowed(:update_note)
expect(policies).to be_allowed(:admin_note)
expect(policies).to be_allowed(:resolve_note)
expect(policies).to be_allowed(:read_note)
@@ -47,7 +45,6 @@ describe NotePolicy, mdoels: true do
end
it 'can edit a note' do
- expect(policies).to be_allowed(:update_note)
expect(policies).to be_allowed(:admin_note)
expect(policies).to be_allowed(:resolve_note)
expect(policies).to be_allowed(:read_note)
@@ -56,7 +53,6 @@ describe NotePolicy, mdoels: true do
context 'when the note author is not a project member' do
it 'can not edit a note' do
- expect(policies).to be_disallowed(:update_note)
expect(policies).to be_disallowed(:admin_note)
expect(policies).to be_disallowed(:resolve_note)
end
diff --git a/spec/policies/personal_snippet_policy_spec.rb b/spec/policies/personal_snippet_policy_spec.rb
index 50bb0899eba..3809692b373 100644
--- a/spec/policies/personal_snippet_policy_spec.rb
+++ b/spec/policies/personal_snippet_policy_spec.rb
@@ -27,6 +27,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
is_expected.to be_disallowed(:comment_personal_snippet)
+ is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
end
@@ -37,6 +38,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
is_expected.to be_allowed(:comment_personal_snippet)
+ is_expected.to be_allowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
end
@@ -47,6 +49,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
is_expected.to be_allowed(:comment_personal_snippet)
+ is_expected.to be_allowed(:award_emoji)
is_expected.to be_allowed(*author_permissions)
end
end
@@ -61,6 +64,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
is_expected.to be_disallowed(:comment_personal_snippet)
+ is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
end
@@ -71,6 +75,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
is_expected.to be_allowed(:comment_personal_snippet)
+ is_expected.to be_allowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
end
@@ -81,6 +86,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
is_expected.to be_disallowed(:comment_personal_snippet)
+ is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
end
@@ -91,6 +97,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
is_expected.to be_allowed(:comment_personal_snippet)
+ is_expected.to be_allowed(:award_emoji)
is_expected.to be_allowed(*author_permissions)
end
end
@@ -105,6 +112,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
is_expected.to be_disallowed(:comment_personal_snippet)
+ is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
end
@@ -115,6 +123,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
is_expected.to be_disallowed(:comment_personal_snippet)
+ is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
end
@@ -125,6 +134,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
is_expected.to be_disallowed(:comment_personal_snippet)
+ is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
end
@@ -135,6 +145,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
is_expected.to be_allowed(:comment_personal_snippet)
+ is_expected.to be_allowed(:award_emoji)
is_expected.to be_allowed(*author_permissions)
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 905d82b3bb1..8b9c4ac0b4b 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -14,7 +14,8 @@ describe ProjectPolicy do
read_project read_board read_list read_wiki read_issue
read_project_for_iids read_issue_iid read_merge_request_iid read_label
read_milestone read_project_snippet read_project_member read_note
- create_project create_issue create_note upload_file
+ create_project create_issue create_note upload_file create_merge_request_in
+ award_emoji
]
end
@@ -35,7 +36,7 @@ describe ProjectPolicy do
%i[
admin_milestone admin_merge_request update_merge_request create_commit_status
update_commit_status create_build update_build create_pipeline
- update_pipeline create_merge_request create_wiki push_code
+ update_pipeline create_merge_request_from create_wiki push_code
resolve_note create_container_image update_container_image
create_environment create_deployment
]
@@ -43,7 +44,7 @@ describe ProjectPolicy do
let(:base_master_permissions) do
%i[
- delete_protected_branch update_project_snippet update_environment
+ push_to_delete_protected_branch update_project_snippet update_environment
update_deployment admin_project_snippet
admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
@@ -136,13 +137,66 @@ describe ProjectPolicy do
end
end
+ context 'merge requests feature' do
+ subject { described_class.new(owner, project) }
+
+ it 'disallows all permissions when the feature is disabled' do
+ project.project_feature.update(merge_requests_access_level: ProjectFeature::DISABLED)
+
+ mr_permissions = [:create_merge_request_from, :read_merge_request,
+ :update_merge_request, :admin_merge_request,
+ :create_merge_request_in]
+
+ expect_disallowed(*mr_permissions)
+ end
+ end
+
+ shared_examples 'archived project policies' do
+ let(:feature_write_abilities) do
+ described_class::READONLY_FEATURES_WHEN_ARCHIVED.flat_map do |feature|
+ described_class.create_update_admin_destroy(feature)
+ end
+ end
+
+ let(:other_write_abilities) do
+ %i[
+ create_merge_request_in
+ create_merge_request_from
+ push_to_delete_protected_branch
+ push_code
+ request_access
+ upload_file
+ resolve_note
+ award_emoji
+ ]
+ end
+
+ context 'when the project is archived' do
+ before do
+ project.archived = true
+ end
+
+ it 'disables write actions on all relevant project features' do
+ expect_disallowed(*feature_write_abilities)
+ end
+
+ it 'disables some other important write actions' do
+ expect_disallowed(*other_write_abilities)
+ end
+
+ it 'does not disable other other abilities' do
+ expect_allowed(*(regular_abilities - feature_write_abilities - other_write_abilities))
+ end
+ end
+ end
+
shared_examples 'project policies as anonymous' do
context 'abilities for public projects' do
context 'when a project has pending invites' do
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public, namespace: group) }
- let(:user_permissions) { [:create_project, :create_issue, :create_note, :upload_file] }
- let(:anonymous_permissions) { guest_permissions - user_permissions }
+ let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji] }
+ let(:anonymous_permissions) { guest_permissions - user_permissions }
subject { described_class.new(nil, project) }
@@ -154,6 +208,10 @@ describe ProjectPolicy do
expect_allowed(*anonymous_permissions)
expect_disallowed(*user_permissions)
end
+
+ it_behaves_like 'archived project policies' do
+ let(:regular_abilities) { anonymous_permissions }
+ end
end
end
@@ -184,6 +242,10 @@ describe ProjectPolicy do
expect_disallowed(*owner_permissions)
end
+ it_behaves_like 'archived project policies' do
+ let(:regular_abilities) { guest_permissions }
+ end
+
context 'public builds enabled' do
it do
expect_allowed(*guest_permissions)
@@ -224,12 +286,15 @@ describe ProjectPolicy do
it do
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
- expect_allowed(*reporter_permissions)
expect_allowed(*team_member_reporter_permissions)
expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
+
+ it_behaves_like 'archived project policies' do
+ let(:regular_abilities) { reporter_permissions }
+ end
end
end
@@ -247,6 +312,10 @@ describe ProjectPolicy do
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
+
+ it_behaves_like 'archived project policies' do
+ let(:regular_abilities) { developer_permissions }
+ end
end
end
@@ -264,6 +333,10 @@ describe ProjectPolicy do
expect_allowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
+
+ it_behaves_like 'archived project policies' do
+ let(:regular_abilities) { master_permissions }
+ end
end
end
@@ -281,6 +354,10 @@ describe ProjectPolicy do
expect_allowed(*master_permissions)
expect_allowed(*owner_permissions)
end
+
+ it_behaves_like 'archived project policies' do
+ let(:regular_abilities) { owner_permissions }
+ end
end
end
@@ -298,6 +375,10 @@ describe ProjectPolicy do
expect_allowed(*master_permissions)
expect_allowed(*owner_permissions)
end
+
+ it_behaves_like 'archived project policies' do
+ let(:regular_abilities) { owner_permissions }
+ end
end
end
diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb
index cc16d0f156b..4bc005df2fc 100644
--- a/spec/presenters/ci/build_presenter_spec.rb
+++ b/spec/presenters/ci/build_presenter_spec.rb
@@ -217,4 +217,39 @@ describe Ci::BuildPresenter do
end
end
end
+
+ describe '#callout_failure_message' do
+ let(:build) { create(:ci_build, :failed, :script_failure) }
+
+ it 'returns a verbose failure reason' do
+ description = subject.callout_failure_message
+ expect(description).to eq('There has been a script failure. Check the job log for more information')
+ end
+ end
+
+ describe '#recoverable?' do
+ let(:build) { create(:ci_build, :failed, :script_failure) }
+
+ context 'when is a script or missing dependency failure' do
+ let(:failure_reasons) { %w(script_failure missing_dependency_failure) }
+
+ it 'should return false' do
+ failure_reasons.each do |failure_reason|
+ build.update_attribute(:failure_reason, failure_reason)
+ expect(presenter.recoverable?).to be_falsy
+ end
+ end
+ end
+
+ context 'when is any other failure type' do
+ let(:failure_reasons) { %w(unknown_failure api_failure stuck_or_timeout_failure runner_system_failure) }
+
+ it 'should return true' do
+ failure_reasons.each do |failure_reason|
+ build.update_attribute(:failure_reason, failure_reason)
+ expect(presenter.recoverable?).to be_truthy
+ end
+ end
+ end
+ end
end
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 55962f345d4..830d2ee3b20 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -208,6 +208,17 @@ describe ProjectPresenter do
it 'returns nil if user cannot push' do
expect(presenter.new_file_anchor_data).to be_nil
end
+
+ context 'when the project is empty' do
+ let(:project) { create(:project, :empty_repo) }
+
+ # Since we protect the default branch for empty repos
+ it 'is empty for a developer' do
+ project.add_developer(user)
+
+ expect(presenter.new_file_anchor_data).to be_nil
+ end
+ end
end
describe '#readme_anchor_data' do
@@ -321,7 +332,7 @@ describe ProjectPresenter do
expect(presenter.autodevops_anchor_data).to eq(OpenStruct.new(enabled: false,
label: 'Enable Auto DevOps',
- link: presenter.project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')))
+ link: presenter.project_settings_ci_cd_path(project, anchor: 'autodevops-settings')))
end
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 3764aec0c71..f64623d7018 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -861,7 +861,7 @@ describe API::MergeRequests do
expect(json_response['title']).to eq('Test merge_request')
end
- it 'returns 422 when target project has disabled merge requests' do
+ it 'returns 403 when target project has disabled merge requests' do
project.project_feature.update(merge_requests_access_level: 0)
post api("/projects/#{forked_project.id}/merge_requests", user2),
@@ -871,7 +871,7 @@ describe API::MergeRequests do
author: user2,
target_project_id: project.id
- expect(response).to have_gitlab_http_status(422)
+ expect(response).to have_gitlab_http_status(403)
end
it "returns 400 when source_branch is missing" do
diff --git a/spec/requests/api/project_snapshots_spec.rb b/spec/requests/api/project_snapshots_spec.rb
new file mode 100644
index 00000000000..07a920f8d28
--- /dev/null
+++ b/spec/requests/api/project_snapshots_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe API::ProjectSnapshots do
+ include WorkhorseHelpers
+
+ let(:project) { create(:project) }
+ let(:admin) { create(:admin) }
+
+ describe 'GET /projects/:id/snapshot' do
+ def expect_snapshot_response_for(repository)
+ type, params = workhorse_send_data
+
+ expect(type).to eq('git-snapshot')
+ expect(params).to eq(
+ 'GitalyServer' => {
+ 'address' => Gitlab::GitalyClient.address(repository.project.repository_storage),
+ 'token' => Gitlab::GitalyClient.token(repository.project.repository_storage)
+ },
+ 'GetSnapshotRequest' => Gitaly::GetSnapshotRequest.new(
+ repository: repository.gitaly_repository
+ ).to_json
+ )
+ end
+
+ it 'returns authentication error as project owner' do
+ get api("/projects/#{project.id}/snapshot", project.owner)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'returns authentication error as unauthenticated user' do
+ get api("/projects/#{project.id}/snapshot", nil)
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+
+ it 'requests project repository raw archive as administrator' do
+ get api("/projects/#{project.id}/snapshot", admin), wiki: '0'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect_snapshot_response_for(project.repository)
+ end
+
+ it 'requests wiki repository raw archive as administrator' do
+ get api("/projects/#{project.id}/snapshot", admin), wiki: '1'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect_snapshot_response_for(project.wiki.repository)
+ end
+ end
+end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 2ec29a79e93..85a571b8f0e 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1,6 +1,18 @@
# -*- coding: utf-8 -*-
require 'spec_helper'
+shared_examples 'languages and percentages JSON response' do
+ let(:expected_languages) { project.repository.languages.map { |language| language.values_at(:label, :value)}.to_h }
+
+ it 'returns expected language values' do
+ get api("/projects/#{project.id}/languages", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq(expected_languages)
+ expect(json_response.count).to be > 1
+ end
+end
+
describe API::Projects do
let(:user) { create(:user) }
let(:user2) { create(:user) }
@@ -673,7 +685,8 @@ describe API::Projects do
issues_enabled: false,
merge_requests_enabled: false,
wiki_enabled: false,
- request_access_enabled: true
+ request_access_enabled: true,
+ jobs_enabled: true
})
post api("/projects/user/#{user.id}", admin), project
@@ -1694,6 +1707,42 @@ describe API::Projects do
end
end
+ describe 'GET /projects/:id/languages' do
+ context 'with an authorized user' do
+ it_behaves_like 'languages and percentages JSON response' do
+ let(:project) { project3 }
+ end
+
+ it 'returns not_found(404) for not existing project' do
+ get api("/projects/9999999999/languages", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with not authorized user' do
+ it 'returns not_found for existing but unauthorized project' do
+ get api("/projects/#{project3.id}/languages", user3)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'without user' do
+ let(:project_public) { create(:project, :public, :repository) }
+
+ it_behaves_like 'languages and percentages JSON response' do
+ let(:project) { project_public }
+ end
+
+ it 'returns not_found for existing but unauthorized project' do
+ get api("/projects/#{project3.id}/languages", nil)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
describe 'DELETE /projects/:id' do
context 'when authenticated as user' do
it 'removes project' do
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 741800ff61d..9e6d69e3874 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -427,5 +427,20 @@ describe API::Repositories do
let(:request) { get api(route, guest) }
end
end
+
+ # Regression: https://gitlab.com/gitlab-org/gitlab-ce/issues/45363
+ describe 'Links header contains working URLs when no `order_by` nor `sort` is given' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+
+ it 'returns `Link` header that includes URLs with default value for `order_by` & `sort`' do
+ get api(route, current_user)
+
+ first_link_url = response.headers['Link'].split(';').first
+
+ expect(first_link_url).to include('order_by=commits')
+ expect(first_link_url).to include('sort=asc')
+ end
+ end
end
end
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 28d51ac86c6..17c7a511857 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -406,7 +406,7 @@ describe API::Runner do
expect(json_response['image']).to eq({ 'name' => 'ruby:2.1', 'entrypoint' => '/bin/sh' })
expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil,
'alias' => nil, 'command' => nil },
- { 'name' => 'docker:dind', 'entrypoint' => '/bin/sh',
+ { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh',
'alias' => 'docker', 'command' => 'sleep 30' }])
expect(json_response['steps']).to eq(expected_steps)
expect(json_response['artifacts']).to eq(expected_artifacts)
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index f406d2ffb22..e8196980a8c 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -212,6 +212,18 @@ describe API::Users do
expect(json_response.last['id']).to eq(user.id)
end
+ it 'returns users with 2fa enabled' do
+ admin
+ user
+ user_with_2fa = create(:user, :two_factor_via_otp)
+
+ get api('/users', admin), { two_factor: 'enabled' }
+
+ expect(response).to match_response_schema('public_api/v4/user/admins')
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['id']).to eq(user_with_2fa.id)
+ end
+
it 'returns 400 when provided incorrect sort params' do
get api('/users', admin), { order_by: 'magic', sort: 'asc' }
diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb
index 6b748369f0d..be70cb24dce 100644
--- a/spec/requests/api/v3/merge_requests_spec.rb
+++ b/spec/requests/api/v3/merge_requests_spec.rb
@@ -340,7 +340,7 @@ describe API::MergeRequests do
expect(json_response['title']).to eq('Test merge_request')
end
- it "returns 422 when target project has disabled merge requests" do
+ it "returns 403 when target project has disabled merge requests" do
project.project_feature.update(merge_requests_access_level: 0)
post v3_api("/projects/#{forked_project.id}/merge_requests", user2),
@@ -350,7 +350,7 @@ describe API::MergeRequests do
author: user2,
target_project_id: project.id
- expect(response).to have_gitlab_http_status(422)
+ expect(response).to have_gitlab_http_status(403)
end
it "returns 400 when source_branch is missing" do
diff --git a/spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb b/spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb
new file mode 100644
index 00000000000..ac7b1575ec0
--- /dev/null
+++ b/spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../rubocop/cop/avoid_break_from_strong_memoize'
+
+describe RuboCop::Cop::AvoidBreakFromStrongMemoize do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ it 'flags violation for break inside strong_memoize' do
+ expect_offense(<<~RUBY)
+ strong_memoize(:result) do
+ break if something
+ ^^^^^ Do not use break inside strong_memoize, use next instead.
+
+ do_an_heavy_calculation
+ end
+ RUBY
+ end
+
+ it 'flags violation for break inside strong_memoize nested blocks' do
+ expect_offense(<<~RUBY)
+ strong_memoize do
+ items.each do |item|
+ break item
+ ^^^^^^^^^^ Do not use break inside strong_memoize, use next instead.
+ end
+ end
+ RUBY
+ end
+
+ it "doesn't flag violation for next inside strong_memoize" do
+ expect_no_offenses(<<~RUBY)
+ strong_memoize(:result) do
+ next if something
+
+ do_an_heavy_calculation
+ end
+ RUBY
+ end
+
+ it "doesn't flag violation for break inside blocks" do
+ expect_no_offenses(<<~RUBY)
+ call do
+ break if something
+
+ do_an_heavy_calculation
+ end
+ RUBY
+ end
+
+ it "doesn't call add_offense twice for nested blocks" do
+ source = <<~RUBY
+ call do
+ strong_memoize(:result) do
+ break if something
+
+ do_an_heavy_calculation
+ end
+ end
+ RUBY
+ expect_any_instance_of(described_class).to receive(:add_offense).once
+
+ inspect_source(source)
+ end
+
+ it "doesn't check when block is empty" do
+ expect_no_offenses(<<~RUBY)
+ strong_memoize(:result) do
+ end
+ RUBY
+ end
+end
diff --git a/spec/rubocop/cop/avoid_return_from_blocks_spec.rb b/spec/rubocop/cop/avoid_return_from_blocks_spec.rb
new file mode 100644
index 00000000000..a5c280a7adc
--- /dev/null
+++ b/spec/rubocop/cop/avoid_return_from_blocks_spec.rb
@@ -0,0 +1,127 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../rubocop/cop/avoid_return_from_blocks'
+
+describe RuboCop::Cop::AvoidReturnFromBlocks do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ it 'flags violation for return inside a block' do
+ expect_offense(<<~RUBY)
+ call do
+ do_something
+ return if something_else
+ ^^^^^^ Do not return from a block, use next or break instead.
+ end
+ RUBY
+ end
+
+ it "doesn't call add_offense twice for nested blocks" do
+ source = <<~RUBY
+ call do
+ call do
+ something
+ return if something_else
+ end
+ end
+ RUBY
+ expect_any_instance_of(described_class).to receive(:add_offense).once
+
+ inspect_source(source)
+ end
+
+ it 'flags violation for return inside included > def > block' do
+ expect_offense(<<~RUBY)
+ included do
+ def a_method
+ return if something
+
+ call do
+ return if something_else
+ ^^^^^^ Do not return from a block, use next or break instead.
+ end
+ end
+ end
+ RUBY
+ end
+
+ shared_examples 'examples with whitelisted method' do |whitelisted_method|
+ it "doesn't flag violation for return inside #{whitelisted_method}" do
+ expect_no_offenses(<<~RUBY)
+ items.#{whitelisted_method} do |item|
+ do_something
+ return if something_else
+ end
+ RUBY
+ end
+ end
+
+ %i[each each_filename times loop].each do |whitelisted_method|
+ it_behaves_like 'examples with whitelisted method', whitelisted_method
+ end
+
+ shared_examples 'examples with def methods' do |def_method|
+ it "doesn't flag violation for return inside #{def_method}" do
+ expect_no_offenses(<<~RUBY)
+ helpers do
+ #{def_method} do
+ return if something
+
+ do_something_more
+ end
+ end
+ RUBY
+ end
+ end
+
+ %i[define_method lambda].each do |def_method|
+ it_behaves_like 'examples with def methods', def_method
+ end
+
+ it "doesn't flag violation for return inside a lambda" do
+ expect_no_offenses(<<~RUBY)
+ lambda do
+ do_something
+ return if something_else
+ end
+ RUBY
+ end
+
+ it "doesn't flag violation for return used inside a method definition" do
+ expect_no_offenses(<<~RUBY)
+ describe Klass do
+ def a_method
+ do_something
+ return if something_else
+ end
+ end
+ RUBY
+ end
+
+ it "doesn't flag violation for next inside a block" do
+ expect_no_offenses(<<~RUBY)
+ call do
+ do_something
+ next if something_else
+ end
+ RUBY
+ end
+
+ it "doesn't flag violation for break inside a block" do
+ expect_no_offenses(<<~RUBY)
+ call do
+ do_something
+ break if something_else
+ end
+ RUBY
+ end
+
+ it "doesn't check when block is empty" do
+ expect_no_offenses(<<~RUBY)
+ call do
+ end
+ RUBY
+ end
+end
diff --git a/spec/rubocop/cop/gitlab/has_many_through_scope_spec.rb b/spec/rubocop/cop/gitlab/has_many_through_scope_spec.rb
deleted file mode 100644
index 6d769c8e6fd..00000000000
--- a/spec/rubocop/cop/gitlab/has_many_through_scope_spec.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-require 'spec_helper'
-
-require 'rubocop'
-require 'rubocop/rspec/support'
-
-require_relative '../../../../rubocop/cop/gitlab/has_many_through_scope'
-
-describe RuboCop::Cop::Gitlab::HasManyThroughScope do # rubocop:disable RSpec/FilePath
- include CopHelper
-
- subject(:cop) { described_class.new }
-
- context 'in a model file' do
- before do
- allow(cop).to receive(:in_model?).and_return(true)
- end
-
- context 'when the model does not use has_many :through' do
- it 'does not register an offense' do
- expect_no_offenses(<<-RUBY)
- class User < ActiveRecord::Base
- has_many :tags, source: 'UserTag'
- end
- RUBY
- end
- end
-
- context 'when the model uses has_many :through' do
- context 'when the association has no scope defined' do
- it 'registers an offense on the association' do
- expect_offense(<<-RUBY)
- class User < ActiveRecord::Base
- has_many :tags, through: :user_tags
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
- end
- RUBY
- end
- end
-
- context 'when the association has a scope defined' do
- context 'when the scope does not disable auto-loading' do
- it 'registers an offense on the scope' do
- expect_offense(<<-RUBY)
- class User < ActiveRecord::Base
- has_many :tags, -> { where(active: true) }, through: :user_tags
- ^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
- end
- RUBY
- end
- end
-
- context 'when the scope has auto_include(false)' do
- it 'does not register an offense' do
- expect_no_offenses(<<-RUBY)
- class User < ActiveRecord::Base
- has_many :tags, -> { where(active: true).auto_include(false).reorder(nil) }, through: :user_tags
- end
- RUBY
- end
- end
- end
- end
- end
-
- context 'outside of a migration spec file' do
- it 'does not register an offense' do
- expect_no_offenses(<<-RUBY)
- class User < ActiveRecord::Base
- has_many :tags, through: :user_tags
- end
- RUBY
- end
- end
-end
diff --git a/spec/serializers/entity_date_helper_spec.rb b/spec/serializers/entity_date_helper_spec.rb
index b9cc2f64831..36da8d33a44 100644
--- a/spec/serializers/entity_date_helper_spec.rb
+++ b/spec/serializers/entity_date_helper_spec.rb
@@ -32,6 +32,7 @@ describe EntityDateHelper do
end
it 'converts 86560 seconds' do
+ Rails.logger.debug date_helper_class.inspect
expect(date_helper_class.distance_of_time_as_hash(86560)).to eq(days: 1, mins: 2, seconds: 40)
end
@@ -42,4 +43,58 @@ describe EntityDateHelper do
it 'converts 986760 seconds' do
expect(date_helper_class.distance_of_time_as_hash(986760)).to eq(days: 11, hours: 10, mins: 6)
end
+
+ describe '#remaining_days_in_words' do
+ around do |example|
+ Timecop.freeze(Time.utc(2017, 3, 17)) { example.run }
+ end
+
+ context 'when less than 31 days remaining' do
+ let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) }
+
+ it 'returns days remaining' do
+ expect(milestone_remaining).to eq("<strong>12</strong> days remaining")
+ end
+ end
+
+ context 'when less than 1 year and more than 30 days remaining' do
+ let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) }
+
+ it 'returns months remaining' do
+ expect(milestone_remaining).to eq("<strong>2</strong> months remaining")
+ end
+ end
+
+ context 'when more than 1 year remaining' do
+ let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) }
+
+ it 'returns years remaining' do
+ expect(milestone_remaining).to eq("<strong>1</strong> year remaining")
+ end
+ end
+
+ context 'when milestone is expired' do
+ let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 2.days.ago.utc)) }
+
+ it 'returns "Past due"' do
+ expect(milestone_remaining).to eq("<strong>Past due</strong>")
+ end
+ end
+
+ context 'when milestone has start_date in the future' do
+ let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) }
+
+ it 'returns "Upcoming"' do
+ expect(milestone_remaining).to eq("<strong>Upcoming</strong>")
+ end
+ end
+
+ context 'when milestone has start_date in the past' do
+ let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, start_date: 2.days.ago.utc)) }
+
+ it 'returns days elapsed' do
+ expect(milestone_remaining).to eq("<strong>2</strong> days elapsed")
+ end
+ end
+ end
end
diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb
index 24a6f1a2a8a..c90396ebb28 100644
--- a/spec/serializers/job_entity_spec.rb
+++ b/spec/serializers/job_entity_spec.rb
@@ -133,22 +133,65 @@ describe JobEntity do
context 'when job failed' do
let(:job) { create(:ci_build, :script_failure) }
- describe 'status' do
- it 'should contain the failure reason inside label' do
- expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip
- expect(subject[:status][:label]).to eq('failed')
- expect(subject[:status][:tooltip]).to eq('failed <br> (script failure)')
- end
+ it 'contains details' do
+ expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip
+ end
+
+ it 'states that it failed' do
+ expect(subject[:status][:label]).to eq('failed')
+ end
+
+ it 'should indicate the failure reason on tooltip' do
+ expect(subject[:status][:tooltip]).to eq('failed <br> (script failure)')
+ end
+
+ it 'should include a callout message with a verbose output' do
+ expect(subject[:callout_message]).to eq('There has been a script failure. Check the job log for more information')
+ end
+
+ it 'should state that it is not recoverable' do
+ expect(subject[:recoverable]).to be_falsy
+ end
+ end
+
+ context 'when job is allowed to fail' do
+ let(:job) { create(:ci_build, :allowed_to_fail, :script_failure) }
+
+ it 'contains details' do
+ expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip
+ end
+
+ it 'states that it failed' do
+ expect(subject[:status][:label]).to eq('failed (allowed to fail)')
+ end
+
+ it 'should indicate the failure reason on tooltip' do
+ expect(subject[:status][:tooltip]).to eq('failed <br> (script failure) (allowed to fail)')
+ end
+
+ it 'should include a callout message with a verbose output' do
+ expect(subject[:callout_message]).to eq('There has been a script failure. Check the job log for more information')
+ end
+
+ it 'should state that it is not recoverable' do
+ expect(subject[:recoverable]).to be_falsy
+ end
+ end
+
+ context 'when job failed and is recoverable' do
+ let(:job) { create(:ci_build, :api_failure) }
+
+ it 'should state it is recoverable' do
+ expect(subject[:recoverable]).to be_truthy
end
end
context 'when job passed' do
let(:job) { create(:ci_build, :success) }
- describe 'status' do
- it 'should not contain the failure reason inside label' do
- expect(subject[:status][:label]).to eq('passed')
- end
+ it 'should not include callout message or recoverable keys' do
+ expect(subject).not_to include('callout_message')
+ expect(subject).not_to include('recoverable')
end
end
end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 97a563c1ce1..8a537e83d5f 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -370,10 +370,111 @@ module Ci
it_behaves_like 'validation is not active'
end
end
+ end
+
+ describe '#register_success' do
+ let!(:current_time) { Time.new(2018, 4, 5, 14, 0, 0) }
+ let!(:attempt_counter) { double('Gitlab::Metrics::NullMetric') }
+ let!(:job_queue_duration_seconds) { double('Gitlab::Metrics::NullMetric') }
+
+ before do
+ allow(Time).to receive(:now).and_return(current_time)
+
+ # Stub defaults for any metrics other than the ones we're testing
+ allow(Gitlab::Metrics).to receive(:counter)
+ .with(any_args)
+ .and_return(Gitlab::Metrics::NullMetric.instance)
+ allow(Gitlab::Metrics).to receive(:histogram)
+ .with(any_args)
+ .and_return(Gitlab::Metrics::NullMetric.instance)
+
+ # Stub tested metrics
+ allow(Gitlab::Metrics).to receive(:counter)
+ .with(:job_register_attempts_total, anything)
+ .and_return(attempt_counter)
+ allow(Gitlab::Metrics).to receive(:histogram)
+ .with(:job_queue_duration_seconds, anything, anything, anything)
+ .and_return(job_queue_duration_seconds)
+
+ project.update(shared_runners_enabled: true)
+ pending_job.update(created_at: current_time - 3600, queued_at: current_time - 1800)
+ end
+
+ shared_examples 'attempt counter collector' do
+ it 'increments attempt counter' do
+ allow(job_queue_duration_seconds).to receive(:observe)
+ expect(attempt_counter).to receive(:increment)
+
+ execute(runner)
+ end
+ end
+
+ shared_examples 'jobs queueing time histogram collector' do
+ it 'counts job queuing time histogram with expected labels' do
+ allow(attempt_counter).to receive(:increment)
+ expect(job_queue_duration_seconds).to receive(:observe)
+ .with({ shared_runner: expected_shared_runner,
+ jobs_running_for_project: expected_jobs_running_for_project_first_job }, 1800)
+
+ execute(runner)
+ end
+
+ context 'when project already has running jobs' do
+ let!(:build2) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) }
+ let!(:build3) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) }
+
+ it 'counts job queuing time histogram with expected labels' do
+ allow(attempt_counter).to receive(:increment)
+ expect(job_queue_duration_seconds).to receive(:observe)
+ .with({ shared_runner: expected_shared_runner,
+ jobs_running_for_project: expected_jobs_running_for_project_third_job }, 1800)
+
+ execute(runner)
+ end
+ end
+ end
- def execute(runner)
- described_class.new(runner).execute.build
+ shared_examples 'metrics collector' do
+ it_behaves_like 'attempt counter collector'
+ it_behaves_like 'jobs queueing time histogram collector'
end
+
+ context 'when shared runner is used' do
+ let(:runner) { shared_runner }
+ let(:expected_shared_runner) { true }
+ let(:expected_jobs_running_for_project_first_job) { 0 }
+ let(:expected_jobs_running_for_project_third_job) { 2 }
+
+ it_behaves_like 'metrics collector'
+
+ context 'when pending job with queued_at=nil is used' do
+ before do
+ pending_job.update(queued_at: nil)
+ end
+
+ it_behaves_like 'attempt counter collector'
+
+ it "doesn't count job queuing time histogram" do
+ allow(attempt_counter).to receive(:increment)
+ expect(job_queue_duration_seconds).not_to receive(:observe)
+
+ execute(runner)
+ end
+ end
+ end
+
+ context 'when specific runner is used' do
+ let(:runner) { specific_runner }
+ let(:expected_shared_runner) { false }
+ let(:expected_jobs_running_for_project_first_job) { '+Inf' }
+ let(:expected_jobs_running_for_project_third_job) { '+Inf' }
+
+ it_behaves_like 'metrics collector'
+ end
+ end
+
+ def execute(runner)
+ described_class.new(runner).execute.build
end
end
end
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index 6ce75c65c8c..f1acfc48468 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -235,6 +235,8 @@ describe Ci::RetryPipelineService, '#execute' do
context 'when user is not allowed to trigger manual action' do
before do
project.add_developer(user)
+ create(:protected_branch, :masters_can_push,
+ name: pipeline.ref, project: project)
end
context 'when there is a failed manual action present' do
diff --git a/spec/services/events/render_service_spec.rb b/spec/services/events/render_service_spec.rb
index b4a4a44d07b..075cb45e46c 100644
--- a/spec/services/events/render_service_spec.rb
+++ b/spec/services/events/render_service_spec.rb
@@ -9,9 +9,7 @@ describe Events::RenderService do
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)
+ .with(user: user, redaction_context: { only_path: false, xhtml: true })
.and_call_original
expect_any_instance_of(Banzai::ObjectRenderer)
@@ -24,7 +22,7 @@ describe Events::RenderService do
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, {})
+ .with(user: user, redaction_context: {})
.and_call_original
expect_any_instance_of(Banzai::ObjectRenderer)
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index e8216abb08b..a9baccd061a 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -53,8 +53,8 @@ describe Groups::DestroyService do
end
it 'verifies that paths have been deleted' do
- expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey
- expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, group.path)).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, remove_path)).to be_falsey
end
end
end
@@ -71,13 +71,13 @@ describe Groups::DestroyService do
after do
# Clean up stale directories
- gitlab_shell.rm_namespace(project.repository_storage_path, group.path)
- gitlab_shell.rm_namespace(project.repository_storage_path, remove_path)
+ gitlab_shell.rm_namespace(project.repository_storage, group.path)
+ gitlab_shell.rm_namespace(project.repository_storage, remove_path)
end
it 'verifies original paths and projects still exist' do
- expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_truthy
- expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, group.path)).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, remove_path)).to be_falsey
expect(Project.unscoped.count).to eq(1)
expect(Group.unscoped.count).to eq(2)
end
@@ -144,7 +144,7 @@ describe Groups::DestroyService do
let!(:project) { create(:project, :legacy_storage, :empty_repo, namespace: group) }
it 'removes repository' do
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
end
end
@@ -152,7 +152,7 @@ describe Groups::DestroyService do
let!(:project) { create(:project, :empty_repo, namespace: group) }
it 'removes repository' do
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
end
end
end
diff --git a/spec/services/groups/nested_create_service_spec.rb b/spec/services/groups/nested_create_service_spec.rb
index 6491fb34777..86fdd43c1e5 100644
--- a/spec/services/groups/nested_create_service_spec.rb
+++ b/spec/services/groups/nested_create_service_spec.rb
@@ -59,8 +59,11 @@ describe Groups::NestedCreateService do
describe "#execute" do
it 'returns the group if it already existed' do
- parent = create(:group, path: 'a-group', owner: user)
- child = create(:group, path: 'a-sub-group', parent: parent, owner: user)
+ parent = create(:group, path: 'a-group')
+ child = create(:group, path: 'a-sub-group', parent: parent)
+
+ parent.add_owner(user)
+ child.add_owner(user)
expect(service.execute).to eq(child)
end
diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb
index ae819c011de..80bac590a11 100644
--- a/spec/services/labels/transfer_service_spec.rb
+++ b/spec/services/labels/transfer_service_spec.rb
@@ -8,6 +8,7 @@ describe Labels::TransferService do
let(:group_3) { create(:group) }
let(:project_1) { create(:project, namespace: group_2) }
let(:project_2) { create(:project, namespace: group_3) }
+ let(:project_3) { create(:project, namespace: group_1) }
let(:group_label_1) { create(:group_label, group: group_1, name: 'Group Label 1') }
let(:group_label_2) { create(:group_label, group: group_1, name: 'Group Label 2') }
@@ -23,6 +24,7 @@ describe Labels::TransferService do
create(:labeled_issue, project: project_1, labels: [group_label_4])
create(:labeled_issue, project: project_1, labels: [project_label_1])
create(:labeled_issue, project: project_2, labels: [group_label_5])
+ create(:labeled_issue, project: project_3, labels: [group_label_1])
create(:labeled_merge_request, source_project: project_1, labels: [group_label_1, group_label_2])
create(:labeled_merge_request, source_project: project_2, labels: [group_label_5])
end
@@ -52,5 +54,13 @@ describe Labels::TransferService do
expect(project_1.labels.where(title: group_label_4.title)).to be_empty
end
+
+ it 'updates only label links in the given project' do
+ service.execute
+
+ targets = LabelLink.where(label_id: group_label_1.id).map(&:target)
+
+ expect(targets).to eq(project_3.issues)
+ end
end
end
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index 44a83c436cb..736a50b2c15 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe MergeRequests::CreateService do
+ include ProjectForksHelper
+
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:assignee) { create(:user) }
@@ -300,7 +302,7 @@ describe MergeRequests::CreateService do
end
context 'when source and target projects are different' do
- let(:target_project) { create(:project) }
+ let(:target_project) { fork_project(project, nil, repository: true) }
let(:opts) do
{
@@ -334,6 +336,26 @@ describe MergeRequests::CreateService do
.to raise_error Gitlab::Access::AccessDeniedError
end
end
+
+ context 'when the user has access to both projects' do
+ before do
+ target_project.add_developer(user)
+ project.add_developer(user)
+ end
+
+ it 'creates the merge request' do
+ merge_request = described_class.new(project, user, opts).execute
+
+ expect(merge_request).to be_persisted
+ end
+
+ it 'does not create the merge request when the target project is archived' do
+ target_project.update!(archived: true)
+
+ expect { described_class.new(project, user, opts).execute }
+ .to raise_error Gitlab::Access::AccessDeniedError
+ end
+ end
end
context 'when user sets source project id' do
diff --git a/spec/services/notes/render_service_spec.rb b/spec/services/notes/render_service_spec.rb
index faac498037f..f771620bc0d 100644
--- a/spec/services/notes/render_service_spec.rb
+++ b/spec/services/notes/render_service_spec.rb
@@ -4,23 +4,28 @@ 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)
+ expect(Banzai::ObjectRenderer)
+ .to receive(:new)
+ .with(
+ user: user,
+ redaction_context: {
+ 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)
+ .to receive(:render)
+ .with([note], :note)
- described_class.new(user).execute([note], project,
+ described_class.new(user).execute([note],
requested_path: 'foo',
project_wiki: wiki,
ref: 'bar',
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index f8fa2540804..55bbe954491 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -933,6 +933,46 @@ describe NotificationService, :mailer do
let(:notification_trigger) { notification.issue_moved(issue, new_issue, @u_disabled) }
end
end
+
+ describe '#issue_due' do
+ before do
+ issue.update!(due_date: Date.today)
+
+ update_custom_notification(:issue_due, @u_guest_custom, resource: project)
+ update_custom_notification(:issue_due, @u_custom_global)
+ end
+
+ it 'sends email to issue notification recipients, excluding watchers' do
+ notification.issue_due(issue)
+
+ should_email(issue.assignees.first)
+ should_email(issue.author)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
+ should_not_email(@u_watcher)
+ should_not_email(@u_guest_watcher)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ it 'sends the email from the author' do
+ notification.issue_due(issue)
+ email = find_email_for(@subscriber)
+
+ expect(email.header[:from].display_names).to eq([issue.author.name])
+ end
+
+ it_behaves_like 'participating notifications' do
+ let(:participant) { create(:user, username: 'user-participant') }
+ let(:issuable) { issue }
+ let(:notification_trigger) { notification.issue_due(issue) }
+ end
+ end
end
describe 'Merge Requests' do
diff --git a/spec/services/projects/create_from_template_service_spec.rb b/spec/services/projects/create_from_template_service_spec.rb
index 609d678caea..d40e6f1449d 100644
--- a/spec/services/projects/create_from_template_service_spec.rb
+++ b/spec/services/projects/create_from_template_service_spec.rb
@@ -7,7 +7,7 @@ describe Projects::CreateFromTemplateService do
path: user.to_param,
template_name: 'rails',
description: 'project description',
- visibility_level: Gitlab::VisibilityLevel::PRIVATE
+ visibility_level: Gitlab::VisibilityLevel::PUBLIC
}
end
@@ -24,7 +24,23 @@ describe Projects::CreateFromTemplateService do
expect(project).to be_saved
expect(project.scheduled?).to be(true)
- expect(project.description).to match('project description')
- expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ context 'the result project' do
+ before do
+ Sidekiq::Testing.inline! do
+ @project = subject.execute
+ end
+
+ @project.reload
+ end
+
+ it 'overrides template description' do
+ expect(@project.description).to match('project description')
+ end
+
+ it 'overrides template visibility_level' do
+ expect(@project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
end
end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index e35f0f6337a..a8f003b1073 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -171,7 +171,6 @@ describe Projects::CreateService, '#execute' do
context 'when another repository already exists on disk' do
let(:repository_storage) { 'default' }
- let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage].legacy_disk_path }
let(:opts) do
{
@@ -186,7 +185,7 @@ describe Projects::CreateService, '#execute' do
end
after do
- gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
+ gitlab_shell.remove_repository(repository_storage, "#{user.namespace.full_path}/existing")
end
it 'does not allow to create a project when path matches existing repository on disk' do
@@ -222,7 +221,7 @@ describe Projects::CreateService, '#execute' do
end
after do
- gitlab_shell.remove_repository(repository_storage_path, hashed_path)
+ gitlab_shell.remove_repository(repository_storage, hashed_path)
end
it 'does not allow to create a project when path matches existing repository on disk' do
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index a66e3c5e995..b2c52214f48 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -18,8 +18,8 @@ describe Projects::DestroyService do
it 'deletes the project' do
expect(Project.unscoped.all).not_to include(project)
- expect(project.gitlab_shell.exists?(project.repository_storage_path, path + '.git')).to be_falsey
- expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path + '.git')).to be_falsey
+ expect(project.gitlab_shell.exists?(project.repository_storage, path + '.git')).to be_falsey
+ expect(project.gitlab_shell.exists?(project.repository_storage, remove_path + '.git')).to be_falsey
end
end
@@ -252,21 +252,21 @@ describe Projects::DestroyService do
let(:path) { project.disk_path + '.git' }
before do
- expect(project.gitlab_shell.exists?(project.repository_storage_path, path)).to be_truthy
- expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey
+ expect(project.gitlab_shell.exists?(project.repository_storage, path)).to be_truthy
+ expect(project.gitlab_shell.exists?(project.repository_storage, remove_path)).to be_falsey
# Dont run sidekiq to check if renamed repository exists
Sidekiq::Testing.fake! { destroy_project(project, user, {}) }
- expect(project.gitlab_shell.exists?(project.repository_storage_path, path)).to be_falsey
- expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy
+ expect(project.gitlab_shell.exists?(project.repository_storage, path)).to be_falsey
+ expect(project.gitlab_shell.exists?(project.repository_storage, remove_path)).to be_truthy
end
it 'restores the repositories' do
Sidekiq::Testing.fake! { described_class.new(project, user).attempt_repositories_rollback }
- expect(project.gitlab_shell.exists?(project.repository_storage_path, path)).to be_truthy
- expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey
+ expect(project.gitlab_shell.exists?(project.repository_storage, path)).to be_truthy
+ expect(project.gitlab_shell.exists?(project.repository_storage, remove_path)).to be_falsey
end
end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 0f7c46367d0..a93f6f1ddc2 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -112,7 +112,7 @@ describe Projects::ForkService do
end
after do
- gitlab_shell.remove_repository(repository_storage_path, "#{@to_user.namespace.full_path}/#{@from_project.path}")
+ gitlab_shell.remove_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}")
end
it 'does not allow creation' do
diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
index 747bd4529a0..7dca81eb59e 100644
--- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
@@ -16,8 +16,8 @@ describe Projects::HashedStorage::MigrateRepositoryService do
it 'renames project and wiki repositories' do
service.execute
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.git")).to be_truthy
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, "#{hashed_storage.disk_path}.git")).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, "#{hashed_storage.disk_path}.wiki.git")).to be_truthy
end
it 'updates project to be hashed and not read-only' do
@@ -52,8 +52,8 @@ describe Projects::HashedStorage::MigrateRepositoryService do
service.execute
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.git")).to be_falsey
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, "#{hashed_storage.disk_path}.git")).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, "#{hashed_storage.disk_path}.wiki.git")).to be_falsey
expect(project.repository_read_only?).to be_falsey
end
@@ -63,11 +63,11 @@ describe Projects::HashedStorage::MigrateRepositoryService do
before do
hashed_storage.ensure_storage_path_exists
- gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
+ gitlab_shell.mv_repository(project.repository_storage, from_name, to_name)
end
it 'does not try to move nil repository over hashed' do
- expect(gitlab_shell).not_to receive(:mv_repository).with(project.repository_storage_path, from_name, to_name)
+ expect(gitlab_shell).not_to receive(:mv_repository).with(project.repository_storage, from_name, to_name)
expect_move_repository("#{project.disk_path}.wiki", "#{hashed_storage.disk_path}.wiki")
service.execute
@@ -76,7 +76,7 @@ describe Projects::HashedStorage::MigrateRepositoryService do
end
def expect_move_repository(from_name, to_name)
- expect(gitlab_shell).to receive(:mv_repository).with(project.repository_storage_path, from_name, to_name).and_call_original
+ expect(gitlab_shell).to receive(:mv_repository).with(project.repository_storage, from_name, to_name).and_call_original
end
end
end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index ff9b2372a35..3e6483d7e28 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -84,7 +84,7 @@ describe Projects::TransferService do
end
def project_path(project)
- File.join(project.repository_storage_path, "#{project.disk_path}.git")
+ project.repository.path_to_repo
end
def current_path
@@ -94,7 +94,7 @@ describe Projects::TransferService do
it 'rolls back repo location' do
attempt_project_transfer
- expect(Dir.exist?(original_path)).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be(true)
expect(original_path).to eq current_path
end
@@ -165,7 +165,7 @@ describe Projects::TransferService do
end
after do
- gitlab_shell.remove_repository(repository_storage_path, "#{group.full_path}/#{project.path}")
+ gitlab_shell.remove_repository(repository_storage, "#{group.full_path}/#{project.path}")
end
it { expect(@result).to eq false }
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index f48d466d263..3e6073b9861 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -200,7 +200,7 @@ describe Projects::UpdateService do
end
after do
- gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
+ gitlab_shell.remove_repository(repository_storage, "#{user.namespace.full_path}/existing")
end
it 'does not allow renaming when new path matches existing repository on disk' do
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index f793f55e51b..bd835a1fca6 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -306,6 +306,23 @@ describe QuickActions::InterpretService do
end
end
+ shared_examples 'copy_metadata command' do
+ it 'fetches issue or merge request and copies labels and milestone if content contains /copy_metadata reference' do
+ source_issuable # populate the issue
+ todo_label # populate this label
+ inreview_label # populate this label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates[:add_label_ids]).to match_array([inreview_label.id, todo_label.id])
+
+ if source_issuable.milestone
+ expect(updates[:milestone_id]).to eq(source_issuable.milestone.id)
+ else
+ expect(updates).not_to have_key(:milestone_id)
+ end
+ end
+ end
+
shared_examples 'shrug command' do
it 'appends ¯\_(ツ)_/¯ to the comment' do
new_content, _ = service.execute(content, issuable)
@@ -757,6 +774,65 @@ describe QuickActions::InterpretService do
let(:issuable) { issue }
end
+ context '/copy_metadata command' do
+ let(:todo_label) { create(:label, project: project, title: 'To Do') }
+ let(:inreview_label) { create(:label, project: project, title: 'In Review') }
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/copy_metadata' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'copy_metadata command' do
+ let(:source_issuable) { create(:labeled_issue, project: project, labels: [inreview_label, todo_label]) }
+
+ let(:content) { "/copy_metadata #{source_issuable.to_reference}" }
+ let(:issuable) { issue }
+ end
+
+ context 'when the parent issuable has a milestone' do
+ it_behaves_like 'copy_metadata command' do
+ let(:source_issuable) { create(:labeled_issue, project: project, labels: [todo_label, inreview_label], milestone: milestone) }
+
+ let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" }
+ let(:issuable) { issue }
+ end
+ end
+
+ context 'when more than one issuable is passed' do
+ it_behaves_like 'copy_metadata command' do
+ let(:source_issuable) { create(:labeled_issue, project: project, labels: [inreview_label, todo_label]) }
+ let(:other_label) { create(:label, project: project, title: 'Other') }
+ let(:other_source_issuable) { create(:labeled_issue, project: project, labels: [other_label]) }
+
+ let(:content) { "/copy_metadata #{source_issuable.to_reference} #{other_source_issuable.to_reference}" }
+ let(:issuable) { issue }
+ end
+ end
+
+ context 'cross project references' do
+ it_behaves_like 'empty command' do
+ let(:other_project) { create(:project, :public) }
+ let(:source_issuable) { create(:labeled_issue, project: other_project, labels: [todo_label, inreview_label]) }
+ let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { "/copy_metadata imaginary#1234" }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:other_project) { create(:project, :private) }
+ let(:source_issuable) { create(:issue, project: other_project) }
+
+ let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" }
+ let(:issuable) { issue }
+ end
+ end
+ end
+
context '/duplicate command' do
it_behaves_like 'duplicate command' do
let(:issue_duplicate) { create(:issue, project: project) }
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index e28b0ea5cf2..893804f1470 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -909,7 +909,13 @@ describe SystemNoteService do
it 'sets the note text' do
noteable.update_attribute(:time_estimate, 277200)
- expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
+ expect(subject.note).to eq "changed time estimate to 1w 4d 5h,"
+ end
+
+ it 'appends a comma to separate the note from the update_at time' do
+ noteable.update_attribute(:time_estimate, 277200)
+
+ expect(subject.note).to end_with(',')
end
end
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 11c75ddfcf8..76f1e625fda 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -176,7 +176,7 @@ describe Users::DestroyService do
let!(:project) { create(:project, :empty_repo, :legacy_storage, namespace: user.namespace) }
it 'removes repository' do
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
end
end
@@ -184,7 +184,7 @@ describe Users::DestroyService do
let!(:project) { create(:project, :empty_repo, namespace: user.namespace) }
it 'removes repository' do
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 83664bae046..cc61cd7d838 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -32,42 +32,19 @@ require 'rainbow/ext/string'
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
+# Requires helpers, and shared contexts/examples first since they're used in other support files
+Dir[Rails.root.join("spec/support/helpers/*.rb")].each { |f| require f }
+Dir[Rails.root.join("spec/support/shared_contexts/*.rb")].each { |f| require f }
+Dir[Rails.root.join("spec/support/shared_examples/*.rb")].each { |f| require f }
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
RSpec.configure do |config|
config.use_transactional_fixtures = false
config.use_instantiated_fixtures = false
- config.mock_with :rspec
config.verbose_retry = true
config.display_try_failure_messages = true
- config.include Devise::Test::ControllerHelpers, type: :controller
- config.include Devise::Test::ControllerHelpers, type: :view
- config.include Devise::Test::IntegrationHelpers, type: :feature
- config.include Warden::Test::Helpers, type: :request
- config.include LoginHelpers, type: :feature
- config.include SearchHelpers, type: :feature
- config.include CookieHelper, :js
- config.include InputHelper, :js
- config.include SelectionHelper, :js
- config.include InspectRequests, :js
- config.include WaitForRequests, :js
- config.include LiveDebugger, :js
- config.include StubConfiguration
- config.include EmailHelpers, :mailer, type: :mailer
- config.include TestEnv
- config.include ActiveJob::TestHelper
- config.include ActiveSupport::Testing::TimeHelpers
- config.include StubGitlabCalls
- config.include StubGitlabData
- config.include ApiHelpers, :api
- config.include Gitlab::Routing, type: :routing
- config.include MigrationsHelpers, :migration
- config.include StubFeatureFlags
- config.include StubENV
- config.include ExpectOffense
-
config.infer_spec_type_from_file_location!
config.define_derived_metadata(file_path: %r{/spec/}) do |metadata|
@@ -82,7 +59,33 @@ RSpec.configure do |config|
metadata[:type] = match[1].singularize.to_sym if match
end
- config.raise_errors_for_deprecations!
+ config.include ActiveJob::TestHelper
+ config.include ActiveSupport::Testing::TimeHelpers
+ config.include CycleAnalyticsHelpers
+ config.include ExpectOffense
+ config.include FactoryBot::Syntax::Methods
+ config.include FixtureHelpers
+ config.include GitlabRoutingHelper
+ config.include StubFeatureFlags
+ config.include StubGitlabCalls
+ config.include StubGitlabData
+ config.include TestEnv
+ config.include Devise::Test::ControllerHelpers, type: :controller
+ config.include Devise::Test::IntegrationHelpers, type: :feature
+ config.include LoginHelpers, type: :feature
+ config.include SearchHelpers, type: :feature
+ config.include EmailHelpers, :mailer, type: :mailer
+ config.include Warden::Test::Helpers, type: :request
+ config.include Gitlab::Routing, type: :routing
+ config.include Devise::Test::ControllerHelpers, type: :view
+ config.include ApiHelpers, :api
+ config.include CookieHelper, :js
+ config.include InputHelper, :js
+ config.include SelectionHelper, :js
+ config.include InspectRequests, :js
+ config.include WaitForRequests, :js
+ config.include LiveDebugger, :js
+ config.include MigrationsHelpers, :migration
if ENV['CI']
# This includes the first try, i.e. tests will be run 4 times before failing.
@@ -110,10 +113,10 @@ RSpec.configure do |config|
m.call(*args)
shard_name, repository_relative_path = args
- shard_path = Gitlab.config.repositories.storages.fetch(shard_name).legacy_disk_path
# We can't leave the hooks in place after a fork, as those would fail in tests
# The "internal" API is not available
- FileUtils.rm_rf(File.join(shard_path, repository_relative_path, 'hooks'))
+ Gitlab::Shell.new.rm_directory(shard_name,
+ File.join(repository_relative_path, 'hooks'))
end
# Enable all features by default for testing
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 8603b7f3e2c..c0ceb0f6605 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -7,6 +7,16 @@ require 'selenium-webdriver'
# Give CI some extra time
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
+# Define an error class for JS console messages
+JSConsoleError = Class.new(StandardError)
+
+# Filter out innocuous JS console messages
+JS_CONSOLE_FILTER = Regexp.union([
+ '"[HMR] Waiting for update signal from WDS..."',
+ '"[WDS] Hot Module Replacement enabled."',
+ "Download the Vue Devtools extension"
+])
+
Capybara.register_driver :chrome do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
# This enables access to logs with `page.driver.manage.get_log(:browser)`
@@ -25,13 +35,7 @@ Capybara.register_driver :chrome do |app|
options.add_argument("no-sandbox")
# Run headless by default unless CHROME_HEADLESS specified
- unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
- options.add_argument("headless")
-
- # Chrome documentation says this flag is needed for now
- # https://developers.google.com/web/updates/2017/04/headless-chrome#cli
- options.add_argument("disable-gpu")
- end
+ options.add_argument("headless") unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
# Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252
options.add_argument("disable-dev-shm-usage") if ENV['CI'] || ENV['CI_SERVER']
@@ -56,6 +60,8 @@ Capybara::Screenshot.register_driver(:chrome) do |driver, path|
end
RSpec.configure do |config|
+ config.include CapybaraHelpers, type: :feature
+
config.before(:context, :js) do
next if $capybara_server_already_started
@@ -78,6 +84,15 @@ RSpec.configure do |config|
end
config.after(:example, :js) do |example|
+ # when a test fails, display any messages in the browser's console
+ if example.exception
+ console = page.driver.browser.manage.logs.get(:browser)&.reject { |log| log.message =~ JS_CONSOLE_FILTER }
+ if console.present?
+ message = "Unexpected browser console output:\n" + console.map(&:message).join("\n")
+ raise JSConsoleError, message
+ end
+ end
+
# prevent localStorage from introducing side effects based on test order
unless ['', 'about:blank', 'data:,'].include? Capybara.current_session.driver.browser.current_url
execute_script("localStorage.clear();")
diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb
index 3321f920666..368439aa5b0 100644
--- a/spec/support/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb
@@ -56,7 +56,7 @@ shared_examples 'a GitHub-ish import controller: GET status' do
end
it "assigns variables" do
- project = create(:project, import_type: provider, creator_id: user.id)
+ project = create(:project, import_type: provider, namespace: user.namespace)
stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo])
get :status
@@ -69,7 +69,7 @@ shared_examples 'a GitHub-ish import controller: GET status' do
end
it "does not show already added project" do
- project = create(:project, import_type: provider, creator_id: user.id, import_source: 'asd/vim')
+ project = create(:project, import_type: provider, namespace: user.namespace, import_source: 'asd/vim')
stub_client(repos: [repo], orgs: [])
get :status
@@ -257,11 +257,12 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
context 'user has chosen an existing nested namespace and name for the project', :postgresql do
- let(:parent_namespace) { create(:group, name: 'foo', owner: user) }
+ let(:parent_namespace) { create(:group, name: 'foo') }
let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
let(:test_name) { 'test_name' }
before do
+ parent_namespace.add_owner(user)
nested_namespace.add_owner(user)
end
@@ -307,7 +308,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do
let(:test_name) { 'test_name' }
- let!(:parent_namespace) { create(:group, name: 'foo', owner: user) }
+ let!(:parent_namespace) { create(:group, name: 'foo') }
before do
parent_namespace.add_owner(user)
diff --git a/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb b/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb
new file mode 100644
index 00000000000..72912ffb89d
--- /dev/null
+++ b/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+shared_context 'Ldap::OmniauthCallbacksController' do
+ include LoginHelpers
+ include LdapHelpers
+
+ let(:uid) { 'my-uid' }
+ let(:provider) { 'ldapmain' }
+ let(:valid_login?) { true }
+ let(:user) { create(:omniauth_user, extern_uid: uid, provider: provider) }
+ let(:ldap_server_config) do
+ { main: ldap_config_defaults(:main) }
+ end
+
+ def ldap_config_defaults(key, hash = {})
+ {
+ provider_name: "ldap#{key}",
+ attributes: {},
+ encryption: 'plain'
+ }.merge(hash)
+ end
+
+ before do
+ stub_ldap_setting(enabled: true, servers: ldap_server_config)
+ described_class.define_providers!
+ Rails.application.reload_routes!
+
+ mock_auth_hash(provider.to_s, uid, user.email)
+ stub_omniauth_provider(provider, context: request)
+
+ allow(Gitlab::Auth::LDAP::Access).to receive(:allowed?).and_return(valid_login?)
+ end
+end
diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb
deleted file mode 100644
index c7890e49c66..00000000000
--- a/spec/support/factory_bot.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-RSpec.configure do |config|
- config.include FactoryBot::Syntax::Methods
-end
diff --git a/spec/support/generate-seed-repo-rb b/spec/support/generate-seed-repo-rb
index 876b3b8242d..44b3de23b99 100755
--- a/spec/support/generate-seed-repo-rb
+++ b/spec/support/generate-seed-repo-rb
@@ -8,7 +8,7 @@
#
# Usage:
#
-# ./spec/support/generate-seed-repo-rb > spec/support/seed_repo.rb
+# ./spec/support/generate-seed-repo-rb > spec/support/helpers/seed_repo.rb
#
#
diff --git a/spec/support/gitlab-git-test.git/README.md b/spec/support/gitlab-git-test.git/README.md
index f072cd421be..f757e613ee6 100644
--- a/spec/support/gitlab-git-test.git/README.md
+++ b/spec/support/gitlab-git-test.git/README.md
@@ -12,5 +12,5 @@ inflate the size of the gitlab-ce repository.
- make changes in your local clone of gitlab-git-test
- run `git push` which will push to your local source `gitlab-ce/spec/support/gitlab-git-test.git`
- in gitlab-ce: run `spec/support/prepare-gitlab-git-test-for-commit`
-- in gitlab-ce: `git add spec/support/seed_repo.rb spec/support/gitlab-git-test.git`
+- in gitlab-ce: `git add spec/support/helpers/seed_repo.rb spec/support/gitlab-git-test.git`
- commit your changes in gitlab-ce
diff --git a/spec/support/api_helpers.rb b/spec/support/helpers/api_helpers.rb
index ac0c7a9b493..ac0c7a9b493 100644
--- a/spec/support/api_helpers.rb
+++ b/spec/support/helpers/api_helpers.rb
diff --git a/spec/support/bare_repo_operations.rb b/spec/support/helpers/bare_repo_operations.rb
index 8eeaa37d3c5..3f4a4243cb6 100644
--- a/spec/support/bare_repo_operations.rb
+++ b/spec/support/helpers/bare_repo_operations.rb
@@ -1,19 +1,15 @@
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
- def commit_tree(tree_id, msg, parent: EMPTY_TREE_ID)
+ def commit_tree(tree_id, msg, parent: Gitlab::Git::EMPTY_TREE_ID)
commit_tree_args = ['commit-tree', tree_id, '-m', msg]
- commit_tree_args += ['-p', parent] unless parent == EMPTY_TREE_ID
+ commit_tree_args += ['-p', parent] unless parent == Gitlab::Git::EMPTY_TREE_ID
commit_id = execute(commit_tree_args)
commit_id[0]
@@ -21,7 +17,7 @@ class BareRepoOperations
# 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
+ head_id = execute(['show', '--format=format:%H', '--no-patch', branch], allow_failure: true)[0] || Gitlab::Git::EMPTY_TREE_ID
execute(['read-tree', '--empty'])
execute(['read-tree', head_id])
diff --git a/spec/support/board_helpers.rb b/spec/support/helpers/board_helpers.rb
index 507d0432d7f..507d0432d7f 100644
--- a/spec/support/board_helpers.rb
+++ b/spec/support/helpers/board_helpers.rb
diff --git a/spec/support/capybara_helpers.rb b/spec/support/helpers/capybara_helpers.rb
index 868233416bf..bcc2df44708 100644
--- a/spec/support/capybara_helpers.rb
+++ b/spec/support/helpers/capybara_helpers.rb
@@ -41,7 +41,3 @@ module CapybaraHelpers
page.driver.browser.manage.delete_cookie('_gitlab_session')
end
end
-
-RSpec.configure do |config|
- config.include CapybaraHelpers, type: :feature
-end
diff --git a/spec/support/cookie_helper.rb b/spec/support/helpers/cookie_helper.rb
index 5ff7b0b68c9..5ff7b0b68c9 100644
--- a/spec/support/cookie_helper.rb
+++ b/spec/support/helpers/cookie_helper.rb
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index 73cc64c0b74..55359d36597 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -135,7 +135,3 @@ module CycleAnalyticsHelpers
end
end
end
-
-RSpec.configure do |config|
- config.include CycleAnalyticsHelpers
-end
diff --git a/spec/support/database_connection_helpers.rb b/spec/support/helpers/database_connection_helpers.rb
index 763329499f0..763329499f0 100644
--- a/spec/support/database_connection_helpers.rb
+++ b/spec/support/helpers/database_connection_helpers.rb
diff --git a/spec/support/devise_helpers.rb b/spec/support/helpers/devise_helpers.rb
index 66874e10f38..66874e10f38 100644
--- a/spec/support/devise_helpers.rb
+++ b/spec/support/helpers/devise_helpers.rb
diff --git a/spec/support/drag_to_helper.rb b/spec/support/helpers/drag_to_helper.rb
index ae149631ed9..ae149631ed9 100644
--- a/spec/support/drag_to_helper.rb
+++ b/spec/support/helpers/drag_to_helper.rb
diff --git a/spec/support/dropzone_helper.rb b/spec/support/helpers/dropzone_helper.rb
index fe72d320fcf..fe72d320fcf 100644
--- a/spec/support/dropzone_helper.rb
+++ b/spec/support/helpers/dropzone_helper.rb
diff --git a/spec/support/email_helpers.rb b/spec/support/helpers/email_helpers.rb
index 1fb8252459f..1fb8252459f 100644
--- a/spec/support/email_helpers.rb
+++ b/spec/support/helpers/email_helpers.rb
diff --git a/spec/support/fake_migration_classes.rb b/spec/support/helpers/fake_migration_classes.rb
index b0fc8422857..b0fc8422857 100644
--- a/spec/support/fake_migration_classes.rb
+++ b/spec/support/helpers/fake_migration_classes.rb
diff --git a/spec/support/fake_u2f_device.rb b/spec/support/helpers/fake_u2f_device.rb
index a7605cd483a..a7605cd483a 100644
--- a/spec/support/fake_u2f_device.rb
+++ b/spec/support/helpers/fake_u2f_device.rb
diff --git a/spec/support/helpers/features/branches_helpers.rb b/spec/support/helpers/features/branches_helpers.rb
new file mode 100644
index 00000000000..3525d9a70a7
--- /dev/null
+++ b/spec/support/helpers/features/branches_helpers.rb
@@ -0,0 +1,33 @@
+# These helpers allow you to manipulate with sorting features.
+#
+# Usage:
+# describe "..." do
+# include Spec::Support::Helpers::Features::BranchesHelpers
+# ...
+#
+# create_branch("feature")
+# select_branch("master")
+#
+module Spec
+ module Support
+ module Helpers
+ module Features
+ module BranchesHelpers
+ def create_branch(branch_name, source_branch_name = "master")
+ fill_in("branch_name", with: branch_name)
+ select_branch(source_branch_name)
+ click_button("Create branch")
+ end
+
+ def select_branch(branch_name)
+ find(".git-revision-dropdown-toggle").click
+
+ page.within("#new-branch-form .dropdown-menu") do
+ click_link(branch_name)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/filter_item_select_helper.rb b/spec/support/helpers/filter_item_select_helper.rb
index 519e84d359e..519e84d359e 100644
--- a/spec/support/filter_item_select_helper.rb
+++ b/spec/support/helpers/filter_item_select_helper.rb
diff --git a/spec/support/filter_spec_helper.rb b/spec/support/helpers/filter_spec_helper.rb
index b871b7ffc90..721d359c2ee 100644
--- a/spec/support/filter_spec_helper.rb
+++ b/spec/support/helpers/filter_spec_helper.rb
@@ -18,6 +18,11 @@ module FilterSpecHelper
context.reverse_merge!(project: project)
end
+ render_context = Banzai::RenderContext
+ .new(context[:project], context[:current_user])
+
+ context = context.merge(render_context: render_context)
+
described_class.call(html, context)
end
diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index 5f42ff77fb2..5f42ff77fb2 100644
--- a/spec/support/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
diff --git a/spec/support/fixture_helpers.rb b/spec/support/helpers/fixture_helpers.rb
index 8854382dc6b..611d19f36a0 100644
--- a/spec/support/fixture_helpers.rb
+++ b/spec/support/helpers/fixture_helpers.rb
@@ -9,7 +9,3 @@ module FixtureHelpers
File.expand_path(Rails.root.join(dir, 'spec', 'fixtures', filename))
end
end
-
-RSpec.configure do |config|
- config.include FixtureHelpers
-end
diff --git a/spec/support/git_http_helpers.rb b/spec/support/helpers/git_http_helpers.rb
index b8289e6c5f1..b8289e6c5f1 100644
--- a/spec/support/git_http_helpers.rb
+++ b/spec/support/helpers/git_http_helpers.rb
diff --git a/spec/support/helpers/gitlab_verify_helpers.rb b/spec/support/helpers/gitlab_verify_helpers.rb
new file mode 100644
index 00000000000..5df4bf24ec2
--- /dev/null
+++ b/spec/support/helpers/gitlab_verify_helpers.rb
@@ -0,0 +1,25 @@
+module GitlabVerifyHelpers
+ def collect_ranges(args = {})
+ verifier = described_class.new(args.merge(batch_size: 1))
+
+ collect_results(verifier).map { |range, _| range }
+ end
+
+ def collect_failures
+ verifier = described_class.new(batch_size: 1)
+
+ out = {}
+
+ collect_results(verifier).map { |_, failures| out.merge!(failures) }
+
+ out
+ end
+
+ def collect_results(verifier)
+ out = []
+
+ verifier.run_batches { |*args| out << args }
+
+ out
+ end
+end
diff --git a/spec/support/gpg_helpers.rb b/spec/support/helpers/gpg_helpers.rb
index 3f7279a50e0..3f7279a50e0 100644
--- a/spec/support/gpg_helpers.rb
+++ b/spec/support/helpers/gpg_helpers.rb
diff --git a/spec/support/import_spec_helper.rb b/spec/support/helpers/import_spec_helper.rb
index d4eced724fa..d4eced724fa 100644
--- a/spec/support/import_spec_helper.rb
+++ b/spec/support/helpers/import_spec_helper.rb
diff --git a/spec/support/input_helper.rb b/spec/support/helpers/input_helper.rb
index acbb42274ec..acbb42274ec 100644
--- a/spec/support/input_helper.rb
+++ b/spec/support/helpers/input_helper.rb
diff --git a/spec/support/inspect_requests.rb b/spec/support/helpers/inspect_requests.rb
index 88ddc5c7f6c..88ddc5c7f6c 100644
--- a/spec/support/inspect_requests.rb
+++ b/spec/support/helpers/inspect_requests.rb
diff --git a/spec/support/issue_helpers.rb b/spec/support/helpers/issue_helpers.rb
index ffd72515f37..ffd72515f37 100644
--- a/spec/support/issue_helpers.rb
+++ b/spec/support/helpers/issue_helpers.rb
diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb
index 2197bc9d853..086a345dca8 100644
--- a/spec/support/javascript_fixtures_helpers.rb
+++ b/spec/support/helpers/javascript_fixtures_helpers.rb
@@ -31,7 +31,7 @@ module JavaScriptFixturesHelpers
end
def remove_repository(project)
- Gitlab::Shell.new.remove_repository(project.repository_storage_path, project.disk_path)
+ Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path)
end
private
diff --git a/spec/support/jira_service_helper.rb b/spec/support/helpers/jira_service_helper.rb
index 88a7aeba461..88a7aeba461 100644
--- a/spec/support/jira_service_helper.rb
+++ b/spec/support/helpers/jira_service_helper.rb
diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index e46b61b6461..e46b61b6461 100644
--- a/spec/support/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
diff --git a/spec/support/ldap_helpers.rb b/spec/support/helpers/ldap_helpers.rb
index 0e87b3d359d..b90bbc4b106 100644
--- a/spec/support/ldap_helpers.rb
+++ b/spec/support/helpers/ldap_helpers.rb
@@ -18,6 +18,10 @@ module LdapHelpers
allow_any_instance_of(::Gitlab::Auth::LDAP::Config).to receive_messages(messages)
end
+ def stub_ldap_setting(messages)
+ allow(Gitlab.config.ldap).to receive_messages(to_settings(messages))
+ end
+
# Stub an LDAP person search and provide the return entry. Specify `nil` for
# `entry` to simulate when an LDAP person is not found
#
diff --git a/spec/support/live_debugger.rb b/spec/support/helpers/live_debugger.rb
index 911eb48a8ca..911eb48a8ca 100644
--- a/spec/support/live_debugger.rb
+++ b/spec/support/helpers/live_debugger.rb
diff --git a/spec/support/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index db34090e971..72e5c2d66dd 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -112,7 +112,7 @@ module LoginHelpers
}
}
})
- Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:saml]
+ Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]
end
def mock_saml_config
@@ -129,7 +129,7 @@ module LoginHelpers
env = env_from_context(context)
set_devise_mapping(context: context)
- env['omniauth.auth'] = OmniAuth.config.mock_auth[provider]
+ env['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]
end
def stub_omniauth_saml_config(messages)
diff --git a/spec/support/markdown_feature.rb b/spec/support/helpers/markdown_feature.rb
index 39e94ad53de..39e94ad53de 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/helpers/markdown_feature.rb
diff --git a/spec/support/merge_request_helpers.rb b/spec/support/helpers/merge_request_helpers.rb
index 772adff4626..772adff4626 100644
--- a/spec/support/merge_request_helpers.rb
+++ b/spec/support/helpers/merge_request_helpers.rb
diff --git a/spec/support/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb
index 5d6f662e8fe..5d6f662e8fe 100644
--- a/spec/support/migrations_helpers.rb
+++ b/spec/support/helpers/migrations_helpers.rb
diff --git a/spec/support/mobile_helpers.rb b/spec/support/helpers/mobile_helpers.rb
index 3b9eb84e824..3b9eb84e824 100644
--- a/spec/support/mobile_helpers.rb
+++ b/spec/support/helpers/mobile_helpers.rb
diff --git a/spec/support/project_forks_helper.rb b/spec/support/helpers/project_forks_helper.rb
index 2c501a2a27c..2c501a2a27c 100644
--- a/spec/support/project_forks_helper.rb
+++ b/spec/support/helpers/project_forks_helper.rb
diff --git a/spec/support/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb
index 4212be2cc88..4212be2cc88 100644
--- a/spec/support/prometheus_helpers.rb
+++ b/spec/support/helpers/prometheus_helpers.rb
diff --git a/spec/support/helpers/query_recorder.rb b/spec/support/helpers/query_recorder.rb
new file mode 100644
index 00000000000..28536bbef5e
--- /dev/null
+++ b/spec/support/helpers/query_recorder.rb
@@ -0,0 +1,38 @@
+module ActiveRecord
+ class QueryRecorder
+ attr_reader :log, :cached
+
+ def initialize(&block)
+ @log = []
+ @cached = []
+ 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")
+ @log << values[:sql]
+ end
+ end
+
+ def count
+ @log.count
+ end
+
+ def cached_count
+ @cached.count
+ end
+
+ def log_message
+ @log.join("\n\n")
+ end
+ end
+end
diff --git a/spec/support/helpers/quick_actions_helpers.rb b/spec/support/helpers/quick_actions_helpers.rb
new file mode 100644
index 00000000000..361190aa352
--- /dev/null
+++ b/spec/support/helpers/quick_actions_helpers.rb
@@ -0,0 +1,10 @@
+module QuickActionsHelpers
+ def write_note(text)
+ Sidekiq::Testing.fake! do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: text
+ find('.js-comment-submit-button').click
+ end
+ end
+ end
+end
diff --git a/spec/support/rake_helpers.rb b/spec/support/helpers/rake_helpers.rb
index 86bfeed107c..86bfeed107c 100644
--- a/spec/support/rake_helpers.rb
+++ b/spec/support/helpers/rake_helpers.rb
diff --git a/spec/support/reactive_caching_helpers.rb b/spec/support/helpers/reactive_caching_helpers.rb
index e22dd974c6a..e22dd974c6a 100644
--- a/spec/support/reactive_caching_helpers.rb
+++ b/spec/support/helpers/reactive_caching_helpers.rb
diff --git a/spec/support/redis_without_keys.rb b/spec/support/helpers/redis_without_keys.rb
index 6220167dee6..6220167dee6 100644
--- a/spec/support/redis_without_keys.rb
+++ b/spec/support/helpers/redis_without_keys.rb
diff --git a/spec/support/reference_parser_helpers.rb b/spec/support/helpers/reference_parser_helpers.rb
index 5d5e80851e6..c01897ed1a1 100644
--- a/spec/support/reference_parser_helpers.rb
+++ b/spec/support/helpers/reference_parser_helpers.rb
@@ -5,9 +5,11 @@ module ReferenceParserHelpers
shared_examples 'no N+1 queries' do
it 'avoids N+1 queries in #nodes_visible_to_user', :request_store do
+ context = Banzai::RenderContext.new(project, user)
+
record_queries = lambda do |links|
ActiveRecord::QueryRecorder.new do
- described_class.new(project, user).nodes_visible_to_user(user, links)
+ described_class.new(context).nodes_visible_to_user(user, links)
end
end
@@ -19,9 +21,11 @@ module ReferenceParserHelpers
end
it 'avoids N+1 queries in #records_for_nodes', :request_store do
+ context = Banzai::RenderContext.new(project, user)
+
record_queries = lambda do |links|
ActiveRecord::QueryRecorder.new do
- described_class.new(project, user).records_for_nodes(links)
+ described_class.new(context).records_for_nodes(links)
end
end
diff --git a/spec/support/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb
index 3c6956cf5e0..3c6956cf5e0 100644
--- a/spec/support/repo_helpers.rb
+++ b/spec/support/helpers/repo_helpers.rb
diff --git a/spec/support/search_helpers.rb b/spec/support/helpers/search_helpers.rb
index abbbb636d66..abbbb636d66 100644
--- a/spec/support/search_helpers.rb
+++ b/spec/support/helpers/search_helpers.rb
diff --git a/spec/support/seed_helper.rb b/spec/support/helpers/seed_helper.rb
index 11ef1fc477f..8fd107260cc 100644
--- a/spec/support/seed_helper.rb
+++ b/spec/support/helpers/seed_helper.rb
@@ -9,7 +9,7 @@ TEST_MUTABLE_REPO_PATH = 'mutable-repo.git'.freeze
TEST_BROKEN_REPO_PATH = 'broken-repo.git'.freeze
module SeedHelper
- GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __FILE__).freeze
+ GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __dir__).freeze
def ensure_seeds
if File.exist?(SEED_STORAGE_PATH)
@@ -108,11 +108,3 @@ bla/bla.txt
{ 'GIT_TEMPLATE_DIR' => '' }
end
end
-
-RSpec.configure do |config|
- config.include SeedHelper, :seed_helper
-
- config.before(:all, :seed_helper) do
- ensure_seeds
- end
-end
diff --git a/spec/support/seed_repo.rb b/spec/support/helpers/seed_repo.rb
index b4868e82cd7..b4868e82cd7 100644
--- a/spec/support/seed_repo.rb
+++ b/spec/support/helpers/seed_repo.rb
diff --git a/spec/support/select2_helper.rb b/spec/support/helpers/select2_helper.rb
index 90618ba5b19..90618ba5b19 100644
--- a/spec/support/select2_helper.rb
+++ b/spec/support/helpers/select2_helper.rb
diff --git a/spec/support/selection_helper.rb b/spec/support/helpers/selection_helper.rb
index b4725b137b2..b4725b137b2 100644
--- a/spec/support/selection_helper.rb
+++ b/spec/support/helpers/selection_helper.rb
diff --git a/spec/support/helpers/sorting_helper.rb b/spec/support/helpers/sorting_helper.rb
new file mode 100644
index 00000000000..577518d726c
--- /dev/null
+++ b/spec/support/helpers/sorting_helper.rb
@@ -0,0 +1,18 @@
+# Helper allows you to sort items
+#
+# Params
+# value - value for sorting
+#
+# Usage:
+# include SortingHelper
+#
+# sorting_by('Oldest updated')
+#
+module SortingHelper
+ def sorting_by(value)
+ find('button.dropdown-toggle').click
+ page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
+ click_link value
+ end
+ end
+end
diff --git a/spec/support/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb
index a75a3eaefcb..1823099dd9c 100644
--- a/spec/support/stub_configuration.rb
+++ b/spec/support/helpers/stub_configuration.rb
@@ -1,3 +1,6 @@
+require 'active_support/core_ext/hash/transform_values'
+require 'active_support/hash_with_indifferent_access'
+
module StubConfiguration
def stub_application_setting(messages)
add_predicates(messages)
diff --git a/spec/support/stub_env.rb b/spec/support/helpers/stub_env.rb
index 36b90fc68d6..36b90fc68d6 100644
--- a/spec/support/stub_env.rb
+++ b/spec/support/helpers/stub_env.rb
diff --git a/spec/support/stub_feature_flags.rb b/spec/support/helpers/stub_feature_flags.rb
index b96338bf548..b96338bf548 100644
--- a/spec/support/stub_feature_flags.rb
+++ b/spec/support/helpers/stub_feature_flags.rb
diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
index c1618f5086c..c1618f5086c 100644
--- a/spec/support/stub_gitlab_calls.rb
+++ b/spec/support/helpers/stub_gitlab_calls.rb
diff --git a/spec/support/stub_gitlab_data.rb b/spec/support/helpers/stub_gitlab_data.rb
index fa402f35b95..fa402f35b95 100644
--- a/spec/support/stub_gitlab_data.rb
+++ b/spec/support/helpers/stub_gitlab_data.rb
diff --git a/spec/support/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index 6e88641da42..19d744b959a 100644
--- a/spec/support/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -1,4 +1,4 @@
-module StubConfiguration
+module StubObjectStorage
def stub_object_storage_uploader(
config:,
uploader:,
diff --git a/spec/support/test_env.rb b/spec/support/helpers/test_env.rb
index d87f265cdf0..1dad39fdab3 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -218,7 +218,8 @@ module TestEnv
end
def copy_repo(project, bare_repo:, refs:)
- target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.disk_path}.git")
+ target_repo_path = File.expand_path(repos_path + "/#{project.disk_path}.git")
+
FileUtils.mkdir_p(target_repo_path)
FileUtils.cp_r("#{File.expand_path(bare_repo)}/.", target_repo_path)
FileUtils.chmod_R 0755, target_repo_path
@@ -226,7 +227,7 @@ module TestEnv
end
def repos_path
- Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path
+ @repos_path ||= Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path
end
def backup_path
diff --git a/spec/support/upload_helpers.rb b/spec/support/helpers/upload_helpers.rb
index 5eead80c935..5eead80c935 100644
--- a/spec/support/upload_helpers.rb
+++ b/spec/support/helpers/upload_helpers.rb
diff --git a/spec/support/user_activities_helpers.rb b/spec/support/helpers/user_activities_helpers.rb
index 44feb104644..44feb104644 100644
--- a/spec/support/user_activities_helpers.rb
+++ b/spec/support/helpers/user_activities_helpers.rb
diff --git a/spec/support/wait_for_requests.rb b/spec/support/helpers/wait_for_requests.rb
index fda0e29f983..fda0e29f983 100644
--- a/spec/support/wait_for_requests.rb
+++ b/spec/support/helpers/wait_for_requests.rb
diff --git a/spec/support/workhorse_helpers.rb b/spec/support/helpers/workhorse_helpers.rb
index ef1f9f68671..ef1f9f68671 100644
--- a/spec/support/workhorse_helpers.rb
+++ b/spec/support/helpers/workhorse_helpers.rb
diff --git a/spec/support/http_io/http_io_helpers.rb b/spec/support/http_io/http_io_helpers.rb
index 31e07e720cd..2c68c2cd9a6 100644
--- a/spec/support/http_io/http_io_helpers.rb
+++ b/spec/support/http_io/http_io_helpers.rb
@@ -44,10 +44,11 @@ module HttpIOHelpers
def remote_trace_body
@remote_trace_body ||= File.read(expand_fixture_path('trace/sample_trace'))
+ .force_encoding(Encoding::BINARY)
end
def remote_trace_size
- remote_trace_body.length
+ remote_trace_body.bytesize
end
def set_smaller_buffer_size_than(file_size)
diff --git a/spec/support/issuables_list_metadata_shared_examples.rb b/spec/support/issuables_list_metadata_shared_examples.rb
deleted file mode 100644
index e61983c60b4..00000000000
--- a/spec/support/issuables_list_metadata_shared_examples.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
- before do
- @issuable_ids = []
-
- %w[fix improve/awesome].each do |source_branch|
- issuable =
- if issuable_type == :issue
- create(issuable_type, project: project, author: project.creator)
- else
- create(issuable_type, source_project: project, source_branch: source_branch, author: project.creator)
- end
-
- @issuable_ids << issuable.id
- end
- end
-
- it "creates indexed meta-data object for issuable notes and votes count" do
- if action
- get action, author_id: project.creator.id
- else
- get :index, namespace_id: project.namespace, project_id: project
- end
-
- meta_data = assigns(:issuable_meta_data)
-
- aggregate_failures do
- expect(meta_data.keys).to match_array(@issuable_ids)
- expect(meta_data.values).to all(be_kind_of(Issuable::IssuableMeta))
- end
- end
-
- describe "when given empty collection" do
- let(:project2) { create(:project, :public) }
-
- it "doesn't execute any queries with false conditions" do
- get_action =
- if action
- proc { get action, author_id: project.creator.id }
- else
- proc { get :index, namespace_id: project2.namespace, project_id: project2 }
- end
-
- expect(&get_action).not_to make_queries_matching(/WHERE (?:1=0|0=1)/)
- end
- end
-end
diff --git a/spec/support/json_response_helpers.rb b/spec/support/json_response.rb
index aa235529c56..210b0e6d867 100644
--- a/spec/support/json_response_helpers.rb
+++ b/spec/support/json_response.rb
@@ -1,7 +1,3 @@
-shared_context 'JSON response' do
- let(:json_response) { JSON.parse(response.body) }
-end
-
RSpec.configure do |config|
config.include_context 'JSON response'
config.include_context 'JSON response', type: :request
diff --git a/spec/support/background_migrations_matchers.rb b/spec/support/matchers/background_migrations_matchers.rb
index f4127efc6ae..f4127efc6ae 100644
--- a/spec/support/background_migrations_matchers.rb
+++ b/spec/support/matchers/background_migrations_matchers.rb
diff --git a/spec/support/query_recorder.rb b/spec/support/matchers/exceed_query_limit.rb
index 8cf8f45a8b2..88d22a3ddd9 100644
--- a/spec/support/query_recorder.rb
+++ b/spec/support/matchers/exceed_query_limit.rb
@@ -1,42 +1,3 @@
-module ActiveRecord
- class QueryRecorder
- attr_reader :log, :cached
-
- def initialize(&block)
- @log = []
- @cached = []
- 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")
- @log << values[:sql]
- end
- end
-
- def count
- @log.count
- end
-
- def cached_count
- @cached.count
- end
-
- def log_message
- @log.join("\n\n")
- end
- end
-end
-
RSpec::Matchers.define :exceed_query_limit do |expected|
supports_block_expectations
diff --git a/spec/support/matchers/have_emoji.rb b/spec/support/matchers/have_emoji.rb
new file mode 100644
index 00000000000..23fb8e9c1c4
--- /dev/null
+++ b/spec/support/matchers/have_emoji.rb
@@ -0,0 +1,5 @@
+RSpec::Matchers.define :have_emoji do |emoji_name|
+ match do |actual|
+ expect(actual).to have_selector("gl-emoji[data-name='#{emoji_name}']")
+ end
+end
diff --git a/spec/support/prepare-gitlab-git-test-for-commit b/spec/support/prepare-gitlab-git-test-for-commit
index 3047786a599..d08e3ba5481 100755
--- a/spec/support/prepare-gitlab-git-test-for-commit
+++ b/spec/support/prepare-gitlab-git-test-for-commit
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
abort unless [
- system('spec/support/generate-seed-repo-rb', out: 'spec/support/seed_repo.rb'),
+ system('spec/support/generate-seed-repo-rb', out: 'spec/support/helpers/seed_repo.rb'),
system('spec/support/unpack-gitlab-git-test')
].all?
diff --git a/spec/support/routing_helpers.rb b/spec/support/routing_helpers.rb
deleted file mode 100644
index af1f4760804..00000000000
--- a/spec/support/routing_helpers.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-RSpec.configure do |config|
- config.include GitlabRoutingHelper
-end
diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb
new file mode 100644
index 00000000000..dffab22d8b5
--- /dev/null
+++ b/spec/support/rspec.rb
@@ -0,0 +1,12 @@
+require_relative "helpers/stub_configuration"
+require_relative "helpers/stub_object_storage"
+require_relative "helpers/stub_env"
+
+RSpec.configure do |config|
+ config.mock_with :rspec
+ config.raise_errors_for_deprecations!
+
+ config.include StubConfiguration
+ config.include StubObjectStorage
+ config.include StubENV
+end
diff --git a/spec/support/seed.rb b/spec/support/seed.rb
new file mode 100644
index 00000000000..bea2e9c3044
--- /dev/null
+++ b/spec/support/seed.rb
@@ -0,0 +1,7 @@
+RSpec.configure do |config|
+ config.include SeedHelper, :seed_helper
+
+ config.before(:all, :seed_helper) do
+ ensure_seeds
+ end
+end
diff --git a/spec/support/shared_contexts/json_response_shared_context.rb b/spec/support/shared_contexts/json_response_shared_context.rb
new file mode 100644
index 00000000000..df5fc288089
--- /dev/null
+++ b/spec/support/shared_contexts/json_response_shared_context.rb
@@ -0,0 +1,3 @@
+shared_context 'JSON response' do
+ let(:json_response) { JSON.parse(response.body) }
+end
diff --git a/spec/support/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb
index 23f9b46ae0c..23f9b46ae0c 100644
--- a/spec/support/services_shared_context.rb
+++ b/spec/support/shared_contexts/services_shared_context.rb
diff --git a/spec/support/chat_slash_commands_shared_examples.rb b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb
index dc97a39f051..dc97a39f051 100644
--- a/spec/support/chat_slash_commands_shared_examples.rb
+++ b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb
diff --git a/spec/support/email_format_shared_examples.rb b/spec/support/shared_examples/email_format_shared_examples.rb
index b924a208e71..b924a208e71 100644
--- a/spec/support/email_format_shared_examples.rb
+++ b/spec/support/shared_examples/email_format_shared_examples.rb
diff --git a/spec/support/gitlab_verify.rb b/spec/support/shared_examples/gitlab_verify.rb
index 13e2e37624d..560913ca92f 100644
--- a/spec/support/gitlab_verify.rb
+++ b/spec/support/shared_examples/gitlab_verify.rb
@@ -17,29 +17,3 @@ RSpec.shared_examples 'Gitlab::Verify::BatchVerifier subclass' do
end
end
end
-
-module GitlabVerifyHelpers
- def collect_ranges(args = {})
- verifier = described_class.new(args.merge(batch_size: 1))
-
- collect_results(verifier).map { |range, _| range }
- end
-
- def collect_failures
- verifier = described_class.new(batch_size: 1)
-
- out = {}
-
- collect_results(verifier).map { |_, failures| out.merge!(failures) }
-
- out
- end
-
- def collect_results(verifier)
- out = []
-
- verifier.run_batches { |*args| out << args }
-
- out
- end
-end
diff --git a/spec/support/group_members_shared_example.rb b/spec/support/shared_examples/group_members_shared_example.rb
index 547c83c7955..547c83c7955 100644
--- a/spec/support/group_members_shared_example.rb
+++ b/spec/support/shared_examples/group_members_shared_example.rb
diff --git a/spec/support/issuable_shared_examples.rb b/spec/support/shared_examples/issuable_shared_examples.rb
index 42f3b4db23c..42f3b4db23c 100644
--- a/spec/support/issuable_shared_examples.rb
+++ b/spec/support/shared_examples/issuable_shared_examples.rb
diff --git a/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb b/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb
new file mode 100644
index 00000000000..f4bc6f8efa5
--- /dev/null
+++ b/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb
@@ -0,0 +1,62 @@
+shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
+ include ProjectForksHelper
+
+ def get_action(action, project)
+ if action
+ get action, author_id: project.creator.id
+ else
+ get :index, namespace_id: project.namespace, project_id: project
+ end
+ end
+
+ def create_issuable(issuable_type, project, source_branch:)
+ if issuable_type == :issue
+ create(issuable_type, project: project, author: project.creator)
+ else
+ create(issuable_type, source_project: project, source_branch: source_branch, author: project.creator)
+ end
+ end
+
+ before do
+ @issuable_ids = %w[fix improve/awesome].map do |source_branch|
+ create_issuable(issuable_type, project, source_branch: source_branch).id
+ end
+ end
+
+ it "creates indexed meta-data object for issuable notes and votes count" do
+ get_action(action, project)
+
+ meta_data = assigns(:issuable_meta_data)
+
+ aggregate_failures do
+ expect(meta_data.keys).to match_array(@issuable_ids)
+ expect(meta_data.values).to all(be_kind_of(Issuable::IssuableMeta))
+ end
+ end
+
+ it "avoids N+1 queries" do
+ control = ActiveRecord::QueryRecorder.new { get_action(action, project) }
+ issuable = create_issuable(issuable_type, project, source_branch: 'csv')
+
+ if issuable_type == :merge_request
+ issuable.update!(source_project: fork_project(project))
+ end
+
+ expect { get_action(action, project) }.not_to exceed_query_limit(control.count)
+ end
+
+ describe "when given empty collection" do
+ let(:project2) { create(:project, :public) }
+
+ it "doesn't execute any queries with false conditions" do
+ get_empty =
+ if action
+ proc { get action, author_id: project.creator.id }
+ else
+ proc { get :index, namespace_id: project2.namespace, project_id: project2 }
+ end
+
+ expect(&get_empty).not_to make_queries_matching(/WHERE (?:1=0|0=1)/)
+ end
+ end
+end
diff --git a/spec/support/issue_tracker_service_shared_example.rb b/spec/support/shared_examples/issue_tracker_service_shared_example.rb
index a6ab03cb808..a6ab03cb808 100644
--- a/spec/support/issue_tracker_service_shared_example.rb
+++ b/spec/support/shared_examples/issue_tracker_service_shared_example.rb
diff --git a/spec/support/ldap_shared_examples.rb b/spec/support/shared_examples/ldap_shared_examples.rb
index 52c34e78965..52c34e78965 100644
--- a/spec/support/ldap_shared_examples.rb
+++ b/spec/support/shared_examples/ldap_shared_examples.rb
diff --git a/spec/support/legacy_path_redirect_shared_examples.rb b/spec/support/shared_examples/legacy_path_redirect_shared_examples.rb
index f300bdd48b1..f300bdd48b1 100644
--- a/spec/support/legacy_path_redirect_shared_examples.rb
+++ b/spec/support/shared_examples/legacy_path_redirect_shared_examples.rb
diff --git a/spec/support/malicious_regexp_shared_examples.rb b/spec/support/shared_examples/malicious_regexp_shared_examples.rb
index ac5d22298bb..ac5d22298bb 100644
--- a/spec/support/malicious_regexp_shared_examples.rb
+++ b/spec/support/shared_examples/malicious_regexp_shared_examples.rb
diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/shared_examples/mentionable_shared_examples.rb
index 1685decbe94..1685decbe94 100644
--- a/spec/support/mentionable_shared_examples.rb
+++ b/spec/support/shared_examples/mentionable_shared_examples.rb
diff --git a/spec/support/milestone_tabs_examples.rb b/spec/support/shared_examples/milestone_tabs_examples.rb
index 70b499198bf..70b499198bf 100644
--- a/spec/support/milestone_tabs_examples.rb
+++ b/spec/support/shared_examples/milestone_tabs_examples.rb
diff --git a/spec/support/shared_examples/models/atomic_internal_id_spec.rb b/spec/support/shared_examples/models/atomic_internal_id_spec.rb
index 144af4fc475..6a6e13418a9 100644
--- a/spec/support/shared_examples/models/atomic_internal_id_spec.rb
+++ b/spec/support/shared_examples/models/atomic_internal_id_spec.rb
@@ -19,6 +19,14 @@ shared_examples_for 'AtomicInternalId' do
it { is_expected.to validate_numericality_of(internal_id_attribute) }
end
+ describe 'Creating an instance' do
+ subject { instance.save! }
+
+ it 'saves a new instance properly' do
+ expect { subject }.not_to raise_error
+ end
+ end
+
describe 'internal id generation' do
subject { instance.save! }
diff --git a/spec/support/notify_shared_examples.rb b/spec/support/shared_examples/notify_shared_examples.rb
index e2c23607406..e2c23607406 100644
--- a/spec/support/notify_shared_examples.rb
+++ b/spec/support/shared_examples/notify_shared_examples.rb
diff --git a/spec/support/reference_parser_shared_examples.rb b/spec/support/shared_examples/reference_parser_shared_examples.rb
index baf8bcc04b8..baf8bcc04b8 100644
--- a/spec/support/reference_parser_shared_examples.rb
+++ b/spec/support/shared_examples/reference_parser_shared_examples.rb
diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
index 07bc3a51fd8..07bc3a51fd8 100644
--- a/spec/support/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
diff --git a/spec/support/snippet_visibility.rb b/spec/support/shared_examples/snippet_visibility.rb
index 3a7c69b7877..3a7c69b7877 100644
--- a/spec/support/snippet_visibility.rb
+++ b/spec/support/shared_examples/snippet_visibility.rb
diff --git a/spec/support/snippets_shared_examples.rb b/spec/support/shared_examples/snippets_shared_examples.rb
index 85f0facd5c3..85f0facd5c3 100644
--- a/spec/support/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/snippets_shared_examples.rb
diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/shared_examples/taskable_shared_examples.rb
index 4056ff06b84..4056ff06b84 100644
--- a/spec/support/taskable_shared_examples.rb
+++ b/spec/support/shared_examples/taskable_shared_examples.rb
diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/shared_examples/time_tracking_shared_examples.rb
index 909d4e2ee8d..909d4e2ee8d 100644
--- a/spec/support/time_tracking_shared_examples.rb
+++ b/spec/support/shared_examples/time_tracking_shared_examples.rb
diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/shared_examples/unique_ip_check_shared_examples.rb
index e5c8ac6a004..e5c8ac6a004 100644
--- a/spec/support/unique_ip_check_shared_examples.rb
+++ b/spec/support/shared_examples/unique_ip_check_shared_examples.rb
diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/shared_examples/update_invalid_issuable.rb
index 1490287681b..1490287681b 100644
--- a/spec/support/update_invalid_issuable.rb
+++ b/spec/support/shared_examples/update_invalid_issuable.rb
diff --git a/spec/support/updating_mentions_shared_examples.rb b/spec/support/shared_examples/updating_mentions_shared_examples.rb
index 5e3f19ba19e..5e3f19ba19e 100644
--- a/spec/support/updating_mentions_shared_examples.rb
+++ b/spec/support/shared_examples/updating_mentions_shared_examples.rb
diff --git a/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb b/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb
index 934d53e7bba..93c21a99e59 100644
--- a/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb
+++ b/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb
@@ -4,7 +4,7 @@ shared_examples "matches the method pattern" do |method|
let(:pattern) { patterns[method] }
it do
- return skip "No pattern provided, skipping." unless pattern
+ skip "No pattern provided, skipping." unless pattern
expect(target.method(method).call(*args)).to match(pattern)
end
diff --git a/spec/tasks/cache/clear/redis_spec.rb b/spec/tasks/cache/clear/redis_spec.rb
new file mode 100644
index 00000000000..cca2b864e9b
--- /dev/null
+++ b/spec/tasks/cache/clear/redis_spec.rb
@@ -0,0 +1,19 @@
+require 'rake_helper'
+
+describe 'clearing redis cache' do
+ before do
+ Rake.application.rake_require 'tasks/cache'
+ end
+
+ describe 'clearing pipeline status cache' do
+ let(:pipeline_status) { create(:ci_pipeline).project.pipeline_status }
+
+ before do
+ allow(pipeline_status).to receive(:loaded).and_return(nil)
+ end
+
+ it 'clears pipeline status cache' do
+ expect { run_rake_task('cache:clear:redis') }.to change { pipeline_status.has_cache? }
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 0d24782f317..a2e5642a72c 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -195,15 +195,12 @@ describe 'gitlab:app namespace rake task' do
end
context 'multiple repository storages' do
- let(:storage_default) do
- Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/default_storage'))
- end
let(:test_second_storage) do
Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/custom_storage'))
end
let(:storages) do
{
- 'default' => storage_default,
+ 'default' => Gitlab.config.repositories.storages.default,
'test_second_storage' => test_second_storage
}
end
@@ -215,8 +212,7 @@ describe 'gitlab:app namespace rake task' do
before do
# We only need a backup of the repositories for this test
stub_env('SKIP', 'db,uploads,builds,artifacts,lfs,registry')
- FileUtils.mkdir(Settings.absolute('tmp/tests/default_storage'))
- FileUtils.mkdir(Settings.absolute('tmp/tests/custom_storage'))
+
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
# Avoid asking gitaly about the root ref (which will fail beacuse of the
@@ -225,14 +221,23 @@ describe 'gitlab:app namespace rake task' do
end
after do
- FileUtils.rm_rf(Settings.absolute('tmp/tests/default_storage'))
FileUtils.rm_rf(Settings.absolute('tmp/tests/custom_storage'))
end
it 'includes repositories in all repository storages' do
- project_a = create(:project, :repository, repository_storage: 'default')
+ project_a = create(:project, :repository)
project_b = create(:project, :repository, repository_storage: 'test_second_storage')
+ b_storage_dir = File.join(Settings.absolute('tmp/tests/custom_storage'), File.dirname(project_b.disk_path))
+
+ FileUtils.mkdir_p(b_storage_dir)
+
+ # Even when overriding the storage, we have to move it there, so it exists
+ FileUtils.mv(
+ File.join(Settings.absolute(storages['default'].legacy_disk_path), project_b.repository.disk_path + '.git'),
+ Rails.root.join(storages['test_second_storage'].legacy_disk_path, project_b.repository.disk_path + '.git')
+ )
+
expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
tar_contents, exit_status = Gitlab::Popen.popen(
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index 16455e2517b..e7277b337f6 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -75,36 +75,8 @@ describe ObjectStorage do
expect(object).to receive(:file_store).and_return(nil)
end
- context 'when object storage is enabled' do
- context 'when direct uploads are enabled' do
- before do
- stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: true)
- end
-
- it "uses Store::REMOTE" do
- is_expected.to eq(described_class::Store::REMOTE)
- end
- end
-
- context 'when direct uploads are disabled' do
- before do
- stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: false)
- end
-
- it "uses Store::LOCAL" do
- is_expected.to eq(described_class::Store::LOCAL)
- end
- end
- end
-
- context 'when object storage is disabled' do
- before do
- stub_uploads_object_storage(uploader_class, enabled: false)
- end
-
- it "uses Store::LOCAL" do
- is_expected.to eq(described_class::Store::LOCAL)
- end
+ it "uses Store::LOCAL" do
+ is_expected.to eq(described_class::Store::LOCAL)
end
end
@@ -537,6 +509,72 @@ describe ObjectStorage do
end
end
+ context 'when local file is used' do
+ let(:temp_file) { Tempfile.new("test") }
+
+ before do
+ FileUtils.touch(temp_file)
+ end
+
+ after do
+ FileUtils.rm_f(temp_file)
+ end
+
+ context 'when valid file is used' do
+ context 'when valid file is specified' do
+ let(:uploaded_file) { temp_file }
+
+ context 'when object storage and direct upload is specified' do
+ before do
+ stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: true)
+ end
+
+ context 'when file is stored' do
+ subject do
+ uploader.store!(uploaded_file)
+ end
+
+ it 'file to be remotely stored in permament location' do
+ subject
+
+ expect(uploader).to be_exists
+ expect(uploader).not_to be_cached
+ expect(uploader).not_to be_file_storage
+ expect(uploader.path).not_to be_nil
+ expect(uploader.path).not_to include('tmp/upload')
+ expect(uploader.path).not_to include('tmp/cache')
+ expect(uploader.object_store).to eq(described_class::Store::REMOTE)
+ end
+ end
+ end
+
+ context 'when object storage and direct upload is not used' do
+ before do
+ stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: false)
+ end
+
+ context 'when file is stored' do
+ subject do
+ uploader.store!(uploaded_file)
+ end
+
+ it 'file to be remotely stored in permament location' do
+ subject
+
+ expect(uploader).to be_exists
+ expect(uploader).not_to be_cached
+ expect(uploader).to be_file_storage
+ expect(uploader.path).not_to be_nil
+ expect(uploader.path).not_to include('tmp/upload')
+ expect(uploader.path).not_to include('tmp/cache')
+ expect(uploader.object_store).to eq(described_class::Store::LOCAL)
+ end
+ end
+ end
+ end
+ end
+ end
+
context 'when remote file is used' do
let(:temp_file) { Tempfile.new("test") }
@@ -590,9 +628,9 @@ describe ObjectStorage do
expect(uploader).to be_exists
expect(uploader).to be_cached
+ expect(uploader).not_to be_file_storage
expect(uploader.path).not_to be_nil
expect(uploader.path).not_to include('tmp/cache')
- expect(uploader.url).not_to be_nil
expect(uploader.path).not_to include('tmp/cache')
expect(uploader.object_store).to eq(described_class::Store::REMOTE)
end
@@ -607,6 +645,7 @@ describe ObjectStorage do
expect(uploader).to be_exists
expect(uploader).not_to be_cached
+ expect(uploader).not_to be_file_storage
expect(uploader.path).not_to be_nil
expect(uploader.path).not_to include('tmp/upload')
expect(uploader.path).not_to include('tmp/cache')
diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb
index b4359d819a0..099baacf019 100644
--- a/spec/views/admin/dashboard/index.html.haml_spec.rb
+++ b/spec/views/admin/dashboard/index.html.haml_spec.rb
@@ -18,4 +18,10 @@ describe 'admin/dashboard/index.html.haml' do
expect(rendered).to have_content 'GitLab Workhorse'
expect(rendered).to have_content Gitlab::Workhorse.version
end
+
+ it "includes revision of GitLab" do
+ render
+
+ expect(rendered).to have_content "#{Gitlab::VERSION} (#{Gitlab::REVISION})"
+ end
end
diff --git a/spec/views/projects/buttons/_dropdown.html.haml_spec.rb b/spec/views/projects/buttons/_dropdown.html.haml_spec.rb
index d0e692635b9..8b9aab30286 100644
--- a/spec/views/projects/buttons/_dropdown.html.haml_spec.rb
+++ b/spec/views/projects/buttons/_dropdown.html.haml_spec.rb
@@ -8,7 +8,8 @@ describe 'projects/buttons/_dropdown' do
assign(:project, project)
allow(view).to receive(:current_user).and_return(user)
- allow(view).to receive(:can?).and_return(true)
+ allow(view).to receive(:can?).with(user, :push_code, project).and_return(true)
+ allow(view).to receive(:can_collaborate_with_project?).and_return(true)
end
context 'empty repository' do
diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
index 448b925cf34..2fdd28a3be4 100644
--- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -7,6 +7,7 @@ describe 'projects/commit/_commit_box.html.haml' do
before do
assign(:project, project)
assign(:commit, project.commit)
+ allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:can_collaborate_with_project?).and_return(false)
end
@@ -47,7 +48,8 @@ describe 'projects/commit/_commit_box.html.haml' do
context 'viewing a commit' do
context 'as a developer' do
before do
- expect(view).to receive(:can_collaborate_with_project?).and_return(true)
+ project.add_developer(user)
+ allow(view).to receive(:can_collaborate_with_project?).and_return(true)
end
it 'has a link to create a new tag' do
@@ -58,10 +60,6 @@ describe 'projects/commit/_commit_box.html.haml' do
end
context 'as a non-developer' do
- before do
- expect(view).to receive(:can_collaborate_with_project?).and_return(false)
- end
-
it 'does not have a link to create a new tag' do
render
diff --git a/spec/views/projects/settings/ci_cd/_form.html.haml_spec.rb b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
index be9a4d9c57c..d15391911c1 100644
--- a/spec/views/projects/settings/ci_cd/_form.html.haml_spec.rb
+++ b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'projects/settings/ci_cd/_form' do
+describe 'projects/settings/ci_cd/_autodevops_form' do
let(:project) { create(:project, :repository) }
before do
diff --git a/spec/views/shared/milestones/_top.html.haml.rb b/spec/views/shared/milestones/_top.html.haml.rb
new file mode 100644
index 00000000000..516d81c87ac
--- /dev/null
+++ b/spec/views/shared/milestones/_top.html.haml.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe 'shared/milestones/_top.html.haml' do
+ set(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+ let(:milestone) { create(:milestone, project: project) }
+
+ before do
+ allow(milestone).to receive(:milestones) { [] }
+ end
+
+ it 'renders a deprecation message for a legacy milestone' do
+ allow(milestone).to receive(:legacy_group_milestone?) { true }
+
+ render 'shared/milestones/top', milestone: milestone
+
+ expect(rendered).to have_css('.milestone-deprecation-message')
+ end
+
+ it 'renders a deprecation message for a dashboard milestone' do
+ allow(milestone).to receive(:dashboard_milestone?) { true }
+
+ render 'shared/milestones/top', milestone: milestone
+
+ expect(rendered).to have_css('.milestone-deprecation-message')
+ end
+
+ it 'does not render a deprecation message for a non-legacy and non-dashboard milestone' do
+ assign :group, group
+
+ render 'shared/milestones/top', milestone: milestone
+
+ expect(rendered).not_to have_css('.milestone-deprecation-message')
+ end
+end
diff --git a/spec/workers/issue_due_scheduler_worker_spec.rb b/spec/workers/issue_due_scheduler_worker_spec.rb
new file mode 100644
index 00000000000..2710267d384
--- /dev/null
+++ b/spec/workers/issue_due_scheduler_worker_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe IssueDueSchedulerWorker do
+ describe '#perform' do
+ it 'schedules one MailScheduler::IssueDueWorker per project with open issues due tomorrow' do
+ project1 = create(:project)
+ project2 = create(:project)
+ project_closed_issue = create(:project)
+ project_issue_due_another_day = create(:project)
+
+ create(:issue, :opened, project: project1, due_date: Date.tomorrow)
+ create(:issue, :opened, project: project1, due_date: Date.tomorrow)
+ create(:issue, :opened, project: project2, due_date: Date.tomorrow)
+ create(:issue, :closed, project: project_closed_issue, due_date: Date.tomorrow)
+ create(:issue, :opened, project: project_issue_due_another_day, due_date: Date.today)
+
+ expect(MailScheduler::IssueDueWorker).to receive(:bulk_perform_async) do |args|
+ expect(args).to match_array([[project1.id], [project2.id]])
+ end
+
+ described_class.new.perform
+ end
+ end
+end
diff --git a/spec/workers/mail_scheduler/issue_due_worker_spec.rb b/spec/workers/mail_scheduler/issue_due_worker_spec.rb
new file mode 100644
index 00000000000..48ac1b8a1a4
--- /dev/null
+++ b/spec/workers/mail_scheduler/issue_due_worker_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe MailScheduler::IssueDueWorker do
+ describe '#perform' do
+ let(:worker) { described_class.new }
+ let(:project) { create(:project) }
+
+ it 'sends emails for open issues due tomorrow in the project specified' do
+ issue1 = create(:issue, :opened, project: project, due_date: Date.tomorrow)
+ issue2 = create(:issue, :opened, project: project, due_date: Date.tomorrow)
+ create(:issue, :closed, project: project, due_date: Date.tomorrow) # closed
+ create(:issue, :opened, project: project, due_date: 2.days.from_now) # due on another day
+ create(:issue, :opened, due_date: Date.tomorrow) # different project
+
+ expect_any_instance_of(NotificationService).to receive(:issue_due).with(issue1)
+ expect_any_instance_of(NotificationService).to receive(:issue_due).with(issue2)
+
+ worker.perform(project.id)
+ end
+ end
+end
diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
index d57137223ed..39b6783cef8 100644
--- a/vendor/gitignore/Android.gitignore
+++ b/vendor/gitignore/Android.gitignore
@@ -37,8 +37,10 @@ captures/
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
+.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
+.idea/caches
# Keystore files
# Uncomment the following line if you do not want to check your keystore files in.
diff --git a/vendor/gitignore/Elixir.gitignore b/vendor/gitignore/Elixir.gitignore
index b6d65867dac..86e4c3f3905 100644
--- a/vendor/gitignore/Elixir.gitignore
+++ b/vendor/gitignore/Elixir.gitignore
@@ -6,3 +6,4 @@
erl_crash.dump
*.ez
*.beam
+/config/*.secret.exs
diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore
index 9c01e12b050..a83a428c844 100644
--- a/vendor/gitignore/Global/JetBrains.gitignore
+++ b/vendor/gitignore/Global/JetBrains.gitignore
@@ -1,12 +1,12 @@
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
-# User-specific stuff:
+# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
-# Sensitive or high-churn files:
+# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
@@ -14,7 +14,7 @@
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
-# Gradle:
+# Gradle
.idea/**/gradle.xml
.idea/**/libraries
@@ -22,14 +22,12 @@
cmake-build-debug/
cmake-build-release/
-# Mongo Explorer plugin:
+# Mongo Explorer plugin
.idea/**/mongoSettings.xml
-## File-based project format:
+# File-based project format
*.iws
-## Plugin-specific files:
-
# IntelliJ
out/
@@ -47,3 +45,6 @@ com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
diff --git a/vendor/gitignore/Global/Windows.gitignore b/vendor/gitignore/Global/Windows.gitignore
index 846a1db836c..0251dd21ad8 100644
--- a/vendor/gitignore/Global/Windows.gitignore
+++ b/vendor/gitignore/Global/Windows.gitignore
@@ -15,6 +15,7 @@ $RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
+*.msix
*.msm
*.msp
diff --git a/vendor/gitignore/Godot.gitignore b/vendor/gitignore/Godot.gitignore
new file mode 100644
index 00000000000..ba45ca4582e
--- /dev/null
+++ b/vendor/gitignore/Godot.gitignore
@@ -0,0 +1,8 @@
+
+# Godot-specific ignores
+.import/
+export.cfg
+export_presets.cfg
+
+# Mono-specific ignores
+.mono/
diff --git a/vendor/gitignore/Joomla.gitignore b/vendor/gitignore/Joomla.gitignore
index b6bf3a9c96a..378c158bddf 100644
--- a/vendor/gitignore/Joomla.gitignore
+++ b/vendor/gitignore/Joomla.gitignore
@@ -1,4 +1,3 @@
-/.gitignore
/.htaccess
/administrator/cache/*
/administrator/components/com_admin/*
diff --git a/vendor/gitignore/KiCad.gitignore b/vendor/gitignore/KiCad.gitignore
index 208bc4fc591..198392e551e 100644
--- a/vendor/gitignore/KiCad.gitignore
+++ b/vendor/gitignore/KiCad.gitignore
@@ -1,4 +1,5 @@
# For PCBs designed using KiCad: http://www.kicad-pcb.org/
+# Format documentation: http://kicad-pcb.org/help/file-formats/
# Temporary files
*.000
@@ -8,6 +9,10 @@
*~
_autosave-*
*.tmp
+*-cache.lib
+*-rescue.lib
+*-save.pro
+*-save.kicad_pcb
# Netlist files (exported from Eeschema)
*.net
diff --git a/vendor/gitignore/Leiningen.gitignore b/vendor/gitignore/Leiningen.gitignore
index a9fe6fba80d..a4cb69a32cc 100644
--- a/vendor/gitignore/Leiningen.gitignore
+++ b/vendor/gitignore/Leiningen.gitignore
@@ -11,3 +11,4 @@ pom.xml.asc
.lein-plugins/
.lein-failures
.nrepl-port
+.cpcache/
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
index d1bed128fa8..ad46b30886f 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -36,7 +36,7 @@ build/Release
node_modules/
jspm_packages/
-# Typescript v1 declaration files
+# TypeScript v1 declaration files
typings/
# Optional npm cache directory
diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore
index b989be6ca15..894a44cc066 100644
--- a/vendor/gitignore/Python.gitignore
+++ b/vendor/gitignore/Python.gitignore
@@ -53,9 +53,8 @@ coverage.xml
# Django stuff:
*.log
-.static_storage/
-.media/
local_settings.py
+db.sqlite3
# Flask stuff:
instance/
diff --git a/vendor/gitignore/Rails.gitignore b/vendor/gitignore/Rails.gitignore
index 828ab1d556a..e62f78e17bc 100644
--- a/vendor/gitignore/Rails.gitignore
+++ b/vendor/gitignore/Rails.gitignore
@@ -14,6 +14,7 @@ pickle-email-*.html
# TODO Comment out this rule if you are OK with secrets being uploaded to the repo
config/initializers/secret_token.rb
+config/master.key
# Only include if you have production secrets in this file, which is no longer a Rails default
# config/secrets.yml
diff --git a/vendor/gitignore/Rust.gitignore b/vendor/gitignore/Rust.gitignore
index 50281a44270..088ba6ba7d3 100644
--- a/vendor/gitignore/Rust.gitignore
+++ b/vendor/gitignore/Rust.gitignore
@@ -3,7 +3,7 @@
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
-# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
+# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index 78c1c5cd26e..c560658e45c 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -153,7 +153,9 @@ _minted*
*.mw
# nomencl
+*.nlg
*.nlo
+*.nls
# pax
*.pax
diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore
index 75e5b1405da..a7c0c70a0b4 100644
--- a/vendor/gitignore/Unity.gitignore
+++ b/vendor/gitignore/Unity.gitignore
@@ -5,7 +5,7 @@
[Bb]uilds/
Assets/AssetStoreTools*
-# Visual Studio 2015 cache directory
+# Visual Studio cache directory
/.vs/
# Autogenerated VS/MD/Consulo solution and project files
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index 8e930f59c47..29063cf6072 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -64,8 +64,10 @@ StyleCopReport.xml
*.ilk
*.meta
*.obj
+*.iobj
*.pch
*.pdb
+*.ipdb
*.pgc
*.pgd
*.rsp
@@ -248,6 +250,7 @@ ServiceFabricBackup/
*.rdl.data
*.bim.layout
*.bim_*.settings
+*.rptproj.rsuser
# Microsoft Fakes
FakesAssemblies/
@@ -319,3 +322,8 @@ ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
index 4223dc18933..3b77055b644 100644
--- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
@@ -50,9 +50,9 @@ stages:
build:
stage: build
- image: docker:git
+ image: docker:stable-git
services:
- - docker:dind
+ - docker:stable-dind
variables:
DOCKER_DRIVER: overlay2
script:
@@ -76,12 +76,12 @@ test:
- branches
codequality:
- image: docker:latest
+ image: docker:stable
variables:
DOCKER_DRIVER: overlay2
allow_failure: true
services:
- - docker:dind
+ - docker:stable-dind
script:
- setup_docker
- codeclimate
@@ -90,12 +90,12 @@ codequality:
performance:
stage: performance
- image: docker:latest
+ image: docker:stable
variables:
DOCKER_DRIVER: overlay2
allow_failure: true
services:
- - docker:dind
+ - docker:stable-dind
script:
- setup_docker
- performance
@@ -109,25 +109,37 @@ performance:
kubernetes: active
sast:
- image: docker:latest
+ image: docker:stable
variables:
DOCKER_DRIVER: overlay2
allow_failure: true
services:
- - docker:dind
+ - docker:stable-dind
script:
- setup_docker
- sast
artifacts:
paths: [gl-sast-report.json]
+dependency_scanning:
+ image: docker:stable
+ variables:
+ DOCKER_DRIVER: overlay2
+ allow_failure: true
+ services:
+ - docker:stable-dind
+ script:
+ - setup_docker
+ - dependency_scanning
+ artifacts:
+ paths: [gl-dependency-scanning-report.json]
sast:container:
- image: docker:latest
+ image: docker:stable
variables:
DOCKER_DRIVER: overlay2
allow_failure: true
services:
- - docker:dind
+ - docker:stable-dind
script:
- setup_docker
- sast_container
@@ -303,6 +315,9 @@ production:
mv clair-scanner_linux_amd64 clair-scanner
chmod +x clair-scanner
touch clair-whitelist.yml
+ retries=0
+ echo "Waiting for clair daemon to start"
+ while( ! wget -T 10 -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done
./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-sast-container-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true
}
@@ -324,7 +339,6 @@ production:
fi
docker run --env SAST_CONFIDENCE_LEVEL="${SAST_CONFIDENCE_LEVEL:-3}" \
- --env SAST_DISABLE_REMOTE_CHECKS="${SAST_DISABLE_REMOTE_CHECKS:-false}" \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/sast:$SP_VERSION" /app/bin/run /code
@@ -335,6 +349,20 @@ production:
esac
}
+ function dependency_scanning() {
+ case "$CI_SERVER_VERSION" in
+ *-ee)
+ docker run --env DEP_SCAN_DISABLE_REMOTE_CHECKS="${DEP_SCAN_DISABLE_REMOTE_CHECKS:-false}" \
+ --volume "$PWD:/code" \
+ --volume /var/run/docker.sock:/var/run/docker.sock \
+ "registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$SP_VERSION" /code
+ ;;
+ *)
+ echo "GitLab EE is required"
+ ;;
+ esac
+ }
+
function deploy() {
track="${1-stable}"
name="$CI_ENVIRONMENT_SLUG"
@@ -355,10 +383,16 @@ production:
if [[ "$track" == "stable" ]]; then
# for stable track get number of replicas from `PRODUCTION_REPLICAS`
eval new_replicas=\$${env_slug}_REPLICAS
+ if [[ -z "$new_replicas" ]]; then
+ new_replicas=$REPLICAS
+ fi
service_enabled="true"
else
# for all tracks get number of replicas from `CANARY_PRODUCTION_REPLICAS`
eval new_replicas=\$${env_track}_${env_slug}_REPLICAS
+ if [[ -z "$new_replicas" ]]; then
+ eval new_replicas=\${env_track}_REPLICAS
+ fi
fi
if [[ -n "$new_replicas" ]]; then
replicas="$new_replicas"
diff --git a/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml b/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml
index 4d5b6484d6e..ff7c87c29f0 100644
--- a/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml
@@ -7,7 +7,7 @@
image: "chef/chefdk"
services:
- - docker:dind
+ - docker:stable-dind
variables:
DOCKER_HOST: "tcp://docker:2375"
diff --git a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
index eeefadaa019..58d48d1284b 100644
--- a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
@@ -2,7 +2,7 @@
image: docker:latest
services:
- - docker:dind
+ - docker:stable-dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
diff --git a/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml b/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml
index 0ad662cf704..0688f77a1d2 100644
--- a/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml
@@ -32,7 +32,7 @@ before_script:
- apt-get install git nodejs libcurl4-gnutls-dev libicu-dev libmcrypt-dev libvpx-dev libjpeg-dev libpng-dev libxpm-dev zlib1g-dev libfreetype6-dev libxml2-dev libexpat1-dev libbz2-dev libgmp3-dev libldap2-dev unixodbc-dev libpq-dev libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev -yqq
# Install php extensions
- - docker-php-ext-install mbstring mcrypt pdo_mysql curl json intl gd xml zip bz2 opcache
+ - docker-php-ext-install mbstring pdo_mysql curl json intl gd xml zip bz2 opcache
# Install & enable Xdebug for code coverage reports
- pecl install xdebug
diff --git a/vendor/gitlab-ci-yml/Pages/Gatsby.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Gatsby.gitlab-ci.yml
new file mode 100644
index 00000000000..9df2a4797b2
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/Gatsby.gitlab-ci.yml
@@ -0,0 +1,17 @@
+image: node:latest
+
+# This folder is cached between builds
+# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
+cache:
+ paths:
+ - node_modules/
+
+pages:
+ script:
+ - yarn install
+ - ./node_modules/.bin/gatsby build --prefix-paths
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml
index a72b8281401..b8cfb0f56f6 100644
--- a/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml
@@ -1,5 +1,5 @@
# Full project: https://gitlab.com/pages/hugo
-image: publysher/hugo
+image: dettmering/hugo-build
pages:
script:
diff --git a/vendor/gitlab-ci-yml/Python.gitlab-ci.yml b/vendor/gitlab-ci-yml/Python.gitlab-ci.yml
index a2882a5407d..2e0589de652 100644
--- a/vendor/gitlab-ci-yml/Python.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Python.gitlab-ci.yml
@@ -1,8 +1,27 @@
-# This file is a template, and might need editing before it works on your project.
+# Official language image. Look for the different tagged releases at:
+# https://hub.docker.com/r/library/python/tags/
image: python:latest
+# Change pip's cache directory to be inside the project directory since we can
+# only cache local items.
+variables:
+ PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache"
+
+# Pip's cache doesn't store the python packages
+# https://pip.pypa.io/en/stable/reference/pip_install/#caching
+#
+# If you want to also cache the installed packages, you have to install
+# them in a virtualenv and cache it as well.
+cache:
+ paths:
+ - .cache/pip
+ - venv/
+
before_script:
- - python -V # Print out python version for debugging
+ - python -V # Print out python version for debugging
+ - pip install virtualenv
+ - virtualenv venv
+ - source venv/bin/activate
test:
script:
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
index 03115292f02..ca88f867fe5 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -4,7 +4,7 @@
@babel/template,7.0.0-beta.32,MIT
@babel/traverse,7.0.0-beta.32,MIT
@babel/types,7.0.0-beta.32,MIT
-@gitlab-org/gitlab-svgs,1.8.0,SEE LICENSE IN LICENSE
+@gitlab-org/gitlab-svgs,1.17.0,SEE LICENSE IN LICENSE
@types/jquery,2.0.48,MIT
JSONStream,1.3.2,MIT
RedCloth,4.3.2,MIT
@@ -27,10 +27,11 @@ activejob,4.2.10,MIT
activemodel,4.2.10,MIT
activerecord,4.2.10,MIT
activesupport,4.2.10,MIT
-acts-as-taggable-on,4.0.0,MIT
+acts-as-taggable-on,5.0.0,MIT
address,1.0.3,MIT
addressable,2.5.2,Apache 2.0
addressparser,1.0.1,MIT
+aes_key_wrap,1.0.1,MIT
after,0.8.2,MIT
agent-base,2.1.1,MIT
ajv,4.11.8,MIT
@@ -80,8 +81,8 @@ array-unique,0.3.2,MIT
arraybuffer.slice,0.0.7,MIT
arrify,1.0.1,MIT
asana,0.6.0,MIT
-asciidoctor,1.5.3,MIT
-asciidoctor-plantuml,0.0.7,MIT
+asciidoctor,1.5.6.2,MIT
+asciidoctor-plantuml,0.0.8,MIT
asn1,0.2.3,MIT
asn1.js,4.10.1,MIT
assert,1.4.1,MIT
@@ -205,8 +206,8 @@ better-assert,1.0.2,MIT
bfj-node4,5.2.1,MIT
big.js,3.1.3,MIT
binary-extensions,1.11.0,MIT
-bindata,2.4.1,ruby
-bitsyntax,0.0.4,Unknown
+bindata,2.4.3,ruby
+bitsyntax,0.0.4,UNKNOWN
bl,1.1.2,MIT
blackst0ne-mermaid,7.1.0-fixed,MIT
blob,0.0.4,MIT*
@@ -274,7 +275,7 @@ chalk,1.1.3,MIT
chalk,2.3.0,MIT
chalk,2.3.1,MIT
chardet,0.4.2,MIT
-charlock_holmes,0.7.5,MIT
+charlock_holmes,0.7.6,MIT
chart.js,1.0.2,MIT
check-types,7.3.0,MIT
chokidar,1.7.0,MIT
@@ -314,7 +315,7 @@ combine-lists,1.0.1,MIT
combine-source-map,0.7.2,MIT
combine-source-map,0.8.0,MIT
combined-stream,1.0.6,MIT
-commander,2.14.1,MIT
+commander,2.15.1,MIT
commondir,1.0.1,MIT
commonmarker,0.17.8,MIT
component-bind,1.0.0,MIT*
@@ -352,6 +353,7 @@ core-js,2.5.3,MIT
core-util-is,1.0.2,MIT
cosmiconfig,2.1.1,MIT
crack,0.4.3,MIT
+crass,1.0.3,MIT
create-ecdh,4.0.0,MIT
create-error-class,3.0.2,MIT
create-hash,1.1.3,MIT
@@ -457,13 +459,13 @@ 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"
+domain_name,0.5.20170404,"Simplified BSD,New BSD,Mozilla Public License 2.0"
domelementtype,1.1.3,Simplified BSD
domelementtype,1.3.0,Simplified BSD
domhandler,2.4.1,Simplified BSD
domutils,1.6.2,Simplified BSD
-doorkeeper,4.2.6,MIT
-doorkeeper-openid_connect,1.2.0,MIT
+doorkeeper,4.3.1,MIT
+doorkeeper-openid_connect,1.3.0,MIT
dot-prop,4.2.0,MIT
double-ended-queue,2.1.0-0,MIT
dropzone,4.2.0,MIT
@@ -543,7 +545,7 @@ eventemitter3,1.2.0,MIT
events,1.1.1,MIT
eventsource,0.1.6,MIT
evp_bytestokey,1.0.3,MIT
-excon,0.57.1,MIT
+excon,0.60.0,MIT
execa,0.7.0,MIT
execjs,2.6.0,MIT
exit-hook,1.1.1,MIT
@@ -571,7 +573,7 @@ fast-deep-equal,1.0.0,MIT
fast-json-stable-stringify,2.0.0,MIT
fast-levenshtein,2.0.6,MIT
fast_blank,1.0.0,MIT
-fast_gettext,1.4.0,"MIT,ruby"
+fast_gettext,1.6.0,"MIT,ruby"
fastparse,1.1.1,MIT
faye-websocket,0.10.0,MIT
faye-websocket,0.11.1,MIT
@@ -594,15 +596,15 @@ 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.11.0,MIT
-flipper-active_record,0.11.0,MIT
-flipper-active_support_cache_store,0.11.0,MIT
+flipper,0.13.0,MIT
+flipper-active_record,0.13.0,MIT
+flipper-active_support_cache_store,0.13.0,MIT
flowdock,0.7.1,MIT
flush-write-stream,1.0.2,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
+fog-aws,2.0.1,MIT
+fog-core,1.45.0,MIT
+fog-google,1.3.3,MIT
fog-json,1.0.2,MIT
fog-local,0.3.1,MIT
fog-openstack,0.1.21,MIT
@@ -646,8 +648,8 @@ get-value,2.0.6,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.88.0,MIT
+gettext_i18n_rails_js,1.3.0,MIT
+gitaly-proto,0.94.0,MIT
github-linguist,5.3.3,MIT
github-markup,1.6.1,MIT
gitlab-flowdock-git-hook,1.0.1,MIT
@@ -669,12 +671,13 @@ globals,9.18.0,MIT
globby,5.0.0,MIT
globby,6.1.0,MIT
globby,7.1.1,MIT
+goldiloader,2.0.1,MIT
gollum-grit_adapter,1.0.1,MIT
gollum-lib,4.2.7,MIT
gollum-rugged_adapter,0.4.4,MIT
gon,6.1.0,MIT
good-listener,1.2.2,MIT
-google-api-client,0.13.6,Apache 2.0
+google-api-client,0.19.8,Apache 2.0
google-protobuf,3.5.1,New BSD
googleapis-common-protos-types,1.0.1,Apache 2.0
googleauth,0.6.2,Apache 2.0
@@ -682,7 +685,7 @@ got,6.7.1,MIT
got,7.1.0,MIT
gpgme,2.0.13,LGPL-2.1+
graceful-fs,4.1.11,ISC
-grape,1.0.0,MIT
+grape,1.0.2,MIT
grape-entity,0.6.0,MIT
grape-route-helpers,2.1.0,MIT
grape_logging,1.7.0,MIT
@@ -716,7 +719,7 @@ hash-base,2.0.2,MIT
hash-base,3.0.4,MIT
hash-sum,1.0.2,MIT
hash.js,1.1.3,MIT
-hashie,3.5.6,MIT
+hashie,3.5.7,MIT
hashie-forbidden_attributes,0.1.1,MIT
hawk,3.1.3,New BSD
hawk,6.0.2,New BSD
@@ -733,16 +736,16 @@ hosted-git-info,2.2.0,ISC
hpack.js,2.1.6,MIT
html-comment-regex,1.1.1,MIT
html-entities,1.2.0,MIT
-html-pipeline,1.11.0,MIT
+html-pipeline,2.7.1,MIT
html2text,0.2.0,MIT
htmlentities,4.3.4,MIT
htmlescape,1.1.1,MIT
htmlparser2,3.9.2,MIT
-http,0.9.8,MIT
+http,2.2.2,MIT
http-cookie,1.0.3,MIT
http-deceiver,1.2.7,MIT
http-errors,1.6.2,MIT
-http-form_data,1.0.1,MIT
+http-form_data,1.0.3,MIT
http-proxy,1.16.2,MIT
http-proxy-agent,1.0.0,MIT
http-proxy-middleware,0.17.4,MIT
@@ -750,13 +753,13 @@ http-signature,1.1.1,MIT
http-signature,1.2.0,MIT
http_parser.rb,0.6.0,MIT
httparty,0.13.7,MIT
-httpclient,2.8.2,ruby
+httpclient,2.8.3,ruby
httpntlm,1.6.1,MIT
httpreq,0.4.24,MIT
https-browserify,0.0.1,MIT
https-browserify,1.0.0,MIT
https-proxy-agent,1.0.0,MIT
-i18n,0.9.1,MIT
+i18n,0.9.5,MIT
ice_nine,0.11.2,MIT
iconv-lite,0.4.15,MIT
iconv-lite,0.4.19,MIT
@@ -881,7 +884,6 @@ jed,1.1.1,MIT
jira-ruby,1.4.1,MIT
jquery,3.3.1,MIT
jquery-atwho-rails,1.3.2,MIT
-jquery-rails,4.3.1,MIT
jquery-ujs,1.2.2,MIT
jquery.waitforimages,2.2.0,MIT
js-base64,2.1.9,New BSD
@@ -893,7 +895,7 @@ 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-jwt,1.9.2,MIT
json-loader,0.5.7,MIT
json-schema,0.2.3,BSD
json-schema-traverse,0.3.1,MIT
@@ -927,7 +929,7 @@ kind-of,3.2.2,MIT
kind-of,4.0.0,MIT
kind-of,5.1.0,MIT
kind-of,6.0.2,MIT
-kubeclient,2.2.0,MIT
+kubeclient,3.0.0,MIT
labeled-stream-splicer,2.0.0,MIT
latest-version,3.1.0,MIT
lazy-cache,1.0.4,MIT
@@ -938,7 +940,7 @@ lexical-scope,1.2.0,MIT
libbase64,0.1.0,MIT
libmime,3.0.0,MIT
libqp,1.1.0,MIT
-licensee,8.7.0,MIT
+licensee,8.9.2,MIT
lie,3.1.1,MIT
little-plugger,1.1.4,MIT
load-json-file,1.1.0,MIT
@@ -980,7 +982,7 @@ loggly,1.1.1,MIT
loglevel,1.4.1,MIT
lograge,0.5.1,MIT
longest,1.0.1,MIT
-loofah,2.0.3,MIT
+loofah,2.2.2,MIT
loose-envify,1.3.1,MIT
loud-rejection,1.6.0,MIT
lowercase-keys,1.0.0,MIT
@@ -994,7 +996,7 @@ mailgun-js,0.7.15,MIT
make-dir,1.0.0,MIT
map-cache,0.2.2,MIT
map-obj,1.0.1,MIT
-map-stream,0.1.0,Unknown
+map-stream,0.1.0,UNKNOWN
map-visit,1.0.0,MIT
marked,0.3.12,MIT
match-at,0.1.1,MIT
@@ -1022,7 +1024,7 @@ mime-types-data,3.2016.0521,MIT
mimemagic,0.3.0,MIT
mimic-fn,1.1.0,MIT
mimic-response,1.0.0,MIT
-mini_mime,0.1.4,MIT
+mini_mime,1.0.0,MIT
mini_portile2,2.3.0,MIT
minimalistic-assert,1.0.0,ISC
minimalistic-crypto-utils,1.0.1,MIT
@@ -1047,7 +1049,7 @@ multi_xml,0.6.0,MIT
multicast-dns,6.1.1,MIT
multicast-dns-service-types,1.1.0,MIT
multipart-post,2.0.0,MIT
-mustermann,1.0.0,MIT
+mustermann,1.0.2,MIT
mustermann-grape,1.0.0,MIT
mute-stream,0.0.5,ISC
mute-stream,0.0.7,ISC
@@ -1058,7 +1060,7 @@ nanomatch,1.2.9,MIT
natural-compare,1.4.0,MIT
negotiator,0.6.1,MIT
net-ldap,0.16.0,MIT
-net-ssh,4.1.0,MIT
+net-ssh,4.2.0,MIT
netmask,1.0.6,MIT
netrc,0.11.0,MIT
node-forge,0.6.33,New BSD
@@ -1088,7 +1090,7 @@ 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,0.5.4,MIT
oauth-sign,0.8.2,Apache 2.0
oauth2,1.4.0,MIT
object-assign,4.1.1,MIT
@@ -1099,25 +1101,25 @@ object-visit,1.0.1,MIT
object.omit,2.0.1,MIT
object.pick,1.3.0,MIT
obuf,1.1.1,MIT
-octokit,4.6.2,MIT
-oj,2.17.5,MIT
-omniauth,1.4.2,MIT
-omniauth-auth0,1.4.1,MIT
+octokit,4.8.0,MIT
+omniauth,1.8.1,MIT
+omniauth-auth0,2.0.0,MIT
omniauth-authentiq,0.3.1,MIT
omniauth-azure-oauth2,0.0.9,MIT
omniauth-cas3,1.1.4,MIT
omniauth-facebook,4.0.0,MIT
omniauth-github,1.1.2,MIT
omniauth-gitlab,1.0.2,MIT
-omniauth-google-oauth2,0.5.2,MIT
+omniauth-google-oauth2,0.5.3,MIT
+omniauth-jwt,0.0.2,MIT
omniauth-kerberos,0.3.0,MIT
omniauth-multipassword,0.4.2,MIT
omniauth-oauth,1.1.0,MIT
-omniauth-oauth2,1.4.0,MIT
+omniauth-oauth2,1.5.0,MIT
omniauth-oauth2-generic,0.2.2,MIT
-omniauth-saml,1.7.0,MIT
+omniauth-saml,1.10.0,MIT
omniauth-shibboleth,1.2.1,MIT
-omniauth-twitter,1.2.1,MIT
+omniauth-twitter,1.4.0,MIT
omniauth_crowd,2.2.3,MIT
on-finished,2.3.0,MIT
on-headers,1.0.1,MIT
@@ -1181,7 +1183,6 @@ pause-stream,0.0.11,Apache 2.0
pbkdf2,3.0.14,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.1,MIT
peek-pg,1.3.0,MIT
@@ -1248,8 +1249,8 @@ premailer,1.10.4,New BSD
premailer-rails,1.9.7,MIT
prepend-http,1.0.4,MIT
preserve,0.2.0,MIT
+prettier,1.11.1,MIT
prettier,1.8.2,MIT
-prettier,1.9.2,MIT
prismjs,1.6.0,MIT
private,0.1.8,MIT
process,0.11.10,MIT
@@ -1283,18 +1284,18 @@ querystring,0.2.0,MIT
querystring-es3,0.2.1,MIT
querystringify,0.0.4,MIT
querystringify,1.0.0,MIT
-rack,1.6.8,MIT
+rack,1.6.9,MIT
rack-accept,0.4.5,MIT
rack-attack,4.4.1,MIT
rack-cors,1.0.2,MIT
rack-oauth2,1.2.3,MIT
-rack-protection,1.5.3,MIT
+rack-protection,2.0.1,MIT
rack-proxy,0.6.0,MIT
rack-test,0.6.3,MIT
rails,4.2.10,MIT
rails-deprecated_sanitizer,1.0.3,MIT
-rails-dom-testing,1.0.8,MIT
-rails-html-sanitizer,1.0.3,MIT
+rails-dom-testing,1.0.9,MIT
+rails-html-sanitizer,1.0.4,MIT
rails-i18n,4.0.9,MIT
railties,4.2.10,MIT
rainbow,2.2.2,MIT
@@ -1331,7 +1332,7 @@ readdirp,2.1.0,MIT
readline2,1.0.1,MIT
recaptcha,3.0.0,MIT
rechoir,0.6.2,MIT
-recursive-open-struct,1.0.0,MIT
+recursive-open-struct,1.0.5,MIT
recursive-readdir,2.2.1,MIT
redcarpet,3.4.0,MIT
redent,1.0.0,MIT
@@ -1383,7 +1384,7 @@ resolve-from,1.0.1,MIT
resolve-from,3.0.0,MIT
resolve-url,0.2.1,MIT
responders,2.3.0,MIT
-rest-client,2.0.0,MIT
+rest-client,2.0.2,MIT
restore-cursor,1.0.1,MIT
restore-cursor,2.0.0,MIT
ret,0.1.15,MIT
@@ -1399,13 +1400,13 @@ rqrcode,0.7.0,MIT
rqrcode-rails3,0.1.7,MIT
ruby-enum,0.7.2,MIT
ruby-fogbugz,0.2.1,MIT
-ruby-prof,0.16.2,Simplified BSD
-ruby-saml,1.4.1,MIT
+ruby-prof,0.17.0,Simplified BSD
+ruby-saml,1.7.2,MIT
ruby_parser,3.9.0,MIT
rubyntlm,0.6.2,MIT
rubypants,0.2.0,BSD
rufus-scheduler,3.4.0,MIT
-rugged,0.26.0,MIT
+rugged,0.27.0,MIT
run-async,0.1.0,MIT
run-async,2.3.0,MIT
run-queue,1.0.3,ISC
@@ -1436,7 +1437,7 @@ semver,5.3.0,ISC
semver,5.5.0,ISC
semver-diff,2.1.0,MIT
send,0.16.1,MIT
-sentry-raven,2.5.3,Apache 2.0
+sentry-raven,2.7.2,Apache 2.0
serialize-javascript,1.4.0,New BSD
serve-index,1.9.0,MIT
serve-static,1.13.1,MIT
@@ -1507,9 +1508,9 @@ srcset,1.0.0,MIT
sshkey,1.9.0,MIT
sshpk,1.13.1,MIT
ssri,5.2.4,ISC
-state_machines,0.4.0,MIT
-state_machines-activemodel,0.4.0,MIT
-state_machines-activerecord,0.4.0,MIT
+state_machines,0.5.0,MIT
+state_machines-activemodel,0.5.1,MIT
+state_machines-activerecord,0.5.1,MIT
static-extend,0.1.2,MIT
statuses,1.3.1,MIT
statuses,1.4.0,MIT
@@ -1603,7 +1604,7 @@ tweetnacl,0.14.5,Unlicense
type-check,0.3.2,MIT
type-is,1.6.16,MIT
typedarray,0.0.6,MIT
-tzinfo,1.2.4,MIT
+tzinfo,1.2.5,MIT
u2f,0.2.1,MIT
uber,0.1.0,MIT
uglifier,2.7.2,MIT
@@ -1618,7 +1619,7 @@ undefsafe,2.0.2,MIT
underscore,1.7.0,MIT
underscore,1.8.3,MIT
unf,0.1.4,BSD
-unf_ext,0.0.7.4,MIT
+unf_ext,0.0.7.5,MIT
unicorn,5.1.0,ruby
unicorn-worker-killer,0.4.4,ruby
union-value,1.0.0,MIT
diff --git a/vendor/project_templates/express.tar.gz b/vendor/project_templates/express.tar.gz
index dcf5e4a0416..06093deb459 100644
--- a/vendor/project_templates/express.tar.gz
+++ b/vendor/project_templates/express.tar.gz
Binary files differ
diff --git a/vendor/project_templates/rails.tar.gz b/vendor/project_templates/rails.tar.gz
index d4856090ed9..85cc1b6bb78 100644
--- a/vendor/project_templates/rails.tar.gz
+++ b/vendor/project_templates/rails.tar.gz
Binary files differ
diff --git a/vendor/project_templates/spring.tar.gz b/vendor/project_templates/spring.tar.gz
index 6ee7e76f676..e98d3ce7b8f 100644
--- a/vendor/project_templates/spring.tar.gz
+++ b/vendor/project_templates/spring.tar.gz
Binary files differ
diff --git a/yarn.lock b/yarn.lock
index 243b83f4471..7aca6b0d427 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -54,9 +54,9 @@
lodash "^4.2.0"
to-fast-properties "^2.0.0"
-"@gitlab-org/gitlab-svgs@^1.17.0":
- version "1.17.0"
- resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.17.0.tgz#d0c74d9e44c127ccfad16941f352088b86f86c89"
+"@gitlab-org/gitlab-svgs@^1.18.0":
+ version "1.18.0"
+ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.18.0.tgz#7829f0e6de0647dace54c1fcd597ee3424afb233"
"@types/jquery@^2.0.40":
version "2.0.48"
@@ -77,14 +77,7 @@ abbrev@1.0.x:
version "1.0.9"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
-accepts@~1.3.3:
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca"
- dependencies:
- mime-types "~2.1.11"
- negotiator "0.6.1"
-
-accepts@~1.3.4:
+accepts@~1.3.3, accepts@~1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f"
dependencies:
@@ -118,11 +111,7 @@ acorn@^4.0.3:
version "4.0.13"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
-acorn@^5.0.0, acorn@^5.1.1:
- version "5.1.1"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.1.tgz#53fe161111f912ab999ee887a90a0bc52822fd75"
-
-acorn@^5.2.1, acorn@^5.3.0, acorn@^5.4.1:
+acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0, acorn@^5.4.1:
version "5.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102"
@@ -160,16 +149,7 @@ ajv@^4.7.0, ajv@^4.9.1:
co "^4.6.0"
json-stable-stringify "^1.0.1"
-ajv@^5.0.0:
- version "5.2.2"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
- dependencies:
- co "^4.6.0"
- fast-deep-equal "^1.0.0"
- json-schema-traverse "^0.3.0"
- json-stable-stringify "^1.0.1"
-
-ajv@^5.1.0:
+ajv@^5.0.0, ajv@^5.1.0:
version "5.5.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
dependencies:
@@ -242,7 +222,7 @@ ansi-styles@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
-ansi-styles@^3.1.0, ansi-styles@^3.2.0:
+ansi-styles@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88"
dependencies:
@@ -420,13 +400,7 @@ async@1.x, async@^1.4.0, async@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
-async@^2.1.2, async@^2.1.4:
- version "2.4.1"
- resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7"
- dependencies:
- lodash "^4.14.0"
-
-async@^2.4.1:
+async@^2.1.2, async@^2.1.4, async@^2.4.1:
version "2.6.0"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
dependencies:
@@ -696,6 +670,10 @@ babel-plugin-istanbul@^4.1.5:
istanbul-lib-instrument "^1.7.5"
test-exclude "^4.1.1"
+babel-plugin-rewire@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-rewire/-/babel-plugin-rewire-1.1.0.tgz#a6b966d9d8c06c03d95dcda2eec4e2521519549b"
+
babel-plugin-syntax-async-functions@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95"
@@ -1196,11 +1174,7 @@ block-stream@*:
dependencies:
inherits "~2.0.0"
-bluebird@^3.1.1:
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
-
-bluebird@^3.3.0, bluebird@^3.4.6, bluebird@^3.5.1:
+bluebird@^3.1.1, bluebird@^3.3.0, bluebird@^3.4.6, bluebird@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
@@ -1268,14 +1242,7 @@ boxen@^1.2.1:
term-size "^1.2.0"
widest-line "^2.0.0"
-brace-expansion@^1.0.0, brace-expansion@^1.1.8:
- version "1.1.8"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
- dependencies:
- balanced-match "^1.0.0"
- concat-map "0.0.1"
-
-brace-expansion@^1.1.7:
+brace-expansion@^1.0.0, brace-expansion@^1.1.7, brace-expansion@^1.1.8:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
dependencies:
@@ -1621,15 +1588,7 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
-chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba"
- dependencies:
- ansi-styles "^3.1.0"
- escape-string-regexp "^1.0.5"
- supports-color "^4.0.0"
-
-chalk@^2.3.1:
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796"
dependencies:
@@ -1859,9 +1818,9 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5:
dependencies:
delayed-stream "~1.0.0"
-commander@^2.13.0, commander@^2.9.0:
- version "2.14.1"
- resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa"
+commander@^2.13.0, commander@^2.15.1, commander@^2.9.0:
+ version "2.15.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
commondir@^1.0.1:
version "1.0.1"
@@ -2027,18 +1986,10 @@ copy-webpack-plugin@^4.4.1:
p-limit "^1.0.0"
serialize-javascript "^1.4.0"
-core-js@^2.2.0:
+core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0:
version "2.5.3"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e"
-core-js@^2.4.0, core-js@^2.5.0:
- version "2.5.1"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b"
-
-core-js@^2.4.1:
- version "2.4.1"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
-
core-js@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.3.0.tgz#fab83fbb0b2d8dc85fa636c4b9d34c75420c6d65"
@@ -2117,7 +2068,7 @@ cryptiles@3.x.x:
dependencies:
boom "5.x.x"
-crypto-browserify@^3.0.0:
+crypto-browserify@^3.0.0, crypto-browserify@^3.11.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
dependencies:
@@ -2133,21 +2084,6 @@ crypto-browserify@^3.0.0:
randombytes "^2.0.0"
randomfill "^1.0.3"
-crypto-browserify@^3.11.0:
- version "3.11.0"
- resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.0.tgz#3652a0906ab9b2a7e0c3ce66a408e957a2485522"
- dependencies:
- browserify-cipher "^1.0.0"
- browserify-sign "^4.0.0"
- create-ecdh "^4.0.0"
- create-hash "^1.1.0"
- create-hmac "^1.1.0"
- diffie-hellman "^5.0.0"
- inherits "^2.0.1"
- pbkdf2 "^3.0.3"
- public-encrypt "^4.0.0"
- randombytes "^2.0.0"
-
crypto-random-string@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
@@ -2401,7 +2337,7 @@ de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
-debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@~2.6.4, debug@~2.6.6:
+debug@2, debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.6, debug@^2.6.8, debug@~2.6.4, debug@~2.6.6:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
dependencies:
@@ -2413,7 +2349,7 @@ debug@2.2.0, debug@~2.2.0:
dependencies:
ms "0.7.1"
-debug@2.6.8, debug@^2.1.1, debug@^2.6.6, debug@^2.6.8:
+debug@2.6.8:
version "2.6.8"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
dependencies:
@@ -2764,13 +2700,7 @@ encodeurl@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
-end-of-stream@^1.0.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.0.tgz#7a90d833efda6cfa6eac0f4949dbb0fad3a63206"
- dependencies:
- once "^1.4.0"
-
-end-of-stream@^1.1.0:
+end-of-stream@^1.0.0, end-of-stream@^1.1.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
dependencies:
@@ -3095,14 +3025,7 @@ eslint@^3.18.0:
text-table "~0.2.0"
user-home "^2.0.0"
-espree@^3.4.0:
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.0.tgz#98358625bdd055861ea27e2867ea729faf463d8d"
- dependencies:
- acorn "^5.1.1"
- acorn-jsx "^3.0.0"
-
-espree@^3.5.2:
+espree@^3.4.0, espree@^3.5.2:
version "3.5.2"
resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca"
dependencies:
@@ -3730,18 +3653,7 @@ glob@^5.0.15:
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^7.0.0, glob@^7.0.3:
- version "7.1.1"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
- dependencies:
- fs.realpath "^1.0.0"
- inflight "^1.0.4"
- inherits "2"
- minimatch "^3.0.2"
- once "^1.3.0"
- path-is-absolute "^1.0.0"
-
-glob@^7.0.5, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2:
+glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
@@ -4228,11 +4140,7 @@ ignore-by-default@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
-ignore@^3.2.0:
- version "3.3.3"
- resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.3.tgz#432352e57accd87ab3110e82d3fea0e47812156d"
-
-ignore@^3.3.5, ignore@^3.3.7:
+ignore@^3.2.0, ignore@^3.3.5, ignore@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
@@ -4424,14 +4332,10 @@ is-binary-path@^1.0.0:
dependencies:
binary-extensions "^1.0.0"
-is-buffer@^1.1.0:
+is-buffer@^1.1.0, is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
-is-buffer@^1.1.5:
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
-
is-builtin-module@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
@@ -4547,16 +4451,7 @@ is-my-ip-valid@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824"
-is-my-json-valid@^2.10.0:
- version "2.16.0"
- resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz#f079dd9bfdae65ee2038aae8acbc86ab109e3693"
- dependencies:
- generate-function "^2.0.0"
- generate-object-property "^1.1.0"
- jsonpointer "^4.0.0"
- xtend "^4.0.0"
-
-is-my-json-valid@^2.12.4:
+is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4:
version "2.17.2"
resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz#6b2103a288e94ef3de5cf15d29dd85fc4b78d65c"
dependencies:
@@ -4732,10 +4627,6 @@ isbinaryfile@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621"
-isexe@^1.1.1:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0"
-
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -5298,7 +5189,7 @@ lodash.words@^4.0.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.words/-/lodash.words-4.2.0.tgz#5ecfeaf8ecf8acaa8e0c8386295f1993c9cf4036"
-lodash@4.17.4, lodash@^4.11.1, lodash@^4.17.2, lodash@^4.2.0, lodash@^4.3.0:
+lodash@4.17.4:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
@@ -5306,7 +5197,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.15.0, lodash@^4.17.4, lodash@^4.5.0:
+lodash@^4.0.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
@@ -5534,15 +5425,11 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
-"mime-db@>= 1.29.0 < 2":
- version "1.29.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878"
-
-mime-db@~1.33.0:
+"mime-db@>= 1.29.0 < 2", mime-db@~1.33.0:
version "1.33.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
-mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.7:
+mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.7:
version "2.1.18"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
dependencies:
@@ -6100,16 +5987,12 @@ p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
-p-limit@^1.0.0:
+p-limit@^1.0.0, p-limit@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c"
dependencies:
p-try "^1.0.0"
-p-limit@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc"
-
p-locate@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
@@ -6167,11 +6050,7 @@ pako@~0.2.0:
version "0.2.9"
resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
-pako@~1.0.2:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.5.tgz#d2205dfe5b9da8af797e7c163db4d1f84e4600bc"
-
-pako@~1.0.5:
+pako@~1.0.2, pako@~1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
@@ -6649,7 +6528,7 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0
source-map "^0.5.6"
supports-color "^3.2.3"
-postcss@^6.0.1:
+postcss@^6.0.1, postcss@^6.0.14, postcss@^6.0.8:
version "6.0.19"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.19.tgz#76a78386f670b9d9494a655bf23ac012effd1555"
dependencies:
@@ -6657,22 +6536,6 @@ postcss@^6.0.1:
source-map "^0.6.1"
supports-color "^5.2.0"
-postcss@^6.0.14:
- version "6.0.15"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.15.tgz#f460cd6269fede0d1bf6defff0b934a9845d974d"
- dependencies:
- chalk "^2.3.0"
- source-map "^0.6.1"
- supports-color "^5.1.0"
-
-postcss@^6.0.8:
- version "6.0.14"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.14.tgz#5534c72114739e75d0afcf017db853099f562885"
- dependencies:
- chalk "^2.3.0"
- source-map "^0.6.1"
- supports-color "^4.4.0"
-
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@@ -6685,14 +6548,10 @@ preserve@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
-prettier@1.11.1:
+prettier@1.11.1, prettier@^1.7.0:
version "1.11.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75"
-prettier@^1.7.0:
- version "1.8.2"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.8.2.tgz#bff83e7fd573933c607875e5ba3abbdffb96aeb8"
-
prismjs@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.6.0.tgz#118d95fb7a66dba2272e343b345f5236659db365"
@@ -6711,11 +6570,7 @@ process-nextick-args@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
-process@^0.11.0:
- version "0.11.9"
- resolved "https://registry.yarnpkg.com/process/-/process-0.11.9.tgz#7bd5ad21aa6253e7da8682264f1e11d11c0318c1"
-
-process@~0.11.0:
+process@^0.11.0, process@~0.11.0:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
@@ -6894,16 +6749,7 @@ raw-loader@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa"
-rc@^1.0.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
- dependencies:
- deep-extend "~0.4.0"
- ini "~1.3.0"
- minimist "^1.2.0"
- strip-json-comments "~2.0.1"
-
-rc@^1.1.6, rc@^1.1.7:
+rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
version "1.2.5"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.5.tgz#275cd687f6e3b36cc756baa26dfee80a790301fd"
dependencies:
@@ -6975,7 +6821,7 @@ read-pkg@^2.0.0:
normalize-package-data "^2.3.2"
path-type "^2.0.0"
-"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.3.0, readable-stream@^2.3.3:
+"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3:
version "2.3.4"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071"
dependencies:
@@ -6996,19 +6842,7 @@ readable-stream@1.1.x, "readable-stream@1.x >=1.1.9":
isarray "0.0.1"
string_decoder "~0.10.x"
-readable-stream@^2.0.0, readable-stream@^2.1.0, readable-stream@^2.2.2, readable-stream@^2.2.9:
- version "2.3.3"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
- dependencies:
- core-util-is "~1.0.0"
- inherits "~2.0.3"
- isarray "~1.0.0"
- process-nextick-args "~1.0.6"
- safe-buffer "~5.1.1"
- string_decoder "~1.0.3"
- util-deprecate "~1.0.1"
-
-readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@~2.0.0, readable-stream@~2.0.5, readable-stream@~2.0.6:
+readable-stream@~2.0.0, readable-stream@~2.0.5, readable-stream@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
dependencies:
@@ -7350,18 +7184,12 @@ right-align@^0.1.1:
dependencies:
align-text "^0.1.1"
-rimraf@2, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2:
+rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
dependencies:
glob "^7.0.5"
-rimraf@^2.2.8:
- version "2.6.1"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d"
- dependencies:
- glob "^7.0.5"
-
ripemd160@^2.0.0, ripemd160@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7"
@@ -7464,11 +7292,7 @@ semver-diff@^2.0.0:
dependencies:
semver "^5.0.3"
-"semver@2 || 3 || 4 || 5", semver@^5.0.3:
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
-
-semver@^5.1.0, semver@^5.3.0, semver@^5.4.1:
+"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1:
version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
@@ -7937,7 +7761,7 @@ stream-each@^1.1.0:
end-of-stream "^1.1.0"
stream-shift "^1.0.0"
-stream-http@^2.0.0:
+stream-http@^2.0.0, stream-http@^2.3.1:
version "2.8.0"
resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.0.tgz#fd86546dac9b1c91aff8fc5d287b98fafb41bc10"
dependencies:
@@ -7947,16 +7771,6 @@ stream-http@^2.0.0:
to-arraybuffer "^1.0.0"
xtend "^4.0.0"
-stream-http@^2.3.1:
- version "2.6.3"
- resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.6.3.tgz#4c3ddbf9635968ea2cfd4e48d43de5def2625ac3"
- dependencies:
- builtin-status-codes "^3.0.0"
- inherits "^2.0.1"
- readable-stream "^2.1.0"
- to-arraybuffer "^1.0.0"
- xtend "^4.0.0"
-
stream-shift@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
@@ -7989,14 +7803,7 @@ string-width@^1.0.1, string-width@^1.0.2:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
-string-width@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e"
- dependencies:
- is-fullwidth-code-point "^2.0.0"
- strip-ansi "^3.0.0"
-
-string-width@^2.1.0, string-width@^2.1.1:
+string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
dependencies:
@@ -8076,25 +7883,13 @@ supports-color@^3.1.0, supports-color@^3.1.2, supports-color@^3.2.3:
dependencies:
has-flag "^1.0.0"
-supports-color@^4.0.0, supports-color@^4.4.0:
+supports-color@^4.2.1:
version "4.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b"
dependencies:
has-flag "^2.0.0"
-supports-color@^4.2.1:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.1.tgz#65a4bb2631e90e02420dba5554c375a4754bb836"
- dependencies:
- has-flag "^2.0.0"
-
-supports-color@^5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.1.0.tgz#058a021d1b619f7ddf3980d712ea3590ce7de3d5"
- dependencies:
- has-flag "^2.0.0"
-
-supports-color@^5.2.0:
+supports-color@^5.1.0, supports-color@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.2.0.tgz#b0d5333b1184dd3666cbe5aa0b45c5ac7ac17a4a"
dependencies:
@@ -8683,6 +8478,10 @@ vue-template-es2015-compiler@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18"
+vue-virtual-scroll-list@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.2.5.tgz#bcbd010f7cdb035eba8958ebf807c6214d9a167a"
+
vue@^2.5.13:
version "2.5.13"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.13.tgz#95bd31e20efcf7a7f39239c9aa6787ce8cf578e1"
@@ -8828,13 +8627,7 @@ which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
-which@^1.1.1, which@^1.2.1, which@^1.2.9:
- version "1.2.12"
- resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192"
- dependencies:
- isexe "^1.1.1"
-
-which@^1.2.14:
+which@^1.1.1, which@^1.2.1, which@^1.2.14, which@^1.2.9:
version "1.3.0"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
dependencies: