summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml1
-rw-r--r--.gitlab/CODEOWNERS5
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml25
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/pages.gitlab-ci.yml1
-rw-r--r--.gitlab/ci/qa.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml6
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml7
-rw-r--r--.gitlab/ci/yaml.gitlab-ci.yml9
-rw-r--r--.gitlab/issue_templates/Feature Flag Roll Out.md43
-rw-r--r--.gitlab/route-map.yml4
-rw-r--r--.yamllint7
-rw-r--r--CHANGELOG.md63
-rw-r--r--Dangerfile1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_ELASTICSEARCH_INDEXER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile7
-rw-r--r--Gemfile.lock22
-rw-r--r--PROCESS.md4
-rw-r--r--VERSION2
-rw-r--r--app/assets/javascripts/api.js2
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js2
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue4
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue19
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue2
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js3
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.vue2
-rw-r--r--app/assets/javascripts/boards/index.js34
-rw-r--r--app/assets/javascripts/boards/models/assignee.js (renamed from app/assets/javascripts/vue_shared/models/assignee.js)0
-rw-r--r--app/assets/javascripts/boards/models/issue.js2
-rw-r--r--app/assets/javascripts/boards/models/label.js11
-rw-r--r--app/assets/javascripts/boards/models/list.js8
-rw-r--r--app/assets/javascripts/boards/stores/actions.js4
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js40
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js3
-rw-r--r--app/assets/javascripts/branches/branches_delete_modal.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/ajax_variable_list.js11
-rw-r--r--app/assets/javascripts/ci_variable_list/ci_variable_list.js6
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js22
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue121
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue178
-rw-r--r--app/assets/javascripts/clusters/components/knative_domain_editor.vue150
-rw-r--r--app/assets/javascripts/clusters/services/application_state_machine.js1
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js13
-rw-r--r--app/assets/javascripts/commons/polyfills.js33
-rw-r--r--app/assets/javascripts/compare_autocomplete.js2
-rw-r--r--app/assets/javascripts/create_item_dropdown.js2
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue15
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue23
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue1
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue21
-rw-r--r--app/assets/javascripts/diffs/components/edit_button.vue2
-rw-r--r--app/assets/javascripts/diffs/store/actions.js3
-rw-r--r--app/assets/javascripts/emoji/no_emoji_validator.js44
-rw-r--r--app/assets/javascripts/error_tracking_settings/index.js2
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/getters.js4
-rw-r--r--app/assets/javascripts/frequent_items/store/actions.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js1
-rw-r--r--app/assets/javascripts/gl_dropdown.js9
-rw-r--r--app/assets/javascripts/gl_form.js2
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue34
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue43
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue1
-rw-r--r--app/assets/javascripts/ide/components/preview/clientside.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue2
-rw-r--r--app/assets/javascripts/ide/lib/editor_options.js2
-rw-r--r--app/assets/javascripts/ide/lib/keymap.json8
-rw-r--r--app/assets/javascripts/ide/stores/actions.js5
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js5
-rw-r--r--app/assets/javascripts/ide/stores/getters.js12
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js25
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js7
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/mutation_types.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/mutations.js18
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/getters.js2
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js2
-rw-r--r--app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js2
-rw-r--r--app/assets/javascripts/image_diff/image_diff.js4
-rw-r--r--app/assets/javascripts/image_diff/view_types.js2
-rw-r--r--app/assets/javascripts/issuable_index.js2
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue2
-rw-r--r--app/assets/javascripts/jobs/components/stages_dropdown.vue11
-rw-r--r--app/assets/javascripts/label_manager.js2
-rw-r--r--app/assets/javascripts/lib/graphql.js20
-rw-r--r--app/assets/javascripts/lib/utils/accessor.js2
-rw-r--r--app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js9
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js10
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js4
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js6
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js6
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js48
-rw-r--r--app/assets/javascripts/monitoring/components/charts/single_stat.vue37
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue15
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js1
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/mr_notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/notes.js29
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue14
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue4
-rw-r--r--app/assets/javascripts/notes/components/discussion_locked_widget.vue20
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue5
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue45
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue11
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue4
-rw-r--r--app/assets/javascripts/notes/mixins/issuable_state.js15
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/operation_settings/components/external_dashboard.vue34
-rw-r--r--app/assets/javascripts/operation_settings/index.js9
-rw-r--r--app/assets/javascripts/operation_settings/store/actions.js38
-rw-r--r--app/assets/javascripts/operation_settings/store/index.js16
-rw-r--r--app/assets/javascripts/operation_settings/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/operation_settings/store/mutations.js7
-rw-r--r--app/assets/javascripts/operation_settings/store/state.js5
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js1
-rw-r--r--app/assets/javascripts/pages/projects/pages_domains/edit/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/pages_domains/form.js43
-rw-r--r--app/assets/javascripts/pages/projects/pages_domains/new/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue2
-rw-r--r--app/assets/javascripts/pages/projects/project.js12
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js1
-rw-r--r--app/assets/javascripts/pages/projects/settings/operations/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue5
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js2
-rw-r--r--app/assets/javascripts/pages/sessions/new/length_validator.js32
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue13
-rw-r--r--app/assets/javascripts/profile/account/index.js2
-rw-r--r--app/assets/javascripts/profile/profile.js1
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js2
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js6
-rw-r--r--app/assets/javascripts/registry/stores/mutations.js4
-rw-r--r--app/assets/javascripts/reports/components/issues_list.vue20
-rw-r--r--app/assets/javascripts/reports/components/report_section.vue42
-rw-r--r--app/assets/javascripts/reports/constants.js6
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue3
-rw-r--r--app/assets/javascripts/repository/components/table/parent_row.vue4
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue7
-rw-r--r--app/assets/javascripts/repository/queries/getFiles.graphql2
-rw-r--r--app/assets/javascripts/right_sidebar.js4
-rw-r--r--app/assets/javascripts/search_autocomplete.js6
-rw-r--r--app/assets/javascripts/serverless/components/functions.vue36
-rw-r--r--app/assets/javascripts/serverless/constants.js4
-rw-r--r--app/assets/javascripts/serverless/serverless_bundle.js3
-rw-r--r--app/assets/javascripts/serverless/store/actions.js29
-rw-r--r--app/assets/javascripts/serverless/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/serverless/store/mutations.js15
-rw-r--r--app/assets/javascripts/serverless/store/state.js1
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue6
-rw-r--r--app/assets/javascripts/test_utils/index.js4
-rw-r--r--app/assets/javascripts/validators/input_validator.js34
-rw-r--r--app/assets/javascripts/visual_review_toolbar/index.js2
-rw-r--r--app/assets/javascripts/visual_review_toolbar/styles/toolbar.css149
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue32
-rw-r--r--app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue89
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_warning.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue27
-rw-r--r--app/assets/javascripts/vue_shared/components/modal_copy_button.vue121
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/pikaday.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.vue2
-rw-r--r--app/assets/javascripts/vue_shared/models/label.js13
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss4
-rw-r--r--app/assets/stylesheets/components/avatar.scss34
-rw-r--r--app/assets/stylesheets/components/popover.scss19
-rw-r--r--app/assets/stylesheets/errors.scss2
-rw-r--r--app/assets/stylesheets/framework/animations.scss24
-rw-r--r--app/assets/stylesheets/framework/awards.scss3
-rw-r--r--app/assets/stylesheets/framework/blocks.scss9
-rw-r--r--app/assets/stylesheets/framework/buttons.scss9
-rw-r--r--app/assets/stylesheets/framework/common.scss47
-rw-r--r--app/assets/stylesheets/framework/files.scss28
-rw-r--r--app/assets/stylesheets/framework/flash.scss29
-rw-r--r--app/assets/stylesheets/framework/forms.scss109
-rw-r--r--app/assets/stylesheets/framework/lists.scss8
-rw-r--r--app/assets/stylesheets/framework/mixins.scss15
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss52
-rw-r--r--app/assets/stylesheets/framework/snippets.scss4
-rw-r--r--app/assets/stylesheets/framework/timeline.scss4
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_mixins.scss14
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss2
-rw-r--r--app/assets/stylesheets/pages/boards.scss9
-rw-r--r--app/assets/stylesheets/pages/commits.scss2
-rw-r--r--app/assets/stylesheets/pages/diff.scss27
-rw-r--r--app/assets/stylesheets/pages/events.scss6
-rw-r--r--app/assets/stylesheets/pages/groups.scss3
-rw-r--r--app/assets/stylesheets/pages/issuable.scss16
-rw-r--r--app/assets/stylesheets/pages/issues.scss11
-rw-r--r--app/assets/stylesheets/pages/labels.scss3
-rw-r--r--app/assets/stylesheets/pages/login.scss3
-rw-r--r--app/assets/stylesheets/pages/members.scss59
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss81
-rw-r--r--app/assets/stylesheets/pages/note_form.scss41
-rw-r--r--app/assets/stylesheets/pages/notes.scss78
-rw-r--r--app/assets/stylesheets/pages/notifications.scss28
-rw-r--r--app/controllers/acme_challenges_controller.rb17
-rw-r--r--app/controllers/admin/projects_controller.rb11
-rw-r--r--app/controllers/application_controller.rb8
-rw-r--r--app/controllers/clusters/clusters_controller.rb3
-rw-r--r--app/controllers/concerns/import_url_params.rb19
-rw-r--r--app/controllers/concerns/issuable_collections.rb6
-rw-r--r--app/controllers/concerns/milestone_actions.rb8
-rw-r--r--app/controllers/dashboard/projects_controller.rb19
-rw-r--r--app/controllers/graphql_controller.rb3
-rw-r--r--app/controllers/import/fogbugz_controller.rb8
-rw-r--r--app/controllers/import/phabricator_controller.rb35
-rw-r--r--app/controllers/profiles/accounts_controller.rb2
-rw-r--r--app/controllers/profiles/emails_controller.rb4
-rw-r--r--app/controllers/profiles/groups_controller.rb24
-rw-r--r--app/controllers/profiles/passwords_controller.rb10
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb6
-rw-r--r--app/controllers/projects/clusters_controller.rb4
-rw-r--r--app/controllers/projects/environments_controller.rb4
-rw-r--r--app/controllers/projects/git_http_client_controller.rb1
-rw-r--r--app/controllers/projects/imports_controller.rb7
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb10
-rw-r--r--app/controllers/projects/merge_requests_controller.rb48
-rw-r--r--app/controllers/projects/pages_domains_controller.rb4
-rw-r--r--app/controllers/projects/serverless/functions_controller.rb10
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb3
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/controllers/search_controller.rb1
-rw-r--r--app/controllers/sessions_controller.rb9
-rw-r--r--app/finders/clusters/knative_services_finder.rb112
-rw-r--r--app/finders/projects/serverless/functions_finder.rb40
-rw-r--r--app/graphql/gitlab_schema.rb27
-rw-r--r--app/graphql/mutations/merge_requests/base.rb2
-rw-r--r--app/graphql/resolvers/base_resolver.rb2
-rw-r--r--app/graphql/resolvers/concerns/resolves_pipelines.rb2
-rw-r--r--app/graphql/resolvers/issues_resolver.rb6
-rw-r--r--app/graphql/resolvers/merge_requests_resolver.rb4
-rw-r--r--app/graphql/resolvers/namespace_projects_resolver.rb33
-rw-r--r--app/graphql/resolvers/namespace_resolver.rb13
-rw-r--r--app/graphql/types/base_field.rb17
-rw-r--r--app/graphql/types/base_object.rb5
-rw-r--r--app/graphql/types/ci/pipeline_type.rb2
-rw-r--r--app/graphql/types/issue_type.rb6
-rw-r--r--app/graphql/types/merge_request_type.rb2
-rw-r--r--app/graphql/types/namespace_type.rb5
-rw-r--r--app/graphql/types/project_statistics_type.rb16
-rw-r--r--app/graphql/types/project_type.rb4
-rw-r--r--app/graphql/types/query_type.rb5
-rw-r--r--app/graphql/types/tree/blob_type.rb4
-rw-r--r--app/graphql/types/tree/tree_entry_type.rb4
-rw-r--r--app/graphql/types/tree/tree_type.rb10
-rw-r--r--app/helpers/application_settings_helper.rb1
-rw-r--r--app/helpers/blob_helper.rb2
-rw-r--r--app/helpers/ci_variables_helper.rb6
-rw-r--r--app/helpers/emails_helper.rb13
-rw-r--r--app/helpers/groups_helper.rb2
-rw-r--r--app/helpers/labels_helper.rb56
-rw-r--r--app/helpers/merge_requests_helper.rb2
-rw-r--r--app/helpers/notifications_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb1
-rw-r--r--app/helpers/sorting_helper.rb55
-rw-r--r--app/helpers/storage_helper.rb3
-rw-r--r--app/helpers/tracking_helper.rb7
-rw-r--r--app/helpers/visibility_level_helper.rb38
-rw-r--r--app/mailers/emails/issues.rb4
-rw-r--r--app/mailers/emails/members.rb3
-rw-r--r--app/mailers/emails/merge_requests.rb2
-rw-r--r--app/mailers/emails/notes.rb2
-rw-r--r--app/mailers/emails/pages_domains.rb8
-rw-r--r--app/mailers/emails/projects.rb10
-rw-r--r--app/mailers/emails/remote_mirrors.rb2
-rw-r--r--app/mailers/notify.rb16
-rw-r--r--app/models/application_setting_implementation.rb1
-rw-r--r--app/models/ci/build.rb67
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/ci/pipeline.rb10
-rw-r--r--app/models/ci/pipeline_schedule.rb25
-rw-r--r--app/models/clusters/applications/jupyter.rb11
-rw-r--r--app/models/clusters/applications/knative.rb48
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb4
-rw-r--r--app/models/commit.rb11
-rw-r--r--app/models/concerns/artifact_migratable.rb58
-rw-r--r--app/models/concerns/cache_markdown_field.rb88
-rw-r--r--app/models/concerns/maskable.rb4
-rw-r--r--app/models/concerns/milestoneish.rb20
-rw-r--r--app/models/concerns/noteable.rb12
-rw-r--r--app/models/concerns/referable.rb1
-rw-r--r--app/models/concerns/resolvable_note.rb2
-rw-r--r--app/models/concerns/taskable.rb7
-rw-r--r--app/models/concerns/update_project_statistics.rb52
-rw-r--r--app/models/diff_note.rb22
-rw-r--r--app/models/group.rb14
-rw-r--r--app/models/issue.rb8
-rw-r--r--app/models/key.rb5
-rw-r--r--app/models/merge_request.rb79
-rw-r--r--app/models/milestone.rb11
-rw-r--r--app/models/namespace.rb5
-rw-r--r--app/models/notification_recipient.rb30
-rw-r--r--app/models/notification_setting.rb9
-rw-r--r--app/models/pages_domain.rb11
-rw-r--r--app/models/pages_domain_acme_order.rb24
-rw-r--r--app/models/project.rb14
-rw-r--r--app/models/project_auto_devops.rb6
-rw-r--r--app/models/project_ci_cd_setting.rb18
-rw-r--r--app/models/project_feature.rb2
-rw-r--r--app/models/project_services/youtrack_service.rb6
-rw-r--r--app/models/project_statistics.rb20
-rw-r--r--app/models/repository.rb2
-rw-r--r--app/models/todo.rb3
-rw-r--r--app/presenters/blob_presenter.rb4
-rw-r--r--app/presenters/ci/build_runner_presenter.rb19
-rw-r--r--app/presenters/ci/pipeline_presenter.rb2
-rw-r--r--app/presenters/issue_presenter.rb12
-rw-r--r--app/presenters/member_presenter.rb5
-rw-r--r--app/presenters/merge_request_presenter.rb6
-rw-r--r--app/presenters/tree_entry_presenter.rb9
-rw-r--r--app/serializers/issue_entity.rb8
-rw-r--r--app/serializers/merge_request_widget_entity.rb10
-rw-r--r--app/serializers/pipeline_entity.rb1
-rw-r--r--app/services/auto_merge/base_service.rb52
-rw-r--r--app/services/auto_merge/merge_when_pipeline_succeeds_service.rb30
-rw-r--r--app/services/auto_merge_service.rb50
-rw-r--r--app/services/ci/pipeline_schedule_service.rb13
-rw-r--r--app/services/clusters/applications/base_service.rb2
-rw-r--r--app/services/issues/close_service.rb2
-rw-r--r--app/services/merge_requests/base_service.rb26
-rw-r--r--app/services/merge_requests/close_service.rb5
-rw-r--r--app/services/merge_requests/create_pipeline_service.rb37
-rw-r--r--app/services/merge_requests/merge_to_ref_service.rb20
-rw-r--r--app/services/merge_requests/merge_when_pipeline_succeeds_service.rb47
-rw-r--r--app/services/merge_requests/mergeability_check_service.rb82
-rw-r--r--app/services/merge_requests/refresh_service.rb8
-rw-r--r--app/services/merge_requests/update_service.rb2
-rw-r--r--app/services/notification_service.rb2
-rw-r--r--app/services/pages_domains/create_acme_order_service.rb31
-rw-r--r--app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb41
-rw-r--r--app/services/preview_markdown_service.rb4
-rw-r--r--app/services/projects/fork_service.rb20
-rw-r--r--app/services/projects/update_service.rb1
-rw-r--r--app/services/projects/update_statistics_service.rb2
-rw-r--r--app/services/search_service.rb4
-rw-r--r--app/services/service_response.rb15
-rw-r--r--app/services/todos/destroy/base_service.rb2
-rw-r--r--app/services/todos/destroy/confidential_issue_service.rb35
-rw-r--r--app/uploaders/legacy_artifact_uploader.rb27
-rw-r--r--app/views/abuse_reports/new.html.haml8
-rw-r--r--app/views/admin/application_settings/_external_authorization_service_form.html.haml2
-rw-r--r--app/views/admin/application_settings/_outbound.html.haml8
-rw-r--r--app/views/admin/application_settings/_performance_bar.html.haml2
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml2
-rw-r--r--app/views/admin/labels/_form.html.haml5
-rw-r--r--app/views/admin/projects/_projects.html.haml4
-rw-r--r--app/views/ci/variables/_content.html.haml2
-rw-r--r--app/views/ci/variables/_index.html.haml2
-rw-r--r--app/views/ci/variables/_variable_row.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml12
-rw-r--r--app/views/discussions/_notes.html.haml20
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/import/gitlab_projects/new.html.haml23
-rw-r--r--app/views/import/manifest/new.html.haml6
-rw-r--r--app/views/import/phabricator/new.html.haml25
-rw-r--r--app/views/import/shared/_errors.html.haml4
-rw-r--r--app/views/import/shared/_new_project_form.html.haml21
-rw-r--r--app/views/layouts/_search.html.haml2
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml32
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml2
-rw-r--r--app/views/notify/closed_issue_email.html.haml2
-rw-r--r--app/views/notify/closed_issue_email.text.haml2
-rw-r--r--app/views/notify/new_user_email.html.haml3
-rw-r--r--app/views/notify/new_user_email.text.erb11
-rw-r--r--app/views/profiles/active_sessions/_active_session.html.haml11
-rw-r--r--app/views/profiles/active_sessions/index.html.haml4
-rw-r--r--app/views/profiles/emails/index.html.haml44
-rw-r--r--app/views/profiles/gpg_keys/_form.html.haml6
-rw-r--r--app/views/profiles/gpg_keys/_key.html.haml16
-rw-r--r--app/views/profiles/gpg_keys/_key_table.html.haml4
-rw-r--r--app/views/profiles/gpg_keys/index.html.haml12
-rw-r--r--app/views/profiles/keys/_form.html.haml8
-rw-r--r--app/views/profiles/keys/_key.html.haml9
-rw-r--r--app/views/profiles/keys/_key_details.html.haml13
-rw-r--r--app/views/profiles/keys/_key_table.html.haml4
-rw-r--r--app/views/profiles/keys/index.html.haml8
-rw-r--r--app/views/profiles/keys/show.html.haml2
-rw-r--r--app/views/profiles/notifications/_group_settings.html.haml23
-rw-r--r--app/views/profiles/notifications/show.html.haml5
-rw-r--r--app/views/profiles/passwords/edit.html.haml25
-rw-r--r--app/views/profiles/passwords/new.html.haml6
-rw-r--r--app/views/profiles/preferences/show.html.haml21
-rw-r--r--app/views/profiles/two_factor_auths/_codes.html.haml10
-rw-r--r--app/views/profiles/two_factor_auths/codes.html.haml5
-rw-r--r--app/views/profiles/two_factor_auths/create.html.haml4
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml61
-rw-r--r--app/views/projects/_import_project_pane.html.haml28
-rw-r--r--app/views/projects/_merge_request_merge_checks_settings.html.haml2
-rw-r--r--app/views/projects/_new_project_fields.html.haml4
-rw-r--r--app/views/projects/blob/_header_content.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml5
-rw-r--r--app/views/projects/commit/_commit_box.html.haml4
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml2
-rw-r--r--app/views/projects/diffs/_text_file.html.haml2
-rw-r--r--app/views/projects/issues/_new_branch.html.haml8
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml43
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/projects/notes/_more_actions_dropdown.html.haml2
-rw-r--r--app/views/projects/pages_domains/_form.html.haml87
-rw-r--r--app/views/projects/pages_domains/_helper_text.html.haml9
-rw-r--r--app/views/projects/pages_domains/edit.html.haml1
-rw-r--r--app/views/projects/pages_domains/new.html.haml1
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml40
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml38
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml8
-rw-r--r--app/views/projects/settings/operations/_external_dashboard.html.haml3
-rw-r--r--app/views/projects/settings/repository/_protected_branches.html.haml2
-rw-r--r--app/views/projects/settings/repository/show.html.haml7
-rw-r--r--app/views/projects/tags/index.html.haml2
-rw-r--r--app/views/projects/tags/show.html.haml2
-rw-r--r--app/views/projects/tree/_tree_commit_column.html.haml3
-rw-r--r--app/views/repository_check_mailer/notify.html.haml2
-rw-r--r--app/views/search/_form.html.haml1
-rw-r--r--app/views/search/_results.html.haml5
-rw-r--r--app/views/search/results/_issue.html.haml2
-rw-r--r--app/views/search/results/_merge_request.html.haml2
-rw-r--r--app/views/search/results/_milestone.html.haml2
-rw-r--r--app/views/sent_notifications/unsubscribe.html.haml2
-rw-r--r--app/views/shared/_email_with_badge.html.haml2
-rw-r--r--app/views/shared/_import_form.html.haml27
-rw-r--r--app/views/shared/_old_visibility_level.html.haml2
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml2
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_label_page_create.html.haml4
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
-rw-r--r--app/views/shared/issuable/_sort_dropdown.html.haml15
-rw-r--r--app/views/shared/labels/_form.html.haml7
-rw-r--r--app/views/shared/members/_group.html.haml19
-rw-r--r--app/views/shared/members/_member.html.haml41
-rw-r--r--app/views/shared/notes/_hints.html.haml5
-rw-r--r--app/views/shared/notes/_note.html.haml2
-rw-r--r--app/views/shared/notifications/_button.html.haml12
-rw-r--r--app/views/users/show.html.haml2
-rw-r--r--app/workers/all_queues.yml2
-rw-r--r--app/workers/auto_merge_process_worker.rb14
-rw-r--r--app/workers/pages_domain_verification_cron_worker.rb2
-rw-r--r--app/workers/pages_domain_verification_worker.rb2
-rw-r--r--app/workers/pipeline_schedule_worker.rb41
-rw-r--r--app/workers/pipeline_success_worker.rb8
-rw-r--r--app/workers/post_receive.rb2
-rw-r--r--app/workers/project_cache_worker.rb4
-rw-r--r--app/workers/run_pipeline_schedule_worker.rb26
-rw-r--r--app/workers/todos_destroyer/confidential_issue_worker.rb4
-rw-r--r--babel.config.js2
-rw-r--r--changelogs/unreleased/10088-move-code-differences-EE-to-CE.yml5
-rw-r--r--changelogs/unreleased/10795-add-epic-tree-BE-CE-epic-graphql-support.yml5
-rw-r--r--changelogs/unreleased/11204-turn-off-mask-by-default.yml5
-rw-r--r--changelogs/unreleased/28996-create-mvc-ui-in-haml.yml5
-rw-r--r--changelogs/unreleased/37495.yml5
-rw-r--r--changelogs/unreleased/47846-position-is-off-when-visiting-files-with-anchors.yml5
-rw-r--r--changelogs/unreleased/50106-hide-whitespace-changes.yml5
-rw-r--r--changelogs/unreleased/50850-kerrizor-extend-api-to-accept-start_project-option.yml5
-rw-r--r--changelogs/unreleased/51022-added-extended-height-to-labels-dropdown.yml5
-rw-r--r--changelogs/unreleased/51636-task-list-api-pderichs.yml5
-rw-r--r--changelogs/unreleased/53134-multiple-extendes-for-a-job.yml5
-rw-r--r--changelogs/unreleased/54140-non-ar-cache-commit-markdown.yml5
-rw-r--r--changelogs/unreleased/54169-flash-warning-rebrush.yml5
-rw-r--r--changelogs/unreleased/55033-discussion-system-note-alignment.yml5
-rw-r--r--changelogs/unreleased/55125-mr-tab-scrolling.yml5
-rw-r--r--changelogs/unreleased/55253-activity-feed-ui-enhance-line-height.yml5
-rw-r--r--changelogs/unreleased/56959-drop-project_auto_devops_domain.yml5
-rw-r--r--changelogs/unreleased/57037-fix-mr-checkboxes-mobile-alignment.yml5
-rw-r--r--changelogs/unreleased/57414-show-pipeline-iid.yml5
-rw-r--r--changelogs/unreleased/58269-separate-update-patch.yml5
-rw-r--r--changelogs/unreleased/58297-remove-extraneous-gitaly-calls-from-md-rendering.yml5
-rw-r--r--changelogs/unreleased/58941-use-gitlab-serverless-with-existing-knative-installation.yml5
-rw-r--r--changelogs/unreleased/59376-Report-abuse-to-GitLab-should-be-Report-abuse-in-non-gitlab-com-instances.yml5
-rw-r--r--changelogs/unreleased/59587-add-graphql-logging.yml5
-rw-r--r--changelogs/unreleased/60034-default-web-ide-s-merge-request-checkbox-to-true.yml5
-rw-r--r--changelogs/unreleased/60303-replace-sidekiq-mtail-metrics.yml5
-rw-r--r--changelogs/unreleased/60323-inline-validation-for-users-name-and-username-length.yml5
-rw-r--r--changelogs/unreleased/60778-input-text-height.yml5
-rw-r--r--changelogs/unreleased/60819_yamllint_gitlabci.yml5
-rw-r--r--changelogs/unreleased/609120-ref-link.yml5
-rw-r--r--changelogs/unreleased/60987-emoji-picker-popup.yml5
-rw-r--r--changelogs/unreleased/61024-update-resolved-icon.yml5
-rw-r--r--changelogs/unreleased/6104-ee-ce-difference.yml5
-rw-r--r--changelogs/unreleased/61045-charts-with-many-overlapping-series-display-incorrectly.yml5
-rw-r--r--changelogs/unreleased/61323-snippet-copy-icon-button-is-misaligned.yml5
-rw-r--r--changelogs/unreleased/61324-non-project-snippet-new-snippet-button-should-be-green-outline.yml2
-rw-r--r--changelogs/unreleased/61339-Add-underline-to-attach-a-file.yml5
-rw-r--r--changelogs/unreleased/61565-merge-request-discussion-text-jumps-when-resolved.yml5
-rw-r--r--changelogs/unreleased/61788-predefined-colours-dont-have-descriptive-labels.yml5
-rw-r--r--changelogs/unreleased/61821-tooltip-consistency.yml5
-rw-r--r--changelogs/unreleased/61960-translatable-strings-in-issue-closure-emails.yml5
-rw-r--r--changelogs/unreleased/61988-collapse-icon-on-merge-request-diff-larger-than-profile-picture.yml5
-rw-r--r--changelogs/unreleased/62091-remove-time-windows-flag.yml5
-rw-r--r--changelogs/unreleased/62092-missing-padding-next-to-time-windows-dropdown-on-metrics-dashboard.yml5
-rw-r--r--changelogs/unreleased/62124-new-threaded-discussion-design.yml5
-rw-r--r--changelogs/unreleased/62144-fix-option-dropdown-button-size.yml5
-rw-r--r--changelogs/unreleased/62418-project-default-git-depth.yml5
-rw-r--r--changelogs/unreleased/62432-fix-participants-wrapping.yml5
-rw-r--r--changelogs/unreleased/62485-label-weights.yml5
-rw-r--r--changelogs/unreleased/62487-external-policy-desc.yml5
-rw-r--r--changelogs/unreleased/62656-adjusted-dropdown-styles.yml5
-rw-r--r--changelogs/unreleased/62847-url-for-the-next-request-with-pagination-is-missing-port.yml5
-rw-r--r--changelogs/unreleased/62974-follow-up-from-wip-align-merge-request-icons-and-text.yml5
-rw-r--r--changelogs/unreleased/9121-sort-relative-position.yml (renamed from changelogs/unreleased/dm-disable-two-step-rebase.yml)6
-rw-r--r--changelogs/unreleased/9578-adjust-milestone-completion-rate.yml5
-rw-r--r--changelogs/unreleased/abstract-auto-merge.yml5
-rw-r--r--changelogs/unreleased/ac-graphql-stats.yml5
-rw-r--r--changelogs/unreleased/ac-graphql-wikisize.yml5
-rw-r--r--changelogs/unreleased/ac-namespaces-stats-no-coalesce.yml5
-rw-r--r--changelogs/unreleased/add-constraint-for-milestone-dates.yml5
-rw-r--r--changelogs/unreleased/add-wiki-size-to-statistics.yml5
-rw-r--r--changelogs/unreleased/always-show-pipelines-must-succeed-checkbox.yml5
-rw-r--r--changelogs/unreleased/bump-auto-devops-helm-2-14-0.yml5
-rw-r--r--changelogs/unreleased/bvl-use-global-ids-graphql.yml5
-rw-r--r--changelogs/unreleased/cancel-auto-merge-when-merge-request-is-closed.yml5
-rw-r--r--changelogs/unreleased/ce-jej-fix-git-http-with-sso-enforcement.yml5
-rw-r--r--changelogs/unreleased/ci-variable-conjunction.yml5
-rw-r--r--changelogs/unreleased/copy-button-in-modals.yml5
-rw-r--r--changelogs/unreleased/dm-http-hostname-override.yml5
-rw-r--r--changelogs/unreleased/docs-add-chatops-request-doc.yml5
-rw-r--r--changelogs/unreleased/ee-11040-added-conditional-rendering.yml5
-rw-r--r--changelogs/unreleased/feature-gb-use-gitlabktl-to-build-serverless-applications.yml5
-rw-r--r--changelogs/unreleased/fix-allow-lower-case-issue-ids.yml5
-rw-r--r--changelogs/unreleased/fix-gb-fix-serverless-apps-deployment-template.yml5
-rw-r--r--changelogs/unreleased/fix-gb-remove-serverless-app-build-policies-from-template.yml5
-rw-r--r--changelogs/unreleased/fix-search-dropdown-blur-close.yml5
-rw-r--r--changelogs/unreleased/fix-time-window-default.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.43.0.yml5
-rw-r--r--changelogs/unreleased/gt-externalize-profiles-preferences.yml5
-rw-r--r--changelogs/unreleased/gt-open-visibility-help-link-in-a-new-tab.yml5
-rw-r--r--changelogs/unreleased/i18n-active_sessions-in-user-profile.yml5
-rw-r--r--changelogs/unreleased/i18n-email-of-user-profile.yml5
-rw-r--r--changelogs/unreleased/i18n-pgp_ssh_keys-of-user-profile.yml5
-rw-r--r--changelogs/unreleased/improve-email-text-part.yml5
-rw-r--r--changelogs/unreleased/increase-move-issue-dropdown-height.yml5
-rw-r--r--changelogs/unreleased/issue_49897.yml5
-rw-r--r--changelogs/unreleased/jp-label-fix.yml5
-rw-r--r--changelogs/unreleased/osw-avoid-500-on-suggestions-check.yml5
-rw-r--r--changelogs/unreleased/osw-reset-merge-status-from-mergeable-mrs.yml5
-rw-r--r--changelogs/unreleased/osw-sync-merge-ref-upon-mergeability-check.yml5
-rw-r--r--changelogs/unreleased/patch-64.yml5
-rw-r--r--changelogs/unreleased/pb-update-gitaly-1-45-0.yml5
-rw-r--r--changelogs/unreleased/referenced-labels.yml5
-rw-r--r--changelogs/unreleased/remove-legacy-artifacts-related-code.yml5
-rw-r--r--changelogs/unreleased/remove-mr-diff-header-height.yml5
-rw-r--r--changelogs/unreleased/security-58856-persistent-xss-in-note-objects.yml5
-rw-r--r--changelogs/unreleased/security-60039.yml5
-rw-r--r--changelogs/unreleased/security-60143-address-xss-issue-in-wiki-links.yml5
-rw-r--r--changelogs/unreleased/security-fix-confidential-issue-label-visibility-master.yml5
-rw-r--r--changelogs/unreleased/security-fix-project-existence-disclosure-master.yml5
-rw-r--r--changelogs/unreleased/security-fix_milestones_search_api_leak.yml5
-rw-r--r--changelogs/unreleased/security-id-leaked-password-in-import-url-frontend.yml5
-rw-r--r--changelogs/unreleased/security-jej-prevent-web-sign-in-bypass.yml5
-rw-r--r--changelogs/unreleased/security-unsubscribing-from-issue.yml5
-rw-r--r--changelogs/unreleased/set-real-next-run-at-for-preventing-duplciate-pipeline-creations.yml5
-rw-r--r--changelogs/unreleased/sh-add-backtrace-to-sql-queries.yml5
-rw-r--r--changelogs/unreleased/sh-default-visibility-fix.yml5
-rw-r--r--changelogs/unreleased/sh-fix-fogbugz-import.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-55869.yml5
-rw-r--r--changelogs/unreleased/sh-fix-omniauth-generic-strategy.yml5
-rw-r--r--changelogs/unreleased/sh-project-import-visibility-error.yml5
-rw-r--r--changelogs/unreleased/t1.yml5
-rw-r--r--changelogs/unreleased/thomas-nilsson-irfu-gitlab-ce-thomas-nilsson-irfu-master-patch-13137.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-5-2.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-shell-9-3-0.yml5
-rw-r--r--changelogs/unreleased/update-pages.yml5
-rw-r--r--changelogs/unreleased/update-psd-doc.yml5
-rw-r--r--changelogs/unreleased/use-source-ref-name-in-webhook.yml5
-rw-r--r--changelogs/unreleased/weimeng-email-routing.yml5
-rw-r--r--changelogs/unreleased/zj-remove-delta-island-feature-flag.yml3
-rw-r--r--config/environments/development.rb3
-rw-r--r--config/initializers/7_prometheus_metrics.rb6
-rw-r--r--config/initializers/hipchat_client_patch.rb6
-rw-r--r--config/initializers/http_hostname_override.rb49
-rw-r--r--config/routes.rb2
-rw-r--r--config/routes/admin.rb2
-rw-r--r--config/routes/import.rb2
-rw-r--r--config/routes/profile.rb6
-rw-r--r--config/routes/project.rb85
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--config/webpack.config.review_toolbar.js58
-rw-r--r--danger/commit_messages/Dangerfile4
-rw-r--r--danger/plugins/helper.rb3
-rw-r--r--danger/plugins/roulette.rb10
-rw-r--r--danger/roulette/Dangerfile57
-rw-r--r--danger/single_codebase/Dangerfile29
-rw-r--r--db/migrate/20180702134423_generate_missing_routes.rb1
-rw-r--r--db/migrate/20190327163904_add_notification_email_to_notification_settings.rb11
-rw-r--r--db/migrate/20190429082448_create_pages_domain_acme_orders.rb28
-rw-r--r--db/migrate/20190516155724_change_packages_size_defaults_in_project_statistics.rb24
-rw-r--r--db/migrate/20190523112344_limit_milestone_date_years_to_4_digits.rb38
-rw-r--r--db/migrate/20190524062810_generate_lets_encrypt_private_key.rb17
-rw-r--r--db/migrate/20190524071727_add_ssl_valid_period_to_pages_domain.rb16
-rw-r--r--db/migrate/20190527194830_add_wiki_size_to_statistics.rb9
-rw-r--r--db/migrate/20190529142545_add_dns_rebinding_protection_enabled_to_application_settings.rb23
-rw-r--r--db/migrate/20190530042141_add_default_git_depth_to_ci_cd_settings.rb11
-rw-r--r--db/migrate/20190530154715_add_index_to_merge_requests_state_and_merge_status.rb21
-rw-r--r--db/post_migrate/20190522143720_drop_project_auto_devops_domain.rb11
-rw-r--r--db/post_migrate/20190524073827_schedule_fill_valid_time_for_pages_domain_certificates.rb34
-rw-r--r--db/post_migrate/20190527194900_schedule_calculate_wiki_sizes.rb32
-rw-r--r--db/post_migrate/20190528180441_enqueue_reset_merge_status.rb30
-rw-r--r--db/schema.rb27
-rw-r--r--doc/administration/auth/google_secure_ldap.md2
-rw-r--r--doc/administration/auth/oidc.md58
-rw-r--r--doc/administration/compliance.md4
-rw-r--r--doc/administration/custom_hooks.md5
-rw-r--r--doc/administration/geo/disaster_recovery/planned_failover.md2
-rw-r--r--doc/administration/geo/replication/database.md3
-rw-r--r--doc/administration/geo/replication/external_database.md2
-rw-r--r--doc/administration/geo/replication/high_availability.md4
-rw-r--r--doc/administration/geo/replication/index.md4
-rw-r--r--doc/administration/geo/replication/tuning.md2
-rw-r--r--doc/administration/gitaly/index.md36
-rw-r--r--doc/administration/high_availability/README.md8
-rw-r--r--doc/administration/high_availability/database.md117
-rw-r--r--doc/administration/high_availability/gitaly.md81
-rw-r--r--doc/administration/high_availability/img/geo-ha-diagram.pngbin0 -> 43826 bytes
-rw-r--r--doc/administration/high_availability/nfs.md5
-rw-r--r--doc/administration/high_availability/redis.md20
-rw-r--r--doc/administration/incoming_email.md2
-rw-r--r--doc/administration/index.md35
-rw-r--r--doc/administration/integration/terminal.md5
-rw-r--r--doc/administration/job_artifacts.md11
-rw-r--r--doc/administration/logs.md16
-rw-r--r--doc/administration/monitoring/performance/grafana_configuration.md8
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md17
-rw-r--r--doc/administration/operations/fast_ssh_key_lookup.md4
-rw-r--r--doc/administration/packages.md6
-rw-r--r--doc/administration/raketasks/maintenance.md19
-rw-r--r--doc/administration/raketasks/project_import_export.md1
-rw-r--r--doc/administration/repository_storage_paths.md2
-rw-r--r--doc/analytics/README.md4
-rw-r--r--doc/analytics/contribution_analytics.md4
-rw-r--r--doc/api/README.md142
-rw-r--r--doc/api/boards.md179
-rw-r--r--doc/api/commits.md1
-rw-r--r--doc/api/discussions.md216
-rw-r--r--doc/api/epic_issues.md1
-rw-r--r--doc/api/epic_links.md1
-rw-r--r--doc/api/geo_nodes.md12
-rw-r--r--doc/api/graphql/index.md1
-rw-r--r--doc/api/groups.md1
-rw-r--r--doc/api/issues.md50
-rw-r--r--doc/api/jobs.md103
-rw-r--r--doc/api/merge_requests.md96
-rw-r--r--doc/api/notes.md130
-rw-r--r--doc/api/project_clusters.md2
-rw-r--r--doc/api/project_level_variables.md43
-rw-r--r--doc/api/projects.md5
-rw-r--r--doc/api/resource_label_events.md86
-rw-r--r--doc/api/scim.md2
-rw-r--r--doc/api/search.md193
-rw-r--r--doc/api/services.md69
-rw-r--r--doc/api/snippets.md10
-rw-r--r--doc/api/users.md6
-rw-r--r--doc/api/vulnerabilities.md2
-rw-r--r--doc/articles/how_to_configure_ldap_gitlab_ee/index.md4
-rw-r--r--doc/ci/README.md28
-rw-r--r--doc/ci/caching/index.md24
-rw-r--r--doc/ci/chatops/README.md27
-rw-r--r--doc/ci/ci_cd_for_external_repos/bitbucket_integration.md49
-rw-r--r--doc/ci/ci_cd_for_external_repos/github_integration.md49
-rw-r--r--doc/ci/ci_cd_for_external_repos/index.md24
-rw-r--r--doc/ci/docker/README.md4
-rw-r--r--doc/ci/docker/using_docker_build.md52
-rw-r--r--doc/ci/docker/using_docker_images.md63
-rw-r--r--doc/ci/docker/using_kaniko.md42
-rw-r--r--doc/ci/enable_or_disable_ci.md61
-rw-r--r--doc/ci/environments.md43
-rw-r--r--doc/ci/environments/protected_environments.md16
-rw-r--r--doc/ci/git_submodules.md4
-rw-r--r--doc/ci/interactive_web_terminal/index.md4
-rw-r--r--doc/ci/introduction/index.md1
-rw-r--r--doc/ci/junit_test_reports.md4
-rw-r--r--doc/ci/large_repositories/index.md4
-rw-r--r--doc/ci/merge_request_pipelines/index.md4
-rw-r--r--doc/ci/metrics_reports.md4
-rw-r--r--doc/ci/multi_project_pipelines.md19
-rw-r--r--doc/ci/pipelines.md7
-rw-r--r--doc/ci/quick_start/README.md22
-rw-r--r--doc/ci/review_apps/index.md8
-rw-r--r--doc/ci/runners/README.md4
-rw-r--r--doc/ci/services/README.md13
-rw-r--r--doc/ci/services/mysql.md4
-rw-r--r--doc/ci/services/postgres.md4
-rw-r--r--doc/ci/services/redis.md4
-rw-r--r--doc/ci/ssh_keys/README.md1
-rw-r--r--doc/ci/triggers/README.md4
-rw-r--r--doc/ci/variables/README.md39
-rw-r--r--doc/ci/variables/deprecated_variables.md7
-rw-r--r--doc/ci/variables/predefined_variables.md4
-rw-r--r--doc/ci/variables/where_variables_can_be_used.md4
-rw-r--r--doc/ci/yaml/README.md64
-rw-r--r--doc/customization/issue_and_merge_request_template.md4
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/api_graphql_styleguide.md21
-rw-r--r--doc/development/architecture.md8
-rw-r--r--doc/development/chatops_on_gitlabcom.md21
-rw-r--r--doc/development/database_debugging.md2
-rw-r--r--doc/development/documentation/index.md11
-rw-r--r--doc/development/ee_features.md11
-rw-r--r--doc/development/fe_guide/frontend_faq.md14
-rw-r--r--doc/development/fe_guide/style_guide_js.md6
-rw-r--r--doc/development/fe_guide/vue.md2
-rw-r--r--doc/development/feature_flags.md2
-rw-r--r--doc/development/geo.md78
-rw-r--r--doc/development/i18n/externalization.md6
-rw-r--r--doc/development/i18n/proofreader.md2
-rw-r--r--doc/development/i18n/translation.md8
-rw-r--r--doc/development/licensing.md6
-rw-r--r--doc/development/rolling_out_changes_using_feature_flags.md7
-rw-r--r--doc/development/testing_guide/end_to_end/best_practices.md (renamed from qa/docs/best_practices.md)6
-rw-r--r--doc/development/testing_guide/end_to_end/dynamic_element_validation.md113
-rw-r--r--doc/development/testing_guide/end_to_end/index.md (renamed from doc/development/testing_guide/end_to_end_tests.md)10
-rw-r--r--doc/development/testing_guide/end_to_end/page_objects.md (renamed from qa/qa/page/README.md)39
-rw-r--r--doc/development/testing_guide/end_to_end/quick_start_guide.md585
-rw-r--r--doc/development/testing_guide/end_to_end/resources.md (renamed from qa/qa/resource/README.md)6
-rw-r--r--doc/development/testing_guide/end_to_end/style_guide.md (renamed from qa/docs/guidelines.md)12
-rw-r--r--doc/development/testing_guide/index.md2
-rw-r--r--doc/development/testing_guide/smoke.md2
-rw-r--r--doc/development/testing_guide/testing_levels.md2
-rw-r--r--doc/development/understanding_explain_plans.md47
-rw-r--r--doc/git_hooks/git_hooks.md4
-rw-r--r--doc/gitlab-basics/create-project.md2
-rw-r--r--doc/install/README.md3
-rw-r--r--doc/install/aws/index.md24
-rw-r--r--doc/install/azure/index.md20
-rw-r--r--doc/install/database_mysql.md16
-rw-r--r--doc/install/digitaloceandocker.md41
-rw-r--r--doc/install/docker.md6
-rw-r--r--doc/install/google-protobuf.md29
-rw-r--r--doc/install/google_cloud_platform/img/gcp_landing.pngbin9978 -> 0 bytes
-rw-r--r--doc/install/google_cloud_platform/index.md24
-rw-r--r--doc/install/installation.md107
-rw-r--r--doc/install/kubernetes/index.md40
-rw-r--r--doc/install/openshift_and_gitlab/index.md5
-rw-r--r--doc/install/pivotal/index.md53
-rw-r--r--doc/install/redis.md63
-rw-r--r--doc/install/relative_url.md29
-rw-r--r--doc/install/requirements.md44
-rw-r--r--doc/install/structure.md22
-rw-r--r--doc/integration/elasticsearch.md142
-rw-r--r--doc/integration/github.md4
-rw-r--r--doc/integration/saml.md2
-rw-r--r--doc/integration/slash_commands.md4
-rw-r--r--doc/license/README.md4
-rw-r--r--doc/push_rules/push_rules.md2
-rw-r--r--doc/raketasks/backup_restore.md6
-rw-r--r--doc/security/img/two_factor_authentication_group_settings.pngbin19495 -> 0 bytes
-rw-r--r--doc/security/img/two_factor_authentication_settings.pngbin9936 -> 0 bytes
-rw-r--r--doc/security/two_factor_authentication.md35
-rw-r--r--doc/subscriptions/index.md2
-rw-r--r--doc/topics/authentication/index.md10
-rw-r--r--doc/topics/autodevops/index.md41
-rw-r--r--doc/topics/autodevops/quick_start_guide.md2
-rw-r--r--doc/topics/git/how_to_install_git/index.md29
-rw-r--r--doc/topics/git/index.md59
-rw-r--r--doc/topics/git/numerous_undo_possibilities_in_git/index.md121
-rw-r--r--doc/topics/git/troubleshooting_git.md39
-rw-r--r--doc/university/training/topics/unstage.md4
-rw-r--r--doc/update/patch_versions.md2
-rw-r--r--doc/update/upgrading_from_ce_to_ee.md3
-rw-r--r--doc/update/upgrading_from_source.md60
-rw-r--r--doc/user/admin_area/diff_limits.md41
-rw-r--r--doc/user/admin_area/geo_nodes.md4
-rw-r--r--doc/user/admin_area/img/index_runners_search_or_filter.pngbin0 -> 22414 bytes
-rw-r--r--doc/user/admin_area/index.md98
-rw-r--r--doc/user/admin_area/settings/account_and_limit_settings.md8
-rw-r--r--doc/user/admin_area/settings/email.md2
-rw-r--r--doc/user/admin_area/settings/index.md12
-rw-r--r--doc/user/admin_area/settings/instance_template_repository.md16
-rw-r--r--doc/user/admin_area/settings/sign_up_restrictions.md16
-rw-r--r--doc/user/admin_area/settings/terms.md46
-rw-r--r--doc/user/admin_area/settings/third_party_offers.md23
-rw-r--r--doc/user/admin_area/settings/usage_statistics.md16
-rw-r--r--doc/user/admin_area/settings/visibility_and_access_controls.md46
-rw-r--r--doc/user/application_security/container_scanning/index.md4
-rw-r--r--doc/user/application_security/dast/index.md2
-rw-r--r--doc/user/application_security/dependency_scanning/index.md17
-rw-r--r--doc/user/application_security/img/dismissed_info.pngbin0 -> 20244 bytes
-rw-r--r--doc/user/application_security/img/interactive_reports.pngbin23190 -> 93910 bytes
-rw-r--r--doc/user/application_security/index.md10
-rw-r--r--doc/user/application_security/security_dashboard/img/project_security_dashboard.pngbin49062 -> 126356 bytes
-rw-r--r--doc/user/clusters/applications.md31
-rw-r--r--doc/user/clusters/img/jupyter-git-extension.gifbin0 -> 2120084 bytes
-rw-r--r--doc/user/clusters/img/jupyter-gitclone.pngbin0 -> 64120 bytes
-rw-r--r--doc/user/group/clusters/index.md20
-rw-r--r--doc/user/group/contribution_analytics/index.md64
-rw-r--r--doc/user/group/custom_project_templates.md16
-rw-r--r--doc/user/group/epics/index.md16
-rw-r--r--doc/user/group/index.md52
-rw-r--r--doc/user/group/insights/index.md20
-rw-r--r--doc/user/group/issues_analytics/index.md41
-rw-r--r--doc/user/group/roadmap/index.md27
-rw-r--r--doc/user/group/saml_sso/index.md30
-rw-r--r--doc/user/group/saml_sso/scim_setup.md2
-rw-r--r--doc/user/group/security_dashboard/index.md4
-rw-r--r--doc/user/group/subgroups/index.md57
-rw-r--r--doc/user/index.md26
-rw-r--r--doc/user/markdown.md4
-rw-r--r--doc/user/permissions.md261
-rw-r--r--doc/user/profile/account/delete_account.md2
-rw-r--r--doc/user/profile/personal_access_tokens.md18
-rw-r--r--doc/user/project/ci_cd_for_external_repo.md4
-rw-r--r--doc/user/project/clusters/index.md6
-rw-r--r--doc/user/project/clusters/kubernetes_pod_logs.md6
-rw-r--r--doc/user/project/clusters/runbooks/index.md12
-rw-r--r--doc/user/project/clusters/serverless/index.md2
-rw-r--r--doc/user/project/container_registry.md4
-rw-r--r--doc/user/project/deploy_tokens/index.md2
-rw-r--r--doc/user/project/description_templates.md2
-rw-r--r--doc/user/project/file_lock.md14
-rw-r--r--doc/user/project/import/gemnasium.md10
-rw-r--r--doc/user/project/import/github.md4
-rw-r--r--doc/user/project/import/gitlab_com.md5
-rw-r--r--doc/user/project/import/index.md3
-rw-r--r--doc/user/project/import/phabricator.md29
-rw-r--r--doc/user/project/index.md12
-rw-r--r--doc/user/project/insights/index.md7
-rw-r--r--doc/user/project/integrations/img/jira_add_user_to_group.pngbin24838 -> 266180 bytes
-rw-r--r--doc/user/project/integrations/img/jira_added_user_to_group.pngbin0 -> 82473 bytes
-rw-r--r--doc/user/project/integrations/img/jira_create_new_group.pngbin19127 -> 262453 bytes
-rw-r--r--doc/user/project/integrations/img/jira_create_new_user.pngbin12625 -> 173516 bytes
-rw-r--r--doc/user/project/integrations/img/jira_group_access.pngbin19147 -> 112706 bytes
-rw-r--r--doc/user/project/integrations/img/jira_user_management_link.pngbin23906 -> 206155 bytes
-rw-r--r--doc/user/project/integrations/jira.md4
-rw-r--r--doc/user/project/integrations/jira_server_configuration.md45
-rw-r--r--doc/user/project/integrations/project_services.md2
-rw-r--r--doc/user/project/integrations/prometheus_library/kubernetes.md4
-rw-r--r--doc/user/project/integrations/youtrack.md4
-rw-r--r--doc/user/project/issues/create_new_issue.md2
-rw-r--r--doc/user/project/issues/index.md6
-rw-r--r--doc/user/project/issues/issue_data_and_actions.md4
-rw-r--r--doc/user/project/issues/multiple_assignees_for_issues.md2
-rw-r--r--doc/user/project/issues/related_issues.md11
-rw-r--r--doc/user/project/labels.md8
-rw-r--r--doc/user/project/maven_packages.md4
-rw-r--r--doc/user/project/merge_requests/allow_collaboration.md16
-rw-r--r--doc/user/project/merge_requests/authorization_for_merge_requests.md18
-rw-r--r--doc/user/project/merge_requests/browser_performance_testing.md20
-rw-r--r--doc/user/project/merge_requests/cherry_pick_changes.md26
-rw-r--r--doc/user/project/merge_requests/code_quality.md28
-rw-r--r--doc/user/project/merge_requests/code_quality_diff.md2
-rw-r--r--doc/user/project/merge_requests/container_scanning.md4
-rw-r--r--doc/user/project/merge_requests/dast.md4
-rw-r--r--doc/user/project/merge_requests/dependency_scanning.md4
-rw-r--r--doc/user/project/merge_requests/fast_forward_merge.md35
-rw-r--r--doc/user/project/merge_requests/index.md31
-rw-r--r--doc/user/project/merge_requests/license_management.md4
-rw-r--r--doc/user/project/merge_requests/merge_request_approvals.md48
-rw-r--r--doc/user/project/merge_requests/merge_when_pipeline_succeeds.md23
-rw-r--r--doc/user/project/merge_requests/resolve_conflicts.md16
-rw-r--r--doc/user/project/merge_requests/revert_changes.md29
-rw-r--r--doc/user/project/merge_requests/sast.md4
-rw-r--r--doc/user/project/merge_requests/sast_docker.md4
-rw-r--r--doc/user/project/merge_requests/squash_and_merge.md45
-rw-r--r--doc/user/project/merge_requests/versions.md34
-rw-r--r--doc/user/project/merge_requests/work_in_progress_merge_requests.md24
-rw-r--r--doc/user/project/milestones/burndown_charts.md2
-rw-r--r--doc/user/project/milestones/index.md2
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md9
-rw-r--r--doc/user/project/packages/maven_repository.md2
-rw-r--r--doc/user/project/packages/npm_registry.md2
-rw-r--r--doc/user/project/pages/getting_started_part_four.md8
-rw-r--r--doc/user/project/pages/getting_started_part_one.md3
-rw-r--r--doc/user/project/pages/getting_started_part_three.md10
-rw-r--r--doc/user/project/pages/getting_started_part_two.md20
-rw-r--r--doc/user/project/pages/index.md5
-rw-r--r--doc/user/project/pages/introduction.md193
-rw-r--r--doc/user/project/pages/lets_encrypt_for_gitlab_pages.md2
-rw-r--r--doc/user/project/pipelines/job_artifacts.md16
-rw-r--r--doc/user/project/pipelines/settings.md16
-rw-r--r--doc/user/project/repository/index.md6
-rw-r--r--doc/user/project/repository/reducing_the_repo_size_using_git.md9
-rw-r--r--doc/user/project/security_dashboard.md4
-rw-r--r--doc/user/project/settings/index.md4
-rw-r--r--doc/user/search/advanced_global_search.md2
-rw-r--r--doc/user/search/advanced_search_syntax.md2
-rw-r--r--doc/workflow/README.md10
-rw-r--r--doc/workflow/gitlab_flow.md2
-rw-r--r--doc/workflow/img/notification_global_settings.pngbin37542 -> 118914 bytes
-rw-r--r--doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md64
-rw-r--r--doc/workflow/merge_request_approvals.md4
-rw-r--r--doc/workflow/notifications.md8
-rw-r--r--doc/workflow/repository_mirroring.md4
-rw-r--r--lib/api/commits.rb10
-rw-r--r--lib/api/discussions.rb2
-rw-r--r--lib/api/entities.rb8
-rw-r--r--lib/api/helpers.rb2
-rw-r--r--lib/api/helpers/pagination.rb2
-rw-r--r--lib/api/helpers/projects_helpers.rb75
-rw-r--r--lib/api/helpers/settings_helpers.rb19
-rw-r--r--lib/api/merge_requests.rb35
-rw-r--r--lib/api/project_import.rb3
-rw-r--r--lib/api/projects.rb23
-rw-r--r--lib/api/settings.rb52
-rw-r--r--lib/banzai/commit_renderer.rb2
-rw-r--r--lib/banzai/filter/wiki_link_filter/rewriter.rb8
-rw-r--r--lib/banzai/redactor.rb7
-rw-r--r--lib/gitlab.rb5
-rw-r--r--lib/gitlab/background_migration/calculate_wiki_sizes.rb18
-rw-r--r--lib/gitlab/background_migration/fill_valid_time_for_pages_domain_certificate.rb40
-rw-r--r--lib/gitlab/background_migration/reset_merge_status.rb17
-rw-r--r--lib/gitlab/ci/config/entry/image.rb2
-rw-r--r--lib/gitlab/ci/config/entry/job.rb2
-rw-r--r--lib/gitlab/ci/config/entry/service.rb3
-rw-r--r--lib/gitlab/ci/config/extendable/entry.rb59
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/and.rb27
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/base.rb6
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/equals.rb9
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/matches.rb25
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/not_equals.rb9
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb9
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/operator.rb20
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/or.rb27
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb13
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexer.rb19
-rw-r--r--lib/gitlab/ci/pipeline/expression/parser.rb70
-rw-r--r--lib/gitlab/ci/pipeline/expression/statement.rb26
-rw-r--r--lib/gitlab/ci/templates/.yamllint5
-rw-r--r--lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Android.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml6
-rw-r--r--lib/gitlab/ci/templates/C++.gitlab-ci.yml8
-rw-r--r--lib/gitlab/ci/templates/Chef.gitlab-ci.yml34
-rw-r--r--lib/gitlab/ci/templates/Clojure.gitlab-ci.yml8
-rw-r--r--lib/gitlab/ci/templates/Crystal.gitlab-ci.yml6
-rw-r--r--lib/gitlab/ci/templates/Django.gitlab-ci.yml8
-rw-r--r--lib/gitlab/ci/templates/Elixir.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Go.gitlab-ci.yml28
-rw-r--r--lib/gitlab/ci/templates/Grails.gitlab-ci.yml36
-rw-r--r--lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Julia.gitlab-ci.yml15
-rw-r--r--lib/gitlab/ci/templates/Laravel.gitlab-ci.yml20
-rw-r--r--lib/gitlab/ci/templates/Maven.gitlab-ci.yml74
-rw-r--r--lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml10
-rw-r--r--lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/PHP.gitlab-ci.yml24
-rw-r--r--lib/gitlab/ci/templates/Pages/Brunch.gitlab-ci.yml10
-rw-r--r--lib/gitlab/ci/templates/Pages/Doxygen.gitlab-ci.yml10
-rw-r--r--lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml10
-rw-r--r--lib/gitlab/ci/templates/Pages/HTML.gitlab-ci.yml10
-rw-r--r--lib/gitlab/ci/templates/Pages/Harp.gitlab-ci.yml10
-rw-r--r--lib/gitlab/ci/templates/Pages/Hexo.gitlab-ci.yml10
-rw-r--r--lib/gitlab/ci/templates/Pages/Hugo.gitlab-ci.yml10
-rw-r--r--lib/gitlab/ci/templates/Pages/Hyde.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Pages/JBake.gitlab-ci.yml26
-rw-r--r--lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml15
-rw-r--r--lib/gitlab/ci/templates/Pages/Jigsaw.gitlab-ci.yml12
-rw-r--r--lib/gitlab/ci/templates/Pages/Lektor.gitlab-ci.yml8
-rw-r--r--lib/gitlab/ci/templates/Pages/Metalsmith.gitlab-ci.yml12
-rw-r--r--lib/gitlab/ci/templates/Pages/Middleman.gitlab-ci.yml22
-rw-r--r--lib/gitlab/ci/templates/Pages/Nanoc.gitlab-ci.yml8
-rw-r--r--lib/gitlab/ci/templates/Pages/Octopress.gitlab-ci.yml14
-rw-r--r--lib/gitlab/ci/templates/Pages/Pelican.gitlab-ci.yml6
-rw-r--r--lib/gitlab/ci/templates/Python.gitlab-ci.yml28
-rw-r--r--lib/gitlab/ci/templates/Ruby.gitlab-ci.yml16
-rw-r--r--lib/gitlab/ci/templates/Rust.gitlab-ci.yml18
-rw-r--r--lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml32
-rw-r--r--lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Serverless.gitlab-ci.yml18
-rw-r--r--lib/gitlab/ci/templates/Swift.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/dotNET-Core.yml113
-rw-r--r--lib/gitlab/ci/templates/dotNET.gitlab-ci.yml27
-rw-r--r--lib/gitlab/ci/templates/iOS-Fastlane.gitlab-ci.yml8
-rw-r--r--lib/gitlab/danger/helper.rb33
-rw-r--r--lib/gitlab/danger/roulette.rb84
-rw-r--r--lib/gitlab/danger/teammate.rb29
-rw-r--r--lib/gitlab/diff/suggestions_parser.rb6
-rw-r--r--lib/gitlab/git/repository.rb33
-rw-r--r--lib/gitlab/git_ref_validator.rb23
-rw-r--r--lib/gitlab/gitaly_client.rb2
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb7
-rw-r--r--lib/gitlab/github_import/parallel_importer.rb18
-rw-r--r--lib/gitlab/graphql/loaders/batch_model_loader.rb2
-rw-r--r--lib/gitlab/graphql/loaders/batch_project_statistics_loader.rb23
-rw-r--r--lib/gitlab/graphql/query_analyzers/log_query_complexity.rb18
-rw-r--r--lib/gitlab/graphql/query_analyzers/logger_analyzer.rb71
-rw-r--r--lib/gitlab/graphql/representation/tree_entry.rb31
-rw-r--r--lib/gitlab/graphql_logger.rb9
-rw-r--r--lib/gitlab/http.rb9
-rw-r--r--lib/gitlab/http_connection_adapter.rb (renamed from lib/gitlab/proxy_http_connection_adapter.rb)24
-rw-r--r--lib/gitlab/import/set_async_jid.rb27
-rw-r--r--lib/gitlab/import_export/attribute_cleaner.rb7
-rw-r--r--lib/gitlab/import_sources.rb3
-rw-r--r--lib/gitlab/kubernetes/helm/api.rb1
-rw-r--r--lib/gitlab/lets_encrypt/challenge.rb2
-rw-r--r--lib/gitlab/lets_encrypt/client.rb23
-rw-r--r--lib/gitlab/lets_encrypt/order.rb11
-rw-r--r--lib/gitlab/markdown_cache.rb12
-rw-r--r--lib/gitlab/markdown_cache/active_record/extension.rb49
-rw-r--r--lib/gitlab/markdown_cache/field_data.rb35
-rw-r--r--lib/gitlab/markdown_cache/redis/extension.rb63
-rw-r--r--lib/gitlab/markdown_cache/redis/store.rb56
-rw-r--r--lib/gitlab/metrics/samplers/puma_sampler.rb3
-rw-r--r--lib/gitlab/metrics/samplers/ruby_sampler.rb33
-rw-r--r--lib/gitlab/metrics/samplers/unicorn_sampler.rb36
-rw-r--r--lib/gitlab/metrics/system.rb28
-rw-r--r--lib/gitlab/omniauth_initializer.rb6
-rw-r--r--lib/gitlab/path_regex.rb1
-rw-r--r--lib/gitlab/performance_bar/peek_query_tracker.rb7
-rw-r--r--lib/gitlab/phabricator_import.rb12
-rw-r--r--lib/gitlab/phabricator_import/base_worker.rb80
-rw-r--r--lib/gitlab/phabricator_import/cache/map.rb65
-rw-r--r--lib/gitlab/phabricator_import/conduit.rb9
-rw-r--r--lib/gitlab/phabricator_import/conduit/client.rb41
-rw-r--r--lib/gitlab/phabricator_import/conduit/maniphest.rb28
-rw-r--r--lib/gitlab/phabricator_import/conduit/pagination.rb24
-rw-r--r--lib/gitlab/phabricator_import/conduit/response.rb60
-rw-r--r--lib/gitlab/phabricator_import/conduit/tasks_response.rb24
-rw-r--r--lib/gitlab/phabricator_import/import_tasks_worker.rb10
-rw-r--r--lib/gitlab/phabricator_import/importer.rb44
-rw-r--r--lib/gitlab/phabricator_import/issues/importer.rb42
-rw-r--r--lib/gitlab/phabricator_import/issues/task_importer.rb54
-rw-r--r--lib/gitlab/phabricator_import/project_creator.rb78
-rw-r--r--lib/gitlab/phabricator_import/representation/task.rb60
-rw-r--r--lib/gitlab/phabricator_import/worker_state.rb47
-rw-r--r--lib/gitlab/project_search_results.rb6
-rw-r--r--lib/gitlab/search_results.rb32
-rw-r--r--lib/gitlab/setup_helper.rb7
-rw-r--r--lib/gitlab/url_blocker.rb75
-rw-r--r--lib/gitlab/url_sanitizer.rb4
-rw-r--r--lib/tasks/gettext.rake1
-rw-r--r--lib/tasks/gitlab/assets.rake6
-rw-r--r--lib/tasks/gitlab/shell.rake12
-rw-r--r--locale/gitlab.pot582
-rw-r--r--package.json11
-rw-r--r--qa/Gemfile1
-rw-r--r--qa/Gemfile.lock3
-rw-r--r--qa/README.md12
-rw-r--r--qa/docs/writing_tests_from_scratch.md482
-rw-r--r--qa/knapsack/gitlab-ce/review-qa-all_master_report.json42
-rw-r--r--qa/qa/ce/strategy.rb9
-rw-r--r--qa/qa/fixtures/auto_devops_rack/Dockerfile9
-rw-r--r--qa/qa/page/base.rb17
-rw-r--r--qa/qa/page/file/form.rb2
-rw-r--r--qa/qa/page/project/branches/show.rb8
-rw-r--r--qa/qa/page/project/new.rb5
-rw-r--r--qa/qa/page/project/operations/kubernetes/show.rb4
-rw-r--r--qa/qa/page/project/web_ide/edit.rb27
-rw-r--r--qa/qa/resource/base.rb64
-rw-r--r--qa/qa/resource/file.rb31
-rw-r--r--qa/qa/resource/project.rb7
-rw-r--r--qa/qa/resource/repository/project_push.rb2
-rw-r--r--qa/qa/runtime/api/client.rb9
-rw-r--r--qa/qa/runtime/env.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb7
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb9
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb26
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb11
-rw-r--r--qa/qa/specs/runner.rb19
-rw-r--r--qa/qa/support/page/logging.rb13
-rw-r--r--qa/spec/page/logging_spec.rb12
-rw-r--r--qa/spec/runtime/env_spec.rb24
-rw-r--r--qa/spec/spec_helper.rb5
-rw-r--r--spec/controllers/acme_challenges_controller_spec.rb44
-rw-r--r--spec/controllers/application_controller_spec.rb34
-rw-r--r--spec/controllers/concerns/import_url_params_spec.rb56
-rw-r--r--spec/controllers/groups_controller_spec.rb22
-rw-r--r--spec/controllers/import/fogbugz_controller_spec.rb38
-rw-r--r--spec/controllers/import/phabricator_controller_spec.rb92
-rw-r--r--spec/controllers/projects/avatars_controller_spec.rb2
-rw-r--r--spec/controllers/projects/ci/lints_controller_spec.rb4
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb14
-rw-r--r--spec/controllers/projects/imports_controller_spec.rb15
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb31
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb4
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb18
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb34
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb2
-rw-r--r--spec/controllers/projects/serverless/functions_controller_spec.rb92
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb17
-rw-r--r--spec/controllers/projects_controller_spec.rb12
-rw-r--r--spec/controllers/sent_notifications_controller_spec.rb109
-rw-r--r--spec/controllers/sessions_controller_spec.rb34
-rw-r--r--spec/factories/ci/builds.rb11
-rw-r--r--spec/factories/ci/job_artifacts.rb18
-rw-r--r--spec/factories/ci/pipeline_schedule.rb10
-rw-r--r--spec/factories/merge_requests.rb3
-rw-r--r--spec/factories/pages_domain_acme_orders.rb17
-rw-r--r--spec/factories/project_auto_devops.rb1
-rw-r--r--spec/features/admin/admin_appearance_spec.rb2
-rw-r--r--spec/features/admin/admin_sees_project_statistics_spec.rb2
-rw-r--r--spec/features/admin/admin_settings_spec.rb13
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb2
-rw-r--r--spec/features/commits_spec.rb6
-rw-r--r--spec/features/global_search_spec.rb14
-rw-r--r--spec/features/groups/merge_requests_spec.rb2
-rw-r--r--spec/features/instance_statistics/conversational_development_index_spec.rb2
-rw-r--r--spec/features/issuables/issuable_list_spec.rb2
-rw-r--r--spec/features/issues_spec.rb2
-rw-r--r--spec/features/markdown/gitlab_flavored_markdown_spec.rb3
-rw-r--r--spec/features/merge_request/user_creates_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb25
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb21
-rw-r--r--spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb6
-rw-r--r--spec/features/project_variables_spec.rb2
-rw-r--r--spec/features/projects/clusters_spec.rb2
-rw-r--r--spec/features/projects/commits/user_browses_commits_spec.rb2
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb1
-rw-r--r--spec/features/projects/jobs/permissions_spec.rb2
-rw-r--r--spec/features/projects/jobs/user_browses_job_spec.rb4
-rw-r--r--spec/features/projects/jobs/user_browses_jobs_spec.rb6
-rw-r--r--spec/features/projects/jobs_spec.rb18
-rw-r--r--spec/features/projects/new_project_spec.rb30
-rw-r--r--spec/features/projects/pages_lets_encrypt_spec.rb131
-rw-r--r--spec/features/projects/pages_spec.rb36
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb16
-rw-r--r--spec/features/projects/serverless/functions_spec.rb59
-rw-r--r--spec/features/projects/settings/forked_project_settings_spec.rb1
-rw-r--r--spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb2
-rw-r--r--spec/features/projects_spec.rb3
-rw-r--r--spec/features/security/profile_access_spec.rb2
-rw-r--r--spec/features/users/signup_spec.rb44
-rw-r--r--spec/finders/clusters/knative_services_finder_spec.rb105
-rw-r--r--spec/finders/projects/serverless/functions_finder_spec.rb68
-rw-r--r--spec/fixtures/phabricator_responses/auth_failed.json1
-rw-r--r--spec/fixtures/phabricator_responses/maniphest.search.json98
-rw-r--r--spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json422
-rw-r--r--spec/fixtures/security-reports/master/gl-dast-report.json74
-rw-r--r--spec/frontend/activities_spec.js (renamed from spec/javascripts/activities_spec.js)3
-rw-r--r--spec/frontend/api_spec.js (renamed from spec/javascripts/api_spec.js)4
-rw-r--r--spec/frontend/autosave_spec.js (renamed from spec/javascripts/autosave_spec.js)19
-rw-r--r--spec/frontend/boards/modal_store_spec.js4
-rw-r--r--spec/frontend/clusters/clusters_bundle_spec.js33
-rw-r--r--spec/frontend/clusters/components/application_row_spec.js76
-rw-r--r--spec/frontend/clusters/components/applications_spec.js97
-rw-r--r--spec/frontend/clusters/components/knative_domain_editor_spec.js141
-rw-r--r--spec/frontend/clusters/services/application_state_machine_spec.js2
-rw-r--r--spec/frontend/clusters/stores/clusters_store_spec.js5
-rw-r--r--spec/frontend/helpers/jest_helpers.js24
-rw-r--r--spec/frontend/helpers/jquery.js6
-rw-r--r--spec/frontend/helpers/local_storage_helper.js41
-rw-r--r--spec/frontend/helpers/timeout.js34
-rw-r--r--spec/frontend/helpers/vue_test_utils_helper.js4
-rw-r--r--spec/frontend/ide/stores/modules/commit/mutations_spec.js15
-rw-r--r--spec/frontend/lib/utils/number_utility_spec.js11
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js (renamed from spec/javascripts/lib/utils/url_utility_spec.js)116
-rw-r--r--spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap2
-rw-r--r--spec/frontend/notes/components/note_app_spec.js (renamed from spec/javascripts/notes/components/note_app_spec.js)203
-rw-r--r--spec/frontend/notes/old_notes_spec.js (renamed from spec/javascripts/notes_spec.js)493
-rw-r--r--spec/frontend/operation_settings/components/external_dashboard_spec.js160
-rw-r--r--spec/frontend/operation_settings/store/mutations_spec.js19
-rw-r--r--spec/frontend/reports/components/report_section_spec.js40
-rw-r--r--spec/frontend/repository/components/table/parent_row_spec.js64
-rw-r--r--spec/frontend/repository/components/table/row_spec.js12
-rw-r--r--spec/frontend/serverless/components/environment_row_spec.js8
-rw-r--r--spec/frontend/serverless/components/functions_spec.js27
-rw-r--r--spec/frontend/serverless/mock_data.js110
-rw-r--r--spec/frontend/serverless/store/getters_spec.js2
-rw-r--r--spec/frontend/serverless/store/mutations_spec.js4
-rw-r--r--spec/frontend/test_setup.js2
-rw-r--r--spec/frontend/vue_mr_widget/stores/get_state_key_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_warning_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/modal_copy_button_spec.js40
-rw-r--r--spec/frontend/vue_shared/droplab_dropdown_button_spec.js136
-rw-r--r--spec/graphql/features/authorization_spec.rb2
-rw-r--r--spec/graphql/gitlab_schema_spec.rb58
-rw-r--r--spec/graphql/resolvers/base_resolver_spec.rb22
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/namespace_projects_resolver_spec.rb69
-rw-r--r--spec/graphql/types/base_field_spec.rb31
-rw-r--r--spec/graphql/types/issue_type_spec.rb6
-rw-r--r--spec/graphql/types/namespace_type_spec.rb (renamed from spec/graphql/types/namespace_type.rb)2
-rw-r--r--spec/graphql/types/project_statistics_type_spec.rb11
-rw-r--r--spec/graphql/types/project_type_spec.rb2
-rw-r--r--spec/graphql/types/query_type_spec.rb12
-rw-r--r--spec/graphql/types/tree/blob_type_spec.rb2
-rw-r--r--spec/graphql/types/tree/tree_entry_type_spec.rb2
-rw-r--r--spec/helpers/emails_helper_spec.rb8
-rw-r--r--spec/helpers/gitlab_routing_helper_spec.rb2
-rw-r--r--spec/helpers/markup_helper_spec.rb3
-rw-r--r--spec/helpers/storage_helper_spec.rb3
-rw-r--r--spec/helpers/tracking_helper_spec.rb11
-rw-r--r--spec/helpers/visibility_level_helper_spec.rb45
-rw-r--r--spec/javascripts/boards/board_card_spec.js4
-rw-r--r--spec/javascripts/boards/boards_store_spec.js91
-rw-r--r--spec/javascripts/boards/issue_card_spec.js4
-rw-r--r--spec/javascripts/boards/issue_spec.js4
-rw-r--r--spec/javascripts/boards/list_spec.js4
-rw-r--r--spec/javascripts/boards/mock_data.js8
-rw-r--r--spec/javascripts/ci_variable_list/ajax_variable_list_spec.js8
-rw-r--r--spec/javascripts/ci_variable_list/ci_variable_list_spec.js59
-rw-r--r--spec/javascripts/diffs/components/commit_item_spec.js4
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js11
-rw-r--r--spec/javascripts/helpers/vue_test_utils_helper.js4
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/actions_spec.js43
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js73
-rw-r--r--spec/javascripts/ide/mock_data.js1
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js16
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js5
-rw-r--r--spec/javascripts/ide/stores/getters_spec.js32
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js157
-rw-r--r--spec/javascripts/ide/stores/modules/commit/getters_spec.js29
-rw-r--r--spec/javascripts/jobs/components/stages_dropdown_spec.js21
-rw-r--r--spec/javascripts/jobs/mock_data.js1
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js16
-rw-r--r--spec/javascripts/matchers.js2
-rw-r--r--spec/javascripts/monitoring/charts/single_stat_spec.js28
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js106
-rw-r--r--spec/javascripts/notes/components/note_actions_spec.js3
-rw-r--r--spec/javascripts/pipelines/mock_data.js1
-rw-r--r--spec/javascripts/pipelines/pipeline_url_spec.js5
-rw-r--r--spec/javascripts/projects/project_new_spec.js14
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js28
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js4
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js26
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js5
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js4
-rw-r--r--spec/lib/api/helpers/pagination_spec.rb34
-rw-r--r--spec/lib/banzai/commit_renderer_spec.rb4
-rw-r--r--spec/lib/banzai/filter/external_issue_reference_filter_spec.rb7
-rw-r--r--spec/lib/banzai/filter/wiki_link_filter_spec.rb42
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb28
-rw-r--r--spec/lib/banzai/redactor_spec.rb32
-rw-r--r--spec/lib/banzai/renderer_spec.rb14
-rw-r--r--spec/lib/gitlab/background_migration/delete_diff_files_spec.rb6
-rw-r--r--spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb3
-rw-r--r--spec/lib/gitlab/background_migration/reset_merge_status_spec.rb48
-rw-r--r--spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb62
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/service_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/config/extendable/entry_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/config/external/file/remote_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/external/processor_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb5
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb77
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb55
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb120
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb60
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb120
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb77
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb94
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb50
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb237
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb6
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb97
-rw-r--r--spec/lib/gitlab/danger/roulette_spec.rb101
-rw-r--r--spec/lib/gitlab/danger/teammate_spec.rb51
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb64
-rw-r--r--spec/lib/gitlab/git_ref_validator_spec.rb92
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb11
-rw-r--r--spec/lib/gitlab/github_import/parallel_importer_spec.rb11
-rw-r--r--spec/lib/gitlab/graphql/loaders/batch_project_statistics_loader_spec.rb18
-rw-r--r--spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb25
-rw-r--r--spec/lib/gitlab/graphql/representation/tree_entry_spec.rb20
-rw-r--r--spec/lib/gitlab/graphql_logger_spec.rb40
-rw-r--r--spec/lib/gitlab/http_connection_adapter_spec.rb120
-rw-r--r--spec/lib/gitlab/http_spec.rb28
-rw-r--r--spec/lib/gitlab/import/set_async_jid_spec.rb23
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/attribute_cleaner_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/project.json4
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb20
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb6
-rw-r--r--spec/lib/gitlab/import_sources_spec.rb13
-rw-r--r--spec/lib/gitlab/kubernetes/helm/api_spec.rb24
-rw-r--r--spec/lib/gitlab/lets_encrypt/challenge_spec.rb18
-rw-r--r--spec/lib/gitlab/lets_encrypt/client_spec.rb46
-rw-r--r--spec/lib/gitlab/lets_encrypt/order_spec.rb38
-rw-r--r--spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb179
-rw-r--r--spec/lib/gitlab/markdown_cache/field_data_spec.rb15
-rw-r--r--spec/lib/gitlab/markdown_cache/redis/extension_spec.rb76
-rw-r--r--spec/lib/gitlab/markdown_cache/redis/store_spec.rb68
-rw-r--r--spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb27
-rw-r--r--spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb30
-rw-r--r--spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb25
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb24
-rw-r--r--spec/lib/gitlab/omniauth_initializer_spec.rb8
-rw-r--r--spec/lib/gitlab/phabricator_import/base_worker_spec.rb74
-rw-r--r--spec/lib/gitlab/phabricator_import/cache/map_spec.rb66
-rw-r--r--spec/lib/gitlab/phabricator_import/conduit/client_spec.rb59
-rw-r--r--spec/lib/gitlab/phabricator_import/conduit/maniphest_spec.rb39
-rw-r--r--spec/lib/gitlab/phabricator_import/conduit/response_spec.rb79
-rw-r--r--spec/lib/gitlab/phabricator_import/conduit/tasks_response_spec.rb27
-rw-r--r--spec/lib/gitlab/phabricator_import/import_tasks_worker_spec.rb16
-rw-r--r--spec/lib/gitlab/phabricator_import/importer_spec.rb32
-rw-r--r--spec/lib/gitlab/phabricator_import/issues/importer_spec.rb53
-rw-r--r--spec/lib/gitlab/phabricator_import/issues/task_importer_spec.rb54
-rw-r--r--spec/lib/gitlab/phabricator_import/project_creator_spec.rb58
-rw-r--r--spec/lib/gitlab/phabricator_import/representation/task_spec.rb33
-rw-r--r--spec/lib/gitlab/phabricator_import/worker_state_spec.rb46
-rw-r--r--spec/lib/gitlab/search_results_spec.rb24
-rw-r--r--spec/lib/gitlab/shell_spec.rb10
-rw-r--r--spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb7
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb83
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb2
-rw-r--r--spec/lib/gitlab/url_sanitizer_spec.rb34
-rw-r--r--spec/lib/gitlab_spec.rb30
-rw-r--r--spec/lib/mattermost/session_spec.rb7
-rw-r--r--spec/mailers/emails/pages_domains_spec.rb6
-rw-r--r--spec/mailers/notify_spec.rb10
-rw-r--r--spec/migrations/change_packages_size_defaults_in_project_statistics_spec.rb35
-rw-r--r--spec/migrations/enqueue_reset_merge_status_spec.rb49
-rw-r--r--spec/migrations/enqueue_verify_pages_domain_workers_spec.rb6
-rw-r--r--spec/migrations/generate_lets_encrypt_private_key_spec.rb12
-rw-r--r--spec/migrations/generate_missing_routes_spec.rb2
-rw-r--r--spec/migrations/migrate_old_artifacts_spec.rb4
-rw-r--r--spec/migrations/remove_orphaned_label_links_spec.rb6
-rw-r--r--spec/migrations/schedule_fill_valid_time_for_pages_domain_certificates_spec.rb46
-rw-r--r--spec/models/ci/build_spec.rb150
-rw-r--r--spec/models/ci/job_artifact_spec.rb8
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb145
-rw-r--r--spec/models/ci/pipeline_spec.rb34
-rw-r--r--spec/models/clusters/applications/jupyter_spec.rb2
-rw-r--r--spec/models/clusters/applications/knative_spec.rb76
-rw-r--r--spec/models/clusters/cluster_spec.rb5
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb446
-rw-r--r--spec/models/concerns/milestoneish_spec.rb78
-rw-r--r--spec/models/concerns/noteable_spec.rb12
-rw-r--r--spec/models/diff_note_spec.rb8
-rw-r--r--spec/models/issue_spec.rb15
-rw-r--r--spec/models/merge_request_spec.rb143
-rw-r--r--spec/models/milestone_spec.rb71
-rw-r--r--spec/models/namespace_spec.rb6
-rw-r--r--spec/models/pages_domain_acme_order_spec.rb49
-rw-r--r--spec/models/pages_domain_spec.rb11
-rw-r--r--spec/models/project_ci_cd_setting_spec.rb28
-rw-r--r--spec/models/project_services/assembla_service_spec.rb6
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb3
-rw-r--r--spec/models/project_services/buildkite_service_spec.rb10
-rw-r--r--spec/models/project_services/campfire_service_spec.rb24
-rw-r--r--spec/models/project_services/pivotaltracker_service_spec.rb10
-rw-r--r--spec/models/project_services/pushover_service_spec.rb6
-rw-r--r--spec/models/project_services/teamcity_service_spec.rb3
-rw-r--r--spec/models/project_spec.rb19
-rw-r--r--spec/models/project_statistics_spec.rb82
-rw-r--r--spec/models/resource_label_event_spec.rb4
-rw-r--r--spec/presenters/blob_presenter_spec.rb10
-rw-r--r--spec/presenters/ci/build_runner_presenter_spec.rb64
-rw-r--r--spec/presenters/issue_presenter_spec.rb29
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb10
-rw-r--r--spec/presenters/tree_entry_presenter_spec.rb16
-rw-r--r--spec/requests/api/commits_spec.rb92
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb67
-rw-r--r--spec/requests/api/graphql/group_query_spec.rb11
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb2
-rw-r--r--spec/requests/api/graphql/namespace/projects_spec.rb82
-rw-r--r--spec/requests/api/graphql/project/project_statistics_spec.rb43
-rw-r--r--spec/requests/api/graphql_spec.rb48
-rw-r--r--spec/requests/api/groups_spec.rb3
-rw-r--r--spec/requests/api/issues/get_project_issues_spec.rb4
-rw-r--r--spec/requests/api/jobs_spec.rb8
-rw-r--r--spec/requests/api/merge_requests_spec.rb67
-rw-r--r--spec/requests/api/projects_spec.rb49
-rw-r--r--spec/requests/api/runner_spec.rb26
-rw-r--r--spec/requests/api/search_spec.rb46
-rw-r--r--spec/requests/api/settings_spec.rb1
-rw-r--r--spec/requests/api/system_hooks_spec.rb8
-rw-r--r--spec/requests/api/task_completion_status_spec.rb85
-rw-r--r--spec/routing/import_routing_spec.rb12
-rw-r--r--spec/routing/project_routing_spec.rb19
-rw-r--r--spec/serializers/merge_request_widget_entity_spec.rb46
-rw-r--r--spec/services/auto_merge/base_service_spec.rb144
-rw-r--r--spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb (renamed from spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb)48
-rw-r--r--spec/services/auto_merge_service_spec.rb140
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb6
-rw-r--r--spec/services/ci/pipeline_schedule_service_spec.rb28
-rw-r--r--spec/services/ci/retry_build_service_spec.rb5
-rw-r--r--spec/services/merge_requests/close_service_spec.rb8
-rw-r--r--spec/services/merge_requests/create_pipeline_service_spec.rb71
-rw-r--r--spec/services/merge_requests/merge_to_ref_service_spec.rb41
-rw-r--r--spec/services/merge_requests/mergeability_check_service_spec.rb187
-rw-r--r--spec/services/merge_requests/push_options_handler_service_spec.rb5
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb10
-rw-r--r--spec/services/merge_requests/update_service_spec.rb3
-rw-r--r--spec/services/pages_domains/create_acme_order_service_spec.rb63
-rw-r--r--spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb146
-rw-r--r--spec/services/preview_markdown_service_spec.rb4
-rw-r--r--spec/services/projects/fork_service_spec.rb24
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_service_spec.rb19
-rw-r--r--spec/services/projects/update_pages_service_spec.rb59
-rw-r--r--spec/services/projects/update_service_spec.rb1
-rw-r--r--spec/services/projects/update_statistics_service_spec.rb12
-rw-r--r--spec/services/service_response_spec.rb16
-rw-r--r--spec/services/submit_usage_ping_service_spec.rb4
-rw-r--r--spec/services/suggestions/create_service_spec.rb2
-rw-r--r--spec/services/todos/destroy/confidential_issue_service_spec.rb58
-rw-r--r--spec/services/web_hook_service_spec.rb10
-rw-r--r--spec/support/features/reportable_note_shared_examples.rb4
-rw-r--r--spec/support/features/variable_list_shared_examples.rb32
-rw-r--r--spec/support/helpers/git_helpers.rb8
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb46
-rw-r--r--spec/support/helpers/lets_encrypt_helpers.rb40
-rw-r--r--spec/support/helpers/stub_requests.rb40
-rw-r--r--spec/support/matchers/eq_pem.rb11
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb (renamed from spec/support/shared_context/policies/project_policy_shared_context.rb)0
-rw-r--r--spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb116
-rw-r--r--spec/support/shared_examples/models/update_project_statistics_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/notify_shared_examples.rb45
-rw-r--r--spec/tasks/gitlab/artifacts/migrate_rake_spec.rb55
-rw-r--r--spec/tasks/gitlab/shell_rake_spec.rb18
-rw-r--r--spec/tasks/tokens_spec.rb4
-rw-r--r--spec/uploaders/legacy_artifact_uploader_spec.rb61
-rw-r--r--spec/uploaders/workers/object_storage/background_move_worker_spec.rb34
-rw-r--r--spec/views/projects/commit/_commit_box.html.haml_spec.rb4
-rw-r--r--spec/views/projects/jobs/_build.html.haml_spec.rb10
-rw-r--r--spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb8
-rw-r--r--spec/workers/auto_merge_process_worker_spec.rb31
-rw-r--r--spec/workers/expire_build_instance_artifacts_worker_spec.rb6
-rw-r--r--spec/workers/pages_domain_verification_cron_worker_spec.rb7
-rw-r--r--spec/workers/pages_domain_verification_worker_spec.rb7
-rw-r--r--spec/workers/pipeline_schedule_worker_spec.rb57
-rw-r--r--spec/workers/pipeline_success_worker_spec.rb26
-rw-r--r--spec/workers/project_cache_worker_spec.rb5
-rw-r--r--spec/workers/run_pipeline_schedule_worker_spec.rb32
-rw-r--r--spec/workers/todos_destroyer/confidential_issue_worker_spec.rb13
-rw-r--r--vendor/assets/javascripts/visual_review_toolbar.js (renamed from public/visual-review-toolbar.js)355
-rw-r--r--vendor/jupyter/values.yaml18
-rw-r--r--yarn.lock47
1450 files changed, 23516 insertions, 9091 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index cc71ae9245a..d49b620f22c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -44,3 +44,4 @@ include:
- local: .gitlab/ci/review.gitlab-ci.yml
- local: .gitlab/ci/setup.gitlab-ci.yml
- local: .gitlab/ci/test-metadata.gitlab-ci.yml
+ - local: .gitlab/ci/yaml.gitlab-ci.yml
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS
index 0156a4d749a..d49fa4a49a0 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS
@@ -1,6 +1,6 @@
# Backend Maintainers are the default for all ruby files
-*.rb @ashmckenzie @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @mkozono @nick.thomas @rspeicher @rymai @smcgivern @mayra-cabrera @reprazent
-*.rake @ashmckenzie @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @mkozono @nick.thomas @rspeicher @rymai @smcgivern @mayra-cabrera @reprazent
+*.rb @ashmckenzie @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @mkozono @mayra-cabrera @nick.thomas @rspeicher @rymai @reprazent @smcgivern @tkuah
+*.rake @ashmckenzie @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @mkozono @mayra-cabrera @nick.thomas @rspeicher @rymai @reprazent @smcgivern @tkuah
# Technical writing team are the default reviewers for everything in `doc/`
/doc/ @axil @marcia
@@ -18,3 +18,4 @@ db/ @abrandl @NikolayS
/lib/gitlab/auth/ldap/ @dblessing @mkozono
/lib/gitlab/ci/templates/ @nolith @zj
/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @DylanGriffith @mayra-cabrera @tkuah
+/lib/gitlab/ci/templates/Security/ @plafoucriere @gonzoyumo @twoodham
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index 986ba7558d5..fd5733593ef 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -33,15 +33,16 @@ gitlab:assets:compile:
DOCKER_HOST: tcp://docker:2375
script:
- node --version
- - yarn install --frozen-lockfile --production --cache-folder .yarn-cache
+ - retry yarn install --frozen-lockfile --production --cache-folder .yarn-cache
- free -m
- - bundle exec rake gitlab:assets:compile
+ - retry bundle exec rake gitlab:assets:compile
- time scripts/build_assets_image
- scripts/clean-old-cached-assets
+ - rm -f /etc/apt/sources.list.d/google*.list # We don't need to update Chrome here
# Play dependent manual jobs
- install_api_client_dependencies_with_apt
- - play_job "review-build-cng" || true # this job might not exist so ignore the failure if it cannot be played
- - play_job "schedule:review-build-cng" || true # this job might not exist so ignore the failure if it cannot be played
+ - play_job "review-build-cng" || true # this job might not exist so ignore the failure if it cannot be played
+ - play_job "schedule:review-build-cng" || true # this job might not exist so ignore the failure if it cannot be played
artifacts:
name: webpack-report
expire_in: 31d
@@ -64,9 +65,9 @@ compile-assets:
stage: prepare
script:
- node --version
- - yarn install --frozen-lockfile --cache-folder .yarn-cache
+ - retry yarn install --frozen-lockfile --cache-folder .yarn-cache
- free -m
- - bundle exec rake gitlab:assets:compile
+ - retry bundle exec rake gitlab:assets:compile
- scripts/clean-old-cached-assets
variables:
# we override the max_old_space_size to prevent OOM errors
@@ -140,8 +141,8 @@ jest:
extends: .dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg
dependencies:
- - compile-assets
- - setup-test-env
+ - compile-assets
+ - setup-test-env
script:
- scripts/gitaly-test-spawn
- date
@@ -153,8 +154,8 @@ jest:
expire_in: 31d
when: always
paths:
- - coverage-frontend/
- - junit_jest.xml
+ - coverage-frontend/
+ - junit_jest.xml
reports:
junit: junit_jest.xml
cache:
@@ -219,7 +220,7 @@ lint:javascript:report:
before_script: []
script:
- date
- - yarn run eslint-report || true # ignore exit code
+ - yarn run eslint-report || true # ignore exit code
artifacts:
name: eslint-report
expire_in: 31d
@@ -234,7 +235,7 @@ jsdoc:
before_script: []
script:
- date
- - yarn run jsdoc || true # ignore exit code
+ - yarn run jsdoc || true # ignore exit code
artifacts:
name: jsdoc
expire_in: 31d
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index cf87f5eb39c..aa25bad00e8 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -1,6 +1,6 @@
.dedicated-runner:
retry:
- max: 2 # This is confusing but this means "3 runs at max".
+ max: 2 # This is confusing but this means "3 runs at max".
when:
- unknown_failure
- api_failure
diff --git a/.gitlab/ci/pages.gitlab-ci.yml b/.gitlab/ci/pages.gitlab-ci.yml
index 7d9136b8213..f7b18b809b4 100644
--- a/.gitlab/ci/pages.gitlab-ci.yml
+++ b/.gitlab/ci/pages.gitlab-ci.yml
@@ -1,4 +1,3 @@
-
pages:
extends: .dedicated-no-docs-no-db-pull-cache-job
before_script: []
diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml
index 6b23af952e6..ee9e467886a 100644
--- a/.gitlab/ci/qa.gitlab-ci.yml
+++ b/.gitlab/ci/qa.gitlab-ci.yml
@@ -1,6 +1,6 @@
package-and-qa:
image: ruby:2.6-alpine
- stage: review # So even if review-deploy failed we can still run this
+ stage: review # So even if review-deploy failed we can still run this
when: manual
before_script: []
dependencies: []
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 3f0c899d704..33e7b00784c 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -136,7 +136,7 @@ setup-test-env:
stage: prepare
script:
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
- - scripts/gitaly-test-build # Do not use 'bundle exec' here
+ - scripts/gitaly-test-build # Do not use 'bundle exec' here
artifacts:
expire_in: 7d
paths:
@@ -342,8 +342,8 @@ coverage:
name: coverage
expire_in: 31d
paths:
- - coverage/index.html
- - coverage/assets/
+ - coverage/index.html
+ - coverage/assets/
except:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 80356fa1dc2..9b764028be9 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -76,8 +76,8 @@ schedule:review-build-cng:
.review-deploy-base: &review-deploy-base
<<: *review-base
+ allow_failure: true
stage: review
- retry: 2
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
@@ -128,6 +128,7 @@ review-stop:
.review-qa-base: &review-qa-base
<<: *review-docker
+ allow_failure: true
stage: qa
variables:
<<: *review-docker-variables
@@ -157,7 +158,6 @@ review-stop:
review-qa-smoke:
<<: *review-qa-base
- retry: 2
script:
- gitlab-qa Test::Instance::Smoke "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
@@ -165,7 +165,10 @@ review-qa-all:
<<: *review-qa-base
allow_failure: true
when: manual
+ parallel: 5
script:
+ - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/review-qa-all_master_report.json
+ - export KNAPSACK_TEST_FILE_PATTERN=qa/specs/features/**/*_spec.rb
- gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
.review-performance-base: &review-performance-base
diff --git a/.gitlab/ci/yaml.gitlab-ci.yml b/.gitlab/ci/yaml.gitlab-ci.yml
new file mode 100644
index 00000000000..401318d2df2
--- /dev/null
+++ b/.gitlab/ci/yaml.gitlab-ci.yml
@@ -0,0 +1,9 @@
+# Yamllint of *.yml for .gitlab-ci.yml.
+# This uses rules from project root `.yamllint`.
+lint-ci-gitlab:
+ extends: .dedicated-runner
+ before_script: []
+ dependencies: []
+ image: sdesbure/yamllint:latest
+ script:
+ - yamllint .gitlab-ci.yml .gitlab/ci lib/gitlab/ci/templates
diff --git a/.gitlab/issue_templates/Feature Flag Roll Out.md b/.gitlab/issue_templates/Feature Flag Roll Out.md
new file mode 100644
index 00000000000..b7db5a33faf
--- /dev/null
+++ b/.gitlab/issue_templates/Feature Flag Roll Out.md
@@ -0,0 +1,43 @@
+<!-- Title suggestion: [Feature flag] Enable description of feature -->
+
+## What
+
+Remove the `:feature_name` feature flag ...
+
+## Owners
+
+- Team: NAME_OF_TEAM
+- Most appropriate slack channel to reach out to: `#g_TEAM_NAME`
+- Best individual to reach out to: NAME
+
+## Expectations
+
+### What are we expecting to happen?
+
+### What might happen if this goes wrong?
+
+### What can we monitor to detect problems with this?
+
+<!-- Which dashboards from https://dashboards.gitlab.net are most relevant? -->
+
+## Beta groups/projects
+
+If applicable, any groups/projects that are happy to have this feature turned on early. Some organizations may wish to test big changes they are interested in with a small subset of users ahead of time for example.
+
+- `gitlab-org/gitlab-ce`/`gitlab-org/gitlab-ee` projects
+- `gitlab-org`/`gitlab-com` groups
+- ...
+
+## Roll Out Steps
+
+- [ ] Enable on staging
+- [ ] Test on staging
+- [ ] Ensure that documentation has been updated
+- [ ] Enable on GitLab.com for individual groups/projects listed above and verify behaviour
+- [ ] Announce on the issue an estimated time this will be enabled on GitLab.com
+- [ ] Enable on GitLab.com by running chatops command in `#production`
+- [ ] Cross post chatops slack command to `#support_gitlab-com` and in your team channel
+- [ ] Announce on the issue that the flag has been enabled
+- [ ] Remove feature flag and add changelog entry
+
+/label ~"feature flag"
diff --git a/.gitlab/route-map.yml b/.gitlab/route-map.yml
index 0b37dc68f8b..cc0c2856d49 100644
--- a/.gitlab/route-map.yml
+++ b/.gitlab/route-map.yml
@@ -1,3 +1,3 @@
# Documentation
-- source: /doc/(.+?)\.md/ # doc/administration/build_artifacts.md
- public: '\1.html' # doc/administration/build_artifacts.html
+- source: /doc/(.+?)\.md/ # doc/administration/build_artifacts.md
+ public: '\1.html' # doc/administration/build_artifacts.html
diff --git a/.yamllint b/.yamllint
new file mode 100644
index 00000000000..df7cdf404bc
--- /dev/null
+++ b/.yamllint
@@ -0,0 +1,7 @@
+---
+
+extends: default
+
+rules:
+ line-length: disable
+ document-start: disable
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 88521222b8a..8498658b39c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,34 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 11.11.3 (2019-06-10)
+
+### Fixed (5 changes)
+
+- Fix invalid visibility string comparison in project import. !28612
+- Remove a default git depth in Pipelines for merge requests. !28926
+- Fix connection to Tiller error while uninstalling. !29131
+- Fix label click scrolling to top. !29202
+- Make OpenID Connect work without requiring a name. !29312
+
+
+## 11.11.2 (2019-06-04)
+
+### Fixed (7 changes)
+
+- Update SAST.gitlab-ci.yml - Add SAST_GITLEAKS_ENTROPY_LEVEL. !28607
+- Fix OmniAuth OAuth2Generic strategy not loading. !28680
+- Use source ref in pipeline webhook. !28772
+- Fix migration failure when groups are missing route. !29022
+- Stop two-step rebase from hanging when errors occur. !29068
+- Fix project settings not being able to update. !29097
+- Fix display of 'Promote to group label' button.
+
+### Other (1 change)
+
+- Fix input group height.
+
+
## 11.11.0 (2019-05-22)
### Security (1 change)
@@ -187,6 +215,23 @@ entry.
- Add some frozen string to spec/**/*.rb. (gfyoung)
+## 11.10.6 (2019-06-04)
+
+### Fixed (7 changes, 1 of them is from the community)
+
+- Allow a member to have an access level equal to parent group. !27913
+- Fix uploading of LFS tracked file through UI. !28052
+- Use 3-way merge for squashing commits. !28078
+- Use a path for the related merge requests endpoint. !28171
+- Fix project visibility level validation. !28305 (Peter Marko)
+- Fix Rugged get_tree_entries recursive flag not working. !28494
+- Use source ref in pipeline webhook. !28772
+
+### Other (1 change)
+
+- Fix input group height.
+
+
## 11.10.4 (2019-05-01)
### Fixed (12 changes)
@@ -478,6 +523,24 @@ entry.
- Removes EE differences for environment_item.vue.
+## 11.9.12 (2019-05-30)
+
+### Security (12 changes, 1 of them is from the community)
+
+- Protect Gitlab::HTTP against DNS rebinding attack.
+- Fix project visibility level validation. (Peter Marko)
+- Update Knative version.
+- Add DNS rebinding protection settings.
+- Prevent XSS injection in note imports.
+- Prevent invalid branch for merge request.
+- Filter relative links in wiki for XSS.
+- Fix confidential issue label disclosure on milestone view.
+- Fix url redaction for issue links.
+- Resolve: Milestones leaked via search API.
+- Prevent bypass of restriction disabling web password sign in.
+- Hide confidential issue title on unsubscribe for anonymous users.
+
+
## 11.9.10 (2019-04-26)
### Security (5 changes)
diff --git a/Dangerfile b/Dangerfile
index 9e3a08949b0..d0a605f8d8e 100644
--- a/Dangerfile
+++ b/Dangerfile
@@ -1,5 +1,6 @@
# frozen_string_literal: true
danger.import_plugin('danger/plugins/helper.rb')
+danger.import_plugin('danger/plugins/roulette.rb')
unless helper.release_automation?
danger.import_dangerfile(path: 'danger/metadata')
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index e640847f99c..0a3db35b241 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.42.1
+1.46.0
diff --git a/GITLAB_ELASTICSEARCH_INDEXER_VERSION b/GITLAB_ELASTICSEARCH_INDEXER_VERSION
index 9084fa2f716..26aaba0e866 100644
--- a/GITLAB_ELASTICSEARCH_INDEXER_VERSION
+++ b/GITLAB_ELASTICSEARCH_INDEXER_VERSION
@@ -1 +1 @@
-1.1.0
+1.2.0
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index bc80560fad6..dc1e644a101 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-1.5.0
+1.6.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index deeb3d66ef0..b13d146a7b0 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-9.2.0
+9.3.0
diff --git a/Gemfile b/Gemfile
index 66348414506..c22a3191c3c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -275,7 +275,7 @@ gem 'virtus', '~> 1.0.1'
gem 'base32', '~> 0.3.0'
# Sentry integration
-gem 'sentry-raven', '~> 2.7'
+gem 'sentry-raven', '~> 2.9'
gem 'premailer-rails', '~> 1.9.7'
@@ -336,7 +336,7 @@ group :development, :test do
gem 'database_cleaner', '~> 1.7.0'
gem 'factory_bot_rails', '~> 4.8.2'
gem 'rspec-rails', '~> 3.7.0'
- gem 'rspec-retry', '~> 0.4.5'
+ gem 'rspec-retry', '~> 0.6.1'
gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3'
gem 'rspec-parameterized', require: false
@@ -403,6 +403,7 @@ gem 'ruby-prof', '~> 0.17.0'
gem 'rbtrace', '~> 0.4', require: false
gem 'memory_profiler', '~> 0.9', require: false
gem 'benchmark-memory', '~> 0.1', require: false
+gem 'activerecord-explain-analyze', '~> 0.1', require: false
# OAuth
gem 'oauth2', '~> 1.4'
@@ -425,7 +426,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 1.27.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 1.32.0', require: 'gitaly'
gem 'grpc', '~> 1.19.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 3f85fa958bd..785a77bc202 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -38,6 +38,9 @@ GEM
activemodel (= 5.1.7)
activesupport (= 5.1.7)
arel (~> 8.0)
+ activerecord-explain-analyze (0.1.0)
+ activerecord (>= 4)
+ pg
activerecord_sane_schema_dumper (1.0)
rails (>= 5, < 6)
activesupport (5.1.7)
@@ -139,9 +142,9 @@ GEM
concord (0.1.5)
adamantium (~> 0.2.0)
equalizer (~> 0.0.9)
- concurrent-ruby (1.1.3)
- concurrent-ruby-ext (1.1.3)
- concurrent-ruby (= 1.1.3)
+ concurrent-ruby (1.1.5)
+ concurrent-ruby-ext (1.1.5)
+ concurrent-ruby (= 1.1.5)
connection_pool (2.2.2)
crack (0.4.3)
safe_yaml (~> 1.0.0)
@@ -295,7 +298,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (1.27.0)
+ gitaly-proto (1.32.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-default_value_for (3.1.1)
@@ -795,8 +798,8 @@ GEM
rspec-expectations (~> 3.7.0)
rspec-mocks (~> 3.7.0)
rspec-support (~> 3.7.0)
- rspec-retry (0.4.5)
- rspec-core
+ rspec-retry (0.6.1)
+ rspec-core (> 3.3)
rspec-set (0.1.3)
rspec-support (3.7.1)
rspec_junit_formatter (0.4.1)
@@ -1012,6 +1015,7 @@ DEPENDENCIES
RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0)
acme-client (~> 2.0.2)
+ activerecord-explain-analyze (~> 0.1)
activerecord_sane_schema_dumper (= 1.0)
acts-as-taggable-on (~> 6.0)
addressable (~> 2.5.2)
@@ -1083,7 +1087,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 1.27.0)
+ gitaly-proto (~> 1.32.0)
github-markup (~> 1.7.0)
gitlab-default_value_for (~> 3.1.1)
gitlab-labkit (~> 0.2.0)
@@ -1195,7 +1199,7 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.7.0)
- rspec-retry (~> 0.4.5)
+ rspec-retry (~> 0.6.1)
rspec-set (~> 0.1.3)
rspec_junit_formatter
rspec_profiling (~> 0.0.5)
@@ -1213,7 +1217,7 @@ DEPENDENCIES
scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7)
selenium-webdriver (~> 3.141)
- sentry-raven (~> 2.7)
+ sentry-raven (~> 2.9)
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 4.0.1)
diff --git a/PROCESS.md b/PROCESS.md
index 1f99cebe081..3c40f658070 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -113,7 +113,7 @@ corresponding exception request to be created.
A level of common sense should be applied when deciding whether to have a feature
behind a feature flag off or on by default.
-The following guideliness can be applied to help make this decision:
+The following guidelines can be applied to help make this decision:
* If the feature is not fully ready or functioning, the feature flag should be disabled by default.
* If the feature is ready but there are concerns about performance or impact, the feature flag should be enabled by default, but
@@ -125,7 +125,7 @@ For more information on rolling out changes using feature flags, read [through t
In order to build the final package and present the feature for self-hosted
customers, the feature flag should be removed. This should happen before the
22nd, ideally _at least_ 2 days before. That means MRs with feature
-flags being picked at the 19th would have a quite tight schedule, so picking
+flags being picked at the 19th would have quite a tight schedule, so picking
these _earlier_ is preferable.
While rare, release managers may decide to reject picking a change into a stable
diff --git a/VERSION b/VERSION
index 6a02cfa7c06..11bf1ec035c 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-11.12.0-pre
+12.0.0-pre
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index e583a8affd4..7cebb88f3a4 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -12,7 +12,7 @@ const Api = {
groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id',
- projectLabelsPath: '/:namespace_path/:project_path/labels',
+ projectLabelsPath: '/:namespace_path/:project_path/-/labels',
projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests',
projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index 670f66b005e..c8eb96a625c 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -37,7 +37,7 @@ export default class ShortcutsIssuable extends Shortcuts {
}
// Sanity check: Make sure the selected text comes from a discussion : it can either contain a message...
- let foundMessage = !!documentFragment.querySelector('.md');
+ let foundMessage = Boolean(documentFragment.querySelector('.md'));
// ... Or come from a message
if (!foundMessage) {
diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue
index 47a46502bff..1cbd31729cd 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.vue
+++ b/app/assets/javascripts/boards/components/board_blank_state.vue
@@ -1,6 +1,5 @@
<script>
/* global ListLabel */
-import _ from 'underscore';
import Cookies from 'js-cookie';
import boardsStore from '../stores/boards_store';
@@ -29,8 +28,6 @@ export default {
});
});
- boardsStore.state.lists = _.sortBy(boardsStore.state.lists, 'position');
-
// Save the labels
gl.boardService
.generateDefaultLists()
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index b8882203cc7..179148b6887 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -66,7 +66,7 @@ export default {
eventHub.$emit('clearDetailIssue');
} else {
eventHub.$emit('newDetailIssue', this.issue);
- boardsStore.detail.list = this.list;
+ boardsStore.setListDetail(this.list);
}
}
},
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index c9972d051aa..b1a8b13f3ac 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -142,8 +142,10 @@ export default {
const card = this.$refs.issue[e.oldIndex];
card.showDetail = false;
- boardsStore.moving.list = card.list;
- boardsStore.moving.issue = boardsStore.moving.list.findIssue(+e.item.dataset.issueId);
+
+ const { list } = card;
+ const issue = list.findIssue(Number(e.item.dataset.issueId));
+ boardsStore.startMoving(list, issue);
sortableStart();
},
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index dc1bdc23b5e..cc6af8e88cd 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -72,8 +72,8 @@ export default {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable();
- boardsStore.detail.issue = issue;
- boardsStore.detail.list = this.list;
+ boardsStore.setIssueDetail(issue);
+ boardsStore.setListDetail(this.list);
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index 17de7b2cf1e..a8516f178fc 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -6,7 +6,6 @@ import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import eventHub from '../eventhub';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue';
import boardsStore from '../stores/boards_store';
@@ -136,23 +135,7 @@ export default {
const labelTitle = encodeURIComponent(label.title);
const filter = `label_name[]=${labelTitle}`;
- this.applyFilter(filter);
- },
- applyFilter(filter) {
- const filterPath = boardsStore.filter.path.split('&');
- const filterIndex = filterPath.indexOf(filter);
-
- if (filterIndex === -1) {
- filterPath.push(filter);
- } else {
- filterPath.splice(filterIndex, 1);
- }
-
- boardsStore.filter.path = filterPath.join('&');
-
- boardsStore.updateFiltersUrl();
-
- eventHub.$emit('updateTokens');
+ boardsStore.toggleFilter(filter);
},
labelStyle(label) {
return {
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
index 8e09e265cfb..defa1f75ba2 100644
--- a/app/assets/javascripts/boards/components/modal/index.vue
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -124,7 +124,7 @@ export default {
data.issues.forEach(issueObj => {
const issue = new ListIssue(issueObj);
const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
- issue.selected = !!foundSelectedIssue;
+ issue.selected = Boolean(foundSelectedIssue);
this.issues.push(issue);
});
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index a5ed695af35..c8a9cb1c296 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -2,7 +2,6 @@
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
-import _ from 'underscore';
import CreateLabelDropdown from '../../create_label';
import boardsStore from '../stores/boards_store';
@@ -78,8 +77,6 @@ export default function initNewListDropdown() {
color: label.color,
},
});
-
- boardsStore.state.lists = _.sortBy(boardsStore.state.lists, 'position');
}
},
});
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
index a2b8a0af236..4ab2b17301f 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
@@ -48,7 +48,7 @@ export default Vue.extend({
list.removeIssue(issue);
});
- boardsStore.detail.issue = {};
+ boardsStore.clearDetailIssue();
},
/**
* Build the default patch request.
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 4995a8d9367..f2f37d22b97 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -1,11 +1,10 @@
import $ from 'jquery';
-import _ from 'underscore';
import Vue from 'vue';
import Flash from '~/flash';
import { __ } from '~/locale';
-import '~/vue_shared/models/label';
-import '~/vue_shared/models/assignee';
+import './models/label';
+import './models/assignee';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
@@ -106,18 +105,23 @@ export default () => {
gl.boardService
.all()
.then(res => res.data)
- .then(data => {
- data.forEach(board => {
- const list = boardsStore.addList(board, this.defaultAvatar);
-
- if (list.type === 'closed') {
- list.position = Infinity;
- } else if (list.type === 'backlog') {
- list.position = -1;
+ .then(lists => {
+ lists.forEach(listObj => {
+ let { position } = listObj;
+ if (listObj.list_type === 'closed') {
+ position = Infinity;
+ } else if (listObj.list_type === 'backlog') {
+ position = -1;
}
- });
- this.state.lists = _.sortBy(this.state.lists, 'position');
+ boardsStore.addList(
+ {
+ ...listObj,
+ position,
+ },
+ this.defaultAvatar,
+ );
+ });
boardsStore.addBlankState();
this.loading = false;
@@ -164,10 +168,10 @@ export default () => {
});
}
- boardsStore.detail.issue = newIssue;
+ boardsStore.setIssueDetail(newIssue);
},
clearDetailIssue() {
- boardsStore.detail.issue = {};
+ boardsStore.clearDetailIssue();
},
toggleSubscription(id) {
const { issue } = boardsStore.detail;
diff --git a/app/assets/javascripts/vue_shared/models/assignee.js b/app/assets/javascripts/boards/models/assignee.js
index 4a29b0d0581..4a29b0d0581 100644
--- a/app/assets/javascripts/vue_shared/models/assignee.js
+++ b/app/assets/javascripts/boards/models/assignee.js
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index f8ff20cb0cd..f858b162c6b 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -4,7 +4,7 @@
/* global ListAssignee */
import Vue from 'vue';
-import '~/vue_shared/models/label';
+import './label';
import { isEE, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import IssueProject from './project';
import boardsStore from '../stores/boards_store';
diff --git a/app/assets/javascripts/boards/models/label.js b/app/assets/javascripts/boards/models/label.js
new file mode 100644
index 00000000000..cd2a2c0137f
--- /dev/null
+++ b/app/assets/javascripts/boards/models/label.js
@@ -0,0 +1,11 @@
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+export default class ListLabel {
+ constructor(obj) {
+ Object.assign(this, convertObjectPropsToCamelCase(obj, { dropKeys: ['priority'] }), {
+ priority: obj.priority !== null ? obj.priority : Infinity,
+ });
+ }
+}
+
+window.ListLabel = ListLabel;
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 7e5d0e0f888..a9d88f19146 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -2,8 +2,8 @@
/* global ListIssue */
import { __ } from '~/locale';
-import ListLabel from '~/vue_shared/models/label';
-import ListAssignee from '~/vue_shared/models/assignee';
+import ListLabel from './label';
+import ListAssignee from './assignee';
import { isEE, urlParamsToObject } from '~/lib/utils/common_utils';
import boardsStore from '../stores/boards_store';
import ListMilestone from './milestone';
@@ -37,8 +37,8 @@ class List {
this.type = obj.list_type;
const typeInfo = this.getTypeInfo(this.type);
- this.preset = !!typeInfo.isPreset;
- this.isExpandable = !!typeInfo.isExpandable;
+ this.preset = Boolean(typeInfo.isPreset);
+ this.isExpandable = Boolean(typeInfo.isExpandable);
this.isExpanded = true;
this.page = 1;
this.loading = true;
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 51565c597e6..da82b52330a 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,7 +1,5 @@
-import { __ } from '~/locale';
-
const notImplemented = () => {
- throw new Error(__('Not implemented!'));
+ throw new Error('Not implemented!');
};
export default {
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 70861fbf2b3..4b3b44574a8 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -8,6 +8,7 @@ import Cookies from 'js-cookie';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
+import eventHub from '../eventhub';
const boardsStore = {
disabled: false,
@@ -45,7 +46,7 @@ const boardsStore = {
},
addList(listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar);
- this.state.lists.push(list);
+ this.state.lists = _.sortBy([...this.state.lists, list], 'position');
return list;
},
@@ -82,8 +83,6 @@ const boardsStore = {
title: __('Welcome to your Issue Board!'),
position: 0,
});
-
- this.state.lists = _.sortBy(this.state.lists, 'position');
},
removeBlankState() {
this.removeList('blank');
@@ -111,6 +110,11 @@ const boardsStore = {
});
listFrom.update();
},
+
+ startMoving(list, issue) {
+ Object.assign(this.moving, { list, issue });
+ },
+
moveIssueToList(listFrom, listTo, issue, newIndex) {
const issueTo = listTo.findIssue(issue.id);
const issueLists = issue.getLists();
@@ -185,9 +189,39 @@ const boardsStore = {
findListByLabelId(id) {
return this.state.lists.find(list => list.type === 'label' && list.label.id === id);
},
+
+ toggleFilter(filter) {
+ const filterPath = this.filter.path.split('&');
+ const filterIndex = filterPath.indexOf(filter);
+
+ if (filterIndex === -1) {
+ filterPath.push(filter);
+ } else {
+ filterPath.splice(filterIndex, 1);
+ }
+
+ this.filter.path = filterPath.join('&');
+
+ this.updateFiltersUrl();
+
+ eventHub.$emit('updateTokens');
+ },
+
+ setListDetail(newList) {
+ this.detail.list = newList;
+ },
+
updateFiltersUrl() {
window.history.pushState(null, null, `?${this.filter.path}`);
},
+
+ clearDetailIssue() {
+ this.setIssueDetail({});
+ },
+
+ setIssueDetail(issueDetail) {
+ this.detail.issue = issueDetail;
+ },
};
BoardsStoreEE.initEESpecific(boardsStore);
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 8e61b93e824..77ba68be07e 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -1,8 +1,7 @@
import * as mutationTypes from './mutation_types';
-import { __ } from '~/locale';
const notImplemented = () => {
- throw new Error(__('Not implemented!'));
+ throw new Error('Not implemented!');
};
export default {
diff --git a/app/assets/javascripts/branches/branches_delete_modal.js b/app/assets/javascripts/branches/branches_delete_modal.js
index f34496f84c6..f4c3fa185d8 100644
--- a/app/assets/javascripts/branches/branches_delete_modal.js
+++ b/app/assets/javascripts/branches/branches_delete_modal.js
@@ -23,7 +23,7 @@ class DeleteModal {
const branchData = e.currentTarget.dataset;
this.branchName = branchData.branchName || '';
this.deletePath = branchData.deletePath || '';
- this.isMerged = !!branchData.isMerged;
+ this.isMerged = Boolean(branchData.isMerged);
this.updateModal();
}
diff --git a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
index 592e1fd1c31..0bba2a2e160 100644
--- a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
@@ -27,15 +27,24 @@ function generateErrorBoxContent(errors) {
// Used for the variable list on CI/CD projects/groups settings page
export default class AjaxVariableList {
- constructor({ container, saveButton, errorBox, formField = 'variables', saveEndpoint }) {
+ constructor({
+ container,
+ saveButton,
+ errorBox,
+ formField = 'variables',
+ saveEndpoint,
+ maskableRegex,
+ }) {
this.container = container;
this.saveButton = saveButton;
this.errorBox = errorBox;
this.saveEndpoint = saveEndpoint;
+ this.maskableRegex = maskableRegex;
this.variableList = new VariableList({
container: this.container,
formField,
+ maskableRegex,
});
this.bindEvents();
diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
index 0390a3bf96a..0303e4e51dd 100644
--- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
@@ -16,9 +16,10 @@ function createEnvironmentItem(value) {
}
export default class VariableList {
- constructor({ container, formField }) {
+ constructor({ container, formField, maskableRegex }) {
this.$container = $(container);
this.formField = formField;
+ this.maskableRegex = new RegExp(maskableRegex);
this.environmentDropdownMap = new WeakMap();
this.inputMap = {
@@ -196,9 +197,8 @@ export default class VariableList {
validateMaskability($row) {
const invalidInputClass = 'gl-field-error-outline';
- const maskableRegex = /^\w{8,}$/; // Eight or more alphanumeric characters plus underscores
const variableValue = $row.find(this.inputMap.secret_value.selector).val();
- const isValueMaskable = maskableRegex.test(variableValue) || variableValue === '';
+ const isValueMaskable = this.maskableRegex.test(variableValue) || variableValue === '';
const isMaskedChecked = $row.find(this.inputMap.masked.selector).val() === 'true';
// Show a validation error if the user wants to mask an unmaskable variable value
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 70af333a0dd..aacfa0d87e6 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -142,8 +142,7 @@ export default class Clusters {
addListeners() {
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication);
- eventHub.$on('upgradeApplication', data => this.upgradeApplication(data));
- eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId));
+ eventHub.$on('updateApplication', data => this.updateApplication(data));
eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data));
eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data));
eventHub.$on('uninstallApplication', data => this.uninstallApplication(data));
@@ -155,8 +154,7 @@ export default class Clusters {
removeListeners() {
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication);
- eventHub.$off('upgradeApplication', this.upgradeApplication);
- eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess);
+ eventHub.$off('updateApplication', this.updateApplication);
eventHub.$off('saveKnativeDomain');
eventHub.$off('setKnativeHostname');
eventHub.$off('uninstallApplication');
@@ -331,19 +329,13 @@ export default class Clusters {
});
}
- upgradeApplication(data) {
- const appId = data.id;
-
+ updateApplication({ id: appId, params }) {
this.store.updateApplication(appId);
- this.service.installApplication(appId, data.params).catch(() => {
+ this.service.installApplication(appId, params).catch(() => {
this.store.notifyUpdateFailure(appId);
});
}
- dismissUpgradeSuccess(appId) {
- this.store.acknowledgeSuccessfulUpdate(appId);
- }
-
toggleIngressDomainHelpText({ externalIp }, { externalIp: newExternalIp }) {
if (externalIp !== newExternalIp) {
this.ingressDomainHelpText.classList.toggle('hide', !newExternalIp);
@@ -353,8 +345,10 @@ export default class Clusters {
saveKnativeDomain(data) {
const appId = data.id;
- this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING);
- this.service.updateApplication(appId, data.params);
+ this.store.updateApplication(appId);
+ this.service.updateApplication(appId, data.params).catch(() => {
+ this.store.notifyUpdateFailure(appId);
+ });
}
setKnativeHostname(data) {
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index 5f7675bb432..4771090aa7e 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -2,7 +2,7 @@
/* eslint-disable vue/require-default-prop */
import { GlLink, GlModalDirective } from '@gitlab/ui';
import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
-import { s__, sprintf } from '../../locale';
+import { s__, __, sprintf } from '~/locale';
import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
@@ -85,10 +85,14 @@ export default {
type: String,
required: false,
},
- upgradeAvailable: {
+ updateAvailable: {
type: Boolean,
required: false,
},
+ updateable: {
+ type: Boolean,
+ default: true,
+ },
updateSuccessful: {
type: Boolean,
required: false,
@@ -109,11 +113,6 @@ export default {
required: false,
default: false,
},
- updateAcknowledged: {
- type: Boolean,
- required: false,
- default: true,
- },
installApplicationRequestParams: {
type: Object,
required: false,
@@ -138,7 +137,7 @@ export default {
);
},
hasLogo() {
- return !!this.logoUrl;
+ return Boolean(this.logoUrl);
},
identiconId() {
// generate a deterministic integer id for the identicon background
@@ -170,11 +169,11 @@ export default {
installButtonLabel() {
let label;
if (this.canInstall) {
- label = s__('ClusterIntegration|Install');
+ label = __('Install');
} else if (this.isInstalling) {
- label = s__('ClusterIntegration|Installing');
+ label = __('Installing');
} else if (this.installed) {
- label = s__('ClusterIntegration|Installed');
+ label = __('Installed');
}
return label;
@@ -183,7 +182,7 @@ export default {
return this.manageLink && this.status === APPLICATION_STATUS.INSTALLED;
},
manageButtonLabel() {
- return s__('ClusterIntegration|Manage');
+ return __('Manage');
},
hasError() {
return this.installFailed || this.uninstallFailed;
@@ -203,42 +202,42 @@ export default {
},
versionLabel() {
if (this.updateFailed) {
- return s__('ClusterIntegration|Upgrade failed');
- } else if (this.isUpgrading) {
- return s__('ClusterIntegration|Upgrading');
+ return __('Update failed');
+ } else if (this.isUpdating) {
+ return __('Updating');
}
- return s__('ClusterIntegration|Upgraded');
+ return __('Updated');
},
- upgradeFailureDescription() {
+ updateFailureDescription() {
return s__('ClusterIntegration|Update failed. Please check the logs and try again.');
},
- upgradeSuccessDescription() {
- return sprintf(s__('ClusterIntegration|%{title} upgraded successfully.'), {
+ updateSuccessDescription() {
+ return sprintf(s__('ClusterIntegration|%{title} updated successfully.'), {
title: this.title,
});
},
- upgradeButtonLabel() {
+ updateButtonLabel() {
let label;
- if (this.upgradeAvailable && !this.updateFailed && !this.isUpgrading) {
- label = s__('ClusterIntegration|Upgrade');
- } else if (this.isUpgrading) {
- label = s__('ClusterIntegration|Updating');
+ if (this.updateAvailable && !this.updateFailed && !this.isUpdating) {
+ label = __('Update');
+ } else if (this.isUpdating) {
+ label = __('Updating');
} else if (this.updateFailed) {
- label = s__('ClusterIntegration|Retry update');
+ label = __('Retry update');
}
return label;
},
- isUpgrading() {
+ isUpdating() {
// Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend
return this.status === APPLICATION_STATUS.UPDATING;
},
- shouldShowUpgradeDetails() {
+ shouldShowUpdateDetails() {
// This method only returns true when;
- // Upgrade was successful OR Upgrade failed
- // AND new upgrade is unavailable AND version information is present.
- return (this.updateSuccessful || this.updateFailed) && !this.upgradeAvailable && this.version;
+ // Update was successful OR Update failed
+ // AND new update is unavailable AND version information is present.
+ return (this.updateSuccessful || this.updateFailed) && !this.updateAvailable && this.version;
},
uninstallSuccessDescription() {
return sprintf(s__('ClusterIntegration|%{title} uninstalled successfully.'), {
@@ -249,7 +248,7 @@ export default {
watch: {
updateSuccessful(updateSuccessful) {
if (updateSuccessful) {
- this.$toast.show(this.upgradeSuccessDescription);
+ this.$toast.show(this.updateSuccessDescription);
}
},
uninstallSuccessful(uninstallSuccessful) {
@@ -265,8 +264,8 @@ export default {
params: this.installApplicationRequestParams,
});
},
- upgradeClicked() {
- eventHub.$emit('upgradeApplication', {
+ updateClicked() {
+ eventHub.$emit('updateApplication', {
id: this.id,
params: this.installApplicationRequestParams,
});
@@ -326,36 +325,38 @@ export default {
</ul>
</div>
- <div
- v-if="shouldShowUpgradeDetails"
- class="form-text text-muted label p-0 js-cluster-application-upgrade-details"
- >
- {{ versionLabel }}
- <span v-if="updateSuccessful">to</span>
-
- <gl-link
- v-if="updateSuccessful"
- :href="chartRepo"
- target="_blank"
- class="js-cluster-application-upgrade-version"
- >chart v{{ version }}</gl-link
+ <div v-if="updateable">
+ <div
+ v-if="shouldShowUpdateDetails"
+ class="form-text text-muted label p-0 js-cluster-application-update-details"
>
- </div>
+ {{ versionLabel }}
+ <span v-if="updateSuccessful">to</span>
- <div
- v-if="updateFailed && !isUpgrading"
- class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message"
- >
- {{ upgradeFailureDescription }}
+ <gl-link
+ v-if="updateSuccessful"
+ :href="chartRepo"
+ target="_blank"
+ class="js-cluster-application-update-version"
+ >chart v{{ version }}</gl-link
+ >
+ </div>
+
+ <div
+ v-if="updateFailed && !isUpdating"
+ class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-update-details"
+ >
+ {{ updateFailureDescription }}
+ </div>
+ <loading-button
+ v-if="updateAvailable || updateFailed || isUpdating"
+ class="btn btn-primary js-cluster-application-update-button mt-2"
+ :loading="isUpdating"
+ :disabled="isUpdating"
+ :label="updateButtonLabel"
+ @click="updateClicked"
+ />
</div>
- <loading-button
- v-if="upgradeAvailable || updateFailed || isUpgrading"
- class="btn btn-primary js-cluster-application-upgrade-button mt-2"
- :loading="isUpgrading"
- :disabled="isUpgrading"
- :label="upgradeButtonLabel"
- @click="upgradeClicked"
- />
</div>
<div
:class="{ 'section-25': showManageButton, 'section-15': !showManageButton }"
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 73760da9b98..970f5a7b297 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -15,6 +15,7 @@ import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import KnativeDomainEditor from './knative_domain_editor.vue';
import { CLUSTER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '~/clusters/event_hub';
@@ -25,6 +26,7 @@ export default {
clipboardButton,
LoadingButton,
GlLoadingIcon,
+ KnativeDomainEditor,
},
props: {
type: {
@@ -154,64 +156,21 @@ export default {
knative() {
return this.applications.knative;
},
- knativeInstalled() {
- return (
- this.knative.status === APPLICATION_STATUS.INSTALLED ||
- this.knativeUpgrading ||
- this.knativeUpgradeFailed ||
- this.knative.status === APPLICATION_STATUS.UPDATED
- );
- },
- knativeUpgrading() {
- return (
- this.knative.status === APPLICATION_STATUS.UPDATING ||
- this.knative.status === APPLICATION_STATUS.SCHEDULED
- );
- },
- knativeUpgradeFailed() {
- return this.knative.status === APPLICATION_STATUS.UPDATE_ERRORED;
- },
- knativeExternalEndpoint() {
- return this.knative.externalIp || this.knative.externalHostname;
- },
- knativeDescription() {
- return sprintf(
- _.escape(
- s__(
- `ClusterIntegration|Installing Knative may incur additional costs. Learn more about %{pricingLink}.`,
- ),
- ),
- {
- pricingLink: `<strong><a href="https://cloud.google.com/compute/pricing#lb"
- target="_blank" rel="noopener noreferrer">
- ${_.escape(s__('ClusterIntegration|pricing'))}</a></strong>`,
- },
- false,
- );
- },
- canUpdateKnativeEndpoint() {
- return this.knativeExternalEndpoint && !this.knativeUpgradeFailed && !this.knativeUpgrading;
- },
- knativeHostname: {
- get() {
- return this.knative.hostname;
- },
- set(hostname) {
- eventHub.$emit('setKnativeHostname', {
- id: 'knative',
- hostname,
- });
- },
- },
},
created() {
this.helmInstallIllustration = helmInstallIllustration;
},
methods: {
- saveKnativeDomain() {
+ saveKnativeDomain(hostname) {
eventHub.$emit('saveKnativeDomain', {
id: 'knative',
- params: { hostname: this.knative.hostname },
+ params: { hostname },
+ });
+ },
+ setKnativeHostname(hostname) {
+ eventHub.$emit('setKnativeHostname', {
+ id: 'knative',
+ hostname,
});
},
},
@@ -318,9 +277,9 @@ export default {
generated endpoint in order to access
your application after it has been deployed.`)
}}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
- __('More information')
- }}</a>
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
</p>
</div>
@@ -330,9 +289,9 @@ export default {
the process of being assigned. Please check your Kubernetes
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
- __('More information')
- }}</a>
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
</p>
</template>
<template v-if="!ingressInstalled">
@@ -361,9 +320,9 @@ export default {
<div slot="description">
<p v-html="certManagerDescription"></p>
<div class="form-group">
- <label for="cert-manager-issuer-email">{{
- s__('ClusterIntegration|Issuer Email')
- }}</label>
+ <label for="cert-manager-issuer-email">
+ {{ s__('ClusterIntegration|Issuer Email') }}
+ </label>
<div class="input-group">
<input
v-model="applications.cert_manager.email"
@@ -417,7 +376,7 @@ export default {
:request-reason="applications.runner.requestReason"
:version="applications.runner.version"
:chart-repo="applications.runner.chartRepo"
- :upgrade-available="applications.runner.upgradeAvailable"
+ :update-available="applications.runner.updateAvailable"
:installed="applications.runner.installed"
:install-failed="applications.runner.installFailed"
:update-successful="applications.runner.updateSuccessful"
@@ -491,9 +450,9 @@ export default {
s__(`ClusterIntegration|Replace this with your own hostname if you want.
If you do so, point hostname to Ingress IP Address from above.`)
}}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
- __('More information')
- }}</a>
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
</p>
</div>
</template>
@@ -514,6 +473,7 @@ export default {
:uninstallable="applications.knative.uninstallable"
:uninstall-successful="applications.knative.uninstallSuccessful"
:uninstall-failed="applications.knative.uninstallFailed"
+ :updateable="false"
:disabled="!helmInstalled"
v-bind="applications.knative"
title-link="https://github.com/knative/docs"
@@ -525,9 +485,9 @@ export default {
s__(`ClusterIntegration|You must have an RBAC-enabled cluster
to install Knative.`)
}}
- <a :href="helpPath" target="_blank" rel="noopener noreferrer">{{
- __('More information')
- }}</a>
+ <a :href="helpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
</p>
<br />
</span>
@@ -540,83 +500,13 @@ export default {
}}
</p>
- <div class="row">
- <template v-if="knativeInstalled || (helmInstalled && rbac)">
- <div
- :class="{ 'col-md-6': knativeInstalled, 'col-12': helmInstalled && rbac }"
- class="form-group col-sm-12 mb-0"
- >
- <label for="knative-domainname">
- <strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong>
- </label>
- <input
- id="knative-domainname"
- v-model="knativeHostname"
- type="text"
- class="form-control js-knative-domainname"
- />
- </div>
- </template>
- <template v-if="knativeInstalled">
- <div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0">
- <label for="knative-endpoint">
- <strong>{{ s__('ClusterIntegration|Knative Endpoint:') }}</strong>
- </label>
- <div v-if="knativeExternalEndpoint" class="input-group">
- <input
- id="knative-endpoint"
- :value="knativeExternalEndpoint"
- type="text"
- class="form-control js-knative-endpoint"
- readonly
- />
- <span class="input-group-append">
- <clipboard-button
- :text="knativeExternalEndpoint"
- :title="s__('ClusterIntegration|Copy Knative Endpoint to clipboard')"
- class="input-group-text js-knative-endpoint-clipboard-btn"
- />
- </span>
- </div>
- <div v-else class="input-group">
- <input type="text" class="form-control js-endpoint" readonly />
- <gl-loading-icon
- class="position-absolute align-self-center ml-2 js-knative-ip-loading-icon"
- />
- </div>
- </div>
-
- <p class="form-text text-muted col-12">
- {{
- s__(
- `ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`,
- )
- }}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
- __('More information')
- }}</a>
- </p>
-
- <p
- v-if="!knativeExternalEndpoint"
- class="settings-message js-no-knative-endpoint-message mt-2 mr-3 mb-0 ml-3"
- >
- {{
- s__(`ClusterIntegration|The endpoint is in
- the process of being assigned. Please check your Kubernetes
- cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
- }}
- </p>
-
- <button
- v-if="canUpdateKnativeEndpoint"
- class="btn btn-success js-knative-save-domain-button mt-3 ml-3"
- @click="saveKnativeDomain"
- >
- {{ s__('ClusterIntegration|Save changes') }}
- </button>
- </template>
- </div>
+ <knative-domain-editor
+ v-if="knative.installed || (helmInstalled && rbac)"
+ :knative="knative"
+ :ingress-dns-help-path="ingressDnsHelpPath"
+ @save="saveKnativeDomain"
+ @set="setKnativeHostname"
+ />
</div>
</application-row>
</div>
diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
new file mode 100644
index 00000000000..480228619a5
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
@@ -0,0 +1,150 @@
+<script>
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+import { APPLICATION_STATUS } from '~/clusters/constants';
+
+const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
+
+export default {
+ components: {
+ LoadingButton,
+ ClipboardButton,
+ GlLoadingIcon,
+ },
+ props: {
+ knative: {
+ type: Object,
+ required: true,
+ },
+ ingressDnsHelpPath: {
+ type: String,
+ default: '',
+ },
+ },
+ computed: {
+ saveButtonDisabled() {
+ return [UNINSTALLING, UPDATING].includes(this.knative.status);
+ },
+ saving() {
+ return [UPDATING].includes(this.knative.status);
+ },
+ saveButtonLabel() {
+ return this.saving ? this.__('Saving') : this.__('Save changes');
+ },
+ knativeInstalled() {
+ return this.knative.installed;
+ },
+ knativeExternalEndpoint() {
+ return this.knative.externalIp || this.knative.externalHostname;
+ },
+ knativeUpdateSuccessful() {
+ return this.knative.updateSuccessful;
+ },
+ knativeHostname: {
+ get() {
+ return this.knative.hostname;
+ },
+ set(hostname) {
+ this.$emit('set', hostname);
+ },
+ },
+ },
+ watch: {
+ knativeUpdateSuccessful(updateSuccessful) {
+ if (updateSuccessful) {
+ this.$toast.show(s__('ClusterIntegration|Knative domain name was updated successfully.'));
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="row">
+ <div
+ v-if="knative.updateFailed"
+ class="bs-callout bs-callout-danger cluster-application-banner col-12 mt-2 mb-2 js-cluster-knative-domain-name-failure-message"
+ >
+ {{ s__('ClusterIntegration|Something went wrong while updating Knative domain name.') }}
+ </div>
+
+ <template>
+ <div
+ :class="{ 'col-md-6': knativeInstalled, 'col-12': !knativeInstalled }"
+ class="form-group col-sm-12 mb-0"
+ >
+ <label for="knative-domainname">
+ <strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong>
+ </label>
+ <input
+ id="knative-domainname"
+ v-model="knativeHostname"
+ type="text"
+ class="form-control js-knative-domainname"
+ />
+ </div>
+ </template>
+ <template v-if="knativeInstalled">
+ <div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0">
+ <label for="knative-endpoint">
+ <strong>{{ s__('ClusterIntegration|Knative Endpoint:') }}</strong>
+ </label>
+ <div v-if="knativeExternalEndpoint" class="input-group">
+ <input
+ id="knative-endpoint"
+ :value="knativeExternalEndpoint"
+ type="text"
+ class="form-control js-knative-endpoint"
+ readonly
+ />
+ <span class="input-group-append">
+ <clipboard-button
+ :text="knativeExternalEndpoint"
+ :title="s__('ClusterIntegration|Copy Knative Endpoint to clipboard')"
+ class="input-group-text js-knative-endpoint-clipboard-btn"
+ />
+ </span>
+ </div>
+ <div v-else class="input-group">
+ <input type="text" class="form-control js-endpoint" readonly />
+ <gl-loading-icon
+ class="position-absolute align-self-center ml-2 js-knative-ip-loading-icon"
+ />
+ </div>
+ </div>
+
+ <p class="form-text text-muted col-12">
+ {{
+ s__(
+ `ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`,
+ )
+ }}
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
+ </p>
+
+ <p
+ v-if="!knativeExternalEndpoint"
+ class="settings-message js-no-knative-endpoint-message mt-2 mr-3 mb-0 ml-3"
+ >
+ {{
+ s__(`ClusterIntegration|The endpoint is in
+ the process of being assigned. Please check your Kubernetes
+ cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
+ }}
+ </p>
+
+ <loading-button
+ class="btn-success js-knative-save-domain-button mt-3 ml-3"
+ :loading="saving"
+ :disabled="saveButtonDisabled"
+ :label="saveButtonLabel"
+ @click="$emit('save', knativeHostname)"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters/services/application_state_machine.js b/app/assets/javascripts/clusters/services/application_state_machine.js
index 14b80a116a7..17ea4d77795 100644
--- a/app/assets/javascripts/clusters/services/application_state_machine.js
+++ b/app/assets/javascripts/clusters/services/application_state_machine.js
@@ -123,7 +123,6 @@ const applicationStateMachine = {
target: INSTALLED,
effects: {
updateSuccessful: true,
- updateAcknowledged: false,
},
},
[UPDATE_ERRORED]: {
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 1b4d7e8372c..f64f0ca616f 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -56,8 +56,7 @@ export default class ClusterStore {
title: s__('ClusterIntegration|GitLab Runner'),
version: null,
chartRepo: 'https://gitlab.com/charts/gitlab-runner',
- upgradeAvailable: null,
- updateAcknowledged: true,
+ updateAvailable: null,
updateSuccessful: false,
updateFailed: false,
},
@@ -77,6 +76,8 @@ export default class ClusterStore {
isEditingHostName: false,
externalIp: null,
externalHostname: null,
+ updateSuccessful: false,
+ updateFailed: false,
},
},
};
@@ -134,10 +135,6 @@ export default class ClusterStore {
this.state.applications[appId] = transitionApplicationState(currentAppState, event);
}
- acknowledgeSuccessfulUpdate(appId) {
- this.state.applications[appId].updateAcknowledged = true;
- }
-
updateAppProperty(appId, prop, value) {
this.state.applications[appId][prop] = value;
}
@@ -152,7 +149,7 @@ export default class ClusterStore {
status,
status_reason: statusReason,
version,
- update_available: upgradeAvailable,
+ update_available: updateAvailable,
can_uninstall: uninstallable,
} = serverAppEntry;
const currentApplicationState = this.state.applications[appId] || {};
@@ -189,7 +186,7 @@ export default class ClusterStore {
serverAppEntry.external_hostname || this.state.applications.knative.externalHostname;
} else if (appId === RUNNER) {
this.state.applications.runner.version = version;
- this.state.applications.runner.upgradeAvailable = upgradeAvailable;
+ this.state.applications.runner.updateAvailable = updateAvailable;
}
});
}
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index 9216d4ab372..d0cc4897aeb 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -1,20 +1,21 @@
// ECMAScript polyfills
-import 'core-js/fn/array/fill';
-import 'core-js/fn/array/find';
-import 'core-js/fn/array/find-index';
-import 'core-js/fn/array/from';
-import 'core-js/fn/array/includes';
-import 'core-js/fn/object/assign';
-import 'core-js/fn/object/values';
-import 'core-js/fn/object/entries';
-import 'core-js/fn/promise';
-import 'core-js/fn/promise/finally';
-import 'core-js/fn/string/code-point-at';
-import 'core-js/fn/string/from-code-point';
-import 'core-js/fn/string/includes';
-import 'core-js/fn/symbol';
-import 'core-js/es6/map';
-import 'core-js/es6/weak-map';
+import 'core-js/es/array/fill';
+import 'core-js/es/array/find';
+import 'core-js/es/array/find-index';
+import 'core-js/es/array/from';
+import 'core-js/es/array/includes';
+import 'core-js/es/object/assign';
+import 'core-js/es/object/values';
+import 'core-js/es/object/entries';
+import 'core-js/es/promise';
+import 'core-js/es/promise/finally';
+import 'core-js/es/string/code-point-at';
+import 'core-js/es/string/from-code-point';
+import 'core-js/es/string/includes';
+import 'core-js/es/symbol';
+import 'core-js/es/map';
+import 'core-js/es/weak-map';
+import 'core-js/modules/web.url';
// Browser polyfills
import 'formdata-polyfill';
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index 37a3ceb5341..5bfe158ceda 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -40,7 +40,7 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = (
},
selectable: true,
filterable: true,
- filterRemote: !!$dropdown.data('refsUrl'),
+ filterRemote: Boolean($dropdown.data('refsUrl')),
fieldName: $dropdown.data('fieldName'),
filterInput: 'input[type="search"]',
renderRow: function(ref) {
diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js
index 916b190f469..fa0f04c7d82 100644
--- a/app/assets/javascripts/create_item_dropdown.js
+++ b/app/assets/javascripts/create_item_dropdown.js
@@ -12,7 +12,7 @@ export default class CreateItemDropdown {
this.fieldName = options.fieldName;
this.onSelect = options.onSelect || (() => {});
this.getDataOption = options.getData;
- this.getDataRemote = !!options.filterRemote;
+ this.getDataRemote = Boolean(options.filterRemote);
this.createNewItemFromValueOption = options.createNewItemFromValue;
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index a767379d662..bd7259ce3ee 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -69,7 +69,7 @@ export default {
:link-href="authorUrl"
:img-src="authorAvatar"
:img-alt="authorName"
- :img-size="36"
+ :img-size="40"
class="avatar-cell d-none d-sm-block"
/>
<div class="commit-detail flex-list">
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index 2b3d6d1a3fa..d59b1136677 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -8,6 +8,7 @@ import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_d
import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue';
import InlineDiffView from './inline_diff_view.vue';
import ParallelDiffView from './parallel_diff_view.vue';
+import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import NoteForm from '../../notes/components/note_form.vue';
import ImageDiffOverlay from './image_diff_overlay.vue';
import DiffDiscussions from './diff_discussions.vue';
@@ -26,6 +27,7 @@ export default {
ImageDiffOverlay,
NotDiffableViewer,
NoPreviewViewer,
+ userAvatarLink,
DiffFileDrafts: () => import('ee_component/batch_comments/components/diff_file_drafts.vue'),
},
mixins: [diffLineNoteFormMixin, draftCommentsMixin],
@@ -47,7 +49,7 @@ export default {
}),
...mapGetters('diffs', ['isInlineView', 'isParallelView']),
...mapGetters('diffs', ['getCommentFormForDiffFile']),
- ...mapGetters(['getNoteableData', 'noteableType']),
+ ...mapGetters(['getNoteableData', 'noteableType', 'getUserData']),
diffMode() {
return getDiffMode(this.diffFile);
},
@@ -72,6 +74,9 @@ export default {
diffFileHash() {
return this.diffFile.file_hash;
},
+ author() {
+ return this.getUserData;
+ },
},
methods: {
...mapActions('diffs', ['saveDiffDiscussion', 'closeDiffFileCommentForm']),
@@ -134,6 +139,14 @@ export default {
:can-comment="getNoteableData.current_user.can_create_note"
/>
<div v-if="showNotesContainer" class="note-container">
+ <user-avatar-link
+ v-if="diffFileCommentForm && author"
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="author.name"
+ :img-size="40"
+ class="d-none d-sm-block new-comment"
+ />
<diff-discussions
v-if="diffFile.discussions.length"
class="diff-file-discussions"
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 392de1c9f23..eb9f1465945 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -240,7 +240,7 @@ export default {
css-class="btn-default btn-transparent btn-clipboard"
/>
- <small v-if="isModeChanged" ref="fileMode">
+ <small v-if="isModeChanged" ref="fileMode" class="mr-1">
{{ diffFile.a_mode }} → {{ diffFile.b_mode }}
</small>
@@ -254,16 +254,17 @@ export default {
<diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" />
<div class="btn-group" role="group">
<template v-if="diffFile.blob && diffFile.blob.readable_text">
- <button
- :disabled="!diffHasDiscussions(diffFile)"
- :class="{ active: hasExpandedDiscussions }"
- :title="s__('MergeRequests|Toggle comments for this file')"
- class="js-btn-vue-toggle-comments btn"
- type="button"
- @click="handleToggleDiscussions"
- >
- <icon name="comment" />
- </button>
+ <span v-gl-tooltip.hover :title="s__('MergeRequests|Toggle comments for this file')">
+ <gl-button
+ :disabled="!diffHasDiscussions(diffFile)"
+ :class="{ active: hasExpandedDiscussions }"
+ class="js-btn-vue-toggle-comments btn"
+ type="button"
+ @click="handleToggleDiscussions"
+ >
+ <icon name="comment" />
+ </gl-button>
+ </span>
<edit-button
v-if="!diffFile.deleted_file"
diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
index 0c0a0faa59d..7cf3d90d468 100644
--- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -86,7 +86,6 @@ export default {
:key="note.id"
:img-src="note.author.avatar_url"
:tooltip-text="getTooltipText(note)"
- :size="19"
class="diff-comment-avatar js-diff-comment-avatar"
@click.native="toggleDiscussions"
/>
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index 41670b45798..c209b857652 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -4,11 +4,13 @@ import { s__ } from '~/locale';
import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form';
import noteForm from '../../notes/components/note_form.vue';
import autosave from '../../notes/mixins/autosave';
+import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import { DIFF_NOTE_TYPE } from '../constants';
export default {
components: {
noteForm,
+ userAvatarLink,
},
mixins: [autosave, diffLineNoteFormMixin],
props: {
@@ -41,7 +43,16 @@ export default {
diffViewType: state => state.diffs.diffViewType,
}),
...mapGetters('diffs', ['getDiffFileByHash']),
- ...mapGetters(['isLoggedIn', 'noteableType', 'getNoteableData', 'getNotesDataByProp']),
+ ...mapGetters([
+ 'isLoggedIn',
+ 'noteableType',
+ 'getNoteableData',
+ 'getNotesDataByProp',
+ 'getUserData',
+ ]),
+ author() {
+ return this.getUserData;
+ },
formData() {
return {
noteableData: this.noteableData,
@@ -99,6 +110,14 @@ export default {
<template>
<div class="content discussion-form discussion-form-container discussion-notes">
+ <user-avatar-link
+ v-if="author"
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="author.name"
+ :img-size="40"
+ class="d-none d-sm-block"
+ />
<note-form
ref="noteForm"
:is-editing="true"
diff --git a/app/assets/javascripts/diffs/components/edit_button.vue b/app/assets/javascripts/diffs/components/edit_button.vue
index f0cc5de4b33..dcb79cd5e16 100644
--- a/app/assets/javascripts/diffs/components/edit_button.vue
+++ b/app/assets/javascripts/diffs/components/edit_button.vue
@@ -38,7 +38,7 @@ export default {
<template>
<gl-button
- v-gl-tooltip.bottom
+ v-gl-tooltip.top
:href="editPath"
:title="__('Edit file')"
class="js-edit-blob"
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 35297b7c264..479afc50113 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -211,11 +211,12 @@ export const scrollToLineIfNeededParallel = (_, line) => {
}
};
-export const loadCollapsedDiff = ({ commit, getters }, file) =>
+export const loadCollapsedDiff = ({ commit, getters, state }, file) =>
axios
.get(file.load_collapsed_diff_url, {
params: {
commit_id: getters.commitId,
+ w: state.showWhitespace ? '0' : '1',
},
})
.then(res => {
diff --git a/app/assets/javascripts/emoji/no_emoji_validator.js b/app/assets/javascripts/emoji/no_emoji_validator.js
index 0fd4dd74953..384d62a133a 100644
--- a/app/assets/javascripts/emoji/no_emoji_validator.js
+++ b/app/assets/javascripts/emoji/no_emoji_validator.js
@@ -1,10 +1,11 @@
import { __ } from '~/locale';
import emojiRegex from 'emoji-regex';
+import InputValidator from '../validators/input_validator';
-const invalidInputClass = 'gl-field-error-outline';
-
-export default class NoEmojiValidator {
+export default class NoEmojiValidator extends InputValidator {
constructor(opts = {}) {
+ super();
+
const container = opts.container || '';
this.noEmojiEmelents = document.querySelectorAll(`${container} .js-block-emoji`);
@@ -19,45 +20,14 @@ export default class NoEmojiValidator {
const { value } = this.inputDomElement;
+ this.errorMessage = __('Invalid input, please avoid emojis');
+
this.validatePattern(value);
this.setValidationStateAndMessage();
}
validatePattern(value) {
const pattern = emojiRegex();
- this.hasEmojis = new RegExp(pattern).test(value);
-
- if (this.hasEmojis) {
- this.inputDomElement.setCustomValidity(__('Invalid input, please avoid emojis'));
- } else {
- this.inputDomElement.setCustomValidity('');
- }
- }
-
- setValidationStateAndMessage() {
- if (!this.inputDomElement.checkValidity()) {
- this.setInvalidState();
- } else {
- this.clearFieldValidationState();
- }
- }
-
- clearFieldValidationState() {
- this.inputDomElement.classList.remove(invalidInputClass);
- this.inputErrorMessage.classList.add('hide');
- }
-
- setInvalidState() {
- this.inputDomElement.classList.add(invalidInputClass);
- this.setErrorMessage();
- }
-
- setErrorMessage() {
- if (this.hasEmojis) {
- this.inputErrorMessage.innerHTML = this.inputDomElement.validationMessage;
- } else {
- this.inputErrorMessage.innerHTML = this.inputDomElement.title;
- }
- this.inputErrorMessage.classList.remove('hide');
+ this.invalidInput = new RegExp(pattern).test(value);
}
}
diff --git a/app/assets/javascripts/error_tracking_settings/index.js b/app/assets/javascripts/error_tracking_settings/index.js
index e39452353f5..ce315963723 100644
--- a/app/assets/javascripts/error_tracking_settings/index.js
+++ b/app/assets/javascripts/error_tracking_settings/index.js
@@ -1,10 +1,8 @@
import Vue from 'vue';
import ErrorTrackingSettings from './components/app.vue';
import createStore from './store';
-import initSettingsPanels from '~/settings_panels';
export default () => {
- initSettingsPanels();
const formContainerEl = document.querySelector('.js-error-tracking-form');
const {
dataset: { apiHost, enabled, project, token, listProjectsEndpoint, operationsSettingsEndpoint },
diff --git a/app/assets/javascripts/error_tracking_settings/store/getters.js b/app/assets/javascripts/error_tracking_settings/store/getters.js
index a008b181907..d77e5f15469 100644
--- a/app/assets/javascripts/error_tracking_settings/store/getters.js
+++ b/app/assets/javascripts/error_tracking_settings/store/getters.js
@@ -2,10 +2,10 @@ import _ from 'underscore';
import { __, s__, sprintf } from '~/locale';
import { getDisplayName } from '../utils';
-export const hasProjects = state => !!state.projects && state.projects.length > 0;
+export const hasProjects = state => Boolean(state.projects) && state.projects.length > 0;
export const isProjectInvalid = (state, getters) =>
- !!state.selectedProject &&
+ Boolean(state.selectedProject) &&
getters.hasProjects &&
!state.projects.some(project => _.isMatch(state.selectedProject, project));
diff --git a/app/assets/javascripts/frequent_items/store/actions.js b/app/assets/javascripts/frequent_items/store/actions.js
index 3dd89a82a42..ba62ab67e50 100644
--- a/app/assets/javascripts/frequent_items/store/actions.js
+++ b/app/assets/javascripts/frequent_items/store/actions.js
@@ -51,7 +51,7 @@ export const fetchSearchedItems = ({ state, dispatch }, searchQuery) => {
const params = {
simple: true,
per_page: 20,
- membership: !!gon.current_user_id,
+ membership: Boolean(gon.current_user_id),
};
if (state.namespace === 'projects') {
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index f437954881c..0af9aabd8cf 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import 'at.js';
import _ from 'underscore';
import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache';
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index a143d79097b..05f34391323 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -307,8 +307,8 @@ GitLabDropdown = (function() {
// Set Defaults
this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT);
- this.highlight = !!this.options.highlight;
- this.icon = !!this.options.icon;
+ this.highlight = Boolean(this.options.highlight);
+ this.icon = Boolean(this.options.icon);
this.filterInputBlur =
this.options.filterInputBlur != null ? this.options.filterInputBlur : true;
// If no input is passed create a default one
@@ -565,6 +565,11 @@ GitLabDropdown = (function() {
!$target.data('isLink')
) {
e.stopPropagation();
+
+ // This prevents automatic scrolling to the top
+ if ($target.is('a')) {
+ return false;
+ }
}
return true;
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 5a6d44ef838..a66555838ba 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -13,7 +13,7 @@ export default class GLForm {
const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {};
Object.keys(this.enableGFM).forEach(item => {
if (item !== 'emojis') {
- this.enableGFM[item] = !!dataSources[item];
+ this.enableGFM[item] = Boolean(dataSources[item]);
}
});
// Before we start, we should clean up any previous data for this form
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
index 7c769ab7fa0..7b4e03be8eb 100644
--- a/app/assets/javascripts/ide/components/activity_bar.vue
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -78,7 +78,7 @@ export default {
data-container="body"
data-placement="right"
type="button"
- class="ide-sidebar-link js-ide-commit-mode"
+ class="ide-sidebar-link js-ide-commit-mode qa-commit-mode-tab"
@click.prevent="changedActivityView($event, $options.activityBarViews.commit)"
>
<icon name="commit" />
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index 1824a0f6147..685d8a6b245 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -1,23 +1,24 @@
<script>
import _ from 'underscore';
-import { mapActions, mapState, mapGetters, createNamespacedHelpers } from 'vuex';
+import { mapState, mapGetters, createNamespacedHelpers } from 'vuex';
import { sprintf, __ } from '~/locale';
import consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
+import NewMergeRequestOption from './new_merge_request_option.vue';
-const { mapState: mapCommitState, mapGetters: mapCommitGetters } = createNamespacedHelpers(
+const { mapState: mapCommitState, mapActions: mapCommitActions } = createNamespacedHelpers(
'commit',
);
export default {
components: {
RadioGroup,
+ NewMergeRequestOption,
},
computed: {
...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
- ...mapCommitState(['commitAction', 'shouldCreateMR', 'shouldDisableNewMrOption']),
- ...mapGetters(['currentProject', 'currentBranch', 'currentMergeRequest']),
- ...mapCommitGetters(['shouldDisableNewMrOption']),
+ ...mapCommitState(['commitAction']),
+ ...mapGetters(['currentBranch']),
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
@@ -25,12 +26,12 @@ export default {
false,
);
},
- disableMergeRequestRadio() {
+ containsStagedChanges() {
return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
},
},
watch: {
- disableMergeRequestRadio() {
+ containsStagedChanges() {
this.updateSelectedCommitAction();
},
},
@@ -38,11 +39,11 @@ export default {
this.updateSelectedCommitAction();
},
methods: {
- ...mapActions('commit', ['updateCommitAction', 'toggleShouldCreateMR']),
+ ...mapCommitActions(['updateCommitAction']),
updateSelectedCommitAction() {
if (this.currentBranch && !this.currentBranch.can_push) {
this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH);
- } else if (this.disableMergeRequestRadio) {
+ } else if (this.containsStagedChanges) {
this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
}
},
@@ -56,7 +57,7 @@ export default {
</script>
<template>
- <div class="append-bottom-15 ide-commit-radios">
+ <div class="append-bottom-15 ide-commit-options">
<radio-group
:value="$options.commitToCurrentBranch"
:disabled="currentBranch && !currentBranch.can_push"
@@ -69,17 +70,6 @@ export default {
:label="__('Create a new branch')"
:show-input="true"
/>
- <hr class="my-2" />
- <label class="mb-0">
- <input
- :checked="shouldCreateMR"
- :disabled="shouldDisableNewMrOption"
- type="checkbox"
- @change="toggleShouldCreateMR"
- />
- <span class="prepend-left-10" :class="{ 'text-secondary': shouldDisableNewMrOption }">
- {{ __('Start a new merge request') }}
- </span>
- </label>
+ <new-merge-request-option />
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue
new file mode 100644
index 00000000000..b2e7b15089c
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue
@@ -0,0 +1,43 @@
+<script>
+import { mapGetters, createNamespacedHelpers } from 'vuex';
+
+const {
+ mapState: mapCommitState,
+ mapGetters: mapCommitGetters,
+ mapActions: mapCommitActions,
+} = createNamespacedHelpers('commit');
+
+export default {
+ computed: {
+ ...mapCommitState(['shouldCreateMR']),
+ ...mapCommitGetters(['isCommittingToCurrentBranch', 'isCommittingToDefaultBranch']),
+ ...mapGetters(['hasMergeRequest', 'isOnDefaultBranch']),
+ currentBranchHasMr() {
+ return this.hasMergeRequest && this.isCommittingToCurrentBranch;
+ },
+ showNewMrOption() {
+ return (
+ this.isCommittingToDefaultBranch || !this.currentBranchHasMr || this.isCommittingToNewBranch
+ );
+ },
+ },
+ mounted() {
+ this.setShouldCreateMR();
+ },
+ methods: {
+ ...mapCommitActions(['toggleShouldCreateMR', 'setShouldCreateMR']),
+ },
+};
+</script>
+
+<template>
+ <div v-if="showNewMrOption">
+ <hr class="my-2" />
+ <label class="mb-0">
+ <input :checked="shouldCreateMR" type="checkbox" @change="toggleShouldCreateMR" />
+ <span class="prepend-left-10">
+ {{ __('Start a new merge request') }}
+ </span>
+ </label>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 412b07553dc..f67666f1fbf 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -134,6 +134,7 @@ export default {
<template>
<gl-modal
id="ide-new-entry"
+ class="qa-new-file-modal"
:header-title-text="modalTitle"
:footer-primary-button-text="buttonLabel"
footer-primary-button-variant="success"
diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue
index c98dda00817..6999746f115 100644
--- a/app/assets/javascripts/ide/components/preview/clientside.vue
+++ b/app/assets/javascripts/ide/components/preview/clientside.vue
@@ -105,7 +105,7 @@ export default {
.then(() => {
this.initManager('#ide-preview', this.sandboxOpts, {
fileResolver: {
- isFile: p => Promise.resolve(!!this.entries[createPathWithExt(p)]),
+ isFile: p => Promise.resolve(Boolean(this.entries[createPathWithExt(p)])),
readFile: p => this.loadFileContent(createPathWithExt(p)).then(content => content),
},
});
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 99f1d4a573d..5201c33b1b4 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -30,7 +30,7 @@ export default {
...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommittedChanges', 'activeFile']),
...mapGetters('commit', ['discardDraftButtonDisabled']),
showStageUnstageArea() {
- return !!(this.someUncommittedChanges || this.lastCommitMsg || !this.unusedSeal);
+ return Boolean(this.someUncommittedChanges || this.lastCommitMsg || !this.unusedSeal);
},
activeFileKey() {
return this.activeFile ? this.activeFile.key : null;
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index e15b2a6f76b..b0c4969c5e4 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -125,6 +125,7 @@ export default {
'setFileEOL',
'updateViewer',
'removePendingTab',
+ 'triggerFilesChange',
]),
initEditor() {
if (this.shouldHideEditor) return;
@@ -256,6 +257,7 @@ export default {
'is-added': file.tempFile,
}"
class="multi-file-editor-holder"
+ @focusout="triggerFilesChange"
></div>
<content-viewer
v-if="showContentViewer"
diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js
index e35595ab1fd..dac2a8e8b51 100644
--- a/app/assets/javascripts/ide/lib/editor_options.js
+++ b/app/assets/javascripts/ide/lib/editor_options.js
@@ -11,7 +11,7 @@ export const defaultEditorOptions = {
export default [
{
- readOnly: model => !!model.file.file_lock,
+ readOnly: model => Boolean(model.file.file_lock),
quickSuggestions: model => !(model.language === 'markdown'),
},
];
diff --git a/app/assets/javascripts/ide/lib/keymap.json b/app/assets/javascripts/ide/lib/keymap.json
index 131abfebbed..2db87c07dde 100644
--- a/app/assets/javascripts/ide/lib/keymap.json
+++ b/app/assets/javascripts/ide/lib/keymap.json
@@ -7,5 +7,13 @@
"name": "toggleFileFinder",
"params": true
}
+ },
+ {
+ "id": "save-files",
+ "label": "Save files",
+ "bindings": ["CtrlCmd+KEY_S"],
+ "action": {
+ "name": "triggerFilesChange"
+ }
}
]
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index dc8ca732879..5429b834708 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -99,6 +99,7 @@ export const createTempEntry = (
commit(types.TOGGLE_FILE_OPEN, file.path);
commit(types.ADD_FILE_TO_CHANGED, file.path);
dispatch('setFileActive', file.path);
+ dispatch('triggerFilesChange');
}
if (parentPath && !state.entries[parentPath].opened) {
@@ -210,6 +211,8 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => {
if (entry.parentPath && state.entries[entry.parentPath].tree.length === 0) {
dispatch('deleteEntry', entry.parentPath);
}
+
+ dispatch('triggerFilesChange');
};
export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
@@ -240,6 +243,8 @@ export const renameEntry = (
if (!entryPath && !entry.tempFile) {
dispatch('deleteEntry', path);
}
+
+ dispatch('triggerFilesChange');
};
export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) =>
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index e7e8ac6d80b..dc40a1fa6a2 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -265,3 +265,8 @@ export const removePendingTab = ({ commit }, file) => {
eventHub.$emit(`editor.update.model.dispose.${file.key}`);
};
+
+export const triggerFilesChange = () => {
+ // Used in EE for file mirroring
+ eventHub.$emit('ide.files.change');
+};
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 7a88ac5b116..406903129db 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -42,9 +42,10 @@ export const emptyRepo = state =>
export const currentTree = state =>
state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
-export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
+export const hasChanges = state =>
+ Boolean(state.changedFiles.length) || Boolean(state.stagedFiles.length);
-export const hasMergeRequest = state => !!state.currentMergeRequestId;
+export const hasMergeRequest = state => Boolean(state.currentMergeRequestId);
export const allBlobs = state =>
Object.keys(state.entries)
@@ -70,7 +71,7 @@ export const isCommitModeActive = state => state.currentActivityView === activit
export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review;
export const someUncommittedChanges = state =>
- !!(state.changedFiles.length || state.stagedFiles.length);
+ Boolean(state.changedFiles.length || state.stagedFiles.length);
export const getChangesInFolder = state => path => {
const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f.path, path)).length;
@@ -96,7 +97,12 @@ export const lastCommit = (state, getters) => {
export const currentBranch = (state, getters) =>
getters.currentProject && getters.currentProject.branches[state.currentBranchId];
+export const branchName = (_state, getters) => getters.currentBranch && getters.currentBranch.name;
+
export const packageJson = state => state.entries[packageJsonPath];
+export const isOnDefaultBranch = (_state, getters) =>
+ getters.currentProject && getters.currentProject.default_branch === getters.branchName;
+
// 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 f81bdb8a30e..51062f092ad 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -18,15 +18,34 @@ export const discardDraft = ({ commit }) => {
commit(types.UPDATE_COMMIT_MESSAGE, '');
};
-export const updateCommitAction = ({ commit, rootGetters }, commitAction) => {
+export const updateCommitAction = ({ commit, dispatch }, commitAction) => {
commit(types.UPDATE_COMMIT_ACTION, {
commitAction,
- currentMergeRequest: rootGetters.currentMergeRequest,
});
+ dispatch('setShouldCreateMR');
};
export const toggleShouldCreateMR = ({ commit }) => {
commit(types.TOGGLE_SHOULD_CREATE_MR);
+ commit(types.INTERACT_WITH_NEW_MR);
+};
+
+export const setShouldCreateMR = ({
+ commit,
+ getters,
+ rootGetters,
+ state: { interactedWithNewMR },
+}) => {
+ const committingToExistingMR =
+ getters.isCommittingToCurrentBranch &&
+ rootGetters.hasMergeRequest &&
+ !rootGetters.isOnDefaultBranch;
+
+ if ((getters.isCommittingToDefaultBranch && !interactedWithNewMR) || committingToExistingMR) {
+ commit(types.TOGGLE_SHOULD_CREATE_MR, false);
+ } else if (!interactedWithNewMR) {
+ commit(types.TOGGLE_SHOULD_CREATE_MR, true);
+ }
};
export const updateBranchName = ({ commit }, branchName) => {
@@ -102,7 +121,7 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState, rootGetter
eventHub.$emit(`editor.update.model.content.${file.key}`, {
content: file.content,
- changed: !!changedFile,
+ changed: Boolean(changedFile),
});
});
};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js
index 6aa5d22a4ea..64779e9e4df 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js
@@ -48,8 +48,11 @@ export const preBuiltCommitMessage = (state, _, rootState) => {
export const isCreatingNewBranch = state => state.commitAction === consts.COMMIT_TO_NEW_BRANCH;
-export const shouldDisableNewMrOption = (state, _getters, _rootState, rootGetters) =>
- rootGetters.currentMergeRequest && state.commitAction === consts.COMMIT_TO_CURRENT_BRANCH;
+export const isCommittingToCurrentBranch = state =>
+ state.commitAction === consts.COMMIT_TO_CURRENT_BRANCH;
+
+export const isCommittingToDefaultBranch = (_state, getters, _rootState, rootGetters) =>
+ getters.isCommittingToCurrentBranch && rootGetters.isOnDefaultBranch;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
index 7ad8f3570b7..b81918156b0 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
@@ -3,3 +3,4 @@ export const UPDATE_COMMIT_ACTION = 'UPDATE_COMMIT_ACTION';
export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME';
export const UPDATE_LOADING = 'UPDATE_LOADING';
export const TOGGLE_SHOULD_CREATE_MR = 'TOGGLE_SHOULD_CREATE_MR';
+export const INTERACT_WITH_NEW_MR = 'INTERACT_WITH_NEW_MR';
diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutations.js b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
index be0f894c059..14957d283bb 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
@@ -1,5 +1,4 @@
import * as types from './mutation_types';
-import consts from './constants';
export default {
[types.UPDATE_COMMIT_MESSAGE](state, commitMessage) {
@@ -7,14 +6,8 @@ export default {
commitMessage,
});
},
- [types.UPDATE_COMMIT_ACTION](state, { commitAction, currentMergeRequest }) {
- Object.assign(state, {
- commitAction,
- shouldCreateMR:
- commitAction === consts.COMMIT_TO_CURRENT_BRANCH && currentMergeRequest
- ? false
- : state.shouldCreateMR,
- });
+ [types.UPDATE_COMMIT_ACTION](state, { commitAction }) {
+ Object.assign(state, { commitAction });
},
[types.UPDATE_NEW_BRANCH_NAME](state, newBranchName) {
Object.assign(state, {
@@ -26,9 +19,12 @@ export default {
submitCommitLoading,
});
},
- [types.TOGGLE_SHOULD_CREATE_MR](state) {
+ [types.TOGGLE_SHOULD_CREATE_MR](state, shouldCreateMR) {
Object.assign(state, {
- shouldCreateMR: !state.shouldCreateMR,
+ shouldCreateMR: shouldCreateMR === undefined ? !state.shouldCreateMR : shouldCreateMR,
});
},
+ [types.INTERACT_WITH_NEW_MR](state) {
+ Object.assign(state, { interactedWithNewMR: true });
+ },
};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/state.js b/app/assets/javascripts/ide/stores/modules/commit/state.js
index 5c0e6a41ca1..53647a7e3e3 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/state.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/state.js
@@ -4,4 +4,5 @@ export default () => ({
newBranchName: '',
submitCommitLoading: false,
shouldCreateMR: false,
+ interactedWithNewMR: false,
});
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
index ef7cd4ff8e8..1d127d915d7 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
@@ -1,6 +1,6 @@
import { states } from './constants';
-export const hasLatestPipeline = state => !state.isLoadingPipeline && !!state.latestPipeline;
+export const hasLatestPipeline = state => !state.isLoadingPipeline && Boolean(state.latestPipeline);
export const pipelineFailed = state =>
state.latestPipeline && state.latestPipeline.details.status.text === states.failed;
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 344b189decf..ae42b87c9a7 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -142,7 +142,7 @@ export default {
Object.assign(state.entries[file.path], {
raw: file.content,
- changed: !!changedFile,
+ changed: Boolean(changedFile),
staged: false,
prevPath: '',
moved: false,
diff --git a/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js
index 05000c73052..7051a968dac 100644
--- a/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js
@@ -14,7 +14,7 @@ export function addCommentIndicator(containerEl, { x, y }) {
export function removeCommentIndicator(imageFrameEl) {
const commentIndicatorEl = imageFrameEl.querySelector('.comment-indicator');
const imageEl = imageFrameEl.querySelector('img');
- const willRemove = !!commentIndicatorEl;
+ const willRemove = Boolean(commentIndicatorEl);
let meta = {};
if (willRemove) {
diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js
index 3587f073a00..26c1b0ec7be 100644
--- a/app/assets/javascripts/image_diff/image_diff.js
+++ b/app/assets/javascripts/image_diff/image_diff.js
@@ -6,8 +6,8 @@ import { isImageLoaded } from '../lib/utils/image_utility';
export default class ImageDiff {
constructor(el, options) {
this.el = el;
- this.canCreateNote = !!(options && options.canCreateNote);
- this.renderCommentBadge = !!(options && options.renderCommentBadge);
+ this.canCreateNote = Boolean(options && options.canCreateNote);
+ this.renderCommentBadge = Boolean(options && options.renderCommentBadge);
this.$noteContainer = $('.note-container', this.el);
this.imageBadges = [];
}
diff --git a/app/assets/javascripts/image_diff/view_types.js b/app/assets/javascripts/image_diff/view_types.js
index ab0a595571f..1a5123de220 100644
--- a/app/assets/javascripts/image_diff/view_types.js
+++ b/app/assets/javascripts/image_diff/view_types.js
@@ -5,5 +5,5 @@ export const viewTypes = {
};
export function isValidViewType(validate) {
- return !!Object.getOwnPropertyNames(viewTypes).find(viewType => viewType === validate);
+ return Boolean(Object.getOwnPropertyNames(viewTypes).find(viewType => viewType === validate));
}
diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js
index f51c7a2f990..16f88cddce3 100644
--- a/app/assets/javascripts/issuable_index.js
+++ b/app/assets/javascripts/issuable_index.js
@@ -12,7 +12,7 @@ export default class IssuableIndex {
}
initBulkUpdate(pagePrefix) {
const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
- const alreadyInitialized = !!this.bulkUpdateSidebar;
+ const alreadyInitialized = Boolean(this.bulkUpdateSidebar);
if (userCanBulkUpdate && !alreadyInitialized) {
IssuableBulkUpdateActions.init({
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index ab0b4231255..e88ca4747c5 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -156,7 +156,7 @@ export default {
return this.store.formState;
},
hasUpdated() {
- return !!this.state.updatedAt;
+ return Boolean(this.state.updatedAt);
},
issueChanged() {
const {
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
index d2f33dc31a7..1e1dce5f4fc 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -71,7 +71,7 @@ export default {
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation,
}"
- class="title"
+ class="title qa-title"
dir="auto"
v-html="titleHtml"
></h2>
diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue
index 6e92b599b0a..cb073a9b04d 100644
--- a/app/assets/javascripts/jobs/components/stages_dropdown.vue
+++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue
@@ -2,6 +2,7 @@
import _ from 'underscore';
import { GlLink } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
@@ -9,6 +10,7 @@ export default {
CiIcon,
Icon,
GlLink,
+ PipelineLink,
},
props: {
pipeline: {
@@ -48,9 +50,12 @@ export default {
<ci-icon :status="pipeline.details.status" class="vertical-align-middle" />
<span class="font-weight-bold">{{ s__('Job|Pipeline') }}</span>
- <gl-link :href="pipeline.path" class="js-pipeline-path link-commit qa-pipeline-path"
- >#{{ pipeline.id }}</gl-link
- >
+ <pipeline-link
+ :href="pipeline.path"
+ :pipeline-id="pipeline.id"
+ :pipeline-iid="pipeline.iid"
+ class="js-pipeline-path link-commit qa-pipeline-path"
+ />
<template v-if="hasRef">
{{ s__('Job|for') }}
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index c6dd21cd2d4..7064731a5ea 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -53,7 +53,7 @@ export default class LabelManager {
toggleEmptyState($label, $btn, action) {
this.emptyState.classList.toggle(
'hidden',
- !!this.prioritizedLabels[0].querySelector(':scope > li'),
+ Boolean(this.prioritizedLabels[0].querySelector(':scope > li')),
);
}
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 47e91dedd5a..5857f9e22ae 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -1,6 +1,8 @@
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { createUploadLink } from 'apollo-upload-client';
+import { ApolloLink } from 'apollo-link';
+import { BatchHttpLink } from 'apollo-link-batch-http';
import csrf from '~/lib/utils/csrf';
export default (resolvers = {}, config = {}) => {
@@ -11,13 +13,19 @@ export default (resolvers = {}, config = {}) => {
uri = `${config.baseUrl}${uri}`.replace(/\/{3,}/g, '/');
}
+ const httpOptions = {
+ uri,
+ headers: {
+ [csrf.headerKey]: csrf.token,
+ },
+ };
+
return new ApolloClient({
- link: createUploadLink({
- uri,
- headers: {
- [csrf.headerKey]: csrf.token,
- },
- }),
+ link: ApolloLink.split(
+ operation => operation.getContext().hasUpload,
+ createUploadLink(httpOptions),
+ new BatchHttpLink(httpOptions),
+ ),
cache: new InMemoryCache(config.cacheConfig),
resolvers,
});
diff --git a/app/assets/javascripts/lib/utils/accessor.js b/app/assets/javascripts/lib/utils/accessor.js
index 1d18992af63..39cffedcac6 100644
--- a/app/assets/javascripts/lib/utils/accessor.js
+++ b/app/assets/javascripts/lib/utils/accessor.js
@@ -2,7 +2,7 @@ function isPropertyAccessSafe(base, property) {
let safe;
try {
- safe = !!base[property];
+ safe = Boolean(base[property]);
} catch (error) {
safe = false;
}
diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js
index a24c71aeab1..28a7ebfdc69 100644
--- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js
+++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js
@@ -51,6 +51,7 @@ export default class LinkedTabs {
this.defaultAction = this.options.defaultAction;
this.action = this.options.action || this.defaultAction;
+ this.hashedTabs = this.options.hashedTabs || false;
if (this.action === 'show') {
this.action = this.defaultAction;
@@ -58,6 +59,10 @@ export default class LinkedTabs {
this.currentLocation = window.location;
+ if (this.hashedTabs) {
+ this.action = this.currentLocation.hash || this.action;
+ }
+
const tabSelector = `${this.options.parentEl} a[data-toggle="tab"]`;
// since this is a custom event we need jQuery :(
@@ -91,7 +96,9 @@ export default class LinkedTabs {
copySource.replace(/\/+$/, '');
- const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
+ const newState = this.hashedTabs
+ ? copySource
+ : `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
window.history.replaceState(
{
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index b236daff1e0..cc5e12aa467 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -94,6 +94,8 @@ export const handleLocationHash = () => {
const fixedNav = document.querySelector('.navbar-gitlab');
const performanceBar = document.querySelector('#js-peek');
const topPadding = 8;
+ const diffFileHeader = document.querySelector('.js-file-title');
+ const versionMenusContainer = document.querySelector('.mr-version-menus-container');
let adjustment = 0;
if (fixedNav) adjustment -= fixedNav.offsetHeight;
@@ -114,6 +116,14 @@ export const handleLocationHash = () => {
adjustment -= performanceBar.offsetHeight;
}
+ if (diffFileHeader) {
+ adjustment -= diffFileHeader.offsetHeight;
+ }
+
+ if (versionMenusContainer) {
+ adjustment -= versionMenusContainer.offsetHeight;
+ }
+
if (isInMRPage()) {
adjustment -= topPadding;
}
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 32cafb74d91..d521c462ad8 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -81,7 +81,7 @@ export const getDayName = date =>
*/
export const formatDate = datetime => {
if (_.isString(datetime) && datetime.match(/\d+-\d+\d+ /)) {
- throw new Error('Invalid date');
+ throw new Error(__('Invalid date'));
}
return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
};
@@ -513,7 +513,7 @@ export const stringifyTime = (timeObject, fullNameFormat = false) => {
const reducedTime = _.reduce(
timeObject,
(memo, unitValue, unitName) => {
- const isNonZero = !!unitValue;
+ const isNonZero = Boolean(unitValue);
if (fullNameFormat && isNonZero) {
// Remove traling 's' if unit value is singular
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index 9ddfb4bca11..61c8b8803d7 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -100,3 +100,9 @@ export function numberToHumanSize(size) {
* @returns {Float} The summed value
*/
export const sum = (a = 0, b = 0) => a + b;
+
+/**
+ * Checks if the provided number is odd
+ * @param {Int} number
+ */
+export const isOdd = (number = 0) => number % 2;
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 84a617acb42..b7922e29bb0 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -223,9 +223,9 @@ export function insertMarkdownText({
return tag.replace(textPlaceholder, val);
}
if (val.indexOf(tag) === 0) {
- return '' + val.replace(tag, '');
+ return String(val.replace(tag, ''));
} else {
- return '' + tag + val;
+ return String(tag) + val;
}
})
.join('\n');
@@ -233,7 +233,7 @@ export function insertMarkdownText({
} else if (tag.indexOf(textPlaceholder) > -1) {
textToInsert = tag.replace(textPlaceholder, selected);
} else {
- textToInsert = '' + startChar + tag + selected + (wrap ? tag : ' ');
+ textToInsert = String(startChar) + tag + selected + (wrap ? tag : ' ');
}
if (removedFirstNewLine) {
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index bdfd06fc250..32fd0990374 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,3 +1,5 @@
+import { join as joinPaths } from 'path';
+
// Returns an array containing the value(s) of the
// of the key passed as an argument
export function getParameterValues(sParam) {
@@ -121,4 +123,48 @@ export function webIDEUrl(route = undefined) {
return returnUrl;
}
-export { join as joinPaths } from 'path';
+/**
+ * Returns current base URL
+ */
+export function getBaseURL() {
+ const { protocol, host } = window.location;
+ return `${protocol}//${host}`;
+}
+
+/**
+ * Returns true if url is an absolute or root-relative URL
+ *
+ * @param {String} url
+ */
+export function isAbsoluteOrRootRelative(url) {
+ return /^(https?:)?\//.test(url);
+}
+
+/**
+ * Checks if the provided URL is a safe URL (absolute http(s) or root-relative URL)
+ *
+ * @param {String} url that will be checked
+ * @returns {Boolean}
+ */
+export function isSafeURL(url) {
+ if (!isAbsoluteOrRootRelative(url)) {
+ return false;
+ }
+
+ try {
+ const parsedUrl = new URL(url, getBaseURL());
+ return ['http:', 'https:'].includes(parsedUrl.protocol);
+ } catch (e) {
+ return false;
+ }
+}
+
+export function getWebSocketProtocol() {
+ return window.location.protocol.replace('http', 'ws');
+}
+
+export function getWebSocketUrl(path) {
+ return `${getWebSocketProtocol()}//${joinPaths(window.location.host, path)}`;
+}
+
+export { joinPaths };
diff --git a/app/assets/javascripts/monitoring/components/charts/single_stat.vue b/app/assets/javascripts/monitoring/components/charts/single_stat.vue
new file mode 100644
index 00000000000..b03a6ca1806
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/charts/single_stat.vue
@@ -0,0 +1,37 @@
+<script>
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+
+export default {
+ components: {
+ GlSingleStat,
+ },
+ inheritAttrs: false,
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ value: {
+ type: Number,
+ required: true,
+ },
+ unit: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ valueWithUnit() {
+ return `${this.value}${this.unit}`;
+ },
+ },
+};
+</script>
+<template>
+ <div class="prometheus-graph col-12 col-lg-6">
+ <div class="prometheus-graph-header">
+ <h5 ref="graphTitle" class="prometheus-graph-title">{{ title }}</h5>
+ </div>
+ <gl-single-stat :value="valueWithUnit" :title="title" variant="success" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index a55a47c277d..2314f7b80cf 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -38,7 +38,7 @@ export default {
GlModalDirective,
},
props: {
- externalDashboardPath: {
+ externalDashboardUrl: {
type: String,
required: false,
default: '',
@@ -106,10 +106,6 @@ export default {
type: String,
required: true,
},
- showTimeWindowDropdown: {
- type: Boolean,
- required: true,
- },
customMetricsAvailable: {
type: Boolean,
required: false,
@@ -172,7 +168,7 @@ export default {
if (!this.hasMetrics) {
this.setGettingStartedEmptyState();
} else {
- this.fetchData(getTimeDiff(this.timeWindows.eightHours));
+ this.fetchData(getTimeDiff(this.selectedTimeWindow));
sidebarMutationObserver = new MutationObserver(this.onSidebarMutation);
sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
@@ -248,7 +244,7 @@ export default {
>
</gl-dropdown>
</div>
- <div v-if="showTimeWindowDropdown" class="d-flex align-items-center">
+ <div class="d-flex align-items-center prepend-left-8">
<strong>{{ s__('Metrics|Show last') }}</strong>
<gl-dropdown
class="prepend-left-10 js-time-window-dropdown"
@@ -299,10 +295,11 @@ export default {
</gl-modal>
</div>
<gl-button
- v-if="externalDashboardPath.length"
+ v-if="externalDashboardUrl.length"
class="js-external-dashboard-link prepend-left-8"
variant="primary"
- :href="externalDashboardPath"
+ :href="externalDashboardUrl"
+ target="_blank"
>
{{ __('View full dashboard') }}
<icon name="external-link" />
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index 57771ccf4d9..62c0f44c1e6 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -16,7 +16,6 @@ export default (props = {}) => {
props: {
...el.dataset,
hasMetrics: parseBoolean(el.dataset.hasMetrics),
- showTimeWindowDropdown: gon.features.metricsTimeWindow,
...props,
},
});
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index 74c4ae64712..3fd9e07fa8b 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -8,8 +8,5 @@ export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA';
export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS';
export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAILURE';
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
-export const SET_METRICS_ENDPOINT = 'SET_METRICS_ENDPOINT';
-export const SET_ENVIRONMENTS_ENDPOINT = 'SET_ENVIRONMENTS_ENDPOINT';
-export const SET_DEPLOYMENTS_ENDPOINT = 'SET_DEPLOYMENTS_ENDPOINT';
export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE';
diff --git a/app/assets/javascripts/mr_notes/stores/getters.js b/app/assets/javascripts/mr_notes/stores/getters.js
index b10e9f9f9f1..e48cfcd9564 100644
--- a/app/assets/javascripts/mr_notes/stores/getters.js
+++ b/app/assets/javascripts/mr_notes/stores/getters.js
@@ -1,5 +1,5 @@
export default {
isLoggedIn(state, getters) {
- return !!getters.getUserData.id;
+ return Boolean(getters.getUserData.id);
},
};
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index d03f4508fb8..a7156bd2406 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -7,6 +7,10 @@ no-unused-vars, no-shadow, no-useless-escape, class-methods-use-this */
/* global ResolveService */
/* global mrRefreshWidgetUrl */
+/*
+old_notes_spec.js is the spec for the legacy, jQuery notes application. It has nothing to do with the new, fancy Vue notes app.
+ */
+
import $ from 'jquery';
import _ from 'underscore';
import Cookies from 'js-cookie';
@@ -986,6 +990,14 @@ export default class Notes {
form.find('#note_position').val(dataHolder.attr('data-position'));
form
+ .prepend(
+ `<div class="avatar-note-form-holder"><div class="content"><a href="${escape(
+ gon.current_username,
+ )}" class="user-avatar-link d-none d-sm-block"><img class="avatar s40" src="${encodeURI(
+ gon.current_user_avatar_url,
+ )}" alt="${escape(gon.current_user_fullname)}" /></a></div></div>`,
+ )
+ .append('</div>')
.find('.js-close-discussion-note-form')
.show()
.removeClass('hide');
@@ -1021,6 +1033,9 @@ export default class Notes {
target: $link,
lineType: link.dataset.lineType,
showReplyInput,
+ currentUsername: gon.current_username,
+ currentUserAvatar: gon.current_user_avatar_url,
+ currentUserFullname: gon.current_user_fullname,
});
}
@@ -1049,7 +1064,15 @@ export default class Notes {
this.setupDiscussionNoteForm($link, newForm);
}
- toggleDiffNote({ target, lineType, forceShow, showReplyInput = false }) {
+ toggleDiffNote({
+ target,
+ lineType,
+ forceShow,
+ showReplyInput = false,
+ currentUsername,
+ currentUserAvatar,
+ currentUserFullname,
+ }) {
var $link,
addForm,
hasNotes,
@@ -1542,7 +1565,9 @@ export default class Notes {
<div class="note-header">
<div class="note-header-info">
<a href="/${_.escape(currentUsername)}">
- <span class="d-none d-sm-inline-block">${_.escape(currentUsername)}</span>
+ <span class="d-none d-sm-inline-block bold">${_.escape(
+ currentUsername,
+ )}</span>
<span class="note-headline-light">${_.escape(currentUsername)}</span>
</a>
</div>
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 688c06878ac..075c28e8d07 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -337,6 +337,8 @@ Please check your network connection and try again.`;
v-if="hasWarning(getNoteableData)"
:is-locked="isLocked(getNoteableData)"
:is-confidential="isConfidential(getNoteableData)"
+ :locked-issue-docs-path="lockedIssueDocsPath"
+ :confidential-issue-docs-path="confidentialIssueDocsPath"
/>
<markdown-field
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index c7cfc0f0f3b..efd84f5722c 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -49,22 +49,26 @@ export default {
</script>
<template>
- <div v-if="resolvableDiscussionsCount > 0" class="line-resolve-all-container prepend-top-8">
- <div>
+ <div v-if="resolvableDiscussionsCount > 0" class="line-resolve-all-container full-width-mobile">
+ <div class="full-width-mobile d-flex d-sm-block">
<div :class="{ 'has-next-btn': hasNextButton }" class="line-resolve-all">
<span
:class="{ 'is-active': allResolved }"
class="line-resolve-btn is-disabled"
type="button"
>
- <icon name="check-circle" />
+ <icon :name="allResolved ? 'check-circle-filled' : 'check-circle'" />
</span>
<span class="line-resolve-text">
{{ resolvedDiscussionsCount }}/{{ resolvableDiscussionsCount }}
{{ n__('discussion resolved', 'discussions resolved', resolvableDiscussionsCount) }}
</span>
</div>
- <div v-if="resolveAllDiscussionsIssuePath && !allResolved" class="btn-group" role="group">
+ <div
+ v-if="resolveAllDiscussionsIssuePath && !allResolved"
+ class="btn-group btn-group-sm"
+ role="group"
+ >
<a
v-gl-tooltip
:href="resolveAllDiscussionsIssuePath"
@@ -74,7 +78,7 @@ export default {
<icon name="issue-new" />
</a>
</div>
- <div v-if="isLoggedIn && !allResolved" class="btn-group" role="group">
+ <div v-if="isLoggedIn && !allResolved" class="btn-group btn-group-sm" role="group">
<button
v-gl-tooltip
title="Jump to first unresolved discussion"
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index 47951591e82..eb3fbbe1385 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -105,12 +105,12 @@ export default {
<template>
<div
v-if="displayFilters"
- class="discussion-filter-container js-discussion-filter-container d-inline-block align-bottom"
+ class="discussion-filter-container js-discussion-filter-container d-inline-block align-bottom full-width-mobile"
>
<button
id="discussion-filter-dropdown"
ref="dropdownToggle"
- class="btn btn-default qa-discussion-filter"
+ class="btn btn-sm qa-discussion-filter"
data-toggle="dropdown"
aria-expanded="false"
>
diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
index c469a6b7bcd..53f509185a8 100644
--- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue
+++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
@@ -1,12 +1,24 @@
<script>
+import { GlLink } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
+import { __, sprintf } from '~/locale';
import Issuable from '~/vue_shared/mixins/issuable';
+import issuableStateMixin from '../mixins/issuable_state';
export default {
components: {
Icon,
+ GlLink,
+ },
+ mixins: [Issuable, issuableStateMixin],
+ computed: {
+ lockedIssueWarning() {
+ return sprintf(
+ __('This %{issuableDisplayName} is locked. Only project members can comment.'),
+ { issuableDisplayName: this.issuableDisplayName },
+ );
+ },
},
- mixins: [Issuable],
};
</script>
@@ -15,7 +27,11 @@ export default {
<span class="issuable-note-warning inline">
<icon :size="16" name="lock" class="icon" />
<span>
- This {{ issuableDisplayName }} is locked. Only <b>project members</b> can comment.
+ {{ lockedIssueWarning }}
+
+ <gl-link :href="lockedIssueDocsPath" target="_blank" class="learn-more">
+ {{ __('Learn more') }}
+ </gl-link>
</span>
</span>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index 5b6163a6214..228bb652597 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -49,7 +49,7 @@ export default {
computed: {
...mapGetters(['userCanReply']),
hasReplies() {
- return !!this.replies.length;
+ return Boolean(this.replies.length);
},
replies() {
return this.discussion.notes.slice(1);
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 5a4ff15d198..844d0c3e376 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -135,7 +135,7 @@ export default {
@click="onResolve"
>
<template v-if="!isResolving">
- <icon name="check-circle" />
+ <icon :name="isResolved ? 'check-circle-filled' : 'check-circle'" />
</template>
<gl-loading-icon v-else inline />
</button>
@@ -147,6 +147,7 @@ export default {
class="note-action-button note-emoji-button js-add-award js-note-emoji"
href="#"
title="Add reaction"
+ data-position="right"
>
<icon css-classes="link-highlight award-control-icon-neutral" name="slight-smile" />
<icon css-classes="link-highlight award-control-icon-positive" name="smiley" />
@@ -194,7 +195,7 @@ export default {
</button>
<ul class="dropdown-menu more-actions-dropdown dropdown-open-left">
<li v-if="canReportAsAbuse">
- <a :href="reportAbusePath">{{ __('Report abuse to GitLab') }}</a>
+ <a :href="reportAbusePath">{{ __('Report abuse to admin') }}</a>
</li>
<li v-if="noteUrl">
<button
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index acbb91ce7be..09ecb695214 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -234,6 +234,8 @@ export default {
v-if="hasWarning(getNoteableData)"
:is-locked="isLocked(getNoteableData)"
:is-confidential="isConfidential(getNoteableData)"
+ :locked-issue-docs-path="lockedIssueDocsPath"
+ :confidential-issue-docs-path="confidentialIssueDocsPath"
/>
<markdown-field
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 5c59c0c32dd..fbf82fab9e9 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -82,7 +82,7 @@ export default {
:data-username="author.username"
>
<slot name="note-header-info"></slot>
- <span class="note-header-author-name">{{ author.name }}</span>
+ <span class="note-header-author-name bold">{{ author.name }}</span>
<span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
<span class="note-headline-light">@{{ author.username }}</span>
</a>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 2c549e7abdd..eb6a4a67fff 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -87,7 +87,11 @@ export default {
'unresolvedDiscussionsCount',
'hasUnresolvedDiscussions',
'showJumpToNextDiscussion',
+ 'getUserData',
]),
+ currentUser() {
+ return this.getUserData;
+ },
author() {
return this.firstNote.author;
},
@@ -377,6 +381,14 @@ Please check your network connection and try again.`;
:class="{ 'is-replying': isReplying }"
class="discussion-reply-holder"
>
+ <user-avatar-link
+ v-if="!isReplying && currentUser"
+ :link-href="currentUser.path"
+ :img-src="currentUser.avatar_url"
+ :img-alt="currentUser.name"
+ :img-size="40"
+ class="d-none d-sm-block"
+ />
<discussion-actions
v-if="!isReplying && userCanReply"
:discussion="discussion"
@@ -388,18 +400,27 @@ Please check your network connection and try again.`;
@resolve="resolveHandler"
@jumpToNextDiscussion="jumpToNextDiscussion"
/>
- <note-form
- v-if="isReplying"
- ref="noteForm"
- :discussion="discussion"
- :is-editing="false"
- :line="diffLine"
- save-button-title="Comment"
- :autosave-key="autosaveKey"
- @handleFormUpdateAddToReview="addReplyToReview"
- @handleFormUpdate="saveReply"
- @cancelForm="cancelReplyForm"
- />
+ <div v-if="isReplying" class="avatar-note-form-holder">
+ <user-avatar-link
+ v-if="currentUser"
+ :link-href="currentUser.path"
+ :img-src="currentUser.avatar_url"
+ :img-alt="currentUser.name"
+ :img-size="40"
+ class="d-none d-sm-block"
+ />
+ <note-form
+ ref="noteForm"
+ :discussion="discussion"
+ :is-editing="false"
+ :line="diffLine"
+ save-button-title="Comment"
+ :autosave-key="autosaveKey"
+ @handleFormUpdateAddToReview="addReplyToReview"
+ @handleFormUpdate="saveReply"
+ @cancelForm="cancelReplyForm"
+ />
+ </div>
<note-signed-out-widget v-if="!userCanReply" />
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 47d74c2f892..aa80e25a3e0 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -10,7 +10,7 @@ import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue';
import noteActions from './note_actions.vue';
-import noteBody from './note_body.vue';
+import NoteBody from './note_body.vue';
import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
@@ -21,7 +21,7 @@ export default {
userAvatarLink,
noteHeader,
noteActions,
- noteBody,
+ NoteBody,
TimelineEntryItem,
},
mixins: [noteable, resolvable, draftMixin],
@@ -75,7 +75,7 @@ export default {
};
},
canReportAsAbuse() {
- return !!this.note.report_abuse_path && this.author.id !== this.getUserData.id;
+ return Boolean(this.note.report_abuse_path) && this.author.id !== this.getUserData.id;
},
noteAnchorId() {
return `note_${this.note.id}`;
@@ -209,7 +209,10 @@ export default {
// we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content
this.note.note = noteText;
- this.$refs.noteBody.note.note = noteText;
+ const { noteBody } = this.$refs;
+ if (noteBody) {
+ noteBody.note.note = noteText;
+ }
},
},
};
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 0f1976db37d..4d00e957973 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -127,6 +127,9 @@ export default {
initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
});
},
+ beforeDestroy() {
+ this.stopPolling();
+ },
methods: {
...mapActions([
'setLoadingState',
@@ -144,6 +147,7 @@ export default {
'expandDiscussion',
'startTaskList',
'convertToDiscussion',
+ 'stopPolling',
]),
fetchNotes() {
if (this.isFetching) return null;
diff --git a/app/assets/javascripts/notes/mixins/issuable_state.js b/app/assets/javascripts/notes/mixins/issuable_state.js
index 97f3ea0d5de..d97d9f6850a 100644
--- a/app/assets/javascripts/notes/mixins/issuable_state.js
+++ b/app/assets/javascripts/notes/mixins/issuable_state.js
@@ -1,11 +1,22 @@
+import { mapGetters } from 'vuex';
+
export default {
+ computed: {
+ ...mapGetters(['getNoteableDataByProp']),
+ lockedIssueDocsPath() {
+ return this.getNoteableDataByProp('locked_discussion_docs_path');
+ },
+ confidentialIssueDocsPath() {
+ return this.getNoteableDataByProp('confidential_issues_docs_path');
+ },
+ },
methods: {
isConfidential(issue) {
- return !!issue.confidential;
+ return Boolean(issue.confidential);
},
isLocked(issue) {
- return !!issue.discussion_locked;
+ return Boolean(issue.discussion_locked);
},
hasWarning(issue) {
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 2d150e64ef7..d7982be3e4b 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -20,7 +20,7 @@ export const getNoteableData = state => state.noteableData;
export const getNoteableDataByProp = state => prop => state.noteableData[prop];
-export const userCanReply = state => !!state.noteableData.current_user.can_create_note;
+export const userCanReply = state => Boolean(state.noteableData.current_user.can_create_note);
export const openState = state => state.noteableData.state;
diff --git a/app/assets/javascripts/operation_settings/components/external_dashboard.vue b/app/assets/javascripts/operation_settings/components/external_dashboard.vue
index 0a87d193b72..ed518611d0b 100644
--- a/app/assets/javascripts/operation_settings/components/external_dashboard.vue
+++ b/app/assets/javascripts/operation_settings/components/external_dashboard.vue
@@ -1,4 +1,5 @@
<script>
+import { mapState, mapActions } from 'vuex';
import { GlButton, GlFormGroup, GlFormInput, GlLink } from '@gitlab/ui';
export default {
@@ -8,26 +9,34 @@ export default {
GlFormInput,
GlLink,
},
- props: {
- externalDashboardPath: {
- type: String,
- required: false,
- default: '',
- },
- externalDashboardHelpPagePath: {
- type: String,
- required: true,
+ computed: {
+ ...mapState([
+ 'externalDashboardHelpPagePath',
+ 'externalDashboardUrl',
+ 'operationsSettingsEndpoint',
+ ]),
+ userDashboardUrl: {
+ get() {
+ return this.externalDashboardUrl;
+ },
+ set(url) {
+ this.setExternalDashboardUrl(url);
+ },
},
},
+ methods: {
+ ...mapActions(['setExternalDashboardUrl', 'updateExternalDashboardUrl']),
+ },
};
</script>
<template>
- <section class="settings expanded">
+ <section class="settings no-animate">
<div class="settings-header">
<h4 class="js-section-header">
{{ s__('ExternalMetrics|External Dashboard') }}
</h4>
+ <gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
<p class="js-section-sub-header">
{{
s__(
@@ -44,11 +53,12 @@ export default {
:description="s__('ExternalMetrics|Enter the URL of the dashboard you want to link to')"
>
<gl-form-input
- :value="externalDashboardPath"
+ v-model="userDashboardUrl"
placeholder="https://my-org.gitlab.io/my-dashboards"
+ @keydown.enter.native.prevent="updateExternalDashboardUrl"
/>
</gl-form-group>
- <gl-button variant="success">
+ <gl-button variant="success" @click="updateExternalDashboardUrl">
{{ __('Save Changes') }}
</gl-button>
</form>
diff --git a/app/assets/javascripts/operation_settings/index.js b/app/assets/javascripts/operation_settings/index.js
index 1171f3ece9f..6946578e6d2 100644
--- a/app/assets/javascripts/operation_settings/index.js
+++ b/app/assets/javascripts/operation_settings/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import store from './store';
import ExternalDashboardForm from './components/external_dashboard.vue';
export default () => {
@@ -14,13 +15,9 @@ export default () => {
return new Vue({
el,
+ store: store(el.dataset),
render(createElement) {
- return createElement(ExternalDashboardForm, {
- props: {
- ...el.dataset,
- expanded: false,
- },
- });
+ return createElement(ExternalDashboardForm);
},
});
};
diff --git a/app/assets/javascripts/operation_settings/store/actions.js b/app/assets/javascripts/operation_settings/store/actions.js
new file mode 100644
index 00000000000..ec05b0c76cf
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/store/actions.js
@@ -0,0 +1,38 @@
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import createFlash from '~/flash';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import * as mutationTypes from './mutation_types';
+
+export const setExternalDashboardUrl = ({ commit }, url) =>
+ commit(mutationTypes.SET_EXTERNAL_DASHBOARD_URL, url);
+
+export const updateExternalDashboardUrl = ({ state, dispatch }) =>
+ axios
+ .patch(state.operationsSettingsEndpoint, {
+ project: {
+ metrics_setting_attributes: {
+ external_dashboard_url: state.externalDashboardUrl,
+ },
+ },
+ })
+ .then(() => dispatch('receiveExternalDashboardUpdateSuccess'))
+ .catch(error => dispatch('receiveExternalDashboardUpdateError', error));
+
+export const receiveExternalDashboardUpdateSuccess = () => {
+ /**
+ * The operations_controller currently handles successful requests
+ * by creating a flash banner messsage to notify the user.
+ */
+ refreshCurrentPage();
+};
+
+export const receiveExternalDashboardUpdateError = (_, error) => {
+ const { response } = error;
+ const message = response.data && response.data.message ? response.data.message : '';
+
+ createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert');
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/operation_settings/store/index.js b/app/assets/javascripts/operation_settings/store/index.js
new file mode 100644
index 00000000000..e96bb1e8aad
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/store/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import createState from './state';
+import * as actions from './actions';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export const createStore = initialState =>
+ new Vuex.Store({
+ state: createState(initialState),
+ actions,
+ mutations,
+ });
+
+export default createStore;
diff --git a/app/assets/javascripts/operation_settings/store/mutation_types.js b/app/assets/javascripts/operation_settings/store/mutation_types.js
new file mode 100644
index 00000000000..237d2b6122f
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/store/mutation_types.js
@@ -0,0 +1,3 @@
+/* eslint-disable import/prefer-default-export */
+
+export const SET_EXTERNAL_DASHBOARD_URL = 'SET_EXTERNAL_DASHBOARD_URL';
diff --git a/app/assets/javascripts/operation_settings/store/mutations.js b/app/assets/javascripts/operation_settings/store/mutations.js
new file mode 100644
index 00000000000..64bb33bb89f
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/store/mutations.js
@@ -0,0 +1,7 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_EXTERNAL_DASHBOARD_URL](state, url) {
+ state.externalDashboardUrl = url;
+ },
+};
diff --git a/app/assets/javascripts/operation_settings/store/state.js b/app/assets/javascripts/operation_settings/store/state.js
new file mode 100644
index 00000000000..72167141c48
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/store/state.js
@@ -0,0 +1,5 @@
+export default (initialState = {}) => ({
+ externalDashboardUrl: initialState.externalDashboardUrl || '',
+ operationsSettingsEndpoint: initialState.operationsSettingsEndpoint,
+ externalDashboardHelpPagePath: initialState.externalDashboardHelpPagePath,
+});
diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
index ae0a8c74964..8a5300c9266 100644
--- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
@@ -12,5 +12,6 @@ document.addEventListener('DOMContentLoaded', () => {
saveButton: variableListEl.querySelector('.js-ci-variables-save-button'),
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint,
+ maskableRegex: variableListEl.dataset.maskableRegex,
});
});
diff --git a/app/assets/javascripts/pages/projects/pages_domains/edit/index.js b/app/assets/javascripts/pages/projects/pages_domains/edit/index.js
new file mode 100644
index 00000000000..27e4433ad4d
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pages_domains/edit/index.js
@@ -0,0 +1,3 @@
+import initForm from '~/pages/projects/pages_domains/form';
+
+document.addEventListener('DOMContentLoaded', initForm);
diff --git a/app/assets/javascripts/pages/projects/pages_domains/form.js b/app/assets/javascripts/pages/projects/pages_domains/form.js
new file mode 100644
index 00000000000..1d0dbfe0406
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pages_domains/form.js
@@ -0,0 +1,43 @@
+import setupToggleButtons from '~/toggle_buttons';
+
+export default () => {
+ const toggleContainer = document.querySelector('.js-auto-ssl-toggle-container');
+
+ if (toggleContainer) {
+ const onToggleButtonClicked = isAutoSslEnabled => {
+ Array.from(document.querySelectorAll('.js-shown-if-auto-ssl')).forEach(el => {
+ if (isAutoSslEnabled) {
+ el.classList.remove('d-none');
+ } else {
+ el.classList.add('d-none');
+ }
+ });
+
+ Array.from(document.querySelectorAll('.js-shown-unless-auto-ssl')).forEach(el => {
+ if (isAutoSslEnabled) {
+ el.classList.add('d-none');
+ } else {
+ el.classList.remove('d-none');
+ }
+ });
+
+ Array.from(document.querySelectorAll('.js-enabled-if-auto-ssl')).forEach(el => {
+ if (isAutoSslEnabled) {
+ el.removeAttribute('disabled');
+ } else {
+ el.setAttribute('disabled', 'disabled');
+ }
+ });
+
+ Array.from(document.querySelectorAll('.js-enabled-unless-auto-ssl')).forEach(el => {
+ if (isAutoSslEnabled) {
+ el.setAttribute('disabled', 'disabled');
+ } else {
+ el.removeAttribute('disabled');
+ }
+ });
+ };
+
+ setupToggleButtons(toggleContainer, onToggleButtonClicked);
+ }
+};
diff --git a/app/assets/javascripts/pages/projects/pages_domains/new/index.js b/app/assets/javascripts/pages/projects/pages_domains/new/index.js
new file mode 100644
index 00000000000..27e4433ad4d
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pages_domains/new/index.js
@@ -0,0 +1,3 @@
+import initForm from '~/pages/projects/pages_domains/form';
+
+document.addEventListener('DOMContentLoaded', initForm);
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index bd4309e47ad..bb490919a9a 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -29,7 +29,7 @@ export default {
// The text input is editable when there's a custom interval, or when it's
// a preset interval and the user clicks the 'custom' radio button
isEditable() {
- return !!(this.customInputEnabled || !this.intervalIsPreset);
+ return Boolean(this.customInputEnabled || !this.intervalIsPreset);
},
},
watch: {
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index b288989b252..f0d529758d5 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -39,6 +39,11 @@ export default class Project {
$label.text(activeText);
});
+ $('#modal-geo-info').data({
+ cloneUrlSecondary: $this.attr('href'),
+ cloneUrlPrimary: $this.data('primaryUrl') || '',
+ });
+
if (mobileCloneField) {
mobileCloneField.dataset.clipboardText = url;
} else {
@@ -67,6 +72,13 @@ export default class Project {
.remove();
return e.preventDefault();
});
+ $('.hide-shared-runner-limit-message').on('click', function(e) {
+ var $alert = $(this).parents('.shared-runner-quota-message');
+ var scope = $alert.data('scope');
+ Cookies.set('hide_shared_runner_quota_message', 'false', { path: scope });
+ $alert.remove();
+ e.preventDefault();
+ });
$('.hide-auto-devops-implicitly-enabled-banner').on('click', function(e) {
const projectId = $(this).data('project-id');
const cookieKey = `hide_auto_devops_implicitly_enabled_banner_${projectId}`;
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 15c6fb550c1..885247335a4 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -21,6 +21,7 @@ document.addEventListener('DOMContentLoaded', () => {
saveButton: variableListEl.querySelector('.js-ci-variables-save-button'),
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint,
+ maskableRegex: variableListEl.dataset.maskableRegex,
});
// hide extra auto devops settings based checkbox state
diff --git a/app/assets/javascripts/pages/projects/settings/operations/show/index.js b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
index 5270a7924ec..98e19705976 100644
--- a/app/assets/javascripts/pages/projects/settings/operations/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
@@ -1,7 +1,9 @@
import mountErrorTrackingForm from '~/error_tracking_settings';
import mountOperationSettings from '~/operation_settings';
+import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
mountErrorTrackingForm();
mountOperationSettings();
+ initSettingsPanels();
});
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 19d9903c988..dea7c586868 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -175,11 +175,6 @@ export default {
if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true);
else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false);
},
-
- buildsAccessLevel(value, oldValue) {
- if (value === 0) toggleHiddenClassBySelector('.builds-feature', true);
- else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false);
- },
},
methods: {
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index e1a3f42a71f..3f5a3e15c2c 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import LengthValidator from './length_validator';
import UsernameValidator from './username_validator';
import NoEmojiValidator from '../../../emoji/no_emoji_validator';
import SigninTabsMemoizer from './signin_tabs_memoizer';
@@ -6,6 +7,7 @@ import OAuthRememberMe from './oauth_remember_me';
import preserveUrlFragment from './preserve_url_fragment';
document.addEventListener('DOMContentLoaded', () => {
+ new LengthValidator(); // eslint-disable-line no-new
new UsernameValidator(); // eslint-disable-line no-new
new SigninTabsMemoizer(); // eslint-disable-line no-new
new NoEmojiValidator(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/sessions/new/length_validator.js b/app/assets/javascripts/pages/sessions/new/length_validator.js
new file mode 100644
index 00000000000..3d687ca08cc
--- /dev/null
+++ b/app/assets/javascripts/pages/sessions/new/length_validator.js
@@ -0,0 +1,32 @@
+import InputValidator from '../../../validators/input_validator';
+
+const errorMessageClass = 'gl-field-error';
+
+export default class LengthValidator extends InputValidator {
+ constructor(opts = {}) {
+ super();
+
+ const container = opts.container || '';
+ const validateLengthElements = document.querySelectorAll(`${container} .js-validate-length`);
+
+ validateLengthElements.forEach(element =>
+ element.addEventListener('input', this.eventHandler.bind(this)),
+ );
+ }
+
+ eventHandler(event) {
+ this.inputDomElement = event.target;
+ this.inputErrorMessage = this.inputDomElement.parentElement.querySelector(
+ `.${errorMessageClass}`,
+ );
+
+ const { value } = this.inputDomElement;
+ const { maxLengthMessage, maxLength } = this.inputDomElement.dataset;
+
+ this.errorMessage = maxLengthMessage;
+
+ this.invalidInput = value.length > parseInt(maxLength, 10);
+
+ this.setValidationStateAndMessage();
+ }
+}
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index b2e365e5cde..f3a71ee434c 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -83,6 +83,8 @@ export default {
v-if="shouldRenderContent"
:status="status"
:item-id="pipeline.id"
+ :item-iid="pipeline.iid"
+ :item-id-tooltip="__('Pipeline ID (IID)')"
:time="pipeline.created_at"
:user="pipeline.user"
:actions="actions"
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index c41ecab1294..00c02e15562 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -2,6 +2,7 @@
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import _ from 'underscore';
import { __, sprintf } from '~/locale';
+import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import popover from '~/vue_shared/directives/popover';
@@ -19,6 +20,7 @@ export default {
components: {
UserAvatarLink,
GlLink,
+ PipelineLink,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -59,10 +61,13 @@ export default {
};
</script>
<template>
- <div class="table-section section-10 d-none d-sm-none d-md-block pipeline-tags">
- <gl-link :href="pipeline.path" class="js-pipeline-url-link">
- <span class="pipeline-id">#{{ pipeline.id }}</span>
- </gl-link>
+ <div class="table-section section-10 d-none d-sm-none d-md-block pipeline-tags section-wrap">
+ <pipeline-link
+ :href="pipeline.path"
+ :pipeline-id="pipeline.id"
+ :pipeline-iid="pipeline.iid"
+ class="js-pipeline-url-link"
+ />
<div class="label-container">
<span
v-if="pipeline.flags.latest"
diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js
index 59c13e1a042..f0d9642a2b2 100644
--- a/app/assets/javascripts/profile/account/index.js
+++ b/app/assets/javascripts/profile/account/index.js
@@ -35,7 +35,7 @@ export default () => {
return createElement('delete-account-modal', {
props: {
actionUrl: deleteAccountModalEl.dataset.actionUrl,
- confirmWithPassword: !!deleteAccountModalEl.dataset.confirmWithPassword,
+ confirmWithPassword: Boolean(deleteAccountModalEl.dataset.confirmWithPassword),
username: deleteAccountModalEl.dataset.username,
},
});
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 6e3800021b4..8dd37aee7e1 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -39,6 +39,7 @@ export default class Profile {
bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
+ $('.js-group-notification-email').on('change', this.submitForm);
$('#user_notification_email').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
this.form.on('submit', this.onSubmitForm);
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js
index 4834a856271..f05ad7773a2 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js
@@ -57,7 +57,7 @@ export const validateProjectBilling = ({ dispatch, commit, state }) =>
resp => {
const { billingEnabled } = resp.result;
- commit(types.SET_PROJECT_BILLING_STATUS, !!billingEnabled);
+ commit(types.SET_PROJECT_BILLING_STATUS, Boolean(billingEnabled));
dispatch('setIsValidatingProjectBilling', false);
resolve();
},
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js
index e39f02d0894..f9e2e2f74fb 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js
@@ -1,3 +1,3 @@
-export const hasProject = state => !!state.selectedProject.projectId;
-export const hasZone = state => !!state.selectedZone;
-export const hasMachineType = state => !!state.selectedMachineType;
+export const hasProject = state => Boolean(state.selectedProject.projectId);
+export const hasZone = state => Boolean(state.selectedZone);
+export const hasMachineType = state => Boolean(state.selectedMachineType);
diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js
index 1ac699c538f..8ace6657ad1 100644
--- a/app/assets/javascripts/registry/stores/mutations.js
+++ b/app/assets/javascripts/registry/stores/mutations.js
@@ -9,7 +9,7 @@ export default {
[types.SET_REPOS_LIST](state, list) {
Object.assign(state, {
repos: list.map(el => ({
- canDelete: !!el.destroy_path,
+ canDelete: Boolean(el.destroy_path),
destroyPath: el.destroy_path,
id: el.id,
isLoading: false,
@@ -42,7 +42,7 @@ export default {
location: element.location,
createdAt: element.created_at,
destroyPath: element.destroy_path,
- canDelete: !!element.destroy_path,
+ canDelete: Boolean(element.destroy_path),
}));
},
diff --git a/app/assets/javascripts/reports/components/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue
index 50f2910e02d..ee07efea3b0 100644
--- a/app/assets/javascripts/reports/components/issues_list.vue
+++ b/app/assets/javascripts/reports/components/issues_list.vue
@@ -52,11 +52,21 @@ export default {
required: false,
default: '',
},
- showReportSectionStatus: {
+ showReportSectionStatusIcon: {
type: Boolean,
required: false,
default: true,
},
+ issuesUlElementClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ issueItemClass: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
issuesWithState() {
@@ -67,6 +77,9 @@ export default {
...this.resolvedIssues.map(wrapIssueWithState(STATUS_SUCCESS)),
];
},
+ wclass() {
+ return `report-block-list ${this.issuesUlElementClass}`;
+ },
},
};
</script>
@@ -77,7 +90,7 @@ export default {
:size="$options.typicalReportItemHeight"
class="report-block-container"
wtag="ul"
- wclass="report-block-list"
+ :wclass="wclass"
>
<report-item
v-for="(wrapped, index) in issuesWithState"
@@ -86,7 +99,8 @@ export default {
:status="wrapped.status"
:component="component"
:is-new="wrapped.isNew"
- :show-report-section-status="showReportSectionStatus"
+ :show-report-section-status-icon="showReportSectionStatusIcon"
+ :class="issueItemClass"
/>
</smart-virtual-list>
</template>
diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue
index 241185e3126..3d576caaf8f 100644
--- a/app/assets/javascripts/reports/components/report_section.vue
+++ b/app/assets/javascripts/reports/components/report_section.vue
@@ -3,10 +3,7 @@ import { __ } from '~/locale';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import Popover from '~/vue_shared/components/help_popover.vue';
import IssuesList from './issues_list.vue';
-
-const LOADING = 'LOADING';
-const ERROR = 'ERROR';
-const SUCCESS = 'SUCCESS';
+import { status } from '../constants';
export default {
name: 'ReportSection',
@@ -42,7 +39,8 @@ export default {
},
successText: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
unresolvedIssues: {
type: Array,
@@ -78,6 +76,21 @@ export default {
required: false,
default: true,
},
+ issuesUlElementClass: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
+ issuesListContainerClass: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
+ issueItemClass: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
},
data() {
@@ -91,13 +104,13 @@ export default {
return this.isCollapsed ? __('Expand') : __('Collapse');
},
isLoading() {
- return this.status === LOADING;
+ return this.status === status.LOADING;
},
loadingFailed() {
- return this.status === ERROR;
+ return this.status === status.ERROR;
},
isSuccess() {
- return this.status === SUCCESS;
+ return this.status === status.SUCCESS;
},
isCollapsible() {
return !this.alwaysOpen && this.hasIssues;
@@ -132,6 +145,15 @@ export default {
hasPopover() {
return Object.keys(this.popoverOptions).length > 0;
},
+ slotName() {
+ if (this.isSuccess) {
+ return 'success';
+ } else if (this.isLoading) {
+ return 'loading';
+ }
+
+ return 'error';
+ },
},
methods: {
toggleCollapsed() {
@@ -147,6 +169,7 @@ export default {
<div class="media-body d-flex flex-align-self-center">
<span class="js-code-text code-text">
{{ headerText }}
+ <slot :name="slotName"></slot>
<popover v-if="hasPopover" :options="popoverOptions" class="prepend-left-5" />
</span>
@@ -172,6 +195,9 @@ export default {
:neutral-issues="neutralIssues"
:component="component"
:show-report-section-status-icon="showReportSectionStatusIcon"
+ :issues-ul-element-class="issuesUlElementClass"
+ :class="issuesListContainerClass"
+ :issue-item-class="issueItemClass"
/>
</slot>
</div>
diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/reports/constants.js
index c323dc543f3..66ac1af062b 100644
--- a/app/assets/javascripts/reports/constants.js
+++ b/app/assets/javascripts/reports/constants.js
@@ -16,3 +16,9 @@ export const STATUS_NEUTRAL = 'neutral';
export const ICON_WARNING = 'warning';
export const ICON_SUCCESS = 'success';
export const ICON_NOTFOUND = 'notfound';
+
+export const status = {
+ LOADING: 'LOADING',
+ ERROR: 'ERROR',
+ SUCCESS: 'SUCCESS',
+};
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index f4df98ac2ff..d2198bcccfe 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -100,7 +100,7 @@ export default {
this.fetchFiles();
}
})
- .catch(() => createFlash(__('An error occurding while fetching folder content.')));
+ .catch(() => createFlash(__('An error occurred while fetching folder content.')));
},
normalizeData(key, data) {
return this.entries[key].concat(data.map(({ node }) => node));
@@ -134,6 +134,7 @@ export default {
:current-path="path"
:path="entry.flatPath"
:type="entry.type"
+ :url="entry.webUrl"
/>
</template>
</tbody>
diff --git a/app/assets/javascripts/repository/components/table/parent_row.vue b/app/assets/javascripts/repository/components/table/parent_row.vue
index b4433f00d8a..3c39f404226 100644
--- a/app/assets/javascripts/repository/components/table/parent_row.vue
+++ b/app/assets/javascripts/repository/components/table/parent_row.vue
@@ -27,8 +27,8 @@ export default {
</script>
<template>
- <tr v-once @click="clickRow">
- <td colspan="3" class="tree-item-file-name">
+ <tr class="tree-item">
+ <td colspan="3" class="tree-item-file-name" @click.self="clickRow">
<router-link :to="parentRoute" :aria-label="__('Go to parent')">
..
</router-link>
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 9a264bef87e..764882a7936 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -21,6 +21,11 @@ export default {
type: String,
required: true,
},
+ url: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
routerLinkTo() {
@@ -59,7 +64,7 @@ export default {
<tr v-once :class="`file_${id}`" class="tree-item" @click="openRow">
<td class="tree-item-file-name">
<i :aria-label="type" role="img" :class="iconName" class="fa fa-fw"></i>
- <component :is="linkComponent" :to="routerLinkTo" class="str-truncated">
+ <component :is="linkComponent" :to="routerLinkTo" :href="url" class="str-truncated">
{{ fullPath }}
</component>
<template v-if="isSubmodule">
diff --git a/app/assets/javascripts/repository/queries/getFiles.graphql b/app/assets/javascripts/repository/queries/getFiles.graphql
index a9b61d28560..7d92bc46455 100644
--- a/app/assets/javascripts/repository/queries/getFiles.graphql
+++ b/app/assets/javascripts/repository/queries/getFiles.graphql
@@ -23,6 +23,7 @@ query getFiles(
edges {
node {
...TreeEntry
+ webUrl
}
}
pageInfo {
@@ -43,6 +44,7 @@ query getFiles(
edges {
node {
...TreeEntry
+ webUrl
}
}
pageInfo {
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 72e061df573..930c0d5e958 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -82,9 +82,9 @@ Sidebar.prototype.toggleTodo = function(e) {
ajaxType = $this.data('deletePath') ? 'delete' : 'post';
if ($this.data('deletePath')) {
- url = '' + $this.data('deletePath');
+ url = String($this.data('deletePath'));
} else {
- url = '' + $this.data('createPath');
+ url = String($this.data('createPath'));
}
$this.tooltip('hide');
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index ab43c2139bf..6aca4067ba7 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -379,7 +379,7 @@ export class SearchAutocomplete {
}
}
}
- this.wrap.toggleClass('has-value', !!e.target.value);
+ this.wrap.toggleClass('has-value', Boolean(e.target.value));
}
onSearchInputFocus() {
@@ -396,7 +396,7 @@ export class SearchAutocomplete {
onClearInputClick(e) {
e.preventDefault();
- this.wrap.toggleClass('has-value', !!e.target.value);
+ this.wrap.toggleClass('has-value', Boolean(e.target.value));
return this.searchInput.val('').focus();
}
@@ -405,7 +405,7 @@ export class SearchAutocomplete {
this.wrap.removeClass('search-active');
// If input is blank then restore state
if (this.searchInput.val() === '') {
- return this.restoreOriginalState();
+ this.restoreOriginalState();
}
this.dropdownMenu.removeClass('show');
}
diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
index f9b4e789563..94341050b86 100644
--- a/app/assets/javascripts/serverless/components/functions.vue
+++ b/app/assets/javascripts/serverless/components/functions.vue
@@ -4,6 +4,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import FunctionRow from './function_row.vue';
import EnvironmentRow from './environment_row.vue';
import EmptyState from './empty_state.vue';
+import { CHECKING_INSTALLED } from '../constants';
export default {
components: {
@@ -13,10 +14,6 @@ export default {
GlLoadingIcon,
},
props: {
- installed: {
- type: Boolean,
- required: true,
- },
clustersPath: {
type: String,
required: true,
@@ -31,8 +28,15 @@ export default {
},
},
computed: {
- ...mapState(['isLoading', 'hasFunctionData']),
+ ...mapState(['installed', 'isLoading', 'hasFunctionData']),
...mapGetters(['getFunctions']),
+
+ checkingInstalled() {
+ return this.installed === CHECKING_INSTALLED;
+ },
+ isInstalled() {
+ return this.installed === true;
+ },
},
created() {
this.fetchFunctions({
@@ -47,15 +51,16 @@ export default {
<template>
<section id="serverless-functions">
- <div v-if="installed">
+ <gl-loading-icon
+ v-if="checkingInstalled"
+ :size="2"
+ class="prepend-top-default append-bottom-default"
+ />
+
+ <div v-else-if="isInstalled">
<div v-if="hasFunctionData">
- <gl-loading-icon
- v-if="isLoading"
- :size="2"
- class="prepend-top-default append-bottom-default"
- />
- <template v-else>
- <div class="groups-list-tree-container">
+ <template>
+ <div class="groups-list-tree-container js-functions-wrapper">
<ul class="content-list group-list-tree">
<environment-row
v-for="(env, index) in getFunctions"
@@ -66,6 +71,11 @@ export default {
</ul>
</div>
</template>
+ <gl-loading-icon
+ v-if="isLoading"
+ :size="2"
+ class="prepend-top-default append-bottom-default js-functions-loader"
+ />
</div>
<div v-else class="empty-state js-empty-state">
<div class="text-content">
diff --git a/app/assets/javascripts/serverless/constants.js b/app/assets/javascripts/serverless/constants.js
index 35f77205f2c..2fa15e56ccb 100644
--- a/app/assets/javascripts/serverless/constants.js
+++ b/app/assets/javascripts/serverless/constants.js
@@ -1,3 +1,7 @@
export const MAX_REQUESTS = 3; // max number of times to retry
export const X_INTERVAL = 5; // Reflects the number of verticle bars on the x-axis
+
+export const CHECKING_INSTALLED = 'checking'; // The backend is still determining whether or not Knative is installed
+
+export const TIMEOUT = 'timeout';
diff --git a/app/assets/javascripts/serverless/serverless_bundle.js b/app/assets/javascripts/serverless/serverless_bundle.js
index 2d3f086ffee..ed3b633d766 100644
--- a/app/assets/javascripts/serverless/serverless_bundle.js
+++ b/app/assets/javascripts/serverless/serverless_bundle.js
@@ -45,7 +45,7 @@ export default class Serverless {
},
});
} else {
- const { statusPath, clustersPath, helpPath, installed } = document.querySelector(
+ const { statusPath, clustersPath, helpPath } = document.querySelector(
'.js-serverless-functions-page',
).dataset;
@@ -56,7 +56,6 @@ export default class Serverless {
render(createElement) {
return createElement(Functions, {
props: {
- installed: installed !== undefined,
clustersPath,
helpPath,
statusPath,
diff --git a/app/assets/javascripts/serverless/store/actions.js b/app/assets/javascripts/serverless/store/actions.js
index 826501c9022..a0a9fdf7ace 100644
--- a/app/assets/javascripts/serverless/store/actions.js
+++ b/app/assets/javascripts/serverless/store/actions.js
@@ -3,13 +3,18 @@ import axios from '~/lib/utils/axios_utils';
import statusCodes from '~/lib/utils/http_status';
import { backOff } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
-import { MAX_REQUESTS } from '../constants';
+import { __ } from '~/locale';
+import { MAX_REQUESTS, CHECKING_INSTALLED, TIMEOUT } from '../constants';
export const requestFunctionsLoading = ({ commit }) => commit(types.REQUEST_FUNCTIONS_LOADING);
export const receiveFunctionsSuccess = ({ commit }, data) =>
commit(types.RECEIVE_FUNCTIONS_SUCCESS, data);
-export const receiveFunctionsNoDataSuccess = ({ commit }) =>
- commit(types.RECEIVE_FUNCTIONS_NODATA_SUCCESS);
+export const receiveFunctionsPartial = ({ commit }, data) =>
+ commit(types.RECEIVE_FUNCTIONS_PARTIAL, data);
+export const receiveFunctionsTimeout = ({ commit }, data) =>
+ commit(types.RECEIVE_FUNCTIONS_TIMEOUT, data);
+export const receiveFunctionsNoDataSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_FUNCTIONS_NODATA_SUCCESS, data);
export const receiveFunctionsError = ({ commit }, error) =>
commit(types.RECEIVE_FUNCTIONS_ERROR, error);
@@ -25,18 +30,25 @@ export const receiveMetricsError = ({ commit }, error) =>
export const fetchFunctions = ({ dispatch }, { functionsPath }) => {
let retryCount = 0;
+ const functionsPartiallyFetched = data => {
+ if (data.functions !== null && data.functions.length) {
+ dispatch('receiveFunctionsPartial', data);
+ }
+ };
+
dispatch('requestFunctionsLoading');
backOff((next, stop) => {
axios
.get(functionsPath)
.then(response => {
- if (response.status === statusCodes.NO_CONTENT) {
+ if (response.data.knative_installed === CHECKING_INSTALLED) {
retryCount += 1;
if (retryCount < MAX_REQUESTS) {
+ functionsPartiallyFetched(response.data);
next();
} else {
- stop(null);
+ stop(TIMEOUT);
}
} else {
stop(response.data);
@@ -45,10 +57,13 @@ export const fetchFunctions = ({ dispatch }, { functionsPath }) => {
.catch(stop);
})
.then(data => {
- if (data !== null) {
+ if (data === TIMEOUT) {
+ dispatch('receiveFunctionsTimeout');
+ createFlash(__('Loading functions timed out. Please reload the page to try again.'));
+ } else if (data.functions !== null && data.functions.length) {
dispatch('receiveFunctionsSuccess', data);
} else {
- dispatch('receiveFunctionsNoDataSuccess');
+ dispatch('receiveFunctionsNoDataSuccess', data);
}
})
.catch(error => {
diff --git a/app/assets/javascripts/serverless/store/mutation_types.js b/app/assets/javascripts/serverless/store/mutation_types.js
index 25b2f7ac38a..b8fa9ea1a01 100644
--- a/app/assets/javascripts/serverless/store/mutation_types.js
+++ b/app/assets/javascripts/serverless/store/mutation_types.js
@@ -1,5 +1,7 @@
export const REQUEST_FUNCTIONS_LOADING = 'REQUEST_FUNCTIONS_LOADING';
export const RECEIVE_FUNCTIONS_SUCCESS = 'RECEIVE_FUNCTIONS_SUCCESS';
+export const RECEIVE_FUNCTIONS_PARTIAL = 'RECEIVE_FUNCTIONS_PARTIAL';
+export const RECEIVE_FUNCTIONS_TIMEOUT = 'RECEIVE_FUNCTIONS_TIMEOUT';
export const RECEIVE_FUNCTIONS_NODATA_SUCCESS = 'RECEIVE_FUNCTIONS_NODATA_SUCCESS';
export const RECEIVE_FUNCTIONS_ERROR = 'RECEIVE_FUNCTIONS_ERROR';
diff --git a/app/assets/javascripts/serverless/store/mutations.js b/app/assets/javascripts/serverless/store/mutations.js
index 991f32a275d..2685a5b11ff 100644
--- a/app/assets/javascripts/serverless/store/mutations.js
+++ b/app/assets/javascripts/serverless/store/mutations.js
@@ -5,12 +5,23 @@ export default {
state.isLoading = true;
},
[types.RECEIVE_FUNCTIONS_SUCCESS](state, data) {
- state.functions = data;
+ state.functions = data.functions;
+ state.installed = data.knative_installed;
state.isLoading = false;
state.hasFunctionData = true;
},
- [types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state) {
+ [types.RECEIVE_FUNCTIONS_PARTIAL](state, data) {
+ state.functions = data.functions;
+ state.installed = true;
+ state.isLoading = true;
+ state.hasFunctionData = true;
+ },
+ [types.RECEIVE_FUNCTIONS_TIMEOUT](state) {
+ state.isLoading = false;
+ },
+ [types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state, data) {
state.isLoading = false;
+ state.installed = data.knative_installed;
state.hasFunctionData = false;
},
[types.RECEIVE_FUNCTIONS_ERROR](state, error) {
diff --git a/app/assets/javascripts/serverless/store/state.js b/app/assets/javascripts/serverless/store/state.js
index afc3f37d7ba..fdd29299749 100644
--- a/app/assets/javascripts/serverless/store/state.js
+++ b/app/assets/javascripts/serverless/store/state.js
@@ -1,5 +1,6 @@
export default () => ({
error: null,
+ installed: 'checking',
isLoading: true,
// functions
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 c03b2a68c78..d84d5344935 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -49,10 +49,10 @@ export default {
},
computed: {
hasTimeSpent() {
- return !!this.timeSpent;
+ return Boolean(this.timeSpent);
},
hasTimeEstimate() {
- return !!this.timeEstimate;
+ return Boolean(this.timeEstimate);
},
showComparisonState() {
return this.hasTimeEstimate && this.hasTimeSpent;
@@ -67,7 +67,7 @@ export default {
return !this.hasTimeEstimate && !this.hasTimeSpent;
},
showHelpState() {
- return !!this.showHelp;
+ return Boolean(this.showHelp);
},
},
created() {
diff --git a/app/assets/javascripts/test_utils/index.js b/app/assets/javascripts/test_utils/index.js
index a55a338eea8..1e75ee60671 100644
--- a/app/assets/javascripts/test_utils/index.js
+++ b/app/assets/javascripts/test_utils/index.js
@@ -1,5 +1,5 @@
-import 'core-js/es6/map';
-import 'core-js/es6/set';
+import 'core-js/es/map';
+import 'core-js/es/set';
import simulateDrag from './simulate_drag';
import simulateInput from './simulate_input';
diff --git a/app/assets/javascripts/validators/input_validator.js b/app/assets/javascripts/validators/input_validator.js
new file mode 100644
index 00000000000..f37373977b8
--- /dev/null
+++ b/app/assets/javascripts/validators/input_validator.js
@@ -0,0 +1,34 @@
+const invalidInputClass = 'gl-field-error-outline';
+
+export default class InputValidator {
+ constructor() {
+ this.inputDomElement = {};
+ this.inputErrorMessage = {};
+ this.errorMessage = null;
+ this.invalidInput = null;
+ }
+
+ setValidationStateAndMessage() {
+ this.setValidationMessage();
+
+ const isInvalidInput = !this.inputDomElement.checkValidity();
+ this.inputDomElement.classList.toggle(invalidInputClass, isInvalidInput);
+ this.inputErrorMessage.classList.toggle('hide', !isInvalidInput);
+ }
+
+ setValidationMessage() {
+ if (this.invalidInput) {
+ this.inputDomElement.setCustomValidity(this.errorMessage);
+ this.inputErrorMessage.innerHTML = this.errorMessage;
+ } else {
+ this.resetValidationMessage();
+ }
+ }
+
+ resetValidationMessage() {
+ if (this.inputDomElement.validationMessage === this.errorMessage) {
+ this.inputDomElement.setCustomValidity('');
+ this.inputErrorMessage.innerHTML = this.inputDomElement.title;
+ }
+ }
+}
diff --git a/app/assets/javascripts/visual_review_toolbar/index.js b/app/assets/javascripts/visual_review_toolbar/index.js
new file mode 100644
index 00000000000..91d0382feac
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/index.js
@@ -0,0 +1,2 @@
+import './styles/toolbar.css';
+import 'vendor/visual_review_toolbar';
diff --git a/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css b/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css
new file mode 100644
index 00000000000..342b3599a44
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css
@@ -0,0 +1,149 @@
+/*
+ As a standalone script, the toolbar has its own css
+ */
+
+#gitlab-collapse > * {
+ pointer-events: none;
+}
+
+#gitlab-form-wrapper {
+ display: flex;
+ flex-direction: column;
+ width: 100%
+}
+
+#gitlab-review-container {
+ max-width: 22rem;
+ max-height: 22rem;
+ overflow: scroll;
+ position: fixed;
+ bottom: 1rem;
+ right: 1rem;
+ display: flex;
+ flex-direction: row-reverse;
+ padding: 1rem;
+ background-color: #fff;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
+ 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+ 'Noto Color Emoji';
+ font-size: .8rem;
+ font-weight: 400;
+ color: #2e2e2e;
+}
+
+.gitlab-open-wrapper {
+ max-width: 22rem;
+ max-height: 22rem;
+}
+
+.gitlab-closed-wrapper {
+ max-width: 3.4rem;
+ max-height: 3.4rem;
+}
+
+.gitlab-button {
+ cursor: pointer;
+ transition: background-color 100ms linear, border-color 100ms linear, color 100ms linear, box-shadow 100ms linear;
+}
+
+.gitlab-button-secondary {
+ background: none #fff;
+ margin: 0 .5rem;
+ border: 1px solid #e3e3e3;
+}
+
+.gitlab-button-secondary:hover {
+ background-color: #f0f0f0;
+ border-color: #e3e3e3;
+ color: #2e2e2e;
+}
+
+.gitlab-button-secondary:active {
+ color: #2e2e2e;
+ background-color: #e1e1e1;
+ border-color: #dadada;
+}
+
+.gitlab-button-success:hover {
+ color: #fff;
+ background-color: #137e3f;
+ border-color: #127339;
+}
+
+.gitlab-button-success:active {
+ background-color: #168f48;
+ border-color: #12753a;
+ color: #fff;
+}
+
+.gitlab-button-success {
+ background-color: #1aaa55;
+ border: 1px solid #168f48;
+ color: #fff;
+}
+
+.gitlab-button-wide {
+ width: 100%;
+}
+
+.gitlab-button-wrapper {
+ margin-top: 1rem;
+ display: flex;
+ align-items: baseline;
+ justify-content: flex-end;
+}
+
+.gitlab-collapse {
+ width: 2.4rem;
+ height: 2.2rem;
+ margin-left: 1rem;
+ padding: .5rem;
+}
+
+.gitlab-collapse-closed {
+ align-self: center;
+}
+
+.gitlab-checkbox-label {
+ padding: 0 .2rem;
+}
+
+.gitlab-checkbox-wrapper {
+ display: flex;
+ align-items: baseline;
+}
+
+.gitlab-label {
+ font-weight: 600;
+ display: inline-block;
+ width: 100%;
+}
+
+.gitlab-link {
+ color: #1b69b6;
+ text-decoration: none;
+ background-color: transparent;
+ background-image: none;
+}
+
+.gitlab-message {
+ padding: .25rem 0;
+ margin: 0;
+ line-height: 1.2rem;
+}
+
+.gitlab-metadata-note {
+ font-size: .7rem;
+ line-height: 1rem;
+ color: #666;
+ margin-bottom: 0;
+}
+
+.gitlab-input {
+ width: 100%;
+ border: 1px solid #dfdfdf;
+ border-radius: 4px;
+ padding: .1rem .2rem;
+ min-height: 2rem;
+ max-width: 17rem;
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index ad0464a3a98..abe5bdd2901 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -77,16 +77,16 @@ export default {
return this.deployment.external_url;
},
hasExternalUrls() {
- return !!(this.deployment.external_url && this.deployment.external_url_formatted);
+ return Boolean(this.deployment.external_url && this.deployment.external_url_formatted);
},
hasDeploymentTime() {
- return !!(this.deployment.deployed_at && this.deployment.deployed_at_formatted);
+ return Boolean(this.deployment.deployed_at && this.deployment.deployed_at_formatted);
},
hasDeploymentMeta() {
- return !!(this.deployment.url && this.deployment.name);
+ return Boolean(this.deployment.url && this.deployment.name);
},
hasMetrics() {
- return !!this.deployment.metrics_url;
+ return Boolean(this.deployment.metrics_url);
},
deployedText() {
return this.$options.deployedTextMap[this.deployment.status];
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue
index 040315b3c66..19a222462b3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue
@@ -37,7 +37,7 @@ export default {
</script>
<template>
- <div class="m-3 ml-5" :class="messageClass">
+ <div class="m-3 ml-7" :class="messageClass">
<slot></slot>
<gl-link v-if="helpPath" :href="helpPath" target="_blank">
<icon :size="16" name="question-o" class="align-middle" />
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 361441640e1..e20a16900d4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -128,7 +128,7 @@ export default {
:disabled="mr.sourceBranchRemoved"
data-target="#modal_merge_info"
data-toggle="modal"
- class="btn btn-default js-check-out-branch append-right-default"
+ class="btn btn-default js-check-out-branch append-right-8"
type="button"
>
{{ s__('mrWidget|Check out branch') }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index f5fa68308bc..c377c16fb13 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -5,6 +5,7 @@ import { sprintf, __ } from '~/locale';
import PipelineStage from '~/pipelines/components/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
+import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline';
@@ -16,6 +17,7 @@ export default {
Icon,
TooltipOnTruncate,
GlLink,
+ PipelineLink,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
},
@@ -112,9 +114,12 @@ export default {
<div class="media-body">
<div class="font-weight-bold js-pipeline-info-container">
{{ s__('Pipeline|Pipeline') }}
- <gl-link :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number"
- >#{{ pipeline.id }}</gl-link
- >
+ <pipeline-link
+ :href="pipeline.path"
+ :pipeline-id="pipeline.id"
+ :pipeline-iid="pipeline.iid"
+ class="pipeline-id pipeline-iid font-weight-normal"
+ />
{{ pipeline.details.status.label }}
<template v-if="hasCommitInfo">
{{ s__('Pipeline|for') }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index 0686409a785..03a15ba81ed 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -56,7 +56,7 @@ export default {
return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline;
},
showVisualReviewAppLink() {
- return !!(this.mr.visualReviewFF && this.mr.visualReviewAppAvailable);
+ return Boolean(this.mr.visualReviewFF && this.mr.visualReviewAppAvailable);
},
},
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
index a3a44dd8e99..83e7d6db9fa 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
@@ -35,9 +35,7 @@ export default {
<status-icon status="warning" />
<div class="media-body space-children">
<span class="bold">
- <template v-if="mr.mergeError"
- >{{ mr.mergeError }}.</template
- >
+ <template v-if="mr.mergeError">{{ mr.mergeError }}</template>
{{ s__('mrWidget|This merge request failed to be merged automatically') }}
</span>
<button
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
index 1b3af2fccf2..88e1ccbaf35 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
@@ -57,7 +57,7 @@ export default {
removeSourceBranch() {
const options = {
sha: this.mr.sha,
- merge_when_pipeline_succeeds: true,
+ auto_merge_strategy: 'merge_when_pipeline_succeeds',
should_remove_source_branch: true,
};
@@ -85,7 +85,7 @@ export default {
<h4 class="d-flex align-items-start">
<span class="append-right-10">
{{ s__('mrWidget|Set by') }}
- <mr-widget-author :author="mr.setToMWPSBy" />
+ <mr-widget-author :author="mr.setToAutoMergeBy" />
{{ s__('mrWidget|to be merged automatically when the pipeline succeeds') }}
</span>
<a
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 851939d5d4e..615d59a7b8e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -31,7 +31,7 @@ export default {
return {
removeSourceBranch: this.mr.shouldRemoveSourceBranch,
mergeWhenBuildSucceeds: false,
- setToMergeWhenPipelineSucceeds: false,
+ autoMergeStrategy: undefined,
isMakingRequest: false,
isMergingImmediately: false,
commitMessage: this.mr.commitMessage,
@@ -42,7 +42,7 @@ export default {
};
},
computed: {
- shouldShowMergeWhenPipelineSucceedsText() {
+ shouldShowAutoMergeText() {
return this.mr.isPipelineActive;
},
status() {
@@ -87,7 +87,7 @@ export default {
mergeButtonText() {
if (this.isMergingImmediately) {
return __('Merge in progress');
- } else if (this.shouldShowMergeWhenPipelineSucceedsText) {
+ } else if (this.shouldShowAutoMergeText) {
return __('Merge when pipeline succeeds');
}
@@ -104,7 +104,7 @@ export default {
return enableSquashBeforeMerge && commitsCount > 1;
},
shouldShowMergeControls() {
- return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText;
+ return this.mr.isMergeAllowed || this.shouldShowAutoMergeText;
},
shouldShowSquashEdit() {
return this.squashBeforeMerge && this.shouldShowSquashBeforeMerge;
@@ -126,12 +126,12 @@ export default {
this.isMergingImmediately = true;
}
- this.setToMergeWhenPipelineSucceeds = mergeWhenBuildSucceeds === true;
+ this.autoMergeStrategy = mergeWhenBuildSucceeds ? 'merge_when_pipeline_succeeds' : undefined;
const options = {
sha: this.mr.sha,
commit_message: this.commitMessage,
- merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds,
+ auto_merge_strategy: this.autoMergeStrategy,
should_remove_source_branch: this.removeSourceBranch === true,
squash: this.squashBeforeMerge,
squash_commit_message: this.squashCommitMessage,
@@ -330,6 +330,7 @@ export default {
:commits-count="mr.commitsCount"
:target-branch="mr.targetBranch"
:is-fast-forward-enabled="mr.ffOnlyEnabled"
+ :class="{ 'border-bottom': mr.mergeError }"
>
<ul class="border-top content-list commits-list flex-list">
<commit-edit
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
index b1f5655a15a..accb9d9fef1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
@@ -29,8 +29,8 @@ export default {
</script>
<template>
- <div class="accept-control inline">
- <label class="merge-param-checkbox">
+ <div class="inline">
+ <label>
<input
:checked="value"
:disabled="isDisabled"
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index bf175eb5f69..d02bb2f341d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -1,6 +1,6 @@
<script>
import _ from 'underscore';
-import { __ } from '~/locale';
+import { sprintf, s__, __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
@@ -97,7 +97,7 @@ export default {
return this.mr.hasCI;
},
shouldRenderRelatedLinks() {
- return !!this.mr.relatedLinks && !this.mr.isNothingToMergeState;
+ return Boolean(this.mr.relatedLinks) && !this.mr.isNothingToMergeState;
},
shouldRenderSourceBranchRemovalStatus() {
return (
@@ -125,6 +125,11 @@ export default {
this.mr.pipeline.target_sha !== this.mr.targetBranchSha,
);
},
+ mergeError() {
+ return sprintf(s__('mrWidget|Merge failed: %{mergeError}. Please try again.'), {
+ mergeError: this.mr.mergeError,
+ });
+ },
},
watch: {
state(newVal, oldVal) {
@@ -370,6 +375,10 @@ export default {
}}
</mr-widget-alert-message>
+ <mr-widget-alert-message v-if="mr.mergeError" type="danger">
+ {{ mergeError }}
+ </mr-widget-alert-message>
+
<source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" />
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index 0cc4fd59f5e..3ab229567f6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -23,8 +23,8 @@ export default function deviseState(data) {
return stateKey.pipelineBlocked;
} else if (this.isSHAMismatch) {
return stateKey.shaMismatch;
- } else if (this.mergeWhenPipelineSucceeds) {
- return this.mergeError ? stateKey.autoMergeFailed : stateKey.mergeWhenPipelineSucceeds;
+ } else if (this.autoMergeEnabled) {
+ return this.mergeError ? stateKey.autoMergeFailed : stateKey.autoMergeEnabled;
} else if (!this.canMerge) {
return stateKey.notAllowedToMerge;
} else if (this.canBeMerged) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 45708d78886..32badb0fb08 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -61,7 +61,7 @@ export default class MergeRequestStore {
this.updatedAt = data.updated_at;
this.metrics = MergeRequestStore.buildMetrics(data.metrics);
- this.setToMWPSBy = MergeRequestStore.formatUserObject(data.merge_user || {});
+ this.setToAutoMergeBy = MergeRequestStore.formatUserObject(data.merge_user || {});
this.mergeUserId = data.merge_user_id;
this.currentUserId = gon.current_user_id;
this.sourceBranchPath = data.source_branch_path;
@@ -70,15 +70,16 @@ export default class MergeRequestStore {
this.targetBranchPath = data.target_branch_commits_path;
this.targetBranchTreePath = data.target_branch_tree_path;
this.conflictResolutionPath = data.conflict_resolution_path;
- this.cancelAutoMergePath = data.cancel_merge_when_pipeline_succeeds_path;
+ this.cancelAutoMergePath = data.cancel_auto_merge_path;
this.removeWIPPath = data.remove_wip_path;
this.sourceBranchRemoved = !data.source_branch_exists;
this.shouldRemoveSourceBranch = data.remove_source_branch || false;
this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
- this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false;
+ this.autoMergeEnabled = Boolean(data.auto_merge_enabled);
+ this.autoMergeStrategy = data.auto_merge_strategy;
this.mergePath = data.merge_path;
this.ffOnlyEnabled = data.ff_only_enabled;
- this.shouldBeRebased = !!data.should_be_rebased;
+ this.shouldBeRebased = Boolean(data.should_be_rebased);
this.statusPath = data.status_path;
this.emailPatchesPath = data.email_patches_path;
this.plainDiffPath = data.plain_diff_path;
@@ -91,9 +92,9 @@ export default class MergeRequestStore {
this.isOpen = data.state === 'opened';
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false;
- this.canMerge = !!data.merge_path;
+ this.canMerge = Boolean(data.merge_path);
this.canCreateIssue = currentUser.can_create_issue || false;
- this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path;
+ this.canCancelAutomaticMerge = Boolean(data.cancel_auto_merge_path);
this.isSHAMismatch = this.sha !== data.diff_head_sha;
this.canBeMerged = data.can_be_merged || false;
this.isMergeAllowed = data.mergeable || false;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
index e080ce5c229..48bc6a867f4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
@@ -13,7 +13,7 @@ const stateToComponentMap = {
unresolvedDiscussions: 'mr-widget-unresolved-discussions',
pipelineBlocked: 'mr-widget-pipeline-blocked',
pipelineFailed: 'mr-widget-pipeline-failed',
- mergeWhenPipelineSucceeds: 'mr-widget-merge-when-pipeline-succeeds',
+ autoMergeEnabled: 'mr-widget-merge-when-pipeline-succeeds',
failedToMerge: 'mr-widget-failed-to-merge',
autoMergeFailed: 'mr-widget-auto-merge-failed',
shaMismatch: 'sha-mismatch',
@@ -45,7 +45,7 @@ export const stateKey = {
pipelineBlocked: 'pipelineBlocked',
shaMismatch: 'shaMismatch',
autoMergeFailed: 'autoMergeFailed',
- mergeWhenPipelineSucceeds: 'mergeWhenPipelineSucceeds',
+ autoMergeEnabled: 'autoMergeEnabled',
notAllowedToMerge: 'notAllowedToMerge',
readyToMerge: 'readyToMerge',
rebase: 'rebase',
diff --git a/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue b/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue
new file mode 100644
index 00000000000..eae4c06467c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue
@@ -0,0 +1,32 @@
+<script>
+import { GlLink, GlTooltipDirective } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ href: {
+ type: String,
+ required: true,
+ },
+ pipelineId: {
+ type: Number,
+ required: true,
+ },
+ pipelineIid: {
+ type: Number,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <gl-link v-gl-tooltip :href="href" :title="__('Pipeline ID (IID)')">
+ <span class="pipeline-id">#{{ pipelineId }}</span>
+ <span class="pipeline-iid">(#{{ pipelineIid }})</span>
+ </gl-link>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue b/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue
new file mode 100644
index 00000000000..7d49c87271d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue
@@ -0,0 +1,89 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import Icon from './icon.vue';
+
+export default {
+ components: {
+ Icon,
+ GlButton,
+ },
+ props: {
+ size: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ primaryButtonClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ dropdownClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ actions: {
+ type: Array,
+ required: true,
+ },
+ defaultAction: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ selectedAction: this.defaultAction,
+ };
+ },
+ computed: {
+ selectedActionTitle() {
+ return this.actions[this.selectedAction].title;
+ },
+ buttonSizeClass() {
+ return `btn-${this.size}`;
+ },
+ },
+ methods: {
+ handlePrimaryActionClick() {
+ this.$emit('onActionClick', this.actions[this.selectedAction]);
+ },
+ handleActionClick(selectedAction) {
+ this.selectedAction = selectedAction;
+ this.$emit('onActionSelect', selectedAction);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="btn-group droplab-dropdown comment-type-dropdown">
+ <gl-button :class="primaryButtonClass" :size="size" @click.prevent="handlePrimaryActionClick">
+ {{ selectedActionTitle }}
+ </gl-button>
+ <button
+ :class="buttonSizeClass"
+ type="button"
+ class="btn dropdown-toggle pl-2 pr-2"
+ data-display="static"
+ data-toggle="dropdown"
+ >
+ <icon name="arrow-down" aria-label="toggle dropdown" />
+ </button>
+ <ul :class="dropdownClass" class="dropdown-menu dropdown-open-top">
+ <template v-for="(action, index) in actions">
+ <li :key="index" :class="{ 'droplab-item-selected': selectedAction === index }">
+ <gl-button class="btn-transparent" @click.prevent="handleActionClick(index)">
+ <i aria-hidden="true" class="fa fa-check icon"> </i>
+ <div class="description">
+ <strong>{{ action.title }}</strong>
+ <p>{{ action.description }}</p>
+ </div>
+ </gl-button>
+ </li>
+ <li v-if="index === 0" :key="`${index}-separator`" class="divider droplab-item-ignore"></li>
+ </template>
+ </ul>
+ </div>
+</template>
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 3f45dc7853b..0bac63b1062 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -37,6 +37,16 @@ export default {
type: Number,
required: true,
},
+ itemIid: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ itemIdTooltip: {
+ type: String,
+ required: false,
+ default: '',
+ },
time: {
type: String,
required: true,
@@ -85,7 +95,12 @@ export default {
<section class="header-main-content">
<ci-icon-badge :status="status" />
- <strong> {{ itemName }} #{{ itemId }} </strong>
+ <strong v-gl-tooltip :title="itemIdTooltip">
+ {{ itemName }} #{{ itemId }}
+ <template v-if="itemIid"
+ >(#{{ itemIid }})</template
+ >
+ </strong>
<template v-if="shouldRenderTriggeredLabel">
triggered
@@ -96,9 +111,8 @@ export default {
<timeago-tooltip :time="time" />
- by
-
<template v-if="user">
+ by
<gl-link
v-gl-tooltip
:href="user.path"
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue b/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
index 7e79e63aa1e..715cf97f0ac 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
@@ -62,6 +62,15 @@ export default {
assigneeName: assignee.name,
});
},
+ // This method is for backward compat
+ // since Graph query would return camelCase
+ // props while Rails would return snake_case
+ webUrl(assignee) {
+ return assignee.web_url || assignee.webUrl;
+ },
+ avatarUrl(assignee) {
+ return assignee.avatar_url || assignee.avatarUrl;
+ },
},
};
</script>
@@ -70,9 +79,9 @@ export default {
<user-avatar-link
v-for="assignee in assigneesToShow"
:key="assignee.id"
- :link-href="assignee.web_url"
+ :link-href="webUrl(assignee)"
:img-alt="avatarUrlTitle(assignee)"
- :img-src="assignee.avatar_url"
+ :img-src="avatarUrl(assignee)"
:img-size="24"
class="js-no-trigger"
tooltip-placement="bottom"
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
index 53e6efa6ea3..9b2ee5062b1 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
@@ -19,10 +19,14 @@ export default {
},
computed: {
milestoneDue() {
- return this.milestone.due_date ? parsePikadayDate(this.milestone.due_date) : null;
+ const dueDate = this.milestone.due_date || this.milestone.dueDate;
+
+ return dueDate ? parsePikadayDate(dueDate) : null;
},
milestoneStart() {
- return this.milestone.start_date ? parsePikadayDate(this.milestone.start_date) : null;
+ const startDate = this.milestone.start_date || this.milestone.startDate;
+
+ return startDate ? parsePikadayDate(startDate) : null;
},
isMilestoneStarted() {
if (!this.milestoneStart) {
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
index e92babc499b..e438ff16a41 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
@@ -1,9 +1,17 @@
<script>
+import { GlLink } from '@gitlab/ui';
+import _ from 'underscore';
+import { sprintf } from '~/locale';
import icon from '../../../vue_shared/components/icon.vue';
+function buildDocsLinkStart(path) {
+ return `<a href="${_.escape(path)}" target="_blank" rel="noopener noreferrer">`;
+}
+
export default {
components: {
icon,
+ GlLink,
},
props: {
isLocked: {
@@ -16,6 +24,16 @@ export default {
default: false,
required: false,
},
+ lockedIssueDocsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ confidentialIssueDocsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
warningIcon() {
@@ -27,6 +45,17 @@ export default {
isLockedAndConfidential() {
return this.isConfidential && this.isLocked;
},
+ confidentialAndLockedDiscussionText() {
+ return sprintf(
+ 'This issue is %{confidentialLinkStart}confidential%{linkEnd} and %{lockedLinkStart}locked%{linkEnd}.',
+ {
+ confidentialLinkStart: buildDocsLinkStart(this.confidentialIssueDocsPath),
+ lockedLinkStart: buildDocsLinkStart(this.lockedIssueDocsPath),
+ linkEnd: '</a>',
+ },
+ false,
+ );
+ },
},
};
</script>
@@ -35,20 +64,26 @@ export default {
<icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" />
<span v-if="isLockedAndConfidential">
- {{ __('This issue is confidential and locked.') }}
+ <span v-html="confidentialAndLockedDiscussionText"></span>
{{
- __(`People without permission will never
-get a notification and won't be able to comment.`)
+ __(`People without permission will never get a notification and won't be able to comment.`)
}}
</span>
<span v-else-if="isConfidential">
{{ __('This is a confidential issue.') }}
- {{ __('Your comment will not be visible to the public.') }}
+ {{ __('People without permission will never get a notification.') }}
+ <gl-link :href="confidentialIssueDocsPath" target="_blank">
+ {{ __('Learn more') }}
+ </gl-link>
</span>
<span v-else-if="isLocked">
- {{ __('This issue is locked.') }} {{ __('Only project members can comment.') }}
+ {{ __('This issue is locked.') }}
+ {{ __('Only project members can comment.') }}
+ <gl-link :href="lockedIssueDocsPath" target="_blank">
+ {{ __('Learn more') }}
+ </gl-link>
</span>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
index b807a35b421..05ad7710a62 100644
--- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
@@ -24,6 +24,11 @@ export default {
required: false,
default: false,
},
+ greyLinkWhenMerged: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
stateTitle() {
@@ -36,6 +41,11 @@ export default {
},
);
},
+ issueableLinkClass() {
+ return this.greyLinkWhenMerged
+ ? `sortable-link ${this.state === 'merged' ? ' text-secondary' : ''}`
+ : 'sortable-link';
+ },
},
};
</script>
@@ -69,7 +79,7 @@ export default {
class="confidential-icon append-right-4 align-self-baseline align-self-md-auto mt-xl-0"
:aria-label="__('Confidential')"
/>
- <a :href="computedPath" class="sortable-link">{{ title }}</a>
+ <a :href="computedPath" :class="issueableLinkClass">{{ title }}</a>
</div>
<div class="item-meta d-flex flex-wrap mt-xl-0 justify-content-xl-end flex-xl-nowrap">
<div
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 3b57b5e8da4..d6c398c8946 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -33,37 +33,36 @@ export default {
<div class="comment-toolbar clearfix">
<div class="toolbar-text">
<template v-if="!hasQuickActionsDocsPath && markdownDocsPath">
- <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1">
- Markdown is supported
- </gl-link>
+ <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1"
+ >Markdown is supported</gl-link
+ >
</template>
<template v-if="hasQuickActionsDocsPath && markdownDocsPath">
- <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1"> Markdown </gl-link>
- and
- <gl-link :href="quickActionsDocsPath" target="_blank" tabindex="-1">
- quick actions
- </gl-link>
+ <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1">Markdown</gl-link> and
+ <gl-link :href="quickActionsDocsPath" target="_blank" tabindex="-1">quick actions</gl-link>
are supported
</template>
</div>
<span v-if="canAttachFile" class="uploading-container">
<span class="uploading-progress-container hide">
- <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"> </i>
- <span class="attaching-file-message"></span> <span class="uploading-progress">0%</span>
+ <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"></i>
+ <span class="attaching-file-message"></span>
+ <span class="uploading-progress">0%</span>
<span class="uploading-spinner">
- <i class="fa fa-spinner fa-spin toolbar-button-icon" aria-hidden="true"> </i>
+ <i class="fa fa-spinner fa-spin toolbar-button-icon" aria-hidden="true"></i>
</span>
</span>
<span class="uploading-error-container hide">
<span class="uploading-error-icon">
- <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"> </i>
+ <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"></i>
</span>
<span class="uploading-error-message"></span>
<button class="retry-uploading-link" type="button">Try again</button> or
<button class="attach-new-file markdown-selector" type="button">attach a new file</button>
</span>
- <button class="markdown-selector button-attach-file" tabindex="-1" type="button">
- <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"> </i> Attach a file
+ <button class="markdown-selector button-attach-file btn-link" tabindex="-1" type="button">
+ <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"></i
+ ><span class="text-attach-file">Attach a file</span>
</button>
<button class="btn btn-default btn-sm hide button-cancel-uploading-files" type="button">
Cancel
diff --git a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
new file mode 100644
index 00000000000..bf59a6abf3f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
@@ -0,0 +1,121 @@
+<script>
+import $ from 'jquery';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+import Clipboard from 'clipboard';
+
+export default {
+ components: {
+ GlButton,
+ Icon,
+ },
+
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+
+ props: {
+ text: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ container: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ modalId: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ target: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ tooltipPlacement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+ tooltipContainer: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+
+ copySuccessText: __('Copied'),
+
+ computed: {
+ modalDomId() {
+ return this.modalId ? `#${this.modalId}` : '';
+ },
+ },
+
+ mounted() {
+ this.$nextTick(() => {
+ this.clipboard = new Clipboard(this.$el, {
+ container:
+ document.querySelector(`${this.modalDomId} div.modal-content`) ||
+ document.getElementById(this.container) ||
+ document.body,
+ });
+ this.clipboard
+ .on('success', e => {
+ this.updateTooltip(e.trigger);
+ this.$emit('success', e);
+ // Clear the selection and blur the trigger so it loses its border
+ e.clearSelection();
+ $(e.trigger).blur();
+ })
+ .on('error', e => this.$emit('error', e));
+ });
+ },
+
+ destroyed() {
+ if (this.clipboard) {
+ this.clipboard.destroy();
+ }
+ },
+
+ methods: {
+ updateTooltip(target) {
+ const $target = $(target);
+ const originalTitle = $target.data('originalTitle');
+
+ if ($target.tooltip) {
+ /**
+ * The original tooltip will continue staying there unless we remove it by hand.
+ * $target.tooltip('hide') isn't working.
+ */
+ $('.tooltip').remove();
+ $target.attr('title', this.$options.copySuccessText);
+ $target.tooltip('_fixTitle');
+ $target.tooltip('show');
+ $target.attr('title', originalTitle);
+ $target.tooltip('_fixTitle');
+ }
+ },
+ },
+};
+</script>
+<template>
+ <gl-button
+ v-gl-tooltip="{ placement: tooltipPlacement, container: tooltipContainer }"
+ :data-clipboard-target="target"
+ :data-clipboard-text="text"
+ :title="title"
+ >
+ <slot>
+ <icon name="duplicate" />
+ </slot>
+ </gl-button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
index a50f49c1279..baed26a157c 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -51,7 +51,7 @@ export default {
<div class="note-header">
<div class="note-header-info">
<a :href="getUserData.path">
- <span class="d-none d-sm-inline-block">{{ getUserData.name }}</span>
+ <span class="d-none d-sm-inline-block bold">{{ getUserData.name }}</span>
<span class="note-headline-light">@{{ getUserData.username }}</span>
</a>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue
index fa502b9beb9..8104d919bf6 100644
--- a/app/assets/javascripts/vue_shared/components/pikaday.vue
+++ b/app/assets/javascripts/vue_shared/components/pikaday.vue
@@ -34,7 +34,7 @@ export default {
format: 'yyyy-mm-dd',
container: this.$el,
defaultDate: this.selectedDate,
- setDefaultDate: !!this.selectedDate,
+ setDefaultDate: Boolean(this.selectedDate),
minDate: this.minDate,
maxDate: this.maxDate,
parse: dateString => parsePikadayDate(dateString),
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue
index 8e0b08032f7..9cce9a4e542 100644
--- a/app/assets/javascripts/vue_shared/components/table_pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue
@@ -121,7 +121,7 @@ export default {
this.change(1);
break;
default:
- this.change(+text);
+ this.change(Number(text));
break;
}
},
diff --git a/app/assets/javascripts/vue_shared/models/label.js b/app/assets/javascripts/vue_shared/models/label.js
deleted file mode 100644
index 2d2732d0661..00000000000
--- a/app/assets/javascripts/vue_shared/models/label.js
+++ /dev/null
@@ -1,13 +0,0 @@
-export default class ListLabel {
- constructor(obj) {
- this.id = obj.id;
- this.title = obj.title;
- this.type = obj.type;
- this.color = obj.color;
- this.textColor = obj.text_color;
- this.description = obj.description;
- this.priority = obj.priority !== null ? obj.priority : Infinity;
- }
-}
-
-window.ListLabel = ListLabel;
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index 93377b8dd91..7f6384f4eea 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -22,7 +22,9 @@ body,
.form-control,
.search form {
// Override default font size used in non-csslab UI
- font-size: 14px;
+ // Use rem to keep default font-size at 14px on body so 1rem still
+ // fits 8px grid, but also allow users to change browser font size
+ font-size: .875rem;
}
legend {
diff --git a/app/assets/stylesheets/components/avatar.scss b/app/assets/stylesheets/components/avatar.scss
index 25ee3ca944d..1afa5ed90f4 100644
--- a/app/assets/stylesheets/components/avatar.scss
+++ b/app/assets/stylesheets/components/avatar.scss
@@ -7,9 +7,6 @@ $avatar-sizes: (
18: (
border-radius: $border-radius-small
),
- 19: (
- border-radius: $border-radius-small
- ),
20: (
border-radius: $border-radius-small
),
@@ -28,17 +25,11 @@ $avatar-sizes: (
line-height: 32px,
border-radius: $border-radius-default
),
- 36: (
- border-radius: $border-radius-default
- ),
40: (
font-size: 16px,
line-height: 38px,
border-radius: $border-radius-default
),
- 46: (
- border-radius: $border-radius-default
- ),
48: (
font-size: 20px,
line-height: 48px,
@@ -54,37 +45,16 @@ $avatar-sizes: (
line-height: 64px,
border-radius: $border-radius-large
),
- 70: (
- font-size: 34px,
- line-height: 70px,
- border-radius: $border-radius-large
- ),
90: (
font-size: 36px,
line-height: 88px,
border-radius: $border-radius-large
),
- 96: (
- font-size: 48px,
- line-height: 96px,
- border-radius: $border-radius-large
- ),
100: (
font-size: 36px,
line-height: 98px,
border-radius: $border-radius-large
),
- 110: (
- font-size: 40px,
- line-height: 108px,
- font-weight: $gl-font-weight-normal,
- border-radius: $border-radius-large
- ),
- 140: (
- font-size: 72px,
- line-height: 138px,
- border-radius: $border-radius-large
- ),
160: (
font-size: 96px,
line-height: 158px,
@@ -97,13 +67,13 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i
.avatar-circle {
float: left;
- margin-right: 15px;
+ margin-right: $gl-padding;
border-radius: $avatar-radius;
border: 1px solid $gray-normal;
@each $size, $size-config in $avatar-sizes {
&.s#{$size} {
- @include avatar-size(#{$size}px, if($size < 36, 8px, 16px));
+ @include avatar-size(#{$size}px, if($size < 48, 8px, 16px));
}
}
}
diff --git a/app/assets/stylesheets/components/popover.scss b/app/assets/stylesheets/components/popover.scss
index d0aa6ec78aa..774be9ef588 100644
--- a/app/assets/stylesheets/components/popover.scss
+++ b/app/assets/stylesheets/components/popover.scss
@@ -39,6 +39,25 @@
}
}
+.onboarding-popover {
+ box-shadow: 0 2px 4px $dropdown-shadow-color;
+
+ .popover-body {
+ font-size: $gl-font-size;
+ line-height: $gl-line-height;
+ padding: $gl-padding;
+ }
+
+ .popover-header {
+ display: none;
+ }
+
+ .accept-mr-label {
+ background-color: $accepting-mr-label-color;
+ color: $white-light;
+ }
+}
+
.onboarding-welcome-page {
.popover {
min-width: auto;
diff --git a/app/assets/stylesheets/errors.scss b/app/assets/stylesheets/errors.scss
index 658e0ff638e..8c32b6c8985 100644
--- a/app/assets/stylesheets/errors.scss
+++ b/app/assets/stylesheets/errors.scss
@@ -17,7 +17,7 @@ body {
text-align: center;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: auto;
- font-size: 14px;
+ font-size: .875rem;
}
h1 {
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 257d788873c..6f5a2e561af 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -268,3 +268,27 @@ $skeleton-line-widths: (
@include webkit-prefix(animation-duration, 1s);
transform-origin: 50% 50%;
}
+
+/* ----------------------------------------------
+ * Generated by Animista on 2019-4-26 17:40:41
+ * w: http://animista.net, t: @cssanimista
+ * ---------------------------------------------- */
+@keyframes slide-in-fwd-bottom {
+ 0% {
+ transform: translateZ(-1400px) translateY(800px);
+ opacity: 0;
+ }
+
+ 100% {
+ transform: translateZ(0) translateY(0);
+ opacity: 1;
+ }
+}
+
+.slide-in-fwd-bottom-enter-active {
+ animation: slide-in-fwd-bottom 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
+}
+
+.slide-in-fwd-bottom-leave-active {
+ animation: slide-in-fwd-bottom 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) both reverse;
+}
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index a8df7e1bfad..7760c48cb92 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -232,9 +232,6 @@
height: $default-icon-size;
width: $default-icon-size;
border-radius: 50%;
- }
-
- path {
fill: $gray-700;
}
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 3aabb66f7a6..65c0ee74c60 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -199,6 +199,7 @@
&.user-cover-block {
padding: 24px 0 0;
+ border-bottom: 1px solid $border-color;
.nav-links {
width: 100%;
@@ -232,14 +233,6 @@
margin-top: -1px;
}
-.nav-block {
- .controls {
- float: right;
- margin-top: 8px;
- padding-bottom: 8px;
- }
-}
-
.content-block {
padding: $gl-padding 0;
border-bottom: 1px solid $white-dark;
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 2c720703822..767832e242c 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -1,12 +1,12 @@
@mixin btn-comment-icon {
border-radius: 50%;
background: $white-light;
- padding: 1px 5px;
+ padding: 1px;
font-size: 12px;
color: $blue-500;
+ border: 1px solid $blue-500;
width: 24px;
height: 24px;
- border: 1px solid $blue-500;
&:hover,
&.inverted {
@@ -249,7 +249,7 @@
padding: 6px 16px;
border-color: $border-color;
color: $gray-darkest;
- background-color: $gray-light;
+ background-color: $white-light;
&:hover,
&:active,
@@ -258,7 +258,6 @@
box-shadow: none;
border-color: lighten($blue-300, 20%);
color: $gray-darkest;
- background-color: $gray-light;
}
}
@@ -339,6 +338,8 @@
svg {
top: auto;
+ width: 16px;
+ height: 16px;
}
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index fc488b85138..db09118ba15 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -489,3 +489,50 @@ img.emoji {
.cursor-pointer {
cursor: pointer;
}
+
+// Make buttons/dropdowns full-width on mobile
+.full-width-mobile {
+ @include media-breakpoint-down(xs) {
+ width: 100%;
+
+ > .dropdown-menu,
+ > .btn {
+ width: 100%;
+ }
+ }
+}
+
+.onboarding-helper-container {
+ bottom: 40px;
+ right: 40px;
+ font-size: $gl-font-size-small;
+ background: $gray-100;
+ width: 200px;
+ border-radius: 24px;
+ box-shadow: 0 2px 4px $issue-boards-card-shadow;
+ z-index: 10000;
+
+ .collapsible {
+ max-height: 0;
+ transition: max-height 0.5s cubic-bezier(0, 1, 0, 1);
+ }
+
+ &.expanded {
+ border-bottom-right-radius: $border-radius-default;
+ border-bottom-left-radius: $border-radius-default;
+
+ .collapsible {
+ max-height: 1000px;
+ transition: max-height 1s ease-in-out;
+ }
+ }
+
+ .avatar {
+ border-color: darken($gray-normal, 10%);
+
+ img {
+ width: 32px;
+ height: 32px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 17c117188b3..536a26a6ffe 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -329,7 +329,7 @@ span.idiff {
background-color: $gray-light;
border-bottom: 1px solid $border-color;
border-top: 1px solid $border-color;
- padding: 5px $gl-padding;
+ padding: $gl-padding-8 $gl-padding;
margin: 0;
border-radius: $border-radius-default $border-radius-default 0 0;
@@ -366,10 +366,6 @@ span.idiff {
color: $gl-text-color;
}
- small {
- margin: 0 10px 0 0;
- }
-
.file-actions .btn {
padding: 0 10px;
font-size: 13px;
@@ -457,6 +453,28 @@ span.idiff {
}
}
+ .note-container {
+ .user-avatar-link.new-comment {
+ position: absolute;
+ margin: 40px $gl-padding 0 116px;
+
+ ~ .note-edit-form form.edit-note {
+ @include media-breakpoint-up(sm) {
+ margin-left: $note-icon-gutter-width;
+ }
+ }
+ }
+ }
+
+ .diff-discussions:not(:last-child) .discussion .discussion-body {
+ padding-bottom: $gl-padding;
+
+ .discussion-reply-holder {
+ border-bottom: 1px solid $gray-100;
+ border-radius: 0;
+ }
+ }
+
.md-previewer {
padding: $gl-padding;
}
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index afa85f0e4ae..e3dd127366d 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -6,6 +6,19 @@
position: relative;
z-index: 1;
+ .flash-notice,
+ .flash-alert,
+ .flash-success,
+ .flash-warning {
+ border-radius: $border-radius-default;
+ color: $white-light;
+
+ .container-fluid,
+ .container-fluid.container-limited {
+ background: transparent;
+ }
+ }
+
.flash-notice {
@extend .alert;
background-color: $blue-500;
@@ -28,7 +41,8 @@
.flash-warning {
@extend .alert;
- background-color: $orange-500;
+ background-color: $orange-100;
+ color: $orange-900;
margin: 0;
}
@@ -60,19 +74,6 @@
margin: 0;
}
- .flash-notice,
- .flash-alert,
- .flash-success,
- .flash-warning {
- border-radius: $border-radius-default;
- color: $white-light;
-
- .container-fluid,
- .container-fluid.container-limited {
- background: transparent;
- }
- }
-
&.flash-container-page {
margin-bottom: 0;
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 4a9c73a1bc9..2a601afff53 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -27,10 +27,16 @@ input[type='text'].danger {
}
label {
+ font-weight: $gl-font-weight-bold;
+
&.inline-label {
margin: 0;
}
+ &.form-check-label {
+ font-weight: $gl-font-weight-normal;
+ }
+
&.label-bold {
font-weight: $gl-font-weight-bold;
}
@@ -41,14 +47,6 @@ label {
margin: 0;
}
-.form-label {
- @extend label;
-}
-
-.form-control-label {
- @extend .col-md-2;
-}
-
.inline-input-group {
width: 250px;
}
@@ -81,44 +79,14 @@ label {
margin-left: 0;
margin-right: 0;
- .form-control-label {
- font-weight: $gl-font-weight-bold;
- padding-top: 4px;
- }
-
.form-control {
height: 29px;
background: $white-light;
font-family: $monospace-font;
}
- .input-group-prepend .btn,
- .input-group-append .btn {
- padding: 3px $gl-btn-padding;
- background-color: $gray-light;
- border: 1px solid $border-color;
- }
-
- .text-block {
- line-height: 0.8;
- padding-top: 9px;
-
- code {
- line-height: 1.8;
- }
-
- img {
- margin-right: $gl-padding;
- }
- }
-
@include media-breakpoint-down(xs) {
padding: 0 $gl-padding;
-
- .form-control-label,
- .text-block {
- padding-left: 0;
- }
}
}
@@ -140,19 +108,6 @@ label {
}
}
-.select-wrapper {
- position: relative;
-
- .fa-chevron-down {
- position: absolute;
- font-size: 10px;
- right: 10px;
- top: 12px;
- color: $gray-darkest;
- pointer-events: none;
- }
-}
-
.select-control {
padding-left: 10px;
padding-right: 10px;
@@ -175,12 +130,6 @@ label {
margin-top: 35px;
}
-.form-group .form-control-label,
-.form-group .form-control-label-full-width {
- font-weight: $gl-font-weight-normal;
-}
-
-
.form-control::placeholder {
color: $gl-text-color-tertiary;
}
@@ -224,7 +173,8 @@ label {
border: 1px solid $green-600;
&:focus {
- box-shadow: 0 0 0 1px $green-600 inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $green-600;
+ box-shadow: 0 0 0 1px $green-600 inset, 0 1px 1px $gl-field-focus-shadow inset,
+ 0 0 4px 0 $green-600;
border: 0 none;
}
}
@@ -233,7 +183,8 @@ label {
border: 1px solid $red-500;
&:focus {
- box-shadow: 0 0 0 1px $red-500 inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $gl-field-focus-shadow-error;
+ box-shadow: 0 0 0 1px $red-500 inset, 0 1px 1px $gl-field-focus-shadow inset,
+ 0 0 4px 0 $gl-field-focus-shadow-error;
border: 0 none;
}
}
@@ -259,16 +210,26 @@ label {
}
}
-.input-icon-wrapper {
+.input-icon-wrapper,
+.select-wrapper {
position: relative;
+}
- .input-icon-right {
- position: absolute;
- right: 0.8em;
- top: 50%;
- transform: translateY(-50%);
- color: $gray-600;
- }
+.select-wrapper > .fa-chevron-down {
+ position: absolute;
+ font-size: 10px;
+ right: 10px;
+ top: 12px;
+ color: $gray-darkest;
+ pointer-events: none;
+}
+
+.input-icon-wrapper > .input-icon-right {
+ position: absolute;
+ right: 0.8em;
+ top: 50%;
+ transform: translateY(-50%);
+ color: $gray-600;
}
.input-md {
@@ -284,3 +245,17 @@ label {
.input-group-text {
max-height: $input-height;
}
+
+.gl-form-checkbox {
+ align-items: baseline;
+
+ &.form-check-inline .form-check-input {
+ align-self: flex-start;
+ margin-right: $gl-padding-8;
+ height: 1.5 * $gl-font-size;
+ }
+
+ .help-text {
+ margin-bottom: 0;
+ }
+}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 298610a0631..555a3fe0dc7 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -177,14 +177,6 @@ ul.content-list {
}
}
- .member-controls {
- float: none;
-
- @include media-breakpoint-up(sm) {
- float: right;
- }
- }
-
// When dragging a list item
&.ui-sortable-helper {
border-bottom: 0;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 18671f7c4d8..e7278554e6e 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -218,16 +218,22 @@
}
}
-@mixin build-trace-top-bar($height) {
+// Used in EE for Web Terminal
+@mixin build-trace-bar($height) {
height: $height;
min-height: $height;
background: $gray-light;
border: 1px solid $border-color;
color: $gl-text-color;
+ padding: $grid-size;
+}
+
+@mixin build-trace-top-bar($height) {
+ @include build-trace-bar($height);
+
position: -webkit-sticky;
position: sticky;
top: $header-height;
- padding: $grid-size;
.with-performance-bar & {
top: $header-height + $performance-bar-height;
@@ -385,3 +391,8 @@
height: $size;
margin-right: $margin-right;
}
+
+@mixin code-icon-size() {
+ width: $gl-font-size * $code-line-height * 0.9;
+ height: $gl-font-size * $code-line-height * 0.9;
+}
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index 31297b9d20c..ada8f2fe1a6 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -13,8 +13,8 @@
a,
button {
- padding: $gl-btn-padding;
- padding-bottom: 11px;
+ padding: $gl-padding-8;
+ padding-bottom: $gl-padding-8 + 1;
font-size: 14px;
line-height: 28px;
color: $gl-text-color-secondary;
@@ -58,8 +58,12 @@
}
.top-area {
- @include clearfix;
border-bottom: 1px solid $border-color;
+ display: flex;
+
+ @include media-breakpoint-down(md) {
+ flex-flow: column-reverse wrap;
+ }
.nav-text {
padding-top: 16px;
@@ -75,9 +79,8 @@
}
.nav-links {
- margin-bottom: 0;
border-bottom: 0;
- float: left;
+ flex: 1;
&.wide {
width: 100%;
@@ -98,16 +101,23 @@
&.mobile-separator {
border-bottom: 1px solid $border-color;
+ margin-bottom: $gl-padding-8;
}
}
}
.nav-controls {
display: inline-block;
- float: right;
text-align: right;
- padding: $gl-padding-8 0;
- margin-bottom: 0;
+
+ @include media-breakpoint-down(sm) {
+ margin-top: $gl-padding-8;
+ }
+
+ @include media-breakpoint-up(md) {
+ display: flex;
+ align-items: center;
+ }
> .btn,
> .btn-container,
@@ -115,8 +125,6 @@
> input,
> form {
margin-right: $gl-padding-top;
- display: inline-block;
- vertical-align: top;
&:last-child {
margin-right: 0;
@@ -143,7 +151,7 @@
@include media-breakpoint-up(lg) { width: 250px; }
}
- @include media-breakpoint-down(xs) {
+ @include media-breakpoint-down(sm) {
padding-bottom: 0;
width: 100%;
@@ -153,7 +161,7 @@
.dropdown-toggle,
.dropdown-menu-toggle,
.form-control {
- margin: 0 0 10px;
+ margin: 0 0 $gl-padding-8;
display: block;
width: 100%;
}
@@ -165,7 +173,7 @@
form {
display: block;
height: auto;
- margin-bottom: 14px;
+ margin-bottom: $gl-padding-8;
input {
width: 100%;
@@ -236,20 +244,11 @@
width: 100%;
}
- @include media-breakpoint-down(xs) {
- flex-flow: row wrap;
-
+ @include media-breakpoint-down(md) {
.nav-controls {
$controls-margin: $btn-margin-5 - 2px;
flex: 0 0 100%;
-
- &.controls-flex {
- display: flex;
- flex-flow: row wrap;
- align-items: center;
- justify-content: center;
- padding: 0 0 $gl-padding-top;
- }
+ margin-top: $gl-padding-8;
.controls-item,
.controls-item-full,
@@ -326,8 +325,8 @@
.fade-right,
.fade-left {
- top: 16px;
- bottom: auto;
+ bottom: $gl-padding;
+ top: auto;
}
&.is-smaller {
@@ -367,6 +366,7 @@
display: flex;
border-bottom: 1px solid $border-color;
overflow: hidden;
+ align-items: center;
.nav-links {
border-bottom: 0;
diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
index 36ab38f1c9d..3ab83f4c8e6 100644
--- a/app/assets/stylesheets/framework/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -22,6 +22,10 @@
.snippet-file-content {
border-radius: 3px;
+
+ .file-title-flex-parent .btn-clipboard {
+ line-height: 28px;
+ }
}
.snippet-header {
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index e8176e59c19..42a739e88f7 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -42,8 +42,8 @@
}
}
- .avatar {
- margin-right: 15px;
+ img.avatar {
+ margin-right: $gl-padding;
}
.controls {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 28768bdf88f..dc451a97e17 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -381,6 +381,7 @@ $breadcrumb-min-height: 48px;
$home-panel-title-row-height: 64px;
$home-panel-avatar-mobile-size: 24px;
$gl-line-height: 16px;
+$gl-line-height-20: 20px;
$gl-line-height-24: 24px;
$gl-line-height-14: 14px;
@@ -640,6 +641,7 @@ $input-lg-width: 320px;
*/
$document-index-color: #888;
$help-shortcut-header-color: #333;
+$accepting-mr-label-color: #69d100;
/*
* Issues
diff --git a/app/assets/stylesheets/page_bundles/_ide_mixins.scss b/app/assets/stylesheets/page_bundles/_ide_mixins.scss
index 896a3466cb4..9465dd5bed6 100644
--- a/app/assets/stylesheets/page_bundles/_ide_mixins.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_mixins.scss
@@ -2,17 +2,17 @@
display: flex;
flex-direction: column;
height: 100%;
- margin-top: -$grid-size;
- margin-bottom: -$grid-size;
- &.build-page .top-bar {
+ .top-bar {
+ @include build-trace-bar(35px);
+
top: 0;
- height: auto;
font-size: 12px;
border-top-right-radius: $border-radius-default;
- }
-
- .top-bar {
margin-left: -$gl-padding;
+
+ .controllers {
+ @include build-controllers(15px, center, false, 0, inline, 0);
+ }
}
}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 0c1067bfacc..f08fa80495d 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -719,7 +719,7 @@ $ide-commit-header-height: 48px;
border: 1px solid $white-dark;
}
-.ide-commit-radios {
+.ide-commit-options {
label {
font-weight: normal;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 09ff518bbdf..5e3652db48f 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -31,14 +31,15 @@
width: 320px;
.dropdown-content {
- max-height: 162px;
+ max-height: 140px;
}
}
.issue-board-dropdown-content {
- margin: 0 8px 10px;
- padding-bottom: 10px;
- border-bottom: 1px solid $dropdown-divider-bg;
+ margin: 0;
+ padding: $gl-padding-4 $gl-padding $gl-padding;
+ border-bottom: 0;
+ color: $gl-text-color-secondary;
}
.issue-boards-page {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 66cd113db84..77a36e59b03 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -154,8 +154,6 @@
}
.avatar-cell {
- width: 46px;
-
img {
margin-right: 0;
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index b3a634e23a3..3b0d740def3 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -15,7 +15,6 @@
position: sticky;
top: $mr-file-header-top;
z-index: 102;
- height: $mr-version-controls-height;
&::before {
content: '';
@@ -615,10 +614,9 @@ table.code {
.diff-comment-avatar-holders {
position: absolute;
- height: 19px;
- width: 19px;
- margin-left: -15px;
+ margin-left: -$gl-padding;
z-index: 100;
+ @include code-icon-size();
&:hover {
.diff-comment-avatar,
@@ -652,26 +650,28 @@ table.code {
.diff-comments-more-count {
position: absolute;
left: 0;
- width: 19px;
- height: 19px;
margin-right: 0;
border-color: $white-light;
cursor: pointer;
transition: all 0.1s ease-out;
+ @include code-icon-size();
@for $i from 1 through 4 {
&:nth-child(#{$i}) {
z-index: (4 - $i);
}
}
+
+ .avatar {
+ @include code-icon-size();
+ }
}
.diff-comments-more-count {
- width: 19px;
- min-width: 19px;
padding-left: 0;
padding-right: 0;
overflow: hidden;
+ @include code-icon-size();
}
.diff-comments-more-count,
@@ -680,12 +680,15 @@ table.code {
}
.diff-notes-collapse {
- width: 24px;
- height: 24px;
+ border: 0;
border-radius: 50%;
padding: 0;
transition: transform 0.1s ease-out;
z-index: 100;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ @include code-icon-size();
.collapse-icon {
height: 50%;
@@ -1005,6 +1008,10 @@ table.code {
display: block;
}
}
+
+ .note-edit-form {
+ margin-left: $note-icon-gutter-width;
+ }
}
.discussion-body .image .frame {
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index e34628002ac..500f5816d38 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -8,7 +8,7 @@
border-bottom: 1px solid $white-normal;
color: $gl-text-color-secondary;
position: relative;
- line-height: $gl-line-height;
+ line-height: $gl-line-height-20;
.system-note-image {
position: absolute;
@@ -48,7 +48,7 @@
}
.event-user-info {
- margin-bottom: $gl-padding-8;
+ margin-bottom: $gl-padding-4;
.author_name {
a {
@@ -67,7 +67,7 @@
}
.event-body {
- margin-top: $gl-padding-8;
+ margin-top: $gl-padding-4;
margin-right: 174px;
color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 0a07747e0d4..656202f4e58 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -35,9 +35,6 @@
}
.group-nav-container .nav-controls {
- align-items: flex-start;
- padding: $gl-padding-top 0 0;
-
.group-filter-form {
flex: 1 1 auto;
margin-right: $gl-padding-8;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 04c66006027..79282f9043c 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -69,7 +69,11 @@
}
.emoji-block {
- padding: 10px 0;
+ padding: $gl-padding-4 0;
+
+ @include media-breakpoint-down(md) {
+ padding: $gl-padding-8 0;
+ }
}
}
@@ -132,6 +136,10 @@
z-index: 200;
overflow: hidden;
+ @include media-breakpoint-down(sm) {
+ z-index: 251;
+ }
+
a:not(.btn) {
color: inherit;
@@ -218,7 +226,7 @@
.title {
color: $gl-text-color;
- margin-bottom: 10px;
+ margin-bottom: $gl-padding-8;
line-height: 1;
.avatar {
@@ -604,7 +612,6 @@
.participants-list {
display: flex;
flex-wrap: wrap;
- margin: -7px;
}
.user-list {
@@ -614,7 +621,7 @@
.participants-author {
display: inline-block;
- padding: 7px;
+ padding: 0 $gl-padding-8 $gl-padding-8 0;
&:nth-of-type(7n) {
padding-right: 0;
@@ -641,7 +648,6 @@
.participants-more,
.user-list-more {
- margin-top: 5px;
margin-left: 5px;
a,
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index c7d2369a6b8..48289c8f381 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -258,8 +258,15 @@ ul.related-merge-requests > li {
}
}
-.discussion-reply-holder .note-edit-form {
- display: block;
+.discussion-reply-holder {
+ .avatar-note-form-holder .note-edit-form {
+ display: block;
+ margin-left: $note-icon-gutter-width;
+
+ @include media-breakpoint-down(xs) {
+ margin-left: 0;
+ }
+ }
}
.issue-sort-dropdown {
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 13288d8bad1..11e8a32389f 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -456,8 +456,9 @@
// Don't hide the overflow in system messages
.system-note-message,
-.issuable-detail,
+.issuable-details,
.md-preview-holder,
+.referenced-commands,
.note-body {
.scoped-label-wrapper {
.badge {
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 297f642681b..d8aabecc036 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -73,7 +73,8 @@
.login-body {
font-size: 13px;
- input + p {
+ input + p,
+ input ~ p.field-validation {
margin-top: 5px;
}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index f8e273a2735..68af01f9ccc 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -20,17 +20,6 @@
}
}
- .list-item-name {
- @include media-breakpoint-up(sm) {
- float: left;
- width: 50%;
- }
-
- strong {
- font-weight: $gl-font-weight-bold;
- }
- }
-
.controls {
@include media-breakpoint-up(sm) {
display: flex;
@@ -43,10 +32,11 @@
.form-group {
margin-bottom: 0;
+ }
- @include media-breakpoint-down(sm) {
- display: block;
- margin-left: 5px;
+ .member-controls {
+ .fa {
+ line-height: inherit;
}
}
@@ -66,23 +56,12 @@
}
.member-form-control {
- @include media-breakpoint-down(sm) {
- width: $dropdown-member-form-control-width;
- margin-left: 0;
- padding-bottom: 5px;
- }
-
@include media-breakpoint-down(xs) {
margin-right: 0;
width: auto;
}
}
-.member-access-text {
- margin-left: auto;
- line-height: 43px;
-}
-
.member-search-form {
position: relative;
@@ -221,9 +200,6 @@
}
.content-list.members-list li {
- display: flex;
- justify-content: space-between;
-
.list-item-name {
float: none;
display: flex;
@@ -252,33 +228,24 @@
align-self: flex-start;
}
+ @include media-breakpoint-down(sm) {
+ .member-access-text {
+ margin: 0 0 $gl-padding-4 ($grid-size * 6);
+ }
+ }
+
@include media-breakpoint-down(xs) {
display: block;
- .controls > .btn {
- margin-left: 0;
- margin-right: 0;
+ .controls > .btn,
+ .controls .member-form-control {
+ margin: 0 0 $gl-padding-8;
display: block;
}
- .controls > .btn:last-child {
- margin-left: 5px;
- margin-right: 5px;
- width: auto;
- }
-
.form-control {
width: 100%;
}
-
- .member-access-text {
- line-height: 0;
- margin-left: 50px;
- }
-
- .member-controls {
- margin-top: 5px;
- }
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index ab5a9e170f0..8cb3fab74e0 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -185,46 +185,6 @@
}
}
}
-
- .accept-control {
- display: inline-block;
- float: left;
- margin: 0;
- margin-left: 20px;
- padding: 5px;
- padding-top: 8px;
- line-height: 20px;
-
- &.right {
- float: right;
- padding-right: 0;
- }
-
- .modify-merge-commit-link {
- padding: 0;
- background-color: transparent;
- border: 0;
- color: $gl-text-color;
-
- &:hover,
- &:focus {
- text-decoration: underline;
- }
- }
-
- .merge-param-checkbox {
- margin: 0;
- }
-
- a .fa-question-circle {
- color: $gl-text-color-secondary;
-
- &:hover,
- &:focus {
- color: $link-hover-color;
- }
- }
- }
}
.ci-widget {
@@ -407,12 +367,6 @@
width: 100%;
text-align: center;
}
-
- .accept-control {
- width: 100%;
- text-align: center;
- margin: 0;
- }
}
.commit-message-editor {
@@ -892,15 +846,40 @@
display: flex;
justify-content: space-between;
- @include media-breakpoint-down(sm) {
- flex-direction: column-reverse;
+ @include media-breakpoint-down(xs) {
+ .discussion-filter-container,
+ .line-resolve-all-container {
+ margin-bottom: $gl-padding-4;
+ }
}
.discussion-filter-container {
- margin-top: $gl-padding-8;
-
&:not(:only-child) {
- padding-right: $gl-padding-8;
+ margin: $gl-padding-4;
+ }
+ }
+
+ .merge-request-tabs {
+ height: $grid-size * 6;
+ }
+}
+
+// Wrap MR tabs/buttons so you don't have to scroll on desktop
+@include media-breakpoint-down(md) {
+ .merge-request-tabs-container,
+ .epic-tabs-container {
+ flex-direction: column-reverse;
+ padding-top: $gl-padding-8;
+ }
+}
+
+@include media-breakpoint-down(lg) {
+ .right-sidebar-expanded {
+ .merge-request-tabs-container,
+ .epic-tabs-container {
+ flex-direction: column-reverse;
+ align-items: flex-start;
+ padding-top: $gl-padding-8;
}
}
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 3343b55d24b..c6bac33e888 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -59,6 +59,7 @@
border-radius: $border-radius-base;
transition: border-color ease-in-out 0.15s,
box-shadow ease-in-out 0.15s;
+ background-color: $white-light;
&.is-focused {
@extend .form-control:focus;
@@ -103,6 +104,11 @@
margin: auto;
align-items: center;
+ a {
+ color: $orange-600;
+ text-decoration: underline;
+ }
+
.icon {
margin-right: $issuable-warning-icon-margin;
vertical-align: text-bottom;
@@ -168,6 +174,16 @@
.discussion-form {
background-color: $white-light;
+
+ @include media-breakpoint-down(xs) {
+ .user-avatar-link {
+ display: none;
+ }
+
+ .note-edit-form {
+ margin-left: 0;
+ }
+ }
}
table {
@@ -234,13 +250,25 @@ table {
.diff-file,
.commit-diff {
.discussion-reply-holder {
- background-color: $white-light;
+ background-color: $gray-light;
border-radius: 0 0 3px 3px;
padding: $gl-padding;
+ border-top: 1px solid $gray-100;
+
+ + .new-note {
+ background-color: $gray-light;
+ border-top: 1px solid $gray-100;
+ }
&.is-replying {
padding-bottom: $gl-padding;
}
+
+ .user-avatar-link {
+ img {
+ margin-top: -3px;
+ }
+ }
}
}
@@ -334,7 +362,7 @@ table {
.toolbar-button-icon {
position: relative;
top: 1px;
- margin-right: 3px;
+ margin-right: $gl-padding-4;
color: inherit;
font-size: 16px;
}
@@ -461,6 +489,15 @@ table {
border: 0;
font-size: 14px;
line-height: 16px;
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+
+ .text-attach-file {
+ text-decoration: underline;
+ }
+ }
}
.markdown-selector {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 50c87e55f56..69dd583bc5b 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -80,21 +80,17 @@ $note-form-margin-left: 72px;
}
}
- li.note {
- border-bottom: 1px solid $border-color;
- }
-
.replies-toggle {
background-color: $gray-light;
padding: $gl-padding-8 $gl-padding;
+ border-top: 1px solid $gray-100;
+ border-bottom: 1px solid $gray-100;
.collapse-replies-btn:hover {
color: $blue-600;
}
&.expanded {
- border-bottom: 1px solid $border-color;
-
span {
cursor: pointer;
}
@@ -211,8 +207,13 @@ $note-form-margin-left: 72px;
display: none;
}
+ .user-avatar-link img {
+ margin-top: $gl-padding-8;
+ }
+
.note-edit-form {
display: block;
+ margin-left: 0;
&.current-note-edit-form + .note-awards {
display: none;
@@ -264,8 +265,8 @@ $note-form-margin-left: 72px;
}
.system-note {
- padding: 6px 21px;
- margin: $gl-padding-24 0;
+ padding: $gl-padding-4 20px;
+ margin: $gl-padding 0;
background-color: transparent;
.note-header-info {
@@ -364,7 +365,7 @@ $note-form-margin-left: 72px;
height: $system-note-icon-size;
border: 1px solid $border-color;
border-radius: $system-note-icon-size;
- margin: -6px $gl-padding 0 0;
+ margin: -6px 20px 0 0;
svg {
width: $system-note-svg-size;
@@ -437,7 +438,9 @@ $note-form-margin-left: 72px;
.diff-file {
.is-over {
.add-diff-note {
- display: inline-block;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
}
}
@@ -517,12 +520,30 @@ $note-form-margin-left: 72px;
}
}
-.commit-diff {
- .notes-content {
- background-color: $white-light;
+.code-commit .notes-content,
+.diff-viewer > .image ~ .note-container {
+ background-color: $white-light;
+
+ .avatar-note-form-holder {
+ .user-avatar-link img {
+ margin: 13px $gl-padding $gl-padding;
+ }
+
+ form,
+ ~ .discussion-form-container {
+ padding: $gl-padding;
+
+ @include media-breakpoint-up(sm) {
+ margin-left: $note-icon-gutter-width;
+ }
+ }
}
}
+.diff-viewer > .image ~ .note-container form.new-note {
+ margin-left: 0;
+}
+
.discussion-header,
.note-header-info {
a {
@@ -548,7 +569,7 @@ $note-form-margin-left: 72px;
}
.discussion-header {
- min-height: 72px;
+ min-height: 74px;
.note-header-info {
padding-bottom: 0;
@@ -561,8 +582,10 @@ $note-form-margin-left: 72px;
}
.unresolved {
- .note-header-info {
- margin-top: $gl-padding-8;
+ .discussion-header {
+ .note-header-info {
+ margin-top: $gl-padding-8;
+ }
}
}
@@ -741,7 +764,7 @@ $note-form-margin-left: 72px;
.add-diff-note {
@include btn-comment-icon;
opacity: 0;
- margin-left: -50px;
+ margin-left: -52px;
position: absolute;
top: 50%;
transform: translateY(-50%);
@@ -760,15 +783,13 @@ $note-form-margin-left: 72px;
background-color: $white-light;
}
- a {
+ a:not(.learn-more) {
color: $blue-600;
}
}
.line-resolve-all-container {
- @include notes-media('min', map-get($grid-breakpoints, sm)) {
- margin-right: 0;
- }
+ margin: $gl-padding-4;
> div {
white-space: nowrap;
@@ -784,6 +805,8 @@ $note-form-margin-left: 72px;
}
.btn {
+ line-height: $gl-line-height;
+
svg {
fill: $gray-darkest;
}
@@ -809,10 +832,11 @@ $note-form-margin-left: 72px;
.line-resolve-all {
vertical-align: middle;
display: inline-block;
- padding: 6px 10px;
+ padding: $gl-padding-4 10px;
background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
+ font-size: $gl-btn-small-font-size;
&.has-next-btn {
border-top-right-radius: 0;
@@ -822,11 +846,16 @@ $note-form-margin-left: 72px;
.line-resolve-btn {
margin-right: 5px;
+ color: $gray-darkest;
svg {
vertical-align: middle;
}
}
+
+ @include media-breakpoint-down(xs) {
+ flex: 1;
+ }
}
.line-resolve-btn {
@@ -836,7 +865,6 @@ $note-form-margin-left: 72px;
background-color: transparent;
border: 0;
outline: 0;
- color: $gray-darkest;
transition: color $general-hover-transition-duration $general-hover-transition-curve;
&.is-disabled {
@@ -900,10 +928,6 @@ $note-form-margin-left: 72px;
.diff-comment-form {
display: block;
}
-
- .add-diff-note svg {
- margin-top: 4px;
- }
}
.discussion-filter-container {
diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss
index e98cb444f0a..e1cbf0e6654 100644
--- a/app/assets/stylesheets/pages/notifications.scss
+++ b/app/assets/stylesheets/pages/notifications.scss
@@ -4,6 +4,34 @@
.dropdown-menu {
@extend .dropdown-menu-right;
}
+
+ @include media-breakpoint-down(sm) {
+ .notification-dropdown {
+ width: 100%;
+ }
+
+ .notification-form {
+ display: block;
+ }
+
+ .notifications-btn,
+ .btn-group {
+ width: 100%;
+ }
+
+ .table-section {
+ border-top: 0;
+ min-height: unset;
+
+ &:not(:first-child) {
+ padding-top: 0;
+ }
+ }
+
+ .update-notifications {
+ width: 100%;
+ }
+ }
}
.notification {
diff --git a/app/controllers/acme_challenges_controller.rb b/app/controllers/acme_challenges_controller.rb
new file mode 100644
index 00000000000..67a39d8870b
--- /dev/null
+++ b/app/controllers/acme_challenges_controller.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AcmeChallengesController < ActionController::Base
+ def show
+ if acme_order
+ render plain: acme_order.challenge_file_content, content_type: 'text/plain'
+ else
+ head :not_found
+ end
+ end
+
+ private
+
+ def acme_order
+ @acme_order ||= PagesDomainAcmeOrder.find_by_domain_and_token(params[:domain], params[:token])
+ end
+end
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index aeff0c96b64..70db15916b9 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -3,7 +3,7 @@
class Admin::ProjectsController < Admin::ApplicationController
include MembersPresentation
- before_action :project, only: [:show, :transfer, :repository_check]
+ before_action :project, only: [:show, :transfer, :repository_check, :destroy]
before_action :group, only: [:show, :transfer]
def index
@@ -35,6 +35,15 @@ class Admin::ProjectsController < Admin::ApplicationController
end
# rubocop: enable CodeReuse/ActiveRecord
+ def destroy
+ ::Projects::DestroyService.new(@project, current_user, {}).async_execute
+ flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.full_name }
+
+ redirect_to admin_projects_path, status: :found
+ rescue Projects::DestroyService::DestroyError => ex
+ redirect_to admin_projects_path, status: 302, alert: ex.message
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def transfer
namespace = Namespace.find_by(id: params[:new_namespace_id])
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 4cbab6811bc..7321f719deb 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -42,7 +42,7 @@ class ApplicationController < ActionController::Base
:bitbucket_server_import_enabled?,
:google_code_import_enabled?, :fogbugz_import_enabled?,
:git_import_enabled?, :gitlab_project_import_enabled?,
- :manifest_import_enabled?
+ :manifest_import_enabled?, :phabricator_import_enabled?
# Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security
# concerns due to caching private data.
@@ -424,6 +424,10 @@ class ApplicationController < ActionController::Base
Group.supports_nested_objects? && Gitlab::CurrentSettings.import_sources.include?('manifest')
end
+ def phabricator_import_enabled?
+ Gitlab::PhabricatorImport.available?
+ end
+
# U2F (universal 2nd factor) devices need a unique identifier for the application
# to perform authentication.
# https://developers.yubico.com/U2F/App_ID.html
@@ -436,6 +440,8 @@ class ApplicationController < ActionController::Base
end
def set_session_storage(&block)
+ return yield if sessionless_user?
+
Gitlab::Session.with_session(session, &block)
end
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index 73ebd4e0e42..80ee7c35906 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -12,9 +12,6 @@ class Clusters::ClustersController < Clusters::BaseController
before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy]
before_action :update_applications_status, only: [:cluster_status]
- before_action only: [:show] do
- push_frontend_feature_flag(:metrics_time_window)
- end
helper_method :token_in_session
diff --git a/app/controllers/concerns/import_url_params.rb b/app/controllers/concerns/import_url_params.rb
new file mode 100644
index 00000000000..e51e4157f50
--- /dev/null
+++ b/app/controllers/concerns/import_url_params.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module ImportUrlParams
+ def import_url_params
+ return {} unless params.dig(:project, :import_url).present?
+
+ { import_url: import_params_to_full_url(params[:project]) }
+ end
+
+ def import_params_to_full_url(params)
+ Gitlab::UrlSanitizer.new(
+ params[:import_url],
+ credentials: {
+ user: params[:import_url_user],
+ password: params[:import_url_password]
+ }
+ ).full_url
+ end
+end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 91e875dca54..9cf25915e92 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -41,6 +41,7 @@ module IssuableCollections
return if pagination_disabled?
@issuables = @issuables.page(params[:page])
+ @issuables = per_page_for_relative_position if params[:sort] == 'relative_position'
@issuable_meta_data = issuable_meta_data(@issuables, collection_type)
@total_pages = issuable_page_count
end
@@ -80,6 +81,11 @@ module IssuableCollections
(row_count.to_f / limit).ceil
end
+ # manual / relative_position sorting allows for 100 items on the page
+ def per_page_for_relative_position
+ @issuables.per(100) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+
def issuable_finder_for(finder_class)
finder_class.new(current_user, finder_options)
end
diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb
index cfff154c3dd..8b8b7db72f8 100644
--- a/app/controllers/concerns/milestone_actions.rb
+++ b/app/controllers/concerns/milestone_actions.rb
@@ -26,16 +26,22 @@ module MilestoneActions
end
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def labels
respond_to do |format|
format.html { redirect_to milestone_redirect_path }
format.json do
+ milestone_labels = @milestone.issue_labels_visible_by_user(current_user)
+
render json: tabs_json("shared/milestones/_labels_tab", {
- labels: @milestone.labels.map { |label| label.present(issuable_subject: @milestone.parent) } # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ labels: milestone_labels.map do |label|
+ label.present(issuable_subject: @milestone.parent)
+ end
})
end
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
private
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 70811f5ea59..65d14781d92 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -6,18 +6,14 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
before_action :set_non_archived_param
+ before_action :projects, only: [:index]
before_action :default_sorting
skip_cross_project_access_check :index, :starred
def index
- @projects = load_projects(params.merge(non_public: true))
-
respond_to do |format|
format.html do
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/40260
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- render
- end
+ render_projects
end
format.atom do
load_events
@@ -51,6 +47,17 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private
+ def projects
+ @projects ||= load_projects(params.merge(non_public: true))
+ end
+
+ def render_projects
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/40260
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ render
+ end
+ end
+
def default_sorting
params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort]
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index e8f38899647..1ce0afac83b 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -53,7 +53,8 @@ class GraphqlController < ApplicationController
{
query: single_query_info[:query],
variables: build_variables(single_query_info[:variables]),
- operation_name: single_query_info[:operationName]
+ operation_name: single_query_info[:operationName],
+ context: context
}
end
end
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
index a37ba682b91..28ead8d44da 100644
--- a/app/controllers/import/fogbugz_controller.rb
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -11,7 +11,7 @@ class Import::FogbugzController < Import::BaseController
def callback
begin
- res = Gitlab::FogbugzImport::Client.new(import_params.symbolize_keys)
+ res = Gitlab::FogbugzImport::Client.new(import_params.to_h.symbolize_keys)
rescue
# If the URI is invalid various errors can occur
return redirect_to new_import_fogbugz_path, alert: _('Could not connect to FogBugz, check your URL')
@@ -26,7 +26,7 @@ class Import::FogbugzController < Import::BaseController
end
def create_user_map
- user_map = params[:users]
+ user_map = user_map_params.to_h[:users]
unless user_map.is_a?(Hash) && user_map.all? { |k, v| !v[:name].blank? }
flash.now[:alert] = _('All users must have a name.')
@@ -99,6 +99,10 @@ class Import::FogbugzController < Import::BaseController
params.permit(:uri, :email, :password)
end
+ def user_map_params
+ params.permit(users: %w(name email gitlab_user))
+ end
+
def verify_fogbugz_import_enabled
render_404 unless fogbugz_import_enabled?
end
diff --git a/app/controllers/import/phabricator_controller.rb b/app/controllers/import/phabricator_controller.rb
new file mode 100644
index 00000000000..d1c04817689
--- /dev/null
+++ b/app/controllers/import/phabricator_controller.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class Import::PhabricatorController < Import::BaseController
+ include ImportHelper
+
+ before_action :verify_import_enabled
+
+ def new
+ end
+
+ def create
+ @project = Gitlab::PhabricatorImport::ProjectCreator
+ .new(current_user, import_params).execute
+
+ if @project&.persisted?
+ redirect_to @project
+ else
+ @name = params[:name]
+ @path = params[:path]
+ @errors = @project&.errors&.full_messages || [_("Invalid import params")]
+
+ render :new
+ end
+ end
+
+ def verify_import_enabled
+ render_404 unless phabricator_import_enabled?
+ end
+
+ private
+
+ def import_params
+ params.permit(:path, :phabricator_server_url, :api_token, :name, :namespace_id)
+ end
+end
diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb
index 0d2a6145d0e..b03f4b7435f 100644
--- a/app/controllers/profiles/accounts_controller.rb
+++ b/app/controllers/profiles/accounts_controller.rb
@@ -17,7 +17,7 @@ class Profiles::AccountsController < Profiles::ApplicationController
if unlink_provider_allowed?(provider)
identity.destroy
else
- flash[:alert] = "You are not allowed to unlink your primary login account"
+ flash[:alert] = _("You are not allowed to unlink your primary login account")
end
redirect_to profile_account_path
diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb
index 503eda250b4..f666a1150a6 100644
--- a/app/controllers/profiles/emails_controller.rb
+++ b/app/controllers/profiles/emails_controller.rb
@@ -28,9 +28,9 @@ class Profiles::EmailsController < Profiles::ApplicationController
def resend_confirmation_instructions
if Emails::ConfirmService.new(current_user, user: current_user).execute(@email)
- flash[:notice] = "Confirmation email sent to #{@email.email}"
+ flash[:notice] = _("Confirmation email sent to %{email}") % { email: @email.email }
else
- flash[:alert] = "There was a problem sending the confirmation email"
+ flash[:alert] = _("There was a problem sending the confirmation email")
end
redirect_to profile_emails_url
diff --git a/app/controllers/profiles/groups_controller.rb b/app/controllers/profiles/groups_controller.rb
new file mode 100644
index 00000000000..c755bcb718a
--- /dev/null
+++ b/app/controllers/profiles/groups_controller.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class Profiles::GroupsController < Profiles::ApplicationController
+ include RoutableActions
+
+ def update
+ group = find_routable!(Group, params[:id])
+ notification_setting = current_user.notification_settings.find_by(source: group) # rubocop: disable CodeReuse/ActiveRecord
+
+ if notification_setting.update(update_params)
+ flash[:notice] = "Notification settings for #{group.name} saved"
+ else
+ flash[:alert] = "Failed to save new settings for #{group.name}"
+ end
+
+ redirect_back_or_default(default: profile_notifications_path)
+ end
+
+ private
+
+ def update_params
+ params.require(:notification_setting).permit(:notification_email)
+ end
+end
diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb
index 7038447581c..d2787c2e450 100644
--- a/app/controllers/profiles/passwords_controller.rb
+++ b/app/controllers/profiles/passwords_controller.rb
@@ -14,7 +14,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController
def create
unless @user.password_automatically_set || @user.valid_password?(user_params[:current_password])
- redirect_to new_profile_password_path, alert: 'You must provide a valid current password'
+ redirect_to new_profile_password_path, alert: _('You must provide a valid current password')
return
end
@@ -29,7 +29,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController
if result[:status] == :success
Users::UpdateService.new(current_user, user: @user, password_expires_at: nil).execute
- redirect_to root_path, notice: 'Password successfully changed'
+ redirect_to root_path, notice: _('Password successfully changed')
else
render :new
end
@@ -45,14 +45,14 @@ class Profiles::PasswordsController < Profiles::ApplicationController
password_attributes[:password_automatically_set] = false
unless @user.password_automatically_set || @user.valid_password?(user_params[:current_password])
- redirect_to edit_profile_password_path, alert: 'You must provide a valid current password'
+ redirect_to edit_profile_password_path, alert: _('You must provide a valid current password')
return
end
result = Users::UpdateService.new(current_user, password_attributes.merge(user: @user)).execute
if result[:status] == :success
- flash[:notice] = "Password was successfully updated. Please login with it"
+ flash[:notice] = _('Password was successfully updated. Please login with it')
redirect_to new_user_session_path
else
@user.reset
@@ -62,7 +62,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController
def reset
current_user.send_reset_password_instructions
- redirect_to edit_profile_password_path, notice: 'We sent you an email with reset password instructions'
+ redirect_to edit_profile_password_path, notice: _('We sent you an email with reset password instructions')
end
private
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 83e14275a8b..95b9344c551 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -18,7 +18,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
two_factor_authentication_reason(
global: lambda do
flash.now[:alert] =
- s_('The global settings require you to enable Two-Factor Authentication for your account.')
+ _('The global settings require you to enable Two-Factor Authentication for your account.')
end,
group: lambda do |groups|
flash.now[:alert] = groups_notification(groups)
@@ -27,7 +27,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
unless two_factor_grace_period_expired?
grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
- flash.now[:alert] = flash.now[:alert] + s_(" You need to do this before %{grace_period_deadline}.") % { grace_period_deadline: l(grace_period_deadline) }
+ flash.now[:alert] = flash.now[:alert] + _(" You need to do this before %{grace_period_deadline}.") % { grace_period_deadline: l(grace_period_deadline) }
end
end
@@ -44,7 +44,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
render 'create'
else
- @error = s_('Invalid pin code')
+ @error = _('Invalid pin code')
@qr_code = build_qr_code
setup_u2f_registration
render 'show'
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index cb02581da37..98cd66cf6f9 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -4,6 +4,10 @@ class Projects::ClustersController < Clusters::ClustersController
prepend_before_action :project
before_action :repository
+ before_action do
+ push_frontend_feature_flag(:prometheus_computed_alerts)
+ end
+
layout 'project'
private
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index c342e1c80b0..e002a4d349b 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -11,10 +11,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index]
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
- push_frontend_feature_flag(:metrics_time_window)
push_frontend_feature_flag(:environment_metrics_use_prometheus_endpoint)
push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards)
push_frontend_feature_flag(:grafana_dashboard_link)
+ push_frontend_feature_flag(:prometheus_computed_alerts)
end
def index
@@ -220,8 +220,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
def metrics_params
- return unless Feature.enabled?(:metrics_time_window, project)
-
params.require([:start, :end])
end
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 7a80da53025..956093b972b 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -15,6 +15,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
alias_method :authenticated_user, :actor
# Git clients will not know what authenticity token to send along
+ skip_around_action :set_session_storage
skip_before_action :verify_authenticity_token
skip_before_action :repository
before_action :authenticate_user
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index 4640be015de..afbf9fd7720 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -2,6 +2,7 @@
class Projects::ImportsController < Projects::ApplicationController
include ContinueParams
+ include ImportUrlParams
# Authorize
before_action :authorize_admin_project!
@@ -67,10 +68,12 @@ class Projects::ImportsController < Projects::ApplicationController
end
def import_params_attributes
- [:import_url]
+ []
end
def import_params
- params.require(:project).permit(import_params_attributes)
+ params.require(:project)
+ .permit(import_params_attributes)
+ .merge(import_url_params)
end
end
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index eb469d2d714..f2a6268b3e9 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -7,11 +7,15 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
private
- # rubocop: disable CodeReuse/ActiveRecord
def merge_request
- @issuable = @merge_request ||= @project.merge_requests.includes(author: :status).find_by!(iid: params[:id])
+ @issuable =
+ @merge_request ||=
+ merge_request_includes(@project.merge_requests).find_by_iid!(params[:id])
+ end
+
+ def merge_request_includes(association)
+ association.includes(:metrics, :assignees, author: :status) # rubocop:disable CodeReuse/ActiveRecord
end
- # rubocop: enable CodeReuse/ActiveRecord
def merge_request_params
params.require(:merge_request).permit(merge_request_params_attributes)
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 8f177895b08..135117926be 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -33,7 +33,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def show
close_merge_request_if_no_source_project
- mark_merge_request_mergeable
+ @merge_request.check_mergeability
respond_to do |format|
format.html do
@@ -145,14 +145,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
render partial: 'projects/merge_requests/widget/commit_change_content', layout: false
end
- def cancel_merge_when_pipeline_succeeds
- unless @merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
+ def cancel_auto_merge
+ unless @merge_request.can_cancel_auto_merge?(current_user)
return access_denied!
end
- ::MergeRequests::MergeWhenPipelineSucceedsService
- .new(@project, current_user)
- .cancel(@merge_request)
+ AutoMergeService.new(project, current_user).cancel(@merge_request)
render json: serialize_widget(@merge_request)
end
@@ -229,12 +227,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def merge_params_attributes
- [:should_remove_source_branch, :commit_message, :squash_commit_message, :squash]
+ [:should_remove_source_branch, :commit_message, :squash_commit_message, :squash, :auto_merge_strategy]
end
- def merge_when_pipeline_succeeds_active?
- params[:merge_when_pipeline_succeeds].present? &&
- @merge_request.head_pipeline && @merge_request.head_pipeline.active?
+ def auto_merge_requested?
+ # Support params[:merge_when_pipeline_succeeds] during the transition period
+ params[:auto_merge_strategy].present? || params[:merge_when_pipeline_succeeds].present?
end
def close_merge_request_if_no_source_project
@@ -253,14 +251,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_request.has_no_commits? && !@merge_request.target_branch_exists?
end
- def mark_merge_request_mergeable
- @merge_request.check_if_can_be_merged
- end
-
def merge!
- # Disable the CI check if merge_when_pipeline_succeeds is enabled since we have
+ # Disable the CI check if auto_merge_strategy is specified since we have
# to wait until CI completes to know
- unless @merge_request.mergeable?(skip_ci_check: merge_when_pipeline_succeeds_active?)
+ unless @merge_request.mergeable?(skip_ci_check: auto_merge_requested?)
return :failed
end
@@ -274,24 +268,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_request.update(merge_error: nil, squash: merge_params.fetch(:squash, false))
- if params[:merge_when_pipeline_succeeds].present?
- return :failed unless @merge_request.actual_head_pipeline
-
- if @merge_request.actual_head_pipeline.active?
- ::MergeRequests::MergeWhenPipelineSucceedsService
- .new(@project, current_user, merge_params)
- .execute(@merge_request)
-
- :merge_when_pipeline_succeeds
- elsif @merge_request.actual_head_pipeline.success?
- # This can be triggered when a user clicks the auto merge button while
- # the tests finish at about the same time
- @merge_request.merge_async(current_user.id, merge_params)
-
- :success
- else
- :failed
- end
+ if auto_merge_requested?
+ AutoMergeService.new(project, current_user, merge_params)
+ .execute(merge_request,
+ params[:auto_merge_strategy] || AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
else
@merge_request.merge_async(current_user.id, merge_params)
diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb
index 58b1bc54181..89f21d8dadb 100644
--- a/app/controllers/projects/pages_domains_controller.rb
+++ b/app/controllers/projects/pages_domains_controller.rb
@@ -65,11 +65,11 @@ class Projects::PagesDomainsController < Projects::ApplicationController
private
def create_params
- params.require(:pages_domain).permit(:key, :certificate, :domain)
+ params.require(:pages_domain).permit(:key, :certificate, :domain, :auto_ssl_enabled)
end
def update_params
- params.require(:pages_domain).permit(:key, :certificate)
+ params.require(:pages_domain).permit(:key, :certificate, :auto_ssl_enabled)
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/controllers/projects/serverless/functions_controller.rb b/app/controllers/projects/serverless/functions_controller.rb
index 79030da64d3..4b0d001fca6 100644
--- a/app/controllers/projects/serverless/functions_controller.rb
+++ b/app/controllers/projects/serverless/functions_controller.rb
@@ -10,15 +10,13 @@ module Projects
format.json do
functions = finder.execute
- if functions.any?
- render json: serialize_function(functions)
- else
- head :no_content
- end
+ render json: {
+ knative_installed: finder.knative_installed,
+ functions: serialize_function(functions)
+ }.to_json
end
format.html do
- @installed = finder.installed?
render
end
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index c4dff95a4b9..1b8d479209b 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -50,7 +50,8 @@ module Projects
:runners_token, :builds_enabled, :build_allow_git_fetch,
:build_timeout_human_readable, :build_coverage_regex, :public_builds,
:auto_cancel_pending_pipelines, :ci_config_path,
- auto_devops_attributes: [:id, :domain, :enabled, :deploy_strategy]
+ auto_devops_attributes: [:id, :domain, :enabled, :deploy_strategy],
+ ci_cd_settings_attributes: [:default_git_depth]
)
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index e88c46144ef..12db493978b 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -7,6 +7,7 @@ class ProjectsController < Projects::ApplicationController
include PreviewMarkdown
include SendFileUpload
include RecordUserLastActivity
+ include ImportUrlParams
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
@@ -333,6 +334,7 @@ class ProjectsController < Projects::ApplicationController
def project_params(attributes: [])
params.require(:project)
.permit(project_params_attributes + attributes)
+ .merge(import_url_params)
end
def project_params_attributes
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index a80ab3bcd28..cb25548c83f 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -25,6 +25,7 @@ class SearchController < ApplicationController
@show_snippets = search_service.show_snippets?
@search_results = search_service.search_results
@search_objects = search_service.search_objects
+ @display_options = search_service.display_options
render_commits if @scope == 'commits'
eager_load_user_status if @scope == 'users'
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 6fea61cf45d..a841859621e 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -18,6 +18,7 @@ class SessionsController < Devise::SessionsController
prepend_before_action :store_redirect_uri, only: [:new]
prepend_before_action :ldap_servers, only: [:new, :create]
prepend_before_action :require_no_authentication_without_flash, only: [:new, :create]
+ prepend_before_action :ensure_password_authentication_enabled!, if: :password_based_login?, only: [:create]
before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha
@@ -138,6 +139,14 @@ class SessionsController < Devise::SessionsController
end
# rubocop: enable CodeReuse/ActiveRecord
+ def ensure_password_authentication_enabled!
+ render_403 unless Gitlab::CurrentSettings.password_authentication_enabled_for_web?
+ end
+
+ def password_based_login?
+ user_params[:login].present? || user_params[:password].present?
+ end
+
def user_params
params.require(:user).permit(:login, :password, :remember_me, :otp_attempt, :device_response)
end
diff --git a/app/finders/clusters/knative_services_finder.rb b/app/finders/clusters/knative_services_finder.rb
new file mode 100644
index 00000000000..7d3b53ef663
--- /dev/null
+++ b/app/finders/clusters/knative_services_finder.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+module Clusters
+ class KnativeServicesFinder
+ include ReactiveCaching
+ include Gitlab::Utils::StrongMemoize
+
+ KNATIVE_STATES = {
+ 'checking' => 'checking',
+ 'installed' => 'installed',
+ 'not_found' => 'not_found'
+ }.freeze
+
+ self.reactive_cache_key = ->(finder) { finder.model_name }
+ self.reactive_cache_worker_finder = ->(_id, *cache_args) { from_cache(*cache_args) }
+
+ attr_reader :cluster, :project
+
+ def initialize(cluster, project)
+ @cluster = cluster
+ @project = project
+ end
+
+ def with_reactive_cache_memoized(*cache_args, &block)
+ strong_memoize(:reactive_cache) do
+ with_reactive_cache(*cache_args, &block)
+ end
+ end
+
+ def clear_cache!
+ clear_reactive_cache!(*cache_args)
+ end
+
+ def self.from_cache(cluster_id, project_id)
+ cluster = Clusters::Cluster.find(cluster_id)
+ project = ::Project.find(project_id)
+
+ new(cluster, project)
+ end
+
+ def calculate_reactive_cache(*)
+ # read_services calls knative_client.discover implicitily. If we stop
+ # detecting services but still want to detect knative, we'll need to
+ # explicitily call: knative_client.discover
+ #
+ # We didn't create it separately to avoid 2 cluster requests.
+ ksvc = read_services
+ pods = knative_client.discovered ? read_pods : []
+ { services: ksvc, pods: pods, knative_detected: knative_client.discovered }
+ end
+
+ def services
+ return [] unless search_namespace
+
+ cached_data = with_reactive_cache_memoized(*cache_args) { |data| data }
+ cached_data.to_h.fetch(:services, [])
+ end
+
+ def cache_args
+ [cluster.id, project.id]
+ end
+
+ def service_pod_details(service)
+ cached_data = with_reactive_cache_memoized(*cache_args) { |data| data }
+ cached_data.to_h.fetch(:pods, []).select do |pod|
+ filter_pods(pod, service)
+ end
+ end
+
+ def knative_detected
+ cached_data = with_reactive_cache_memoized(*cache_args) { |data| data }
+
+ knative_state = cached_data.to_h[:knative_detected]
+
+ return KNATIVE_STATES['checking'] if knative_state.nil?
+ return KNATIVE_STATES['installed'] if knative_state
+
+ KNATIVE_STATES['uninstalled']
+ end
+
+ def model_name
+ self.class.name.underscore.tr('/', '_')
+ end
+
+ private
+
+ def search_namespace
+ @search_namespace ||= cluster.kubernetes_namespace_for(project)
+ end
+
+ def knative_client
+ cluster.kubeclient.knative_client
+ end
+
+ def filter_pods(pod, service)
+ pod["metadata"]["labels"]["serving.knative.dev/service"] == service
+ end
+
+ def read_services
+ knative_client.get_services(namespace: search_namespace).as_json
+ rescue Kubeclient::ResourceNotFoundError
+ []
+ end
+
+ def read_pods
+ cluster.kubeclient.core_client.get_pods(namespace: search_namespace).as_json
+ end
+
+ def id
+ nil
+ end
+ end
+end
diff --git a/app/finders/projects/serverless/functions_finder.rb b/app/finders/projects/serverless/functions_finder.rb
index e5bffccabfe..ebe50806ca1 100644
--- a/app/finders/projects/serverless/functions_finder.rb
+++ b/app/finders/projects/serverless/functions_finder.rb
@@ -14,8 +14,16 @@ module Projects
knative_services.flatten.compact
end
- def installed?
- clusters_with_knative_installed.exists?
+ # Possible return values: Clusters::KnativeServicesFinder::KNATIVE_STATE
+ def knative_installed
+ states = @clusters.map do |cluster|
+ cluster.application_knative
+ cluster.knative_services_finder(project).knative_detected.tap do |state|
+ return state if state == ::Clusters::KnativeServicesFinder::KNATIVE_STATES['checking'] # rubocop:disable Cop/AvoidReturnFromBlocks
+ end
+ end
+
+ states.any? { |state| state == ::Clusters::KnativeServicesFinder::KNATIVE_STATES['installed'] }
end
def service(environment_scope, name)
@@ -25,7 +33,7 @@ module Projects
def invocation_metrics(environment_scope, name)
return unless prometheus_adapter&.can_query?
- cluster = clusters_with_knative_installed.preload_knative.find do |c|
+ cluster = @clusters.find do |c|
environment_scope == c.environment_scope
end
@@ -34,7 +42,7 @@ module Projects
end
def has_prometheus?(environment_scope)
- clusters_with_knative_installed.preload_knative.to_a.any? do |cluster|
+ @clusters.any? do |cluster|
environment_scope == cluster.environment_scope && cluster.application_prometheus_available?
end
end
@@ -42,10 +50,12 @@ module Projects
private
def knative_service(environment_scope, name)
- clusters_with_knative_installed.preload_knative.map do |cluster|
+ @clusters.map do |cluster|
next if environment_scope != cluster.environment_scope
- services = cluster.application_knative.services_for(ns: cluster.kubernetes_namespace_for(project))
+ services = cluster
+ .knative_services_finder(project)
+ .services
.select { |svc| svc["metadata"]["name"] == name }
add_metadata(cluster, services).first unless services.nil?
@@ -53,8 +63,11 @@ module Projects
end
def knative_services
- clusters_with_knative_installed.preload_knative.map do |cluster|
- services = cluster.application_knative.services_for(ns: cluster.kubernetes_namespace_for(project))
+ @clusters.map do |cluster|
+ services = cluster
+ .knative_services_finder(project)
+ .services
+
add_metadata(cluster, services) unless services.nil?
end
end
@@ -65,17 +78,14 @@ module Projects
s["cluster_id"] = cluster.id
if services.length == 1
- s["podcount"] = cluster.application_knative.service_pod_details(
- cluster.kubernetes_namespace_for(project),
- s["metadata"]["name"]).length
+ s["podcount"] = cluster
+ .knative_services_finder(project)
+ .service_pod_details(s["metadata"]["name"])
+ .length
end
end
end
- def clusters_with_knative_installed
- @clusters.with_knative_installed
- end
-
# rubocop: disable CodeReuse/ServiceClass
def prometheus_adapter
@prometheus_adapter ||= ::Prometheus::AdapterService.new(project).prometheus_adapter
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index a63f45f231c..2e5bdbd79c8 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -16,7 +16,7 @@ class GitlabSchema < GraphQL::Schema
use Gitlab::Graphql::Connections
use Gitlab::Graphql::GenericTracing
- query_analyzer Gitlab::Graphql::QueryAnalyzers::LogQueryComplexity.analyzer
+ query_analyzer Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer.new
query(Types::QueryType)
@@ -45,6 +45,31 @@ class GitlabSchema < GraphQL::Schema
super(query_str, **kwargs)
end
+ def id_from_object(object)
+ unless object.respond_to?(:to_global_id)
+ # This is an error in our schema and needs to be solved. So raise a
+ # more meaningfull error message
+ raise "#{object} does not implement `to_global_id`. "\
+ "Include `GlobalID::Identification` into `#{object.class}"
+ end
+
+ object.to_global_id
+ end
+
+ def object_from_id(global_id)
+ gid = GlobalID.parse(global_id)
+
+ unless gid
+ raise Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid GitLab id."
+ end
+
+ if gid.model_class < ApplicationRecord
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(gid.model_class, gid.model_id).find
+ else
+ gid.find
+ end
+ end
+
private
def max_query_complexity(ctx)
diff --git a/app/graphql/mutations/merge_requests/base.rb b/app/graphql/mutations/merge_requests/base.rb
index 7d0cb777ad1..e85d16fc2c5 100644
--- a/app/graphql/mutations/merge_requests/base.rb
+++ b/app/graphql/mutations/merge_requests/base.rb
@@ -10,7 +10,7 @@ module Mutations
required: true,
description: "The project the merge request to mutate is in"
- argument :iid, GraphQL::ID_TYPE,
+ argument :iid, GraphQL::STRING_TYPE,
required: true,
description: "The iid of the merge request to mutate"
diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb
index 31850c2cadb..5b7eb57841c 100644
--- a/app/graphql/resolvers/base_resolver.rb
+++ b/app/graphql/resolvers/base_resolver.rb
@@ -10,7 +10,7 @@ module Resolvers
end
end
- def self.resolver_complexity(args)
+ def self.resolver_complexity(args, child_complexity:)
complexity = 1
complexity += 1 if args[:sort]
complexity += 5 if args[:search]
diff --git a/app/graphql/resolvers/concerns/resolves_pipelines.rb b/app/graphql/resolvers/concerns/resolves_pipelines.rb
index a166211fc18..a6f82cc8505 100644
--- a/app/graphql/resolvers/concerns/resolves_pipelines.rb
+++ b/app/graphql/resolvers/concerns/resolves_pipelines.rb
@@ -20,7 +20,7 @@ module ResolvesPipelines
end
class_methods do
- def resolver_complexity(args)
+ def resolver_complexity(args, child_complexity:)
complexity = super
complexity += 2 if args[:sha]
complexity += 2 if args[:ref]
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index f7e49166ca0..6988b451ec3 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -2,11 +2,11 @@
module Resolvers
class IssuesResolver < BaseResolver
- argument :iid, GraphQL::ID_TYPE,
+ argument :iid, GraphQL::STRING_TYPE,
required: false,
description: 'The IID of the issue, e.g., "1"'
- argument :iids, [GraphQL::ID_TYPE],
+ argument :iids, [GraphQL::STRING_TYPE],
required: false,
description: 'The list of IIDs of issues, e.g., [1, 2]'
argument :state, Types::IssuableStateEnum,
@@ -58,7 +58,7 @@ module Resolvers
IssuesFinder.new(context[:current_user], args).execute
end
- def self.resolver_complexity(args)
+ def self.resolver_complexity(args, child_complexity:)
complexity = super
complexity += 2 if args[:labelName]
diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb
index 90795c797ac..b84e60066e1 100644
--- a/app/graphql/resolvers/merge_requests_resolver.rb
+++ b/app/graphql/resolvers/merge_requests_resolver.rb
@@ -2,11 +2,11 @@
module Resolvers
class MergeRequestsResolver < BaseResolver
- argument :iid, GraphQL::ID_TYPE,
+ argument :iid, GraphQL::STRING_TYPE,
required: false,
description: 'The IID of the merge request, e.g., "1"'
- argument :iids, [GraphQL::ID_TYPE],
+ argument :iids, [GraphQL::STRING_TYPE],
required: false,
description: 'The list of IIDs of issues, e.g., [1, 2]'
diff --git a/app/graphql/resolvers/namespace_projects_resolver.rb b/app/graphql/resolvers/namespace_projects_resolver.rb
new file mode 100644
index 00000000000..677ea808aeb
--- /dev/null
+++ b/app/graphql/resolvers/namespace_projects_resolver.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class NamespaceProjectsResolver < BaseResolver
+ argument :include_subgroups, GraphQL::BOOLEAN_TYPE,
+ required: false,
+ default_value: false,
+ description: 'Include also subgroup projects'
+
+ type Types::ProjectType, null: true
+
+ alias_method :namespace, :object
+
+ def resolve(include_subgroups:)
+ # The namespace could have been loaded in batch by `BatchLoader`.
+ # At this point we need the `id` or the `full_path` of the namespace
+ # to query for projects, so make sure it's loaded and not `nil` before continuing.
+ namespace.sync if namespace.respond_to?(:sync)
+ return Project.none if namespace.nil?
+
+ if include_subgroups
+ namespace.all_projects.with_route
+ else
+ namespace.projects.with_route
+ end
+ end
+
+ def self.resolver_complexity(args, child_complexity:)
+ complexity = super
+ complexity + 10
+ end
+ end
+end
diff --git a/app/graphql/resolvers/namespace_resolver.rb b/app/graphql/resolvers/namespace_resolver.rb
new file mode 100644
index 00000000000..17b3800d151
--- /dev/null
+++ b/app/graphql/resolvers/namespace_resolver.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class NamespaceResolver < BaseResolver
+ prepend FullPathResolver
+
+ type Types::NamespaceType, null: true
+
+ def resolve(full_path:)
+ model_by_full_path(Namespace, full_path)
+ end
+ end
+end
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index 15331129134..dd0d9105df6 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -29,15 +29,18 @@ module Types
# proc because we set complexity depending on arguments and number of
# items which can be loaded.
proc do |ctx, args, child_complexity|
- page_size = @max_page_size || ctx.schema.default_max_page_size
- limit_value = [args[:first], args[:last], page_size].compact.min
-
# Resolvers may add extra complexity depending on used arguments
- complexity = child_complexity + self.resolver&.try(:resolver_complexity, args).to_i
+ complexity = child_complexity + self.resolver&.try(:resolver_complexity, args, child_complexity: child_complexity).to_i
+
+ field_defn = to_graphql
- # Resolvers may add extra complexity depending on number of items being loaded.
- multiplier = self.resolver&.try(:complexity_multiplier, args).to_f
- complexity += complexity * limit_value * multiplier
+ if field_defn.connection?
+ # Resolvers may add extra complexity depending on number of items being loaded.
+ page_size = field_defn.connection_max_page_size || ctx.schema.default_max_page_size
+ limit_value = [args[:first], args[:last], page_size].compact.min
+ multiplier = self.resolver&.try(:complexity_multiplier, args).to_f
+ complexity += complexity * limit_value * multiplier
+ end
complexity.to_i
end
diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb
index 82b78abd573..e40059c46bb 100644
--- a/app/graphql/types/base_object.rb
+++ b/app/graphql/types/base_object.rb
@@ -6,5 +6,10 @@ module Types
prepend Gitlab::Graphql::ExposePermissions
field_class Types::BaseField
+
+ # All graphql fields exposing an id, should expose a global id.
+ def id
+ GitlabSchema.id_from_object(object)
+ end
end
end
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index de7d6570a3e..cff81e5670b 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -10,7 +10,7 @@ module Types
expose_permissions Types::PermissionTypes::Ci::Pipeline
field :id, GraphQL::ID_TYPE, null: false
- field :iid, GraphQL::ID_TYPE, null: false
+ field :iid, GraphQL::STRING_TYPE, null: false
field :sha, GraphQL::STRING_TYPE, null: false
field :before_sha, GraphQL::STRING_TYPE, null: true
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index b21a226d07f..dd5133189dc 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -15,6 +15,10 @@ module Types
field :description, GraphQL::STRING_TYPE, null: true
field :state, IssueStateEnum, null: false
+ field :reference, GraphQL::STRING_TYPE, null: false, method: :to_reference do
+ argument :full, GraphQL::BOOLEAN_TYPE, required: false, default_value: false
+ end
+
field :author, Types::UserType,
null: false,
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.author_id).find }
@@ -37,7 +41,9 @@ module Types
field :upvotes, GraphQL::INT_TYPE, null: false
field :downvotes, GraphQL::INT_TYPE, null: false
field :user_notes_count, GraphQL::INT_TYPE, null: false
+ field :web_path, GraphQL::STRING_TYPE, null: false, method: :issue_path
field :web_url, GraphQL::STRING_TYPE, null: false
+ field :relative_position, GraphQL::INT_TYPE, null: true
field :closed_at, Types::TimeType, null: true
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 120ffe0dfde..85ac3102442 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -11,7 +11,7 @@ module Types
present_using MergeRequestPresenter
field :id, GraphQL::ID_TYPE, null: false
- field :iid, GraphQL::ID_TYPE, null: false
+ field :iid, GraphQL::STRING_TYPE, null: false
field :title, GraphQL::STRING_TYPE, null: false
field :description, GraphQL::STRING_TYPE, null: true
field :state, MergeRequestStateEnum, null: false
diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb
index 36d8ee8c878..f6d91320e50 100644
--- a/app/graphql/types/namespace_type.rb
+++ b/app/graphql/types/namespace_type.rb
@@ -15,5 +15,10 @@ module Types
field :visibility, GraphQL::STRING_TYPE, null: true
field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true, method: :lfs_enabled?
field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true
+
+ field :projects,
+ Types::ProjectType.connection_type,
+ null: false,
+ resolver: ::Resolvers::NamespaceProjectsResolver
end
end
diff --git a/app/graphql/types/project_statistics_type.rb b/app/graphql/types/project_statistics_type.rb
new file mode 100644
index 00000000000..62537361918
--- /dev/null
+++ b/app/graphql/types/project_statistics_type.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Types
+ class ProjectStatisticsType < BaseObject
+ graphql_name 'ProjectStatistics'
+
+ field :commit_count, GraphQL::INT_TYPE, null: false
+
+ field :storage_size, GraphQL::INT_TYPE, null: false
+ field :repository_size, GraphQL::INT_TYPE, null: false
+ field :lfs_objects_size, GraphQL::INT_TYPE, null: false
+ field :build_artifacts_size, GraphQL::INT_TYPE, null: false
+ field :packages_size, GraphQL::INT_TYPE, null: false
+ field :wiki_size, GraphQL::INT_TYPE, null: true
+ end
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 06a1aab09f6..2236ffa394d 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -69,6 +69,10 @@ module Types
field :namespace, Types::NamespaceType, null: false
field :group, Types::GroupType, null: true
+ field :statistics, Types::ProjectStatisticsType,
+ null: false,
+ resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchProjectStatisticsLoader.new(obj.id).find }
+
field :repository, Types::RepositoryType, null: false
field :merge_requests,
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 40d7de1a49a..536bdb077ad 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -14,6 +14,11 @@ module Types
resolver: Resolvers::GroupResolver,
description: "Find a group"
+ field :namespace, Types::NamespaceType,
+ null: true,
+ resolver: Resolvers::NamespaceResolver,
+ description: "Find a namespace"
+
field :metadata, Types::MetadataType,
null: true,
resolver: Resolvers::MetadataResolver,
diff --git a/app/graphql/types/tree/blob_type.rb b/app/graphql/types/tree/blob_type.rb
index 230624201b0..f2b7d5df2b2 100644
--- a/app/graphql/types/tree/blob_type.rb
+++ b/app/graphql/types/tree/blob_type.rb
@@ -4,7 +4,11 @@ module Types
class BlobType < BaseObject
implements Types::Tree::EntryType
+ present_using BlobPresenter
+
graphql_name 'Blob'
+
+ field :web_url, GraphQL::STRING_TYPE, null: true
end
end
end
diff --git a/app/graphql/types/tree/tree_entry_type.rb b/app/graphql/types/tree/tree_entry_type.rb
index d5cfb898aea..23ec2ef0ec2 100644
--- a/app/graphql/types/tree/tree_entry_type.rb
+++ b/app/graphql/types/tree/tree_entry_type.rb
@@ -4,8 +4,12 @@ module Types
class TreeEntryType < BaseObject
implements Types::Tree::EntryType
+ present_using TreeEntryPresenter
+
graphql_name 'TreeEntry'
description 'Represents a directory'
+
+ field :web_url, GraphQL::STRING_TYPE, null: true
end
end
end
diff --git a/app/graphql/types/tree/tree_type.rb b/app/graphql/types/tree/tree_type.rb
index 1eb6c43972e..1ee93ed9542 100644
--- a/app/graphql/types/tree/tree_type.rb
+++ b/app/graphql/types/tree/tree_type.rb
@@ -4,9 +4,15 @@ module Types
class TreeType < BaseObject
graphql_name 'Tree'
- field :trees, Types::Tree::TreeEntryType.connection_type, null: false
+ field :trees, Types::Tree::TreeEntryType.connection_type, null: false, resolve: -> (obj, args, ctx) do
+ Gitlab::Graphql::Representation::TreeEntry.decorate(obj.trees, obj.repository)
+ end
+
field :submodules, Types::Tree::SubmoduleType.connection_type, null: false
- field :blobs, Types::Tree::BlobType.connection_type, null: false
+
+ field :blobs, Types::Tree::BlobType.connection_type, null: false, resolve: -> (obj, args, ctx) do
+ Gitlab::Graphql::Representation::TreeEntry.decorate(obj.blobs, obj.repository)
+ end
end
end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 971d1052824..4469118f065 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -160,6 +160,7 @@ module ApplicationSettingsHelper
:akismet_api_key,
:akismet_enabled,
:allow_local_requests_from_hooks_and_services,
+ :dns_rebinding_protection_enabled,
:archive_builds_in_human_readable,
:authorized_keys_enabled,
:auto_devops_enabled,
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 7e631053b54..0d6a6496993 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -188,7 +188,7 @@ module BlobHelper
end
def copy_file_path_button(file_path)
- clipboard_button(text: file_path, gfm: "`#{file_path}`", class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard')
+ clipboard_button(text: file_path, gfm: "`#{file_path}`", class: 'btn-clipboard btn-transparent', title: 'Copy file path to clipboard')
end
def copy_blob_source_button(blob)
diff --git a/app/helpers/ci_variables_helper.rb b/app/helpers/ci_variables_helper.rb
index 5bfdeb9e33c..fc51f00d052 100644
--- a/app/helpers/ci_variables_helper.rb
+++ b/app/helpers/ci_variables_helper.rb
@@ -17,7 +17,7 @@ module CiVariablesHelper
if variable && !only_key_value
variable.masked
else
- true
+ false
end
end
@@ -27,4 +27,8 @@ module CiVariablesHelper
%w(File file)
]
end
+
+ def ci_variable_maskable_regex
+ Maskable::REGEX.inspect.sub('\\A', '^').sub('\\z', '$').sub(/^\//, '').sub(/\/[a-z]*$/, '').gsub('\/', '/')
+ end
end
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index dc0e5511fcf..36122d3a22a 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -57,12 +57,6 @@ module EmailsHelper
pluralize(valid_length, unit)
end
- def reset_token_expire_message
- link_tag = link_to('request a new one', new_user_password_url(user_email: @user.email))
- "This link is valid for #{password_reset_token_valid_time}. " \
- "After it expires, you can #{link_tag}."
- end
-
def header_logo
if current_appearance&.header_logo?
image_tag(
@@ -98,16 +92,17 @@ module EmailsHelper
case format
when :html
- " via merge request #{link_to(merge_request.to_reference, merge_request.web_url)}"
+ merge_request_link = link_to(merge_request.to_reference, merge_request.web_url)
+ _("via merge request %{link}").html_safe % { link: merge_request_link }
else
# If it's not HTML nor text then assume it's text to be safe
- " via merge request #{merge_request.to_reference} (#{merge_request.web_url})"
+ _("via merge request %{link}") % { link: "#{merge_request.to_reference} (#{merge_request.web_url})" }
end
when String
# Technically speaking this should be Commit but per
# https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15610#note_163812339
# we can't deserialize Commit without custom serializer for ActiveJob
- " via #{closed_via}"
+ _("via %{closed_via}") % { closed_via: closed_via }
else
""
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 7af766c8544..a3f53ca8dd6 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -99,7 +99,7 @@ module GroupsHelper
end
def remove_group_message(group)
- _("You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
+ _("You are going to remove %{group_name}, this will also remove all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
{ group_name: group.name }
end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index acc8aeae282..db4f29cd996 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -76,29 +76,39 @@ module LabelsHelper
end
def suggested_colors
- [
- '#0033CC',
- '#428BCA',
- '#44AD8E',
- '#A8D695',
- '#5CB85C',
- '#69D100',
- '#004E00',
- '#34495E',
- '#7F8C8D',
- '#A295D6',
- '#5843AD',
- '#8E44AD',
- '#FFECDB',
- '#AD4363',
- '#D10069',
- '#CC0033',
- '#FF0000',
- '#D9534F',
- '#D1D100',
- '#F0AD4E',
- '#AD8D43'
- ]
+ {
+ '#0033CC' => s_('SuggestedColors|UA blue'),
+ '#428BCA' => s_('SuggestedColors|Moderate blue'),
+ '#44AD8E' => s_('SuggestedColors|Lime green'),
+ '#A8D695' => s_('SuggestedColors|Feijoa'),
+ '#5CB85C' => s_('SuggestedColors|Slightly desaturated green'),
+ '#69D100' => s_('SuggestedColors|Bright green'),
+ '#004E00' => s_('SuggestedColors|Very dark lime green'),
+ '#34495E' => s_('SuggestedColors|Very dark desaturated blue'),
+ '#7F8C8D' => s_('SuggestedColors|Dark grayish cyan'),
+ '#A295D6' => s_('SuggestedColors|Slightly desaturated blue'),
+ '#5843AD' => s_('SuggestedColors|Dark moderate blue'),
+ '#8E44AD' => s_('SuggestedColors|Dark moderate violet'),
+ '#FFECDB' => s_('SuggestedColors|Very pale orange'),
+ '#AD4363' => s_('SuggestedColors|Dark moderate pink'),
+ '#D10069' => s_('SuggestedColors|Strong pink'),
+ '#CC0033' => s_('SuggestedColors|Strong red'),
+ '#FF0000' => s_('SuggestedColors|Pure red'),
+ '#D9534F' => s_('SuggestedColors|Soft red'),
+ '#D1D100' => s_('SuggestedColors|Strong yellow'),
+ '#F0AD4E' => s_('SuggestedColors|Soft orange'),
+ '#AD8D43' => s_('SuggestedColors|Dark moderate orange')
+ }
+ end
+
+ def render_suggested_colors
+ colors_html = suggested_colors.map do |color_hex_value, color_name|
+ link_to('', '#', class: "has-tooltip", style: "background-color: #{color_hex_value}", data: { color: color_hex_value }, title: color_name)
+ end
+
+ content_tag(:div, class: 'suggest-colors') do
+ colors_html.join.html_safe
+ end
end
def text_color_for_bg(bg_color)
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 991ca42c445..2de4e92e33e 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -103,7 +103,7 @@ module MergeRequestsHelper
def merge_params(merge_request)
{
- merge_when_pipeline_succeeds: true,
+ auto_merge_strategy: AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS,
should_remove_source_branch: true,
sha: merge_request.diff_head_sha,
squash: merge_request.squash
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index a7ce7667916..11b9cf22142 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -100,4 +100,8 @@ module NotificationsHelper
css_class: "icon notifications-icon js-notifications-icon"
)
end
+
+ def show_unsubscribe_title?(noteable)
+ can?(current_user, "read_#{noteable.to_ability_name}".to_sym, noteable)
+ end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index e587cf4045d..8dee842a22d 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -241,6 +241,7 @@ module ProjectsHelper
# TODO: Remove this method when removing the feature flag
# https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/11209#note_162234863
+ # make sure to remove from the EE specific controller as well: ee/app/controllers/ee/dashboard/projects_controller.rb
def show_projects?(projects, params)
Feature.enabled?(:project_list_filter_bar) || !!(params[:personal] || params[:name] || any_projects?(projects))
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index f2d814e6930..26692934456 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -3,29 +3,30 @@
module SortingHelper
def sort_options_hash
{
- sort_value_created_date => sort_title_created_date,
- sort_value_downvotes => sort_title_downvotes,
- sort_value_due_date => sort_title_due_date,
- sort_value_due_date_later => sort_title_due_date_later,
- sort_value_due_date_soon => sort_title_due_date_soon,
- sort_value_label_priority => sort_title_label_priority,
- sort_value_largest_group => sort_title_largest_group,
- sort_value_largest_repo => sort_title_largest_repo,
- sort_value_milestone => sort_title_milestone,
- sort_value_milestone_later => sort_title_milestone_later,
- sort_value_milestone_soon => sort_title_milestone_soon,
- sort_value_name => sort_title_name,
- sort_value_name_desc => sort_title_name_desc,
- sort_value_oldest_created => sort_title_oldest_created,
- sort_value_oldest_signin => sort_title_oldest_signin,
- sort_value_oldest_updated => sort_title_oldest_updated,
- sort_value_recently_created => sort_title_recently_created,
- sort_value_recently_signin => sort_title_recently_signin,
- sort_value_recently_updated => sort_title_recently_updated,
- sort_value_popularity => sort_title_popularity,
- sort_value_priority => sort_title_priority,
- sort_value_upvotes => sort_title_upvotes,
- sort_value_contacted_date => sort_title_contacted_date
+ sort_value_created_date => sort_title_created_date,
+ sort_value_downvotes => sort_title_downvotes,
+ sort_value_due_date => sort_title_due_date,
+ sort_value_due_date_later => sort_title_due_date_later,
+ sort_value_due_date_soon => sort_title_due_date_soon,
+ sort_value_label_priority => sort_title_label_priority,
+ sort_value_largest_group => sort_title_largest_group,
+ sort_value_largest_repo => sort_title_largest_repo,
+ sort_value_milestone => sort_title_milestone,
+ sort_value_milestone_later => sort_title_milestone_later,
+ sort_value_milestone_soon => sort_title_milestone_soon,
+ sort_value_name => sort_title_name,
+ sort_value_name_desc => sort_title_name_desc,
+ sort_value_oldest_created => sort_title_oldest_created,
+ sort_value_oldest_signin => sort_title_oldest_signin,
+ sort_value_oldest_updated => sort_title_oldest_updated,
+ sort_value_recently_created => sort_title_recently_created,
+ sort_value_recently_signin => sort_title_recently_signin,
+ sort_value_recently_updated => sort_title_recently_updated,
+ sort_value_popularity => sort_title_popularity,
+ sort_value_priority => sort_title_priority,
+ sort_value_upvotes => sort_title_upvotes,
+ sort_value_contacted_date => sort_title_contacted_date,
+ sort_value_relative_position => sort_title_relative_position
}
end
@@ -397,6 +398,10 @@ module SortingHelper
s_('SortOptions|Recent last activity')
end
+ def sort_title_relative_position
+ s_('SortOptions|Manual')
+ end
+
# Values.
def sort_value_access_level_asc
'access_level_asc'
@@ -545,4 +550,8 @@ module SortingHelper
def sort_value_recently_last_activity
'last_activity_on_desc'
end
+
+ def sort_value_relative_position
+ 'relative_position'
+ end
end
diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb
index e80b3f2b54a..ecf37bae6b3 100644
--- a/app/helpers/storage_helper.rb
+++ b/app/helpers/storage_helper.rb
@@ -12,10 +12,11 @@ module StorageHelper
def storage_counters_details(statistics)
counters = {
counter_repositories: storage_counter(statistics.repository_size),
+ counter_wikis: storage_counter(statistics.wiki_size),
counter_build_artifacts: storage_counter(statistics.build_artifacts_size),
counter_lfs_objects: storage_counter(statistics.lfs_objects_size)
}
- _("%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS") % counters
+ _("%{counter_repositories} repositories, %{counter_wikis} wikis, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS") % counters
end
end
diff --git a/app/helpers/tracking_helper.rb b/app/helpers/tracking_helper.rb
new file mode 100644
index 00000000000..51ea79d1ddd
--- /dev/null
+++ b/app/helpers/tracking_helper.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module TrackingHelper
+ def tracking_attrs(label, event, property)
+ {} # CE has no tracking features
+ end
+end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index 9deb783d289..b318b27992a 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -165,8 +165,46 @@ module VisibilityLevelHelper
!form_model.visibility_level_allowed?(level)
end
+ # Visibility level can be restricted in two ways:
+ #
+ # 1. The group permissions (e.g. a subgroup is private, which requires
+ # all projects to be private)
+ # 2. The global allowed visibility settings, set by the admin
+ def selected_visibility_level(form_model, requested_level)
+ requested_level =
+ if requested_level.present?
+ requested_level.to_i
+ else
+ default_project_visibility
+ end
+
+ [requested_level, max_allowed_visibility_level(form_model)].min
+ end
+
private
+ def max_allowed_visibility_level(form_model)
+ # First obtain the maximum visibility for the project or group
+ current_level = max_allowed_visibility_level_by_model(form_model)
+
+ # Now limit this by the global setting
+ Gitlab::VisibilityLevel.closest_allowed_level(current_level)
+ end
+
+ def max_allowed_visibility_level_by_model(form_model)
+ current_level = Gitlab::VisibilityLevel::PRIVATE
+
+ Gitlab::VisibilityLevel.values.sort.each do |value|
+ if disallowed_visibility_level?(form_model, value)
+ break
+ else
+ current_level = value
+ end
+ end
+
+ current_level
+ end
+
def visibility_level_errors_for_group(group, level_name)
group_name = link_to group.name, group_path(group)
change_visiblity = link_to 'change the visibility', edit_group_path(group)
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index 2b046d17122..f3a3203f7ad 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -83,7 +83,7 @@ module Emails
@project = Project.find(project_id)
@results = results
- mail(to: @user.notification_email, subject: subject('Imported issues')) do |format|
+ mail(to: recipient(@user.id, @project.group), subject: subject('Imported issues')) do |format|
format.html { render layout: 'mailer' }
format.text { render layout: 'mailer' }
end
@@ -103,7 +103,7 @@ module Emails
def issue_thread_options(sender_id, recipient_id, reason)
{
from: sender(sender_id),
- to: recipient(recipient_id),
+ to: recipient(recipient_id, @project.group),
subject: subject("#{@issue.title} (##{@issue.iid})"),
'X-GitLab-NotificationReason' => reason
}
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
index 91dfdf58982..2bfa59774d7 100644
--- a/app/mailers/emails/members.rb
+++ b/app/mailers/emails/members.rb
@@ -58,9 +58,8 @@ module Emails
@member_source_type = member_source_type
@member_source = member_source_class.find(source_id)
@invite_email = invite_email
- inviter = User.find(created_by_id)
- mail(to: inviter.notification_email,
+ mail(to: recipient(created_by_id, member_source_type == 'Project' ? @member_source.group : @member_source),
subject: subject('Invitation declined'))
end
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index fc6ed695675..864f9e2975a 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -110,7 +110,7 @@ module Emails
def merge_request_thread_options(sender_id, recipient_id, reason = nil)
{
from: sender(sender_id),
- to: recipient(recipient_id),
+ to: recipient(recipient_id, @project.group),
subject: subject("#{@merge_request.title} (#{@merge_request.to_reference})"),
'X-GitLab-NotificationReason' => reason
}
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 1b3c1f9a8a9..70d296fe3b8 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -51,7 +51,7 @@ module Emails
def note_thread_options(recipient_id)
{
from: sender(@note.author_id),
- to: recipient(recipient_id),
+ to: recipient(recipient_id, @group),
subject: subject("#{@note.noteable.title} (#{@note.noteable.reference_link_text})")
}
end
diff --git a/app/mailers/emails/pages_domains.rb b/app/mailers/emails/pages_domains.rb
index ce449237ef6..2d390666f65 100644
--- a/app/mailers/emails/pages_domains.rb
+++ b/app/mailers/emails/pages_domains.rb
@@ -7,7 +7,7 @@ module Emails
@project = domain.project
mail(
- to: recipient.notification_email,
+ to: recipient(recipient.id, @project.group),
subject: subject("GitLab Pages domain '#{domain.domain}' has been enabled")
)
end
@@ -17,7 +17,7 @@ module Emails
@project = domain.project
mail(
- to: recipient.notification_email,
+ to: recipient(recipient.id, @project.group),
subject: subject("GitLab Pages domain '#{domain.domain}' has been disabled")
)
end
@@ -27,7 +27,7 @@ module Emails
@project = domain.project
mail(
- to: recipient.notification_email,
+ to: recipient(recipient.id, @project.group),
subject: subject("Verification succeeded for GitLab Pages domain '#{domain.domain}'")
)
end
@@ -37,7 +37,7 @@ module Emails
@project = domain.project
mail(
- to: recipient.notification_email,
+ to: recipient(recipient.id, @project.group),
subject: subject("ACTION REQUIRED: Verification failed for GitLab Pages domain '#{domain.domain}'")
)
end
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index 2500622caa7..f81f76f67f7 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -7,20 +7,20 @@ module Emails
@project = Project.find project_id
@target_url = project_url(@project)
@old_path_with_namespace = old_path_with_namespace
- mail(to: @user.notification_email,
+ mail(to: recipient(user_id, @project.group),
subject: subject("Project was moved"))
end
def project_was_exported_email(current_user, project)
@project = project
- mail(to: current_user.notification_email,
+ mail(to: recipient(current_user.id, project.group),
subject: subject("Project was exported"))
end
def project_was_not_exported_email(current_user, project, errors)
@project = project
@errors = errors
- mail(to: current_user.notification_email,
+ mail(to: recipient(current_user.id, @project.group),
subject: subject("Project export error"))
end
@@ -28,7 +28,7 @@ module Emails
@project = project
@user = user
- mail(to: user.notification_email, subject: subject("Project cleanup has completed"))
+ mail(to: recipient(user.id, project.group), subject: subject("Project cleanup has completed"))
end
def repository_cleanup_failure_email(project, user, error)
@@ -36,7 +36,7 @@ module Emails
@user = user
@error = error
- mail(to: user.notification_email, subject: subject("Project cleanup failure"))
+ mail(to: recipient(user.id, project.group), subject: subject("Project cleanup failure"))
end
def repository_push_email(project_id, opts = {})
diff --git a/app/mailers/emails/remote_mirrors.rb b/app/mailers/emails/remote_mirrors.rb
index 2018eb7260b..2d8137843ec 100644
--- a/app/mailers/emails/remote_mirrors.rb
+++ b/app/mailers/emails/remote_mirrors.rb
@@ -6,7 +6,7 @@ module Emails
@remote_mirror = RemoteMirrorFinder.new(id: remote_mirror_id).execute
@project = @remote_mirror.project
- mail(to: recipient(recipient_id), subject: subject('Remote mirror update failed'))
+ mail(to: recipient(recipient_id, @project.group), subject: subject('Remote mirror update failed'))
end
end
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 0b740809f30..576caea4c10 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -73,12 +73,22 @@ class Notify < BaseMailer
# Look up a User by their ID and return their email address
#
- # recipient_id - User ID
+ # recipient_id - User ID
+ # notification_group - The parent group of the notification
#
# Returns a String containing the User's email address.
- def recipient(recipient_id)
+ def recipient(recipient_id, notification_group = nil)
@current_user = User.find(recipient_id)
- @current_user.notification_email
+ group_notification_email = nil
+
+ if notification_group
+ notification_settings = notification_group.notification_settings_for(@current_user, hierarchy_order: :asc)
+ group_notification_email = notification_settings.find { |n| n.notification_email.present? }&.notification_email
+ end
+
+ # Return group-specific email address if present, otherwise return global
+ # email address
+ group_notification_email || @current_user.notification_email
end
# Formats arguments into a String suitable for use as an email subject
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index e51619b0f9c..904d650ef96 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -21,6 +21,7 @@ module ApplicationSettingImplementation
after_sign_up_text: nil,
akismet_enabled: false,
allow_local_requests_from_hooks_and_services: false,
+ dns_rebinding_protection_enabled: true,
authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand
container_registry_token_expire_delay: 5,
default_artifacts_expire_in: '30 days',
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 56786fae6ea..89cc082d0bc 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -2,7 +2,6 @@
module Ci
class Build < CommitStatus
- prepend ArtifactMigratable
include Ci::Processable
include Ci::Metadatable
include Ci::Contextable
@@ -16,11 +15,15 @@ module Ci
include Gitlab::Utils::StrongMemoize
include Deployable
include HasRef
- include UpdateProjectStatistics
BuildArchivedError = Class.new(StandardError)
ignore_column :commands
+ ignore_column :artifacts_file
+ ignore_column :artifacts_metadata
+ ignore_column :artifacts_file_store
+ ignore_column :artifacts_metadata_store
+ ignore_column :artifacts_size
belongs_to :project, inverse_of: :builds
belongs_to :runner
@@ -83,13 +86,7 @@ module Ci
scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) }
scope :with_artifacts_archive, ->() do
- if Feature.enabled?(:ci_enable_legacy_artifacts)
- where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)',
- '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
- else
- where('EXISTS (?)',
- Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
- end
+ where('EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
end
scope :with_existing_job_artifacts, ->(query) do
@@ -111,8 +108,8 @@ module Ci
scope :eager_load_job_artifacts, -> { includes(:job_artifacts) }
- scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
- scope :with_archived_trace_stored_locally, -> { with_archived_trace.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
+ scope :with_artifacts_stored_locally, -> { with_existing_job_artifacts(Ci::JobArtifact.archive.with_files_stored_locally) }
+ scope :with_archived_trace_stored_locally, -> { with_existing_job_artifacts(Ci::JobArtifact.trace.with_files_stored_locally) }
scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
@@ -142,16 +139,10 @@ module Ci
scope :queued_before, ->(time) { where(arel_table[:queued_at].lt(time)) }
- ##
- # TODO: Remove these mounters when we remove :ci_enable_legacy_artifacts feature flag
- mount_uploader :legacy_artifacts_file, LegacyArtifactUploader, mount_on: :artifacts_file
- mount_uploader :legacy_artifacts_metadata, LegacyArtifactUploader, mount_on: :artifacts_metadata
-
acts_as_taggable
add_authentication_token_field :token, encrypted: :optional
- before_save :update_artifacts_size, if: :artifacts_file_changed?
before_save :ensure_token
before_destroy { unscoped_project }
@@ -159,8 +150,6 @@ module Ci
run_after_commit { BuildHooksWorker.perform_async(build.id) }
end
- update_project_statistics stat: :build_artifacts_size, attribute: :artifacts_size
-
class << self
# This is needed for url_for to work,
# as the controller is JobsController
@@ -354,7 +343,7 @@ module Ci
end
def retryable?
- !archived? && (success? || failed?)
+ !archived? && (success? || failed? || canceled?)
end
def retries_count
@@ -542,6 +531,26 @@ module Ci
trace.exist?
end
+ def artifacts_file
+ job_artifacts_archive&.file
+ end
+
+ def artifacts_size
+ job_artifacts_archive&.size
+ end
+
+ def artifacts_metadata
+ job_artifacts_metadata&.file
+ end
+
+ def artifacts?
+ !artifacts_expired? && artifacts_file&.exists?
+ end
+
+ def artifacts_metadata?
+ artifacts? && artifacts_metadata&.exists?
+ end
+
def has_job_artifacts?
job_artifacts.any?
end
@@ -610,14 +619,12 @@ module Ci
# and use that for `ExpireBuildInstanceArtifactsWorker`?
def erase_erasable_artifacts!
job_artifacts.erasable.destroy_all # rubocop: disable DestroyAll
- erase_old_artifacts!
end
def erase(opts = {})
return false unless erasable?
job_artifacts.destroy_all # rubocop: disable DestroyAll
- erase_old_artifacts!
erase_trace!
update_erased!(opts[:erased_by])
end
@@ -655,10 +662,7 @@ module Ci
end
def artifacts_file_for_type(type)
- file = job_artifacts.find_by(file_type: Ci::JobArtifact.file_types[type])&.file
- # TODO: to be removed once legacy artifacts is removed
- file ||= legacy_artifacts_file if type == :archive
- file
+ job_artifacts.find_by(file_type: Ci::JobArtifact.file_types[type])&.file
end
def coverage_regex
@@ -784,13 +788,6 @@ module Ci
private
- def erase_old_artifacts!
- # TODO: To be removed once we get rid of ci_enable_legacy_artifacts feature flag
- remove_artifacts_file!
- remove_artifacts_metadata!
- save
- end
-
def successful_deployment_status
if deployment&.last?
:last
@@ -812,10 +809,6 @@ module Ci
job_artifacts.select { |artifact| artifact.file_type.in?(report_types) }
end
- def update_artifacts_size
- self.artifacts_size = legacy_artifacts_file&.size
- end
-
def erase_trace!
trace.erase!
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 0dbeab30498..f80e98e5bca 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -59,7 +59,7 @@ module Ci
validate :valid_file_format?, unless: :trace?, on: :create
before_save :set_size, if: :file_changed?
- update_project_statistics stat: :build_artifacts_size
+ update_project_statistics project_statistics_name: :build_artifacts_size
after_save :update_file_store, if: :saved_change_to_file?
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 80401ca0a1e..3727a9861aa 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -166,6 +166,16 @@ module Ci
end
end
+ after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline|
+ pipeline.run_after_commit do
+ pipeline.all_merge_requests.each do |merge_request|
+ next unless merge_request.auto_merge_enabled?
+
+ AutoMergeProcessWorker.perform_async(merge_request.id)
+ end
+ end
+ end
+
after_transition any => [:success, :failed] do |pipeline|
pipeline.run_after_commit do
PipelineNotificationWorker.perform_async(pipeline.id)
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index c0a0ca9acf6..c40ad39be61 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -27,9 +27,13 @@ module Ci
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
+ scope :runnable_schedules, -> { active.where("next_run_at < ?", Time.now) }
+ scope :preloaded, -> { preload(:owner, :project) }
accepts_nested_attributes_for :variables, allow_destroy: true
+ alias_attribute :real_next_run, :next_run_at
+
def owned_by?(current_user)
owner == current_user
end
@@ -46,8 +50,14 @@ module Ci
update_attribute(:active, false)
end
+ ##
+ # The `next_run_at` column is set to the actual execution date of `PipelineScheduleWorker`.
+ # This way, a schedule like `*/1 * * * *` won't be triggered in a short interval
+ # when PipelineScheduleWorker runs irregularly by Sidekiq Memory Killer.
def set_next_run_at
- self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now)
+ self.next_run_at = Gitlab::Ci::CronParser.new(Settings.cron_jobs['pipeline_schedule_worker']['cron'],
+ Time.zone.name)
+ .next_time_from(ideal_next_run_at)
end
def schedule_next_run!
@@ -56,15 +66,14 @@ module Ci
update_attribute(:next_run_at, nil) # update without validation
end
- def real_next_run(
- worker_cron: Settings.cron_jobs['pipeline_schedule_worker']['cron'],
- worker_time_zone: Time.zone.name)
- Gitlab::Ci::CronParser.new(worker_cron, worker_time_zone)
- .next_time_from(next_run_at)
- end
-
def job_variables
variables&.map(&:to_runner_variable) || []
end
+
+ private
+
+ def ideal_next_run_at
+ Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now)
+ end
end
end
diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb
index bd9c453e2a4..4aaa1f941e5 100644
--- a/app/models/clusters/applications/jupyter.rb
+++ b/app/models/clusters/applications/jupyter.rb
@@ -61,6 +61,10 @@ module Clusters
"http://#{hostname}/hub/oauth_callback"
end
+ def oauth_scopes
+ 'api read_repository write_repository'
+ end
+
private
def specification
@@ -94,7 +98,8 @@ module Clusters
},
"singleuser" => {
"extraEnv" => {
- "GITLAB_CLUSTER_ID" => cluster.id.to_s
+ "GITLAB_CLUSTER_ID" => cluster.id.to_s,
+ "GITLAB_HOST" => gitlab_host
}
}
}
@@ -112,6 +117,10 @@ module Clusters
Gitlab.config.gitlab.url
end
+ def gitlab_host
+ Gitlab.config.gitlab.host
+ end
+
def content_values
YAML.load_file(chart_values_file).deep_merge!(specification)
end
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index 9fbf5d8af04..d5a3bd62e3d 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -15,9 +15,6 @@ module Clusters
include ::Clusters::Concerns::ApplicationVersion
include ::Clusters::Concerns::ApplicationData
include AfterCommitQueue
- include ReactiveCaching
-
- self.reactive_cache_key = ->(knative) { [knative.class.model_name.singular, knative.id] }
def set_initial_status
return unless not_installable?
@@ -41,8 +38,6 @@ module Clusters
scope :for_cluster, -> (cluster) { where(cluster: cluster) }
- after_save :clear_reactive_cache!
-
def chart
'knative/knative'
end
@@ -77,55 +72,12 @@ module Clusters
ClusterWaitForIngressIpAddressWorker.perform_async(name, id)
end
- def client
- cluster.kubeclient.knative_client
- end
-
- def services
- with_reactive_cache do |data|
- data[:services]
- end
- end
-
- def calculate_reactive_cache
- { services: read_services, pods: read_pods }
- end
-
def ingress_service
cluster.kubeclient.get_service('istio-ingressgateway', 'istio-system')
end
- def services_for(ns: namespace)
- return [] unless services
- return [] unless ns
-
- services.select do |service|
- service.dig('metadata', 'namespace') == ns
- end
- end
-
- def service_pod_details(ns, service)
- with_reactive_cache do |data|
- data[:pods].select { |pod| filter_pods(pod, ns, service) }
- end
- end
-
private
- def read_pods
- cluster.kubeclient.core_client.get_pods.as_json
- end
-
- def filter_pods(pod, namespace, service)
- pod["metadata"]["namespace"] == namespace && pod["metadata"]["labels"]["serving.knative.dev/service"] == service
- end
-
- def read_services
- client.get_services.as_json
- rescue Kubeclient::ResourceNotFoundError
- []
- end
-
def install_knative_metrics
["kubectl apply -f #{METRICS_CONFIG}"] if cluster.application_prometheus_available?
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 0dff91c3fe2..db7fd8524c2 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
- VERSION = '0.5.1'.freeze
+ VERSION = '0.5.2'.freeze
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 57a1e461b2d..e1d6b2a802b 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -223,6 +223,10 @@ module Clusters
end
end
+ def knative_services_finder(project)
+ @knative_services_finder ||= KnativeServicesFinder.new(self, project)
+ end
+
private
def instance_domain
diff --git a/app/models/commit.rb b/app/models/commit.rb
index f412d252e5c..fa0bf36ba49 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -13,6 +13,7 @@ class Commit
include StaticModel
include Presentable
include ::Gitlab::Utils::StrongMemoize
+ include CacheMarkdownField
attr_mentionable :safe_message, pipeline: :single_line
@@ -37,13 +38,9 @@ class Commit
# Used by GFM to match and present link extensions on node texts and hrefs.
LINK_EXTENSION_PATTERN = /(patch)/.freeze
- def banzai_render_context(field)
- pipeline = field == :description ? :commit_description : :single_line
- context = { pipeline: pipeline, project: self.project }
- context[:author] = self.author if self.author
-
- context
- end
+ cache_markdown_field :title, pipeline: :single_line
+ cache_markdown_field :full_title, pipeline: :single_line
+ cache_markdown_field :description, pipeline: :commit_description
class << self
def decorate(commits, project)
diff --git a/app/models/concerns/artifact_migratable.rb b/app/models/concerns/artifact_migratable.rb
deleted file mode 100644
index 7c9f579b480..00000000000
--- a/app/models/concerns/artifact_migratable.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-# Adapter class to unify the interface between mounted uploaders and the
-# Ci::Artifact model
-# Meant to be prepended so the interface can stay the same
-module ArtifactMigratable
- def artifacts_file
- job_artifacts_archive&.file || legacy_artifacts_file
- end
-
- def artifacts_metadata
- job_artifacts_metadata&.file || legacy_artifacts_metadata
- end
-
- def artifacts?
- !artifacts_expired? && artifacts_file&.exists?
- end
-
- def artifacts_metadata?
- artifacts? && artifacts_metadata.exists?
- end
-
- def artifacts_file_changed?
- job_artifacts_archive&.file_changed? || attribute_changed?(:artifacts_file)
- end
-
- def remove_artifacts_file!
- if job_artifacts_archive
- job_artifacts_archive.destroy
- else
- remove_legacy_artifacts_file!
- end
- end
-
- def remove_artifacts_metadata!
- if job_artifacts_metadata
- job_artifacts_metadata.destroy
- else
- remove_legacy_artifacts_metadata!
- end
- end
-
- def artifacts_size
- read_attribute(:artifacts_size).to_i + job_artifacts.sum(:size).to_i
- end
-
- def legacy_artifacts_file
- return unless Feature.enabled?(:ci_enable_legacy_artifacts)
-
- super
- end
-
- def legacy_artifacts_metadata
- return unless Feature.enabled?(:ci_enable_legacy_artifacts)
-
- super
- end
-end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index f90cd1ea690..42203a5f214 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -13,43 +13,9 @@
module CacheMarkdownField
extend ActiveSupport::Concern
- # Increment this number every time the renderer changes its output
- CACHE_COMMONMARK_VERSION_START = 10
- CACHE_COMMONMARK_VERSION = 16
-
# changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze
- # Knows about the relationship between markdown and html field names, and
- # stores the rendering contexts for the latter
- class FieldData
- def initialize
- @data = {}
- end
-
- delegate :[], :[]=, to: :@data
-
- def markdown_fields
- @data.keys
- end
-
- def html_field(markdown_field)
- "#{markdown_field}_html"
- end
-
- def html_fields
- markdown_fields.map { |field| html_field(field) }
- end
-
- def html_fields_whitelisted
- markdown_fields.each_with_object([]) do |field, fields|
- if @data[field].fetch(:whitelisted, false)
- fields << html_field(field)
- end
- end
- end
- end
-
def skip_project_check?
false
end
@@ -85,24 +51,22 @@ module CacheMarkdownField
end.to_h
updates['cached_markdown_version'] = latest_cached_markdown_version
- updates.each {|html_field, data| write_attribute(html_field, data) }
+ updates.each { |field, data| write_markdown_field(field, data) }
end
def refresh_markdown_cache!
updates = refresh_markdown_cache
- return unless persisted? && Gitlab::Database.read_write?
-
- update_columns(updates)
+ save_markdown(updates)
end
def cached_html_up_to_date?(markdown_field)
- html_field = cached_markdown_fields.html_field(markdown_field)
+ return false if cached_html_for(markdown_field).nil? && __send__(markdown_field).present? # rubocop:disable GitlabSecurity/PublicSend
- return false if cached_html_for(markdown_field).nil? && !__send__(markdown_field).nil? # rubocop:disable GitlabSecurity/PublicSend
+ html_field = cached_markdown_fields.html_field(markdown_field)
- markdown_changed = attribute_changed?(markdown_field) || false
- html_changed = attribute_changed?(html_field) || false
+ markdown_changed = markdown_field_changed?(markdown_field)
+ html_changed = markdown_field_changed?(html_field)
latest_cached_markdown_version == cached_markdown_version &&
(html_changed || markdown_changed == html_changed)
@@ -117,21 +81,21 @@ module CacheMarkdownField
end
def cached_html_for(markdown_field)
- raise ArgumentError.new("Unknown field: #{field}") unless
+ raise ArgumentError.new("Unknown field: #{markdown_field}") unless
cached_markdown_fields.markdown_fields.include?(markdown_field)
__send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend
end
def latest_cached_markdown_version
- @latest_cached_markdown_version ||= (CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16) | local_version
+ @latest_cached_markdown_version ||= (Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16) | local_version
end
def local_version
# because local_markdown_version is stored in application_settings which
# uses cached_markdown_version too, we check explicitly to avoid
# endless loop
- return local_markdown_version if has_attribute?(:local_markdown_version)
+ return local_markdown_version if respond_to?(:has_attribute?) && has_attribute?(:local_markdown_version)
settings = Gitlab::CurrentSettings.current_application_settings
@@ -150,32 +114,14 @@ module CacheMarkdownField
included do
cattr_reader :cached_markdown_fields do
- FieldData.new
+ Gitlab::MarkdownCache::FieldData.new
end
- # Always exclude _html fields from attributes (including serialization).
- # They contain unredacted HTML, which would be a security issue
- alias_method :attributes_before_markdown_cache, :attributes
- def attributes
- attrs = attributes_before_markdown_cache
- html_fields = cached_markdown_fields.html_fields
- whitelisted = cached_markdown_fields.html_fields_whitelisted
- exclude_fields = html_fields - whitelisted
-
- exclude_fields.each do |field|
- attrs.delete(field)
- end
-
- if whitelisted.empty?
- attrs.delete('cached_markdown_version')
- end
-
- attrs
+ if self < ActiveRecord::Base
+ include Gitlab::MarkdownCache::ActiveRecord::Extension
+ else
+ prepend Gitlab::MarkdownCache::Redis::Extension
end
-
- # Using before_update here conflicts with elasticsearch-model somehow
- before_create :refresh_markdown_cache, if: :invalidated_markdown_cache?
- before_update :refresh_markdown_cache, if: :invalidated_markdown_cache?
end
class_methods do
@@ -193,10 +139,8 @@ module CacheMarkdownField
# The HTML becomes invalid if any dependent fields change. For now, assume
# author and project invalidate the cache in all circumstances.
define_method(invalidation_method) do
- changed_fields = changed_attributes.keys
- invalidations = changed_fields & [markdown_field.to_s, *INVALIDATED_BY]
- invalidations.delete(markdown_field.to_s) if changed_fields.include?("#{markdown_field}_html")
-
+ invalidations = changed_markdown_fields & [markdown_field.to_s, *INVALIDATED_BY]
+ invalidations.delete(markdown_field.to_s) if changed_markdown_fields.include?("#{markdown_field}_html")
!invalidations.empty? || !cached_html_up_to_date?(markdown_field)
end
end
diff --git a/app/models/concerns/maskable.rb b/app/models/concerns/maskable.rb
index 2943872ffab..e0f2c41b836 100644
--- a/app/models/concerns/maskable.rb
+++ b/app/models/concerns/maskable.rb
@@ -7,9 +7,9 @@ module Maskable
# * No escape characters
# * No variables
# * No spaces
- # * Minimal length of 8 characters
+ # * Minimal length of 8 characters from the Base64 alphabets (RFC4648)
# * Absolutely no fun is allowed
- REGEX = /\A\w{8,}\z/.freeze
+ REGEX = /\A[a-zA-Z0-9_+=\/-]{8,}\z/.freeze
included do
validates :masked, inclusion: { in: [true, false] }
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index e65bbb8ca07..3deb86da6cf 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -1,28 +1,20 @@
# frozen_string_literal: true
module Milestoneish
- def closed_items_count(user)
- memoize_per_user(user, :closed_items_count) do
- (count_issues_by_state(user)['closed'] || 0) + merge_requests.closed_and_merged.size
- end
- end
-
- def total_items_count(user)
- memoize_per_user(user, :total_items_count) do
- total_issues_count(user) + merge_requests.size
- end
- end
-
def total_issues_count(user)
count_issues_by_state(user).values.sum
end
+ def closed_issues_count(user)
+ count_issues_by_state(user)['closed'].to_i
+ end
+
def complete?(user)
- total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user)
+ total_issues_count(user) > 0 && total_issues_count(user) == closed_issues_count(user)
end
def percent_complete(user)
- ((closed_items_count(user) * 100) / total_items_count(user)).abs
+ closed_issues_count(user) * 100 / total_issues_count(user)
rescue ZeroDivisionError
0
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index bfd0c36942b..4b428b0af83 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -3,14 +3,16 @@
module Noteable
extend ActiveSupport::Concern
- # `Noteable` class names that support resolvable notes.
- RESOLVABLE_TYPES = %w(MergeRequest).freeze
-
class_methods do
# `Noteable` class names that support replying to individual notes.
def replyable_types
%w(Issue MergeRequest)
end
+
+ # `Noteable` class names that support resolvable notes.
+ def resolvable_types
+ %w(MergeRequest)
+ end
end
# The timestamp of the note (e.g. the :created_at or :updated_at attribute if provided via
@@ -36,7 +38,7 @@ module Noteable
end
def supports_resolvable_notes?
- RESOLVABLE_TYPES.include?(base_class_name)
+ self.class.resolvable_types.include?(base_class_name)
end
def supports_discussions?
@@ -131,3 +133,5 @@ module Noteable
)
end
end
+
+Noteable.extend(Noteable::ClassMethods)
diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb
index 58143a32fdc..4a506146de3 100644
--- a/app/models/concerns/referable.rb
+++ b/app/models/concerns/referable.rb
@@ -73,6 +73,7 @@ module Referable
(?<url>
#{Regexp.escape(Gitlab.config.gitlab.url)}
\/#{Project.reference_pattern}
+ (?:\/\-)?
\/#{Regexp.escape(route)}
\/#{pattern}
(?<path>
diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb
index 16ea330701d..2d2d5fb7168 100644
--- a/app/models/concerns/resolvable_note.rb
+++ b/app/models/concerns/resolvable_note.rb
@@ -12,7 +12,7 @@ module ResolvableNote
validates :resolved_by, presence: true, if: :resolved?
# Keep this scope in sync with `#potentially_resolvable?`
- scope :potentially_resolvable, -> { where(type: RESOLVABLE_TYPES).where(noteable_type: Noteable::RESOLVABLE_TYPES) }
+ scope :potentially_resolvable, -> { where(type: RESOLVABLE_TYPES).where(noteable_type: Noteable.resolvable_types) }
# Keep this scope in sync with `#resolvable?`
scope :resolvable, -> { potentially_resolvable.user }
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index 2f0e078c807..b42adad94ba 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -75,4 +75,11 @@ module Taskable
def task_status_short
task_status(short: true)
end
+
+ def task_completion_status
+ @task_completion_status ||= {
+ count: tasks.summary.item_count,
+ completed_count: tasks.summary.complete_count
+ }
+ end
end
diff --git a/app/models/concerns/update_project_statistics.rb b/app/models/concerns/update_project_statistics.rb
index 67e1f0ec930..1f881249322 100644
--- a/app/models/concerns/update_project_statistics.rb
+++ b/app/models/concerns/update_project_statistics.rb
@@ -5,43 +5,47 @@
# It deals with `ProjectStatistics.increment_statistic` making sure not to update statistics on a cascade delete from the
# project, and keeping track of value deltas on each save. It updates the DB only when a change is needed.
#
-# How to use
-# - Invoke `update_project_statistics stat: :a_project_statistics_column, attribute: :an_attr_to_track` in a model class body.
+# Example:
#
-# Expectation
-# - `attribute` must be an ActiveRecord attribute
+# module Ci
+# class JobArtifact < ApplicationRecord
+# include UpdateProjectStatistics
+#
+# update_project_statistics project_statistics_name: :build_artifacts_size
+# end
+# end
+#
+# Expectation:
+#
+# - `statistic_attribute` must be an ActiveRecord attribute
# - The model must implement `project` and `project_id`. i.e. direct Project relationship or delegation
+#
module UpdateProjectStatistics
extend ActiveSupport::Concern
class_methods do
- attr_reader :statistic_name, :statistic_attribute
+ attr_reader :project_statistics_name, :statistic_attribute
- # Configure the model to update +stat+ on ProjectStatistics when +attribute+ changes
+ # Configure the model to update `project_statistics_name` on ProjectStatistics,
+ # when `statistic_attribute` changes
+ #
+ # - project_statistics_name: A column of `ProjectStatistics` to update
+ # - statistic_attribute: An attribute of the current model, default to `size`
#
- # +stat+:: a column of ProjectStatistics to update
- # +attribute+:: an attribute of the current model, default to +:size+
- def update_project_statistics(stat:, attribute: :size)
- @statistic_name = stat
- @statistic_attribute = attribute
+ def update_project_statistics(project_statistics_name:, statistic_attribute: :size)
+ @project_statistics_name = project_statistics_name
+ @statistic_attribute = statistic_attribute
after_save(:update_project_statistics_after_save, if: :update_project_statistics_attribute_changed?)
after_destroy(:update_project_statistics_after_destroy, unless: :project_destroyed?)
end
+
private :update_project_statistics
end
included do
private
- def project_destroyed?
- project.pending_delete?
- end
-
- def update_project_statistics_attribute_changed?
- saved_change_to_attribute?(self.class.statistic_attribute)
- end
-
def update_project_statistics_after_save
attr = self.class.statistic_attribute
delta = read_attribute(attr).to_i - attribute_before_last_save(attr).to_i
@@ -49,12 +53,20 @@ module UpdateProjectStatistics
update_project_statistics(delta)
end
+ def update_project_statistics_attribute_changed?
+ saved_change_to_attribute?(self.class.statistic_attribute)
+ end
+
def update_project_statistics_after_destroy
update_project_statistics(-read_attribute(self.class.statistic_attribute).to_i)
end
+ def project_destroyed?
+ project.pending_delete?
+ end
+
def update_project_statistics(delta)
- ProjectStatistics.increment_statistic(project_id, self.class.statistic_name, delta)
+ ProjectStatistics.increment_statistic(project_id, self.class.project_statistics_name, delta)
end
end
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index feabea9b8ba..f75c32633b1 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -15,7 +15,9 @@ class DiffNote < Note
validates :original_position, presence: true
validates :position, presence: true
validates :line_code, presence: true, line_code: true, if: :on_text?
- validates :noteable_type, inclusion: { in: noteable_types }
+ # We need to evaluate the `noteable` types when running the validation since
+ # EE might have added a type when the module was prepended
+ validates :noteable_type, inclusion: { in: -> (_note) { noteable_types } }
validate :positions_complete
validate :verify_supported
validate :diff_refs_match_commit, if: :for_commit?
@@ -44,7 +46,7 @@ class DiffNote < Note
# Returns the diff file from `position`
def latest_diff_file
strong_memoize(:latest_diff_file) do
- position.diff_file(project.repository)
+ position.diff_file(repository)
end
end
@@ -75,7 +77,7 @@ class DiffNote < Note
end
def supports_suggestion?
- return false unless noteable.supports_suggestion? && on_text?
+ return false unless noteable&.supports_suggestion? && on_text?
# We don't want to trigger side-effects of `diff_file` call.
return false unless file = latest_diff_file
return false unless line = file.line_for_position(self.position)
@@ -88,7 +90,7 @@ class DiffNote < Note
end
def banzai_render_context(field)
- super.merge(project: project, suggestions_filter_enabled: supports_suggestion?)
+ super.merge(suggestions_filter_enabled: true)
end
private
@@ -111,7 +113,7 @@ class DiffNote < Note
if note_diff_file
diff = Gitlab::Git::Diff.new(note_diff_file.to_hash)
Gitlab::Diff::File.new(diff,
- repository: project.repository,
+ repository: repository,
diff_refs: original_position.diff_refs)
elsif created_at_diff?(noteable.diff_refs)
# We're able to use the already persisted diffs (Postgres) if we're
@@ -122,7 +124,7 @@ class DiffNote < Note
# `Diff::FileCollection::MergeRequestDiff`.
noteable.diffs(original_position.diff_options).diff_files.first
else
- original_position.diff_file(self.project.repository)
+ original_position.diff_file(repository)
end
# Since persisted diff files already have its content "unfolded"
@@ -137,7 +139,7 @@ class DiffNote < Note
end
def set_line_code
- self.line_code = self.position.line_code(self.project.repository)
+ self.line_code = self.position.line_code(repository)
end
def verify_supported
@@ -171,6 +173,10 @@ class DiffNote < Note
shas << self.position.head_sha
end
- project.repository.keep_around(*shas)
+ repository.keep_around(*shas)
+ end
+
+ def repository
+ noteable.respond_to?(:repository) ? noteable.repository : project.repository
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 53331a19776..cdb4e6e87f6 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -126,10 +126,20 @@ class Group < Namespace
# Overrides notification_settings has_many association
# This allows to apply notification settings from parent groups
# to child groups and projects.
- def notification_settings
+ def notification_settings(hierarchy_order: nil)
source_type = self.class.base_class.name
+ settings = NotificationSetting.where(source_type: source_type, source_id: self_and_ancestors_ids)
- NotificationSetting.where(source_type: source_type, source_id: self_and_ancestors_ids)
+ return settings unless hierarchy_order && self_and_ancestors_ids.length > 1
+
+ settings
+ .joins("LEFT JOIN (#{self_and_ancestors(hierarchy_order: hierarchy_order).to_sql}) AS ordered_groups ON notification_settings.source_id = ordered_groups.id")
+ .select('notification_settings.*, ordered_groups.depth AS depth')
+ .order("ordered_groups.depth #{hierarchy_order}")
+ end
+
+ def notification_settings_for(user, hierarchy_order: nil)
+ notification_settings(hierarchy_order: hierarchy_order).where(user: user)
end
def to_reference(_from = nil, full: nil)
diff --git a/app/models/issue.rb b/app/models/issue.rb
index eb5544f2a12..6da6fbe55cb 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -58,6 +58,7 @@ class Issue < ApplicationRecord
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
scope :order_closest_future_date, -> { reorder('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC') }
+ scope :order_relative_position_asc, -> { reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')) }
scope :preload_associations, -> { preload(:labels, project: :namespace) }
scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) }
@@ -130,9 +131,10 @@ class Issue < ApplicationRecord
def self.sort_by_attribute(method, excluded_labels: [])
case method.to_s
when 'closest_future_date' then order_closest_future_date
- when 'due_date' then order_due_date_asc
- when 'due_date_asc' then order_due_date_asc
- when 'due_date_desc' then order_due_date_desc
+ when 'due_date' then order_due_date_asc
+ when 'due_date_asc' then order_due_date_asc
+ when 'due_date_desc' then order_due_date_desc
+ when 'relative_position' then order_relative_position_asc
else
super
end
diff --git a/app/models/key.rb b/app/models/key.rb
index b097be8cc89..8aa25924c28 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -59,6 +59,11 @@ class Key < ApplicationRecord
"key-#{id}"
end
+ # EE overrides this
+ def can_delete?
+ true
+ end
+
# rubocop: disable CodeReuse/ServiceClass
def update_last_used_at
Keys::LastUsedService.new(self).execute
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 311ba1ce6bd..4fcaac75655 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -165,7 +165,7 @@ class MergeRequest < ApplicationRecord
validates :source_branch, presence: true
validates :target_project, presence: true
validates :target_branch, presence: true
- validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
+ validates :merge_user, presence: true, if: :auto_merge_enabled?, unless: :importing?
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
validate :validate_fork, unless: :closed_without_fork?
validate :validate_target_project, on: :create
@@ -196,6 +196,7 @@ class MergeRequest < ApplicationRecord
alias_attribute :project, :target_project
alias_attribute :project_id, :target_project_id
+ alias_attribute :auto_merge_enabled, :merge_when_pipeline_succeeds
def self.reference_prefix
'!'
@@ -391,7 +392,7 @@ class MergeRequest < ApplicationRecord
def merge_participants
participants = [author]
- if merge_when_pipeline_succeeds? && !participants.include?(merge_user)
+ if auto_merge_enabled? && !participants.include?(merge_user)
participants << merge_user
end
@@ -588,6 +589,8 @@ class MergeRequest < ApplicationRecord
return
end
+ [:source_branch, :target_branch].each { |attr| validate_branch_name(attr) }
+
if opened?
similar_mrs = target_project
.merge_requests
@@ -608,6 +611,16 @@ class MergeRequest < ApplicationRecord
end
end
+ def validate_branch_name(attr)
+ return unless changes_include?(attr)
+
+ branch = read_attribute(attr)
+
+ return unless branch
+
+ errors.add(attr) unless Gitlab::GitRefValidator.validate_merge_request_branch(branch)
+ end
+
def validate_target_project
return true if target_project.merge_requests_enabled?
@@ -712,19 +725,16 @@ class MergeRequest < ApplicationRecord
MergeRequests::ReloadDiffsService.new(self, current_user).execute
end
- # rubocop: enable CodeReuse/ServiceClass
- def check_if_can_be_merged
- return unless self.class.state_machines[:merge_status].check_state?(merge_status) && Gitlab::Database.read_write?
-
- can_be_merged =
- !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
+ def check_mergeability
+ MergeRequests::MergeabilityCheckService.new(self).execute
+ end
+ # rubocop: enable CodeReuse/ServiceClass
- if can_be_merged
- mark_as_mergeable
- else
- mark_as_unmergeable
- end
+ # Returns boolean indicating the merge_status should be rechecked in order to
+ # switch to either can_be_merged or cannot_be_merged.
+ def recheck_merge_status?
+ self.class.state_machines[:merge_status].check_state?(merge_status)
end
def merge_event
@@ -750,7 +760,7 @@ class MergeRequest < ApplicationRecord
def mergeable?(skip_ci_check: false)
return false unless mergeable_state?(skip_ci_check: skip_ci_check)
- check_if_can_be_merged
+ check_mergeability
can_be_merged? && !should_be_rebased?
end
@@ -765,15 +775,6 @@ class MergeRequest < ApplicationRecord
true
end
- def mergeable_to_ref?
- return false unless mergeable_state?(skip_ci_check: true, skip_discussions_check: true)
-
- # Given the `merge_ref_path` will have the same
- # state the `target_branch` would have. Ideally
- # we need to check if it can be merged to it.
- project.repository.can_be_merged?(diff_head_sha, target_branch)
- end
-
def ff_merge_possible?
project.repository.ancestor?(target_branch_sha, diff_head_sha)
end
@@ -782,7 +783,7 @@ class MergeRequest < ApplicationRecord
project.ff_merge_must_be_possible? && !ff_merge_possible?
end
- def can_cancel_merge_when_pipeline_succeeds?(current_user)
+ def can_cancel_auto_merge?(current_user)
can_be_merged_by?(current_user) || self.author == current_user
end
@@ -801,6 +802,16 @@ class MergeRequest < ApplicationRecord
Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
end
+ def auto_merge_strategy
+ return unless auto_merge_enabled?
+
+ merge_params['auto_merge_strategy'] || AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS
+ end
+
+ def auto_merge_strategy=(strategy)
+ merge_params['auto_merge_strategy'] = strategy
+ end
+
def remove_source_branch?
should_remove_source_branch? || force_remove_source_branch?
end
@@ -973,20 +984,6 @@ class MergeRequest < ApplicationRecord
end
end
- def reset_merge_when_pipeline_succeeds
- return unless merge_when_pipeline_succeeds?
-
- self.merge_when_pipeline_succeeds = false
- self.merge_user = nil
- if merge_params
- merge_params.delete('should_remove_source_branch')
- merge_params.delete('commit_message')
- merge_params.delete('squash_commit_message')
- end
-
- self.save
- end
-
# Return array of possible target branches
# depends on target project of MR
def target_branches
@@ -1090,6 +1087,12 @@ class MergeRequest < ApplicationRecord
target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
end
+ # Returns the current merge-ref HEAD commit.
+ #
+ def merge_ref_head
+ project.repository.commit(merge_ref_path)
+ end
+
def ref_path
"refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 787600569fa..37c129e843a 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -58,6 +58,7 @@ class Milestone < ApplicationRecord
validate :uniqueness_of_title, if: :title_changed?
validate :milestone_type_check
validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
+ validate :dates_within_4_digits
strip_attributes :title
@@ -326,6 +327,16 @@ class Milestone < ApplicationRecord
end
end
+ def dates_within_4_digits
+ if start_date && start_date > Date.new(9999, 12, 31)
+ errors.add(:start_date, _("date must not be after 9999-12-31"))
+ end
+
+ if due_date && due_date > Date.new(9999, 12, 31)
+ errors.add(:due_date, _("date must not be after 9999-12-31"))
+ end
+ end
+
def issues_finder_params
{ project_id: project_id }
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 7393ef4b05c..3c270c7396a 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -76,6 +76,7 @@ class Namespace < ApplicationRecord
'namespaces.*',
'COALESCE(SUM(ps.storage_size), 0) AS storage_size',
'COALESCE(SUM(ps.repository_size), 0) AS repository_size',
+ 'COALESCE(SUM(ps.wiki_size), 0) AS wiki_size',
'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size',
'COALESCE(SUM(ps.packages_size), 0) AS packages_size'
@@ -205,12 +206,12 @@ class Namespace < ApplicationRecord
.ancestors(upto: top, hierarchy_order: hierarchy_order)
end
- def self_and_ancestors
+ def self_and_ancestors(hierarchy_order: nil)
return self.class.where(id: id) unless parent_id
Gitlab::ObjectHierarchy
.new(self.class.where(id: id))
- .base_and_ancestors
+ .base_and_ancestors(hierarchy_order: hierarchy_order)
end
# Returns all the descendants of the current namespace.
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index 6889e0d776b..9b2bbb7eba5 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -50,7 +50,7 @@ class NotificationRecipient
when :mention
@type == :mention
when :participating
- !excluded_participating_action? && %i[participating mention watch].include?(@type)
+ @custom_action == :failed_pipeline || %i[participating mention].include?(@type)
when :custom
custom_enabled? || %i[participating mention].include?(@type)
when :watch
@@ -106,12 +106,6 @@ class NotificationRecipient
NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@custom_action)
end
- def excluded_participating_action?
- return false unless @custom_action
-
- NotificationSetting::EXCLUDED_PARTICIPATING_EVENTS.include?(@custom_action)
- end
-
private
def read_ability
@@ -156,23 +150,11 @@ class NotificationRecipient
# Returns the notification_setting of the lowest group in hierarchy with non global level
def closest_non_global_group_notification_settting
return unless @group
- return if indexed_group_notification_settings.empty?
-
- notification_setting = nil
- @group.self_and_ancestors_ids.each do |id|
- notification_setting = indexed_group_notification_settings[id]
- break if notification_setting
- end
-
- notification_setting
- end
-
- def indexed_group_notification_settings
- strong_memoize(:indexed_group_notification_settings) do
- @group.notification_settings.where(user_id: user.id)
- .where.not(level: NotificationSetting.levels[:global])
- .index_by(&:source_id)
- end
+ @group
+ .notification_settings(hierarchy_order: :asc)
+ .where(user: user)
+ .where.not(level: NotificationSetting.levels[:global])
+ .first
end
end
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 61af5c09ae4..8306b11a7b6 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -54,14 +54,11 @@ class NotificationSetting < ApplicationRecord
self.class.email_events(source)
end
- EXCLUDED_PARTICIPATING_EVENTS = [
- :success_pipeline
- ].freeze
-
EXCLUDED_WATCHER_EVENTS = [
:push_to_merge_request,
- :issue_due
- ].push(*EXCLUDED_PARTICIPATING_EVENTS).freeze
+ :issue_due,
+ :success_pipeline
+ ].freeze
def self.find_or_create_for(source)
setting = find_or_initialize_by(source: source)
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 407d85b1520..524df30289e 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -5,6 +5,7 @@ class PagesDomain < ApplicationRecord
VERIFICATION_THRESHOLD = 3.days.freeze
belongs_to :project
+ has_many :acme_orders, class_name: "PagesDomainAcmeOrder"
validates :domain, hostname: { allow_numeric_hostname: true }
validates :domain, uniqueness: { case_sensitive: false }
@@ -134,6 +135,14 @@ class PagesDomain < ApplicationRecord
"#{VERIFICATION_KEY}=#{verification_code}"
end
+ def certificate=(certificate)
+ super(certificate)
+
+ # set nil, if certificate is nil
+ self.certificate_valid_not_before = x509&.not_before
+ self.certificate_valid_not_after = x509&.not_after
+ end
+
private
def set_verification_code
@@ -186,7 +195,7 @@ class PagesDomain < ApplicationRecord
end
def x509
- return unless certificate
+ return unless certificate.present?
@x509 ||= OpenSSL::X509::Certificate.new(certificate)
rescue OpenSSL::X509::CertificateError
diff --git a/app/models/pages_domain_acme_order.rb b/app/models/pages_domain_acme_order.rb
new file mode 100644
index 00000000000..63d7fbc8206
--- /dev/null
+++ b/app/models/pages_domain_acme_order.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class PagesDomainAcmeOrder < ApplicationRecord
+ belongs_to :pages_domain
+
+ scope :expired, -> { where("expires_at < ?", Time.now) }
+
+ validates :pages_domain, presence: true
+ validates :expires_at, presence: true
+ validates :url, presence: true
+ validates :challenge_token, presence: true
+ validates :challenge_file_content, presence: true
+ validates :private_key, presence: true
+
+ attr_encrypted :private_key,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ algorithm: 'aes-256-gcm',
+ encode: true
+
+ def self.find_by_domain_and_token(domain_name, challenge_token)
+ joins(:pages_domain).find_by(pages_domains: { domain: domain_name }, challenge_token: challenge_token)
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 20895923d3b..e64a4b313aa 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -292,6 +292,7 @@ class Project < ApplicationRecord
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data
accepts_nested_attributes_for :auto_devops, update_only: true
+ accepts_nested_attributes_for :ci_cd_settings, update_only: true
accepts_nested_attributes_for :remote_mirrors,
allow_destroy: true,
@@ -310,6 +311,7 @@ class Project < ApplicationRecord
delegate :root_ancestor, to: :namespace, allow_nil: true
delegate :last_pipeline, to: :commit, allow_nil: true
delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: true
+ delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings
# Validations
validates :creator, presence: true, on: :create
@@ -407,6 +409,7 @@ class Project < ApplicationRecord
scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
+ scope :with_merge_requests_available_for_user, ->(current_user) { with_feature_available_for_user(:merge_requests, current_user) }
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
@@ -597,6 +600,17 @@ class Project < ApplicationRecord
def group_ids
joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
end
+
+ # Returns ids of projects with milestones available for given user
+ #
+ # Used on queries to find milestones which user can see
+ # For example: Milestone.where(project_id: ids_with_milestone_available_for(user))
+ def ids_with_milestone_available_for(user)
+ with_issues_enabled = with_issues_available_for_user(user).select(:id)
+ with_merge_requests_enabled = with_merge_requests_available_for_user(user).select(:id)
+
+ from_union([with_issues_enabled, with_merge_requests_enabled]).select(:id)
+ end
end
def all_pipelines
diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb
index 90bcb3067f6..67c12363a3c 100644
--- a/app/models/project_auto_devops.rb
+++ b/app/models/project_auto_devops.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
class ProjectAutoDevops < ApplicationRecord
+ include IgnorableColumn
+
+ ignore_column :domain
+
belongs_to :project
enum deploy_strategy: {
@@ -12,8 +16,6 @@ class ProjectAutoDevops < ApplicationRecord
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
- validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
-
after_save :create_gitlab_deploy_token, if: :needs_to_create_deploy_token?
def predefined_variables
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index 1414164b703..492d50766ea 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -6,6 +6,18 @@ class ProjectCiCdSetting < ApplicationRecord
# The version of the schema that first introduced this model/table.
MINIMUM_SCHEMA_VERSION = 20180403035759
+ DEFAULT_GIT_DEPTH = 50
+
+ before_create :set_default_git_depth
+
+ validates :default_git_depth,
+ numericality: {
+ only_integer: true,
+ greater_than_or_equal_to: 0,
+ less_than_or_equal_to: 1000
+ },
+ allow_nil: true
+
def self.available?
@available ||=
ActiveRecord::Migrator.current_version >= MINIMUM_SCHEMA_VERSION
@@ -15,4 +27,10 @@ class ProjectCiCdSetting < ApplicationRecord
@available = nil
super
end
+
+ private
+
+ def set_default_git_depth
+ self.default_git_depth ||= DEFAULT_GIT_DEPTH
+ end
end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 0542581c6e0..6bcb051bff6 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -72,6 +72,8 @@ class ProjectFeature < ApplicationRecord
default_value_for :wiki_access_level, value: ENABLED, allows_nil: false
default_value_for :repository_access_level, value: ENABLED, allows_nil: false
+ scope :for_project_id, -> (project) { where(project: project) }
+
def feature_available?(feature, user)
# This feature might not be behind a feature flag at all, so default to true
return false unless ::Feature.enabled?(feature, user, default_enabled: true)
diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb
index 957be685aea..175c2ebf197 100644
--- a/app/models/project_services/youtrack_service.rb
+++ b/app/models/project_services/youtrack_service.rb
@@ -5,12 +5,12 @@ class YoutrackService < IssueTrackerService
prop_accessor :description, :project_url, :issues_url
- # {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1
+ # {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030
def self.reference_pattern(only_long: false)
if only_long
- /(?<issue>\b[A-Z][A-Za-z0-9_]*-\d+)/
+ /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+)/
else
- /(?<issue>\b[A-Z][A-Za-z0-9_]*-\d+)|(#{Issue.reference_prefix}(?<issue>\d+))/
+ /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+)|(#{Issue.reference_prefix}(?<issue>\d+))/
end
end
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 6fe8cb40d25..11e3737298c 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -4,11 +4,20 @@ class ProjectStatistics < ApplicationRecord
belongs_to :project
belongs_to :namespace
+ default_value_for :wiki_size, 0
+
+ # older migrations fail due to non-existent attribute without this
+ def wiki_size
+ has_attribute?(:wiki_size) ? super : 0
+ end
+
before_save :update_storage_size
- COLUMNS_TO_REFRESH = [:repository_size, :lfs_objects_size, :commit_count].freeze
+ COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count].freeze
INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size], packages_size: %i[storage_size] }.freeze
+ scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) }
+
def total_repository_size
repository_size + lfs_objects_size
end
@@ -27,22 +36,25 @@ class ProjectStatistics < ApplicationRecord
self.commit_count = project.repository.commit_count
end
- # Repository#size needs to be converted from MB to Byte.
def update_repository_size
self.repository_size = project.repository.size * 1.megabyte
end
+ def update_wiki_size
+ self.wiki_size = project.wiki.repository.size * 1.megabyte
+ end
+
def update_lfs_objects_size
self.lfs_objects_size = project.lfs_objects.sum(:size)
end
# older migrations fail due to non-existent attribute without this
def packages_size
- has_attribute?(:packages_size) ? super.to_i : 0
+ has_attribute?(:packages_size) ? super : 0
end
def update_storage_size
- self.storage_size = repository_size + lfs_objects_size + build_artifacts_size + packages_size
+ self.storage_size = repository_size + wiki_size + lfs_objects_size + build_artifacts_size + packages_size
end
# Since this incremental update method does not call update_storage_size above,
diff --git a/app/models/repository.rb b/app/models/repository.rb
index d43f991bb3e..e05d3dd58ac 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1077,7 +1077,7 @@ class Repository
end
def rebase(user, merge_request)
- if Feature.disabled?(:two_step_rebase, default_enabled: false)
+ if Feature.disabled?(:two_step_rebase, default_enabled: true)
return rebase_deprecated(user, merge_request)
end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 5dcc3e9945a..f1fc5e599eb 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -38,7 +38,9 @@ class Todo < ApplicationRecord
self
end
}, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
+
belongs_to :user
+ belongs_to :issue, -> { where("target_type = 'Issue'") }, foreign_key: :target_id
delegate :name, :email, to: :author, prefix: true, allow_nil: true
@@ -59,6 +61,7 @@ class Todo < ApplicationRecord
scope :for_target, -> (id) { where(target_id: id) }
scope :for_commit, -> (id) { where(commit_id: id) }
scope :with_api_entity_associations, -> { preload(:target, :author, :note, group: :route, project: [:route, { namespace: :route }]) }
+ scope :joins_issue_and_assignees, -> { left_joins(issue: :assignees) }
state_machine :state, initial: :pending do
event :done do
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index 6323c1b3389..c5675ef3ea3 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -13,4 +13,8 @@ class BlobPresenter < Gitlab::View::Presenter::Simple
plain: plain
)
end
+
+ def web_url
+ Gitlab::Routing.url_helpers.project_blob_url(blob.repository.project, File.join(blob.commit_id, blob.path))
+ end
end
diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb
index ed3daf6585b..471b6d3b726 100644
--- a/app/presenters/ci/build_runner_presenter.rb
+++ b/app/presenters/ci/build_runner_presenter.rb
@@ -4,7 +4,6 @@ module Ci
class BuildRunnerPresenter < SimpleDelegator
include Gitlab::Utils::StrongMemoize
- DEFAULT_GIT_DEPTH_MERGE_REQUEST = 10
RUNNER_REMOTE_TAG_PREFIX = 'refs/tags/'.freeze
RUNNER_REMOTE_BRANCH_PREFIX = 'refs/remotes/origin/'.freeze
@@ -26,20 +25,20 @@ module Ci
end
def git_depth
- strong_memoize(:git_depth) do
- git_depth = variables&.find { |variable| variable[:key] == 'GIT_DEPTH' }&.dig(:value)
- git_depth ||= DEFAULT_GIT_DEPTH_MERGE_REQUEST if merge_request_ref?
- git_depth.to_i
- end
+ if git_depth_variable
+ git_depth_variable[:value]
+ else
+ project.default_git_depth
+ end.to_i
end
def refspecs
specs = []
+ specs << refspec_for_merge_request_ref if merge_request_ref?
if git_depth > 0
specs << refspec_for_branch(ref) if branch? || legacy_detached_merge_request_pipeline?
specs << refspec_for_tag(ref) if tag?
- specs << refspec_for_merge_request_ref if merge_request_ref?
else
specs << refspec_for_branch
specs << refspec_for_tag
@@ -90,5 +89,11 @@ module Ci
def refspec_for_merge_request_ref
"+#{ref}:#{ref}"
end
+
+ def git_depth_variable
+ strong_memoize(:git_depth_variable) do
+ variables&.find { |variable| variable[:key] == 'GIT_DEPTH' }
+ end
+ end
end
end
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index 944895904fe..358473d0a74 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -43,7 +43,7 @@ module Ci
if pipeline.ref_exists?
_("for %{link_to_pipeline_ref}").html_safe % { link_to_pipeline_ref: link_to_pipeline_ref }
else
- _("for %{ref}") % { ref: content_tag(:span, pipeline.ref, class: 'ref-name') }
+ _("for %{ref}").html_safe % { ref: content_tag(:span, pipeline.ref, class: 'ref-name') }
end
end
end
diff --git a/app/presenters/issue_presenter.rb b/app/presenters/issue_presenter.rb
index c12a202efbc..c9dc0dbf443 100644
--- a/app/presenters/issue_presenter.rb
+++ b/app/presenters/issue_presenter.rb
@@ -4,6 +4,16 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
presents :issue
def web_url
- Gitlab::UrlBuilder.build(issue)
+ url_builder.url
+ end
+
+ def issue_path
+ url_builder.issue_path(issue)
+ end
+
+ private
+
+ def url_builder
+ @url_builder ||= Gitlab::UrlBuilder.new(issue)
end
end
diff --git a/app/presenters/member_presenter.rb b/app/presenters/member_presenter.rb
index 9e9b6973b8e..2561c3f0244 100644
--- a/app/presenters/member_presenter.rb
+++ b/app/presenters/member_presenter.rb
@@ -32,6 +32,11 @@ class MemberPresenter < Gitlab::View::Presenter::Delegated
request? && can_update?
end
+ # This functionality is only available in EE.
+ def can_override?
+ false
+ end
+
private
def admin_member_permission
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index ba0711ca867..9c44ed711a6 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -22,9 +22,9 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
end
- def cancel_merge_when_pipeline_succeeds_path
- if can_cancel_merge_when_pipeline_succeeds?(current_user)
- cancel_merge_when_pipeline_succeeds_project_merge_request_path(project, merge_request)
+ def cancel_auto_merge_path
+ if can_cancel_auto_merge?(current_user)
+ cancel_auto_merge_project_merge_request_path(project, merge_request)
end
end
diff --git a/app/presenters/tree_entry_presenter.rb b/app/presenters/tree_entry_presenter.rb
new file mode 100644
index 00000000000..7bb10cd1455
--- /dev/null
+++ b/app/presenters/tree_entry_presenter.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class TreeEntryPresenter < Gitlab::View::Presenter::Delegated
+ presents :tree
+
+ def web_url
+ Gitlab::Routing.url_helpers.project_tree_url(tree.repository.project, File.join(tree.commit_id, tree.path))
+ end
+end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 914ad628a99..36e601f45c5 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -44,4 +44,12 @@ class IssueEntity < IssuableEntity
expose :preview_note_path do |issue|
preview_markdown_path(issue.project, target_type: 'Issue', target_id: issue.iid)
end
+
+ expose :confidential_issues_docs_path, if: -> (issue) { issue.confidential? } do |issue|
+ help_page_path('user/project/issues/confidential_issues.md')
+ end
+
+ expose :locked_discussion_docs_path, if: -> (issue) { issue.discussion_locked? } do |issue|
+ help_page_path('user/discussions/index.md', anchor: 'lock-discussions')
+ end
end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index b130f447cce..a428930dbbf 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -9,7 +9,11 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :merge_params
expose :merge_status
expose :merge_user_id
- expose :merge_when_pipeline_succeeds
+ expose :auto_merge_enabled
+ expose :auto_merge_strategy
+ expose :available_auto_merge_strategies do |merge_request|
+ AutoMergeService.new(merge_request.project, current_user).available_strategies(merge_request) # rubocop: disable CodeReuse/ServiceClass
+ end
expose :source_branch
expose :source_branch_protected do |merge_request|
merge_request.source_project.present? && ProtectedBranch.protected?(merge_request.source_project, merge_request.source_branch)
@@ -182,8 +186,8 @@ class MergeRequestWidgetEntity < IssuableEntity
presenter(merge_request).remove_wip_path
end
- expose :cancel_merge_when_pipeline_succeeds_path do |merge_request|
- presenter(merge_request).cancel_merge_when_pipeline_succeeds_path
+ expose :cancel_auto_merge_path do |merge_request|
+ presenter(merge_request).cancel_auto_merge_path
end
expose :create_issue_to_resolve_discussions_path do |merge_request|
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index 9ef93b2387f..ec2698ecbe3 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -4,6 +4,7 @@ class PipelineEntity < Grape::Entity
include RequestAwareEntity
expose :id
+ expose :iid
expose :user, using: UserEntity
expose :active?, as: :active
diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb
new file mode 100644
index 00000000000..058105db3a4
--- /dev/null
+++ b/app/services/auto_merge/base_service.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module AutoMerge
+ class BaseService < ::BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ def execute(merge_request)
+ merge_request.merge_params.merge!(params)
+ merge_request.auto_merge_enabled = true
+ merge_request.merge_user = current_user
+ merge_request.auto_merge_strategy = strategy
+
+ return :failed unless merge_request.save
+
+ yield if block_given?
+
+ strategy.to_sym
+ end
+
+ def cancel(merge_request)
+ if cancel_auto_merge(merge_request)
+ yield if block_given?
+
+ success
+ else
+ error("Can't cancel the automatic merge", 406)
+ end
+ end
+
+ private
+
+ def strategy
+ strong_memoize(:strategy) do
+ self.class.name.demodulize.remove('Service').underscore
+ end
+ end
+
+ def cancel_auto_merge(merge_request)
+ merge_request.auto_merge_enabled = false
+ merge_request.merge_user = nil
+
+ merge_request.merge_params&.except!(
+ 'should_remove_source_branch',
+ 'commit_message',
+ 'squash_commit_message',
+ 'auto_merge_strategy'
+ )
+
+ merge_request.save
+ end
+ end
+end
diff --git a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb
new file mode 100644
index 00000000000..c41073a73e9
--- /dev/null
+++ b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module AutoMerge
+ class MergeWhenPipelineSucceedsService < AutoMerge::BaseService
+ def execute(merge_request)
+ super do
+ if merge_request.saved_change_to_auto_merge_enabled?
+ SystemNoteService.merge_when_pipeline_succeeds(merge_request, project, current_user, merge_request.diff_head_commit)
+ end
+ end
+ end
+
+ def process(merge_request)
+ return unless merge_request.actual_head_pipeline&.success?
+ return unless merge_request.mergeable?
+
+ merge_request.merge_async(merge_request.merge_user_id, merge_request.merge_params)
+ end
+
+ def cancel(merge_request)
+ super do
+ SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, @project, @current_user)
+ end
+ end
+
+ def available_for?(merge_request)
+ merge_request.actual_head_pipeline&.active?
+ end
+ end
+end
diff --git a/app/services/auto_merge_service.rb b/app/services/auto_merge_service.rb
new file mode 100644
index 00000000000..a3a780ff388
--- /dev/null
+++ b/app/services/auto_merge_service.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+class AutoMergeService < BaseService
+ STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS = 'merge_when_pipeline_succeeds'.freeze
+ STRATEGIES = [STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS].freeze
+
+ class << self
+ def all_strategies
+ STRATEGIES
+ end
+
+ def get_service_class(strategy)
+ return unless all_strategies.include?(strategy)
+
+ "::AutoMerge::#{strategy.camelize}Service".constantize
+ end
+ end
+
+ def execute(merge_request, strategy)
+ service = get_service_instance(strategy)
+
+ return :failed unless service&.available_for?(merge_request)
+
+ service.execute(merge_request)
+ end
+
+ def process(merge_request)
+ return unless merge_request.auto_merge_enabled?
+
+ get_service_instance(merge_request.auto_merge_strategy).process(merge_request)
+ end
+
+ def cancel(merge_request)
+ return error("Can't cancel the automatic merge", 406) unless merge_request.auto_merge_enabled?
+
+ get_service_instance(merge_request.auto_merge_strategy).cancel(merge_request)
+ end
+
+ def available_strategies(merge_request)
+ self.class.all_strategies.select do |strategy|
+ get_service_instance(strategy).available_for?(merge_request)
+ end
+ end
+
+ private
+
+ def get_service_instance(strategy)
+ self.class.get_service_class(strategy)&.new(project, current_user, params)
+ end
+end
diff --git a/app/services/ci/pipeline_schedule_service.rb b/app/services/ci/pipeline_schedule_service.rb
new file mode 100644
index 00000000000..387d0351490
--- /dev/null
+++ b/app/services/ci/pipeline_schedule_service.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ci
+ class PipelineScheduleService < BaseService
+ def execute(schedule)
+ # Ensure `next_run_at` is set properly before creating a pipeline.
+ # Otherwise, multiple pipelines could be created in a short interval.
+ schedule.schedule_next_run!
+
+ RunPipelineScheduleWorker.perform_async(schedule.id, schedule.owner.id)
+ end
+ end
+end
diff --git a/app/services/clusters/applications/base_service.rb b/app/services/clusters/applications/base_service.rb
index 14a45437287..a9feb60be6e 100644
--- a/app/services/clusters/applications/base_service.rb
+++ b/app/services/clusters/applications/base_service.rb
@@ -81,7 +81,7 @@ module Clusters
oauth_application_params = {
name: params[:application],
redirect_uri: application.callback_url,
- scopes: 'api read_user openid',
+ scopes: application.oauth_scopes,
owner: current_user
}
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 2a19e57a94f..805721212ba 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -29,7 +29,7 @@ module Issues
event_service.close_issue(issue, current_user)
create_note(issue, closed_via) if system_note
- closed_via = "commit #{closed_via.id}" if closed_via.is_a?(Commit)
+ closed_via = _("commit %{commit_id}") % { commit_id: closed_via.id } if closed_via.is_a?(Commit)
notification_service.async.close_issue(issue, current_user, closed_via: closed_via) if notifications
todo_service.close_issue(issue, current_user)
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index bb9062e9b40..2cfed62ce49 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -60,31 +60,7 @@ module MergeRequests
end
def create_pipeline_for(merge_request, user)
- return unless can_create_pipeline_for?(merge_request)
-
- create_detached_merge_request_pipeline(merge_request, user)
- end
-
- def create_detached_merge_request_pipeline(merge_request, user)
- if can_use_merge_request_ref?(merge_request)
- Ci::CreatePipelineService.new(merge_request.source_project, user,
- ref: merge_request.ref_path)
- .execute(:merge_request_event, merge_request: merge_request)
- else
- Ci::CreatePipelineService.new(merge_request.source_project, user,
- ref: merge_request.source_branch)
- .execute(:merge_request_event, merge_request: merge_request)
- end
- end
-
- def can_create_pipeline_for?(merge_request)
- ##
- # UpdateMergeRequestsWorker could be retried by an exception.
- # pipelines for merge request should not be recreated in such case.
- return false if merge_request.find_actual_head_pipeline&.triggered_by_merge_request?
- return false if merge_request.has_no_commits?
-
- true
+ MergeRequests::CreatePipelineService.new(project, user).execute(merge_request)
end
def can_use_merge_request_ref?(merge_request)
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index e77051bb1c9..b0f6166ea1c 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -18,6 +18,7 @@ module MergeRequests
invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches
cleanup_environments(merge_request)
+ cancel_auto_merge(merge_request)
end
merge_request
@@ -33,5 +34,9 @@ module MergeRequests
merge_request_metrics_service(merge_request).close(close_event)
end
end
+
+ def cancel_auto_merge(merge_request)
+ AutoMergeService.new(project, current_user).cancel(merge_request)
+ end
end
end
diff --git a/app/services/merge_requests/create_pipeline_service.rb b/app/services/merge_requests/create_pipeline_service.rb
new file mode 100644
index 00000000000..03246cc1920
--- /dev/null
+++ b/app/services/merge_requests/create_pipeline_service.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class CreatePipelineService < MergeRequests::BaseService
+ def execute(merge_request)
+ return unless can_create_pipeline_for?(merge_request)
+
+ create_detached_merge_request_pipeline(merge_request)
+ end
+
+ def create_detached_merge_request_pipeline(merge_request)
+ if can_use_merge_request_ref?(merge_request)
+ Ci::CreatePipelineService.new(merge_request.source_project, current_user,
+ ref: merge_request.ref_path)
+ .execute(:merge_request_event, merge_request: merge_request)
+ else
+ Ci::CreatePipelineService.new(merge_request.source_project, current_user,
+ ref: merge_request.source_branch)
+ .execute(:merge_request_event, merge_request: merge_request)
+ end
+ end
+
+ def can_create_pipeline_for?(merge_request)
+ ##
+ # UpdateMergeRequestsWorker could be retried by an exception.
+ # pipelines for merge request should not be recreated in such case.
+ return false if !allow_duplicate && merge_request.find_actual_head_pipeline&.triggered_by_merge_request?
+ return false if merge_request.has_no_commits?
+
+ true
+ end
+
+ def allow_duplicate
+ params[:allow_duplicate]
+ end
+ end
+end
diff --git a/app/services/merge_requests/merge_to_ref_service.rb b/app/services/merge_requests/merge_to_ref_service.rb
index 87147d90c32..8670b9ccf3d 100644
--- a/app/services/merge_requests/merge_to_ref_service.rb
+++ b/app/services/merge_requests/merge_to_ref_service.rb
@@ -20,20 +20,14 @@ module MergeRequests
raise_error('Conflicts detected during merge') unless commit_id
- commit = project.commit(commit_id)
- target_id, source_id = commit.parent_ids
-
- success(commit_id: commit.id,
- target_id: target_id,
- source_id: source_id)
- rescue MergeError => error
+ success(commit_id: commit_id)
+ rescue MergeError, ArgumentError => error
error(error.message)
end
private
def validate!
- authorization_check!
error_check!
end
@@ -43,21 +37,13 @@ module MergeRequests
error =
if !hooks_validation_pass?(merge_request)
hooks_validation_error(merge_request)
- elsif !@merge_request.mergeable_to_ref?
- "Merge request is not mergeable to #{target_ref}"
- elsif !source
+ elsif source.blank?
'No source for merge'
end
raise_error(error) if error
end
- def authorization_check!
- unless Ability.allowed?(current_user, :admin_merge_request, project)
- raise_error("You are not allowed to merge to this ref")
- end
- end
-
def target_ref
merge_request.merge_ref_path
end
diff --git a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
deleted file mode 100644
index 973e5b64e88..00000000000
--- a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-module MergeRequests
- class MergeWhenPipelineSucceedsService < MergeRequests::BaseService
- # Marks the passed `merge_request` to be merged when the pipeline succeeds or
- # updates the params for the automatic merge
- def execute(merge_request)
- merge_request.merge_params.merge!(params)
-
- # The service is also called when the merge params are updated.
- already_approved = merge_request.merge_when_pipeline_succeeds?
-
- unless already_approved
- merge_request.merge_when_pipeline_succeeds = true
- merge_request.merge_user = @current_user
-
- SystemNoteService.merge_when_pipeline_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit)
- end
-
- merge_request.save
- end
-
- # Triggers the automatic merge of merge_request once the pipeline succeeds
- def trigger(pipeline)
- return unless pipeline.success?
-
- pipeline_merge_requests(pipeline) do |merge_request|
- next unless merge_request.merge_when_pipeline_succeeds?
- next unless merge_request.mergeable?
-
- merge_request.merge_async(merge_request.merge_user_id, merge_request.merge_params)
- end
- end
-
- # Cancels the automatic merge
- def cancel(merge_request)
- if merge_request.merge_when_pipeline_succeeds? && merge_request.open?
- merge_request.reset_merge_when_pipeline_succeeds
- SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, @project, @current_user)
-
- success
- else
- error("Can't cancel the automatic merge", 406)
- end
- end
- end
-end
diff --git a/app/services/merge_requests/mergeability_check_service.rb b/app/services/merge_requests/mergeability_check_service.rb
new file mode 100644
index 00000000000..ef833774e65
--- /dev/null
+++ b/app/services/merge_requests/mergeability_check_service.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class MergeabilityCheckService < ::BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ delegate :project, to: :@merge_request
+ delegate :repository, to: :project
+
+ def initialize(merge_request)
+ @merge_request = merge_request
+ end
+
+ # Updates the MR merge_status. Whenever it switches to a can_be_merged state,
+ # the merge-ref is refreshed.
+ #
+ # Returns a ServiceResponse indicating merge_status is/became can_be_merged
+ # and the merge-ref is synced. Success in case of being/becoming mergeable,
+ # error otherwise.
+ def execute
+ return ServiceResponse.error(message: 'Invalid argument') unless merge_request
+ return ServiceResponse.error(message: 'Unsupported operation') if Gitlab::Database.read_only?
+
+ update_merge_status
+
+ unless merge_request.can_be_merged?
+ return ServiceResponse.error(message: 'Merge request is not mergeable')
+ end
+
+ unless payload.fetch(:merge_ref_head)
+ return ServiceResponse.error(message: 'Merge ref was not found')
+ end
+
+ ServiceResponse.success(payload: payload)
+ end
+
+ private
+
+ attr_reader :merge_request
+
+ def payload
+ strong_memoize(:payload) do
+ {
+ merge_ref_head: merge_ref_head_payload
+ }
+ end
+ end
+
+ def merge_ref_head_payload
+ commit = merge_request.merge_ref_head
+
+ return unless commit
+
+ target_id, source_id = commit.parent_ids
+
+ {
+ commit_id: commit.id,
+ source_id: source_id,
+ target_id: target_id
+ }
+ end
+
+ def update_merge_status
+ return unless merge_request.recheck_merge_status?
+
+ if can_git_merge?
+ merge_to_ref && merge_request.mark_as_mergeable
+ else
+ merge_request.mark_as_unmergeable
+ end
+ end
+
+ def can_git_merge?
+ !merge_request.broken? && repository.can_be_merged?(merge_request.diff_head_sha, merge_request.target_branch)
+ end
+
+ def merge_to_ref
+ result = MergeRequests::MergeToRefService.new(project, merge_request.author).execute(merge_request)
+ result[:status] == :success
+ end
+ end
+end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 3abea1ad1ae..08130a531ee 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -24,7 +24,7 @@ module MergeRequests
reload_merge_requests
outdate_suggestions
refresh_pipelines_on_merge_requests
- reset_merge_when_pipeline_succeeds
+ cancel_auto_merge
mark_pending_todos_done
cache_merge_requests_closing_issues
@@ -142,8 +142,10 @@ module MergeRequests
end
end
- def reset_merge_when_pipeline_succeeds
- merge_requests_for_source_branch.each(&:reset_merge_when_pipeline_succeeds)
+ def cancel_auto_merge
+ merge_requests_for_source_branch.each do |merge_request|
+ AutoMergeService.new(project, current_user).cancel(merge_request)
+ end
end
def mark_pending_todos_done
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 55546432ce4..6a0f3000ffb 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -89,7 +89,7 @@ module MergeRequests
merge_request.update(merge_error: nil)
if merge_request.head_pipeline && merge_request.head_pipeline.active?
- MergeRequests::MergeWhenPipelineSucceedsService.new(project, current_user).execute(merge_request)
+ AutoMergeService.new(project, current_user).execute(merge_request, AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
else
merge_request.merge_async(current_user.id, {})
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index f797c0f11c6..5aa804666f0 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -238,7 +238,7 @@ class NotificationService
merge_request,
current_user,
:merged_merge_request_email,
- skip_current_user: !merge_request.merge_when_pipeline_succeeds?
+ skip_current_user: !merge_request.auto_merge_enabled?
)
end
diff --git a/app/services/pages_domains/create_acme_order_service.rb b/app/services/pages_domains/create_acme_order_service.rb
new file mode 100644
index 00000000000..c600f497fa5
--- /dev/null
+++ b/app/services/pages_domains/create_acme_order_service.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module PagesDomains
+ class CreateAcmeOrderService
+ attr_reader :pages_domain
+
+ def initialize(pages_domain)
+ @pages_domain = pages_domain
+ end
+
+ def execute
+ lets_encrypt_client = Gitlab::LetsEncrypt::Client.new
+ order = lets_encrypt_client.new_order(pages_domain.domain)
+
+ challenge = order.new_challenge
+
+ private_key = OpenSSL::PKey::RSA.new(4096)
+ saved_order = pages_domain.acme_orders.create!(
+ url: order.url,
+ expires_at: order.expires,
+ private_key: private_key.to_pem,
+
+ challenge_token: challenge.token,
+ challenge_file_content: challenge.file_content
+ )
+
+ challenge.request_validation
+ saved_order
+ end
+ end
+end
diff --git a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
new file mode 100644
index 00000000000..2dfe1a3d8ca
--- /dev/null
+++ b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module PagesDomains
+ class ObtainLetsEncryptCertificateService
+ attr_reader :pages_domain
+
+ def initialize(pages_domain)
+ @pages_domain = pages_domain
+ end
+
+ def execute
+ pages_domain.acme_orders.expired.delete_all
+ acme_order = pages_domain.acme_orders.first
+
+ unless acme_order
+ ::PagesDomains::CreateAcmeOrderService.new(pages_domain).execute
+ return
+ end
+
+ api_order = ::Gitlab::LetsEncrypt::Client.new.load_order(acme_order.url)
+
+ # https://tools.ietf.org/html/rfc8555#section-7.1.6 - statuses diagram
+ case api_order.status
+ when 'ready'
+ api_order.request_certificate(private_key: acme_order.private_key, domain: pages_domain.domain)
+ when 'valid'
+ save_certificate(acme_order.private_key, api_order)
+ acme_order.destroy!
+ # when 'invalid'
+ # TODO: implement error handling
+ end
+ end
+
+ private
+
+ def save_certificate(private_key, api_order)
+ certificate = api_order.certificate
+ pages_domain.update!(key: private_key, certificate: certificate)
+ end
+ end
+end
diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb
index 7386530f45f..2b4c4ae68e2 100644
--- a/app/services/preview_markdown_service.rb
+++ b/app/services/preview_markdown_service.rb
@@ -38,7 +38,9 @@ class PreviewMarkdownService < BaseService
head_sha: params[:head_sha],
start_sha: params[:start_sha])
- Gitlab::Diff::SuggestionsParser.parse(text, position: position, project: project)
+ Gitlab::Diff::SuggestionsParser.parse(text, position: position,
+ project: project,
+ supports_suggestion: params[:preview_suggestions])
end
def preview_sugestions?
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index fc234bafc57..0b4ab7b8e4d 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -36,18 +36,22 @@ module Projects
def fork_new_project
new_params = {
- visibility_level: allowed_visibility_level,
- description: @project.description,
- name: target_name,
- path: target_path,
- shared_runners_enabled: @project.shared_runners_enabled,
- namespace_id: target_namespace.id,
- fork_network: fork_network,
+ visibility_level: allowed_visibility_level,
+ description: @project.description,
+ name: target_name,
+ path: target_path,
+ shared_runners_enabled: @project.shared_runners_enabled,
+ namespace_id: target_namespace.id,
+ fork_network: fork_network,
+ # We need to set default_git_depth to 0 for the forked project when
+ # @project.default_git_depth is nil in order to keep the same behaviour
+ # and not get ProjectCiCdSetting::DEFAULT_GIT_DEPTH set on create
+ ci_cd_settings_attributes: { default_git_depth: @project.default_git_depth || 0 },
# We need to assign the fork network membership after the project has
# been instantiated to avoid ActiveRecord trying to create it when
# initializing the project, as that would cause a foreign key constraint
# exception.
- relations_block: -> (project) { build_fork_network_member(project) }
+ relations_block: -> (project) { build_fork_network_member(project) }
}
if @project.avatar.present? && @project.avatar.image?
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index dfa7bd20254..2bc04470342 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -64,6 +64,7 @@ module Projects
if project.previous_changes.include?(:visibility_level) && project.private?
# don't enqueue immediately to prevent todos removal in case of a mistake
+ TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, nil, project.id)
TodosDestroyer::ProjectPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id)
elsif (project_changed_feature_keys & todos_features_changes).present?
TodosDestroyer::PrivateFeaturesWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id)
diff --git a/app/services/projects/update_statistics_service.rb b/app/services/projects/update_statistics_service.rb
index f32a779fab0..28677a398f3 100644
--- a/app/services/projects/update_statistics_service.rb
+++ b/app/services/projects/update_statistics_service.rb
@@ -3,7 +3,7 @@
module Projects
class UpdateStatisticsService < BaseService
def execute
- return unless project && project.repository.exists?
+ return unless project
Rails.logger.info("Updating statistics for project #{project.id}")
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index e0cbfac2420..302510341ac 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -52,6 +52,10 @@ class SearchService
@search_objects ||= search_results.objects(scope, params[:page])
end
+ def display_options
+ @display_options ||= search_results.display_options(scope)
+ end
+
private
def search_service
diff --git a/app/services/service_response.rb b/app/services/service_response.rb
index 1de30e68d87..f3437ba16de 100644
--- a/app/services/service_response.rb
+++ b/app/services/service_response.rb
@@ -1,19 +1,20 @@
# frozen_string_literal: true
class ServiceResponse
- def self.success(message: nil)
- new(status: :success, message: message)
+ def self.success(message: nil, payload: {})
+ new(status: :success, message: message, payload: payload)
end
- def self.error(message:, http_status: nil)
- new(status: :error, message: message, http_status: http_status)
+ def self.error(message:, payload: {}, http_status: nil)
+ new(status: :error, message: message, payload: payload, http_status: http_status)
end
- attr_reader :status, :message, :http_status
+ attr_reader :status, :message, :http_status, :payload
- def initialize(status:, message: nil, http_status: nil)
+ def initialize(status:, message: nil, payload: {}, http_status: nil)
self.status = status
self.message = message
+ self.payload = payload
self.http_status = http_status
end
@@ -27,5 +28,5 @@ class ServiceResponse
private
- attr_writer :status, :message, :http_status
+ attr_writer :status, :message, :http_status, :payload
end
diff --git a/app/services/todos/destroy/base_service.rb b/app/services/todos/destroy/base_service.rb
index f3f1dbb5698..7378f10e7c4 100644
--- a/app/services/todos/destroy/base_service.rb
+++ b/app/services/todos/destroy/base_service.rb
@@ -13,7 +13,7 @@ module Todos
# rubocop: disable CodeReuse/ActiveRecord
def without_authorized(items)
- items.where('user_id NOT IN (?)', authorized_users)
+ items.where('todos.user_id NOT IN (?)', authorized_users)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/todos/destroy/confidential_issue_service.rb b/app/services/todos/destroy/confidential_issue_service.rb
index 6276e332448..6cdd8c16894 100644
--- a/app/services/todos/destroy/confidential_issue_service.rb
+++ b/app/services/todos/destroy/confidential_issue_service.rb
@@ -2,36 +2,55 @@
module Todos
module Destroy
+ # Service class for deleting todos that belongs to confidential issues.
+ # It deletes todos for users that are not at least reporters, issue author or assignee.
+ #
+ # Accepts issue_id or project_id as argument.
+ # When issue_id is passed it deletes matching todos for one confidential issue.
+ # When project_id is passed it deletes matching todos for all confidential issues of the project.
class ConfidentialIssueService < ::Todos::Destroy::BaseService
extend ::Gitlab::Utils::Override
- attr_reader :issue
+ attr_reader :issues
# rubocop: disable CodeReuse/ActiveRecord
- def initialize(issue_id)
- @issue = Issue.find_by(id: issue_id)
+ def initialize(issue_id: nil, project_id: nil)
+ @issues =
+ if issue_id
+ Issue.where(id: issue_id)
+ elsif project_id
+ project_confidential_issues(project_id)
+ end
end
# rubocop: enable CodeReuse/ActiveRecord
private
+ def project_confidential_issues(project_id)
+ project = Project.find(project_id)
+
+ project.issues.confidential_only
+ end
+
override :todos
# rubocop: disable CodeReuse/ActiveRecord
def todos
- Todo.where(target: issue)
- .where('user_id != ?', issue.author_id)
- .where('user_id NOT IN (?)', issue.assignees.select(:id))
+ Todo.joins_issue_and_assignees
+ .where(target: issues)
+ .where('issues.confidential = ?', true)
+ .where('todos.user_id != issues.author_id')
+ .where('todos.user_id != issue_assignees.user_id')
end
# rubocop: enable CodeReuse/ActiveRecord
override :todos_to_remove?
def todos_to_remove?
- issue&.confidential?
+ issues&.any?(&:confidential?)
end
override :project_ids
def project_ids
- issue.project_id
+ issues&.distinct&.select(:project_id)
end
override :authorized_users
diff --git a/app/uploaders/legacy_artifact_uploader.rb b/app/uploaders/legacy_artifact_uploader.rb
deleted file mode 100644
index fac3c3dcb8f..00000000000
--- a/app/uploaders/legacy_artifact_uploader.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-##
-# TODO: Remove this uploader when we remove :ci_enable_legacy_artifacts feature flag
-# See https://gitlab.com/gitlab-org/gitlab-ce/issues/58595
-class LegacyArtifactUploader < GitlabUploader
- extend Workhorse::UploadPath
- include ObjectStorage::Concern
-
- ObjectNotReadyError = Class.new(StandardError)
-
- storage_options Gitlab.config.artifacts
-
- alias_method :upload, :model
-
- def store_dir
- dynamic_segment
- end
-
- private
-
- def dynamic_segment
- raise ObjectNotReadyError, 'Build is not ready' unless model.id
-
- File.join(model.created_at.utc.strftime('%Y_%m'), model.project_id.to_s, model.id.to_s)
- end
-end
diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml
index a161fbd064e..c6781e91cfd 100644
--- a/app/views/abuse_reports/new.html.haml
+++ b/app/views/abuse_reports/new.html.haml
@@ -1,10 +1,10 @@
-- page_title _("Report abuse to GitLab")
+- page_title _("Report abuse to admin")
%h3.page-title
- = _("Report abuse to GitLab")
+ = _("Report abuse to admin")
%p
- = _("Please use this form to report users to GitLab who create spam issues, comments or behave inappropriately.")
+ = _("Please use this form to report to the admin users who create spam issues, comments or behave inappropriately.")
%p
- = _("A member of GitLab's abuse team will review your report as soon as possible.")
+ = _("A member of the abuse team will review your report as soon as possible.")
%hr
= form_for @abuse_report, html: { class: 'js-quick-submit js-requires-input'} do |f|
= form_errors(@abuse_report)
diff --git a/app/views/admin/application_settings/_external_authorization_service_form.html.haml b/app/views/admin/application_settings/_external_authorization_service_form.html.haml
index 01f6c7afe61..7587ecbf9d3 100644
--- a/app/views/admin/application_settings/_external_authorization_service_form.html.haml
+++ b/app/views/admin/application_settings/_external_authorization_service_form.html.haml
@@ -5,7 +5,7 @@
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
- = _('External Classification Policy Authorization')
+ = _('External Classification Policy Authorization')
.settings-content
= form_for @application_setting, url: admin_application_settings_path(anchor: 'js-external-auth-settings'), html: { class: 'fieldset-form' } do |f|
diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml
index f4bfb5af385..dd56bb99a06 100644
--- a/app/views/admin/application_settings/_outbound.html.haml
+++ b/app/views/admin/application_settings/_outbound.html.haml
@@ -8,4 +8,12 @@
= f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do
Allow requests to the local network from hooks and services
+ .form-group
+ .form-check
+ = f.check_box :dns_rebinding_protection_enabled, class: 'form-check-input'
+ = f.label :dns_rebinding_protection_enabled, class: 'form-check-label' do
+ = _('Enforce DNS rebinding attack protection')
+ %span.form-text.text-muted
+ = _('Resolves IP addresses once and uses them to submit requests')
+
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml
index f992d531ea5..1e66b635038 100644
--- a/app/views/admin/application_settings/_performance_bar.html.haml
+++ b/app/views/admin/application_settings/_performance_bar.html.haml
@@ -6,7 +6,7 @@
.form-check
= f.check_box :performance_bar_enabled, class: 'form-check-input'
= f.label :performance_bar_enabled, class: 'form-check-label qa-enable-performance-bar-checkbox' do
- Enable the Performance Bar
+ Enable access to the Performance Bar
.form-group
= f.label :performance_bar_allowed_group_path, 'Allowed group', class: 'label-bold'
= f.text_field :performance_bar_allowed_group_path, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml
index d5ba6abe7af..01d61beaf53 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -31,7 +31,7 @@
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- = _('Enable the Performance Bar for a given group.')
+ = _('Enable access to the Performance Bar for a given group.')
= link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
.settings-content
= render 'performance_bar'
diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml
index 49aa62a5408..299d0a12e6c 100644
--- a/app/views/admin/labels/_form.html.haml
+++ b/app/views/admin/labels/_form.html.haml
@@ -24,10 +24,7 @@
%br
= _("Or you can choose one of the suggested colors below")
- .suggest-colors
- - suggested_colors.each do |color|
- = link_to '#', style: "background-color: #{color}", data: { color: color } do
- &nbsp;
+ = render_suggested_colors
.form-actions
= f.submit _('Save'), class: 'btn btn-success js-save-button'
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
index 9117f63f939..2f7ad35eb3e 100644
--- a/app/views/admin/projects/_projects.html.haml
+++ b/app/views/admin/projects/_projects.html.haml
@@ -7,7 +7,7 @@
= link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn"
%button.delete-project-button.btn.btn-danger{ data: { toggle: 'modal',
target: '#delete-project-modal',
- delete_project_url: project_path(project),
+ delete_project_url: admin_project_path(project),
project_name: project.name }, type: 'button' }
= s_('AdminProjects|Delete')
@@ -17,7 +17,7 @@
- if project.archived
%span.badge.badge-warning archived
.title
- = link_to(admin_namespace_project_path(project.namespace, project)) do
+ = link_to(admin_project_path(project)) do
.dash-project-avatar
.avatar-container.rect-avatar.s40
= project_icon(project, alt: '', class: 'avatar project-avatar s40', width: 40, height: 40)
diff --git a/app/views/ci/variables/_content.html.haml b/app/views/ci/variables/_content.html.haml
index d07cbe4589c..0b5c1a806b2 100644
--- a/app/views/ci/variables/_content.html.haml
+++ b/app/views/ci/variables/_content.html.haml
@@ -1,3 +1,3 @@
-= _('Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. Additionally, they will be masked by default so they are hidden in job logs, though they must match certain regexp requirements to do so. You can use environment variables for passwords, secret keys, or whatever you want.')
+= _('Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. Additionally, they can be masked so they are hidden in job logs, though they must match certain regexp requirements to do so. You can use environment variables for passwords, secret keys, or whatever you want.')
= _('You may also add variables that are made available to the running application by prepending the variable key with <code>K8S_SECRET_</code>.').html_safe
= link_to _('More information'), help_page_path('ci/variables/README', anchor: 'variables')
diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index 464b9faf282..94102b4dcd0 100644
--- a/app/views/ci/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -6,7 +6,7 @@
= s_('Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
.row
- .col-lg-12.js-ci-variable-list-section{ data: { save_endpoint: save_endpoint } }
+ .col-lg-12.js-ci-variable-list-section{ data: { save_endpoint: save_endpoint, maskable_regex: ci_variable_maskable_regex } }
.hide.alert.alert-danger.js-ci-variable-error-box
%ul.ci-variable-list
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index ca2521e9bc6..ed4bd5ae19e 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -8,7 +8,7 @@
- value = variable&.value
- is_protected_default = ci_variable_protected_by_default?
- is_protected = ci_variable_protected?(variable, only_key_value)
-- is_masked_default = true
+- is_masked_default = false
- is_masked = ci_variable_masked?(variable, only_key_value)
- id_input_name = "#{form_field}[variables_attributes][][id]"
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 214630d245a..8212fb8bb33 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -34,7 +34,7 @@
= icon('spinner spin')
.todos-filters
- .row-content-block.second-block
+ .issues-details-filters.row-content-block.second-block
= form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form d-sm-flex' do
.filter-categories.flex-fill
.filter-item.inline
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 383fd5130ce..5eba819172b 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -1,3 +1,5 @@
+- max_name_length = 128
+- max_username_length = 255
#register-pane.tab-pane.login-box{ role: 'tabpanel' }
.login-body
= form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f|
@@ -5,13 +7,13 @@
= render "devise/shared/error_messages", resource: resource
.name.form-group
= f.label :name, _('Full name'), class: 'label-bold'
- = f.text_field :name, class: "form-control top qa-new-user-name js-block-emoji", required: true, title: _("This field is required.")
+ = f.text_field :name, class: "form-control top qa-new-user-name js-block-emoji js-validate-length", :data => { :max_length => max_name_length, :max_length_message => s_("SignUp|Name is too long (maximum is %{max_length} characters).") % { max_length: max_name_length } }, required: true, title: _("This field is required.")
.username.form-group
= f.label :username, class: 'label-bold'
- = f.text_field :username, class: "form-control middle qa-new-user-username js-block-emoji", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: _("Please create a username with only alphanumeric characters.")
- %p.validation-error.hide= _('Username is already taken.')
- %p.validation-success.hide= _('Username is available.')
- %p.validation-pending.hide= _('Checking username availability...')
+ = f.text_field :username, class: "form-control middle qa-new-user-username js-block-emoji js-validate-length", :data => { :max_length => max_username_length, :max_length_message => s_("SignUp|Username is too long (maximum is %{max_length} characters).") % { max_length: max_username_length } }, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: _("Please create a username with only alphanumeric characters.")
+ %p.validation-error.field-validation.hide= _('Username is already taken.')
+ %p.validation-success.field-validation.hide= _('Username is available.')
+ %p.validation-pending.field-validation.hide= _('Checking username availability...')
.form-group
= f.label :email, class: 'label-bold'
= f.email_field :email, class: "form-control middle qa-new-user-email", required: true, title: _("Please provide a valid email address.")
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index 30b00ca86b3..0a5541c3e82 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -19,20 +19,24 @@
.discussion-reply-holder
- if can_create_note?
+ %a.user-avatar-link.d-none.d-sm-block{ href: user_path(current_user) }
+ = image_tag avatar_icon_for_user(current_user), alt: current_user.to_reference, class: 'avatar s40'
- if discussion.potentially_resolvable?
- line_type = local_assigns.fetch(:line_type, nil)
- .btn-group.discussion-with-resolve-btn{ role: "group" }
- .btn-group{ role: "group" }
- = link_to_reply_discussion(discussion, line_type)
+ .discussion-with-resolve-btn
+ .btn-group.discussion-with-resolve-btn{ role: "group" }
+ .btn-group{ role: "group" }
+ = link_to_reply_discussion(discussion, line_type)
- = render "discussions/resolve_all", discussion: discussion
+ = render "discussions/resolve_all", discussion: discussion
- .btn-group.discussion-actions
- = render "discussions/new_issue_for_discussion", discussion: discussion, merge_request: discussion.noteable
- = render "discussions/jump_to_next", discussion: discussion
+ .btn-group.discussion-actions
+ = render "discussions/new_issue_for_discussion", discussion: discussion, merge_request: discussion.noteable
+ = render "discussions/jump_to_next", discussion: discussion
- else
- = link_to_reply_discussion(discussion)
+ .discussion-with-resolve-btn
+ = link_to_reply_discussion(discussion)
- elsif !current_user
.disabled-comment.text-center
Please
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 77fe88dacb7..255a9ad038c 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -9,7 +9,7 @@
= render 'groups/home_panel'
.groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
- .top-area.group-nav-container
+ .top-area.group-nav-container.justify-content-between
.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index 5e4595d930b..a19c8911559 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -7,28 +7,7 @@
%hr
= form_tag import_gitlab_project_path, class: 'new_project', multipart: true do
- .row
- .form-group.project-name.col-sm-12
- = label_tag :name, _('Project name'), class: 'label-bold'
- = text_field_tag :name, @name, placeholder: "My awesome project", class: "js-project-name form-control input-lg", autofocus: true
- .form-group.col-12.col-sm-6
- = label_tag :namespace_id, _('Project URL'), class: 'label-bold'
- .form-group
- .input-group
- - if current_user.can_select_namespace?
- .input-group-prepend.has-tooltip{ title: root_url }
- .input-group-text
- = root_url
- = select_tag :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), class: 'select2 js-select-namespace', tabindex: 1
-
- - else
- .input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
- .input-group-text.border-0
- #{user_url(current_user.username)}/
- = hidden_field_tag :namespace_id, value: current_user.namespace_id
- .form-group.col-12.col-sm-6.project-path
- = label_tag :path, _('Project slug'), class: 'label-bold'
- = text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control", tabindex: 2, required: true
+ = render 'import/shared/new_project_form'
.row
.form-group.col-md-12
diff --git a/app/views/import/manifest/new.html.haml b/app/views/import/manifest/new.html.haml
index 056e4922b9e..df00c4d2179 100644
--- a/app/views/import/manifest/new.html.haml
+++ b/app/views/import/manifest/new.html.haml
@@ -4,9 +4,5 @@
%h3.page-title
= _('Manifest file import')
-- if @errors.present?
- .alert.alert-danger
- - @errors.each do |error|
- = error
-
+= render 'import/shared/errors'
= render 'form'
diff --git a/app/views/import/phabricator/new.html.haml b/app/views/import/phabricator/new.html.haml
new file mode 100644
index 00000000000..811e126579e
--- /dev/null
+++ b/app/views/import/phabricator/new.html.haml
@@ -0,0 +1,25 @@
+- title = _('Phabricator Server Import')
+- page_title title
+- breadcrumb_title title
+- header_title _("Projects"), root_path
+
+%h3.page-title
+ = icon 'issues', text: _('Import tasks from Phabricator into issues')
+
+= render 'import/shared/errors'
+
+= form_tag import_phabricator_path, class: 'new_project', method: :post do
+ = render 'import/shared/new_project_form'
+
+ %h4.prepend-top-0= _('Enter in your Phabricator Server URL and personal access token below')
+
+ .form-group.row
+ = label_tag :phabricator_server_url, _('Phabricator Server URL'), class: 'col-form-label col-md-2'
+ .col-md-4
+ = text_field_tag :phabricator_server_url, params[:phabricator_server_url], class: 'form-control append-right-8', placeholder: 'https://your-phabricator-server', size: 40
+ .form-group.row
+ = label_tag :api_token, _('API Token'), class: 'col-form-label col-md-2'
+ .col-md-4
+ = password_field_tag :api_token, params[:api_token], class: 'form-control append-right-8', placeholder: _('Personal Access Token'), size: 40
+ .form-actions
+ = submit_tag _('Import tasks'), class: 'btn btn-success'
diff --git a/app/views/import/shared/_errors.html.haml b/app/views/import/shared/_errors.html.haml
new file mode 100644
index 00000000000..de60c15351f
--- /dev/null
+++ b/app/views/import/shared/_errors.html.haml
@@ -0,0 +1,4 @@
+- if @errors.present?
+ .alert.alert-danger
+ - @errors.each do |error|
+ = error
diff --git a/app/views/import/shared/_new_project_form.html.haml b/app/views/import/shared/_new_project_form.html.haml
new file mode 100644
index 00000000000..4d13d4f2869
--- /dev/null
+++ b/app/views/import/shared/_new_project_form.html.haml
@@ -0,0 +1,21 @@
+.row
+ .form-group.project-name.col-sm-12
+ = label_tag :name, _('Project name'), class: 'label-bold'
+ = text_field_tag :name, @name, placeholder: "My awesome project", class: "js-project-name form-control input-lg", autofocus: true
+ .form-group.col-12.col-sm-6
+ = label_tag :namespace_id, _('Project URL'), class: 'label-bold'
+ .form-group
+ .input-group.flex-nowrap
+ - if current_user.can_select_namespace?
+ .input-group-prepend.flex-shrink-0.has-tooltip{ title: root_url }
+ .input-group-text
+ = root_url
+ = select_tag :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), class: 'select2 js-select-namespace', tabindex: 1
+ - else
+ .input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
+ .input-group-text.border-0
+ #{user_url(current_user.username)}/
+ = hidden_field_tag :namespace_id, value: current_user.namespace_id
+ .form-group.col-12.col-sm-6.project-path
+ = label_tag :path, _('Project slug'), class: 'label-bold'
+ = text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control", tabindex: 2, required: true
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index a6023a1cbb9..496ec3c78b0 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -16,7 +16,7 @@
mr_path: merge_requests_dashboard_path },
aria: { label: _('Search or jump to…') }
%button.hidden.js-dropdown-search-toggle{ type: 'button', data: { toggle: 'dropdown' } }
- .dropdown-menu.dropdown-select
+ .dropdown-menu.dropdown-select.js-dashboard-search-options
= dropdown_content do
%ul
%li.dropdown-menu-empty-item
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 043cca6ad38..c38f96f302a 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -10,4 +10,6 @@
= render 'layouts/page', sidebar: sidebar, nav: nav
= footer_message
+ = render_if_exists "shared/onboarding_guide"
+
= yield :scripts_body
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 47710b9e9e5..54028dc8554 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -19,17 +19,17 @@
- if dashboard_nav_link?(:activity)
= nav_link(path: 'dashboard#activity', html_options: { class: ["d-none d-xl-block", ("d-lg-block" unless has_extra_nav_icons?)] }) do
- = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: _('Activity') do
+ = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity' do
= _('Activity')
- if dashboard_nav_link?(:milestones)
= nav_link(controller: 'dashboard/milestones', html_options: { class: ["d-none d-xl-block", ("d-lg-block" unless has_extra_nav_icons?)] }) do
- = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: _('Milestones') do
+ = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones' do
= _('Milestones')
- if dashboard_nav_link?(:snippets)
= nav_link(controller: 'dashboard/snippets', html_options: { class: ["d-none d-xl-block", ("d-lg-block" unless has_extra_nav_icons?)] }) do
- = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets qa-snippets-link', title: _('Snippets') do
+ = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets qa-snippets-link' do
= _('Snippets')
- if any_dashboard_nav_link?([:groups, :milestones, :activity, :snippets])
@@ -41,47 +41,47 @@
%ul
- if dashboard_nav_link?(:activity)
= nav_link(path: 'dashboard#activity') do
- = link_to activity_dashboard_path, title: _('Activity') do
+ = link_to activity_dashboard_path do
= _('Activity')
- if dashboard_nav_link?(:milestones)
= nav_link(controller: 'dashboard/milestones') do
- = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: _('Milestones') do
+ = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones' do
= _('Milestones')
- if dashboard_nav_link?(:snippets)
= nav_link(controller: 'dashboard/snippets') do
- = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: _('Snippets') do
+ = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets' do
= _('Snippets')
-
- = render_if_exists 'dashboard/operations/nav_link'
+ %li.dropdown.d-lg-none
+ = render_if_exists 'dashboard/operations/nav_link_list'
- if can?(current_user, :read_instance_statistics)
- = nav_link(controller: [:conversational_development_index, :cohorts]) do
- = link_to instance_statistics_root_path, title: _('Instance Statistics'), aria: { label: _('Instance Statistics') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = nav_link(controller: [:conversational_development_index, :cohorts], html_options: { class: 'd-lg-none' }) do
+ = link_to instance_statistics_root_path do
= _('Instance Statistics')
- if current_user.admin?
= nav_link(controller: 'admin/dashboard') do
- = link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: _('Admin Area'), aria: { label: _('Admin Area') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = link_to admin_root_path, class: 'd-lg-none admin-icon qa-admin-area-link' do
= _('Admin Area')
- if Gitlab::Sherlock.enabled?
%li
- = link_to sherlock_transactions_path, class: 'admin-icon', title: _('Sherlock Transactions'),
- data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = link_to sherlock_transactions_path, class: 'd-lg-none admin-icon' do
= _('Sherlock Transactions')
-# Shortcut to Dashboard > Projects
- if dashboard_nav_link?(:projects)
%li.hidden
- = link_to dashboard_projects_path, title: _('Projects'), class: 'dashboard-shortcuts-projects' do
+ = link_to dashboard_projects_path, class: 'dashboard-shortcuts-projects' do
= _('Projects')
- if current_controller?('ide')
%li.line-separator.d-none.d-sm-block
= nav_link(controller: 'ide') do
- = link_to '#', class: 'dashboard-shortcuts-web-ide', title: _('Web IDE') do
+ = link_to '#', class: 'dashboard-shortcuts-web-ide' do
= _('Web IDE')
- = render_if_exists 'dashboard/operations/nav_link'
+ %li.dropdown{ class: 'd-none d-lg-block' }
+ = render_if_exists 'dashboard/operations/nav_link'
- if can?(current_user, :read_instance_statistics)
= nav_link(controller: [:conversational_development_index, :cohorts], html_options: { class: "d-none d-lg-block d-xl-block"}) do
= link_to instance_statistics_root_path, title: _('Instance Statistics'), aria: { label: _('Instance Statistics') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 399305baec1..9b6551552c7 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -36,6 +36,8 @@
= render_if_exists 'projects/sidebar/security_dashboard'
+ = render_if_exists 'projects/sidebar/dependencies'
+
- if can?(current_user, :read_cycle_analytics, @project)
= nav_link(path: 'cycle_analytics#show') do
= link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do
diff --git a/app/views/notify/closed_issue_email.html.haml b/app/views/notify/closed_issue_email.html.haml
index f21cf1ad34b..d3733ab3a09 100644
--- a/app/views/notify/closed_issue_email.html.haml
+++ b/app/views/notify/closed_issue_email.html.haml
@@ -1,2 +1,2 @@
%p
- Issue was closed by #{sanitize_name(@updated_by.name)} #{closure_reason_text(@closed_via, format: formats.first)}.
+ = _("Issue was closed by %{name} %{reason}").html_safe % { name: sanitize_name(@updated_by.name), reason: closure_reason_text(@closed_via, format: formats.first) }
diff --git a/app/views/notify/closed_issue_email.text.haml b/app/views/notify/closed_issue_email.text.haml
index 5567adc9165..ff2548a4b42 100644
--- a/app/views/notify/closed_issue_email.text.haml
+++ b/app/views/notify/closed_issue_email.text.haml
@@ -1,3 +1,3 @@
-Issue was closed by #{sanitize_name(@updated_by.name)} #{closure_reason_text(@closed_via, format: formats.first)}.
+= _("Issue was closed by %{name} %{reason}").html_safe % { name: sanitize_name(@updated_by.name), reason: closure_reason_text(@closed_via, format: formats.first) }
Issue ##{@issue.iid}: #{project_issue_url(@issue.project, @issue)}
diff --git a/app/views/notify/new_user_email.html.haml b/app/views/notify/new_user_email.html.haml
index dfbb5c75bd3..ec135ae994f 100644
--- a/app/views/notify/new_user_email.html.haml
+++ b/app/views/notify/new_user_email.html.haml
@@ -13,4 +13,5 @@
%p
= link_to "Click here to set your password", edit_password_url(@user, reset_password_token: @token)
%p
- = raw reset_token_expire_message
+ This link is valid for #{password_reset_token_valid_time}.
+ After it expires, you can #{link_to("request a new one", new_user_password_url(user_email: @user.email))}.
diff --git a/app/views/notify/new_user_email.text.erb b/app/views/notify/new_user_email.text.erb
index f3f20f3bfba..7e0db75472d 100644
--- a/app/views/notify/new_user_email.text.erb
+++ b/app/views/notify/new_user_email.text.erb
@@ -1,10 +1,17 @@
Hi <%= sanitize_name(@user.name) %>!
+<% if Gitlab::CurrentSettings.allow_signup? %>
+Your account has been created successfully.
+<% else %>
The Administrator created an account for you. Now you are a member of the company GitLab application.
+<% end %>
login.................. <%= @user.email %>
+
<% if @user.created_by_id %>
- <%= link_to "Click here to set your password", edit_password_url(@user, :reset_password_token => @token) %>
+Click here to set your password:
+<%= edit_password_url(@user, :reset_password_token => @token) %>
- <%= reset_token_expire_message %>
+This link is valid for <%= password_reset_token_valid_time %>. After it expires, you can request a new one here:
+<%= new_user_password_url(user_email: @user.email) %>
<% end %>
diff --git a/app/views/profiles/active_sessions/_active_session.html.haml b/app/views/profiles/active_sessions/_active_session.html.haml
index 2bf514d72a5..bb31049111c 100644
--- a/app/views/profiles/active_sessions/_active_session.html.haml
+++ b/app/views/profiles/active_sessions/_active_session.html.haml
@@ -8,18 +8,19 @@
%div
%strong= active_session.ip_address
- if is_current_session
- %div This is your current session
+ %div
+ = _('This is your current session')
- else
%div
- Last accessed on
+ = _('Last accessed on')
= l(active_session.updated_at, format: :short)
%div
%strong= active_session.browser
- on
+ = s_('ProfileSession|on')
%strong= active_session.os
%div
- %strong Signed in
- on
+ %strong= _('Signed in')
+ = s_('ProfileSession|on')
= l(active_session.created_at, format: :short)
diff --git a/app/views/profiles/active_sessions/index.html.haml b/app/views/profiles/active_sessions/index.html.haml
index 8688a52843d..d651319fc3f 100644
--- a/app/views/profiles/active_sessions/index.html.haml
+++ b/app/views/profiles/active_sessions/index.html.haml
@@ -1,4 +1,4 @@
-- page_title 'Active Sessions'
+- page_title _('Active Sessions')
- @content_class = "limit-container-width" unless fluid_layout
.row.prepend-top-default
@@ -6,7 +6,7 @@
%h4.prepend-top-0
= page_title
%p
- This is a list of devices that have logged into your account. Revoke any sessions that you do not recognize.
+ = _('This is a list of devices that have logged into your account. Revoke any sessions that you do not recognize.')
.col-lg-8
.append-bottom-default
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index 1823f191fb3..3c20518c038 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -1,4 +1,4 @@
-- page_title "Emails"
+- page_title _('Emails')
- @content_class = "limit-container-width" unless fluid_layout
.row.prepend-top-default
@@ -6,56 +6,58 @@
%h4.prepend-top-0
= page_title
%p
- Control emails linked to your account
+ = _('Control emails linked to your account')
.col-lg-8
%h4.prepend-top-0
- Add email address
+ = _('Add email address')
= form_for 'email', url: profile_emails_path do |f|
.form-group
- = f.label :email, class: 'label-bold'
+ = f.label :email, _('Email'), class: 'label-bold'
= f.text_field :email, class: 'form-control'
.prepend-top-default
- = f.submit 'Add email address', class: 'btn btn-success'
+ = f.submit _('Add email address'), class: 'btn btn-success'
%hr
%h4.prepend-top-0
- Linked emails (#{@emails.count + 1})
+ = _('Linked emails (%{email_count})') % { email_count: @emails.count + 1 }
.account-well.append-bottom-default
%ul
%li
- Your Primary Email will be used for avatar detection.
+ = _('Your Primary Email will be used for avatar detection.')
%li
- Your Commit Email will be used for web based operations, such as edits and merges.
+ = _('Your Commit Email will be used for web based operations, such as edits and merges.')
%li
- Your Notification Email will be used for account notifications.
+ - address = profile_notifications_path
+ - notification_message = _('Your Default Notification Email will be used for account notifications if a %{openingTag}group-specific email address%{closingTag} is not set.') % { openingTag: "<a href='#{address}'>".html_safe, closingTag: '</a>'.html_safe}
+ = notification_message.html_safe
%li
- Your Public Email will be displayed on your public profile.
+ = _('Your Public Email will be displayed on your public profile.')
%li
- All email addresses will be used to identify your commits.
+ = _('All email addresses will be used to identify your commits.')
%ul.content-list
%li
= render partial: 'shared/email_with_badge', locals: { email: @primary_email, verified: current_user.confirmed? }
%span.float-right
- %span.badge.badge-success Primary email
+ %span.badge.badge-success= s_('Profiles|Primary email')
- if @primary_email === current_user.commit_email
- %span.badge.badge-info Commit email
+ %span.badge.badge-info= s_('Profiles|Commit email')
- if @primary_email === current_user.public_email
- %span.badge.badge-info Public email
+ %span.badge.badge-info= s_('Profiles|Public email')
- if @primary_email === current_user.notification_email
- %span.badge.badge-info Notification email
+ %span.badge.badge-info= s_('Profiles|Default notification email')
- @emails.each do |email|
%li
= render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? }
%span.float-right
- if email.email === current_user.commit_email
- %span.badge.badge-info Commit email
+ %span.badge.badge-info= s_('Profiles|Commit email')
- if email.email === current_user.public_email
- %span.badge.badge-info Public email
+ %span.badge.badge-info= s_('Profiles|Public email')
- if email.email === current_user.notification_email
- %span.badge.badge-info Notification email
+ %span.badge.badge-info= s_('Profiles|Notification email')
- unless email.confirmed?
- - confirm_title = "#{email.confirmation_sent_at ? 'Resend' : 'Send'} confirmation email"
+ - confirm_title = "#{email.confirmation_sent_at ? _('Resend confirmation email') : _('Send confirmation email')}"
= link_to confirm_title, resend_confirmation_instructions_profile_email_path(email), method: :put, class: 'btn btn-sm btn-warning prepend-left-10'
- = link_to profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-danger prepend-left-10' do
- %span.sr-only Remove
+ = link_to profile_email_path(email), data: { confirm: _('Are you sure?')}, method: :delete, class: 'btn btn-sm btn-danger prepend-left-10' do
+ %span.sr-only= _('Remove')
= icon('trash')
diff --git a/app/views/profiles/gpg_keys/_form.html.haml b/app/views/profiles/gpg_keys/_form.html.haml
index 6c4cb614a2b..225487b2638 100644
--- a/app/views/profiles/gpg_keys/_form.html.haml
+++ b/app/views/profiles/gpg_keys/_form.html.haml
@@ -3,8 +3,8 @@
= form_errors(@gpg_key)
.form-group
- = f.label :key, class: 'label-bold'
- = f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: "Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'."
+ = f.label :key, s_('Profiles|Key'), class: 'label-bold'
+ = f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: _("Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'.")
.prepend-top-default
- = f.submit 'Add key', class: "btn btn-success"
+ = f.submit s_('Profiles|Add key'), class: "btn btn-success"
diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml
index d1fd7bc8e71..f8351644df5 100644
--- a/app/views/profiles/gpg_keys/_key.html.haml
+++ b/app/views/profiles/gpg_keys/_key.html.haml
@@ -9,17 +9,19 @@
%code= key.fingerprint
- if key.subkeys.present?
.subkeys
- %span.bold Subkeys:
+ %span.bold
+ = _('Subkeys')
+ = ':'
%ul.subkeys-list
- key.subkeys.each do |subkey|
%li
%code= subkey.fingerprint
.float-right
%span.key-created-at
- created #{time_ago_with_tooltip(key.created_at)}
- = link_to profile_gpg_key_path(key), data: { confirm: 'Are you sure? Removing this GPG key does not affect already signed commits.' }, method: :delete, class: "btn btn-danger prepend-left-10" do
- %span.sr-only Remove
+ = s_('Profiles|Created %{time_ago}'.html_safe) % { time_ago:time_ago_with_tooltip(key.created_at)}
+ = link_to profile_gpg_key_path(key), data: { confirm: _('Are you sure? Removing this GPG key does not affect already signed commits.') }, method: :delete, class: "btn btn-danger prepend-left-10" do
+ %span.sr-only= _('Remove')
= icon('trash')
- = link_to revoke_profile_gpg_key_path(key), data: { confirm: 'Are you sure? All commits that were signed with this GPG key will be unverified.' }, method: :put, class: "btn btn-danger prepend-left-10" do
- %span.sr-only Revoke
- Revoke
+ = link_to revoke_profile_gpg_key_path(key), data: { confirm: _('Are you sure? All commits that were signed with this GPG key will be unverified.') }, method: :put, class: "btn btn-danger prepend-left-10" do
+ %span.sr-only= _('Revoke')
+ = _('Revoke')
diff --git a/app/views/profiles/gpg_keys/_key_table.html.haml b/app/views/profiles/gpg_keys/_key_table.html.haml
index b9b60c218fd..ebbd1c8f672 100644
--- a/app/views/profiles/gpg_keys/_key_table.html.haml
+++ b/app/views/profiles/gpg_keys/_key_table.html.haml
@@ -6,6 +6,6 @@
- else
%p.settings-message.text-center
- if is_admin
- There are no GPG keys associated with this account.
+ = _('There are no GPG keys associated with this account.')
- else
- There are no GPG keys with access to your account.
+ = _('There are no GPG keys with access to your account.')
diff --git a/app/views/profiles/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml
index 1d2e41cb437..f9f898a9225 100644
--- a/app/views/profiles/gpg_keys/index.html.haml
+++ b/app/views/profiles/gpg_keys/index.html.haml
@@ -1,4 +1,4 @@
-- page_title "GPG Keys"
+- page_title _('GPG Keys')
- @content_class = "limit-container-width" unless fluid_layout
.row.prepend-top-default
@@ -6,16 +6,16 @@
%h4.prepend-top-0
= page_title
%p
- GPG keys allow you to verify signed commits.
+ = _('GPG keys allow you to verify signed commits.')
.col-lg-8
%h5.prepend-top-0
- Add a GPG key
+ = _('Add a GPG key')
%p.profile-settings-content
- Before you can add a GPG key you need to
- = link_to 'generate it.', help_page_path('user/project/repository/gpg_signed_commits/index.md')
+ - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/repository/gpg_signed_commits/index.md') }
+ = _('Before you can add a GPG key you need to %{help_link_start}Generate it.%{help_link_end}'.html_safe) % {help_link_start: help_link_start, help_link_end:'</a>'.html_safe }
= render 'form'
%hr
%h5
- Your GPG keys (#{@gpg_keys.count})
+ = _('Your GPG keys (%{count})') % { count:@gpg_keys.count}
.append-bottom-default
= render 'key_table'
diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index 21eef08983c..63ef5eaa172 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -3,11 +3,11 @@
= form_errors(@key)
.form-group
- = f.label :key, class: 'label-bold'
- %p= _("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key.")
- = f.text_area :key, class: "form-control js-add-ssh-key-validation-input qa-key-public-key-field", rows: 8, required: true, placeholder: s_('Profiles|Typically starts with "ssh-rsa …"')
+ = f.label :key, s_('Profiles|Key'), class: 'label-bold'
+ %p= _("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_ed25519.pub' or '~/.ssh/id_rsa.pub' and begins with 'ssh-ed25519' or 'ssh-rsa'. Don't use your private SSH key.")
+ = f.text_area :key, class: "form-control js-add-ssh-key-validation-input qa-key-public-key-field", rows: 8, required: true, placeholder: s_('Profiles|Typically starts with "ssh-ed25519 …" or "ssh-rsa …"')
.form-group
- = f.label :title, class: 'label-bold'
+ = f.label :title, _('Title'), class: 'label-bold'
= f.text_field :title, class: "form-control input-lg qa-key-title-field", required: true, placeholder: s_('Profiles|e.g. My MacBook key')
%p.form-text.text-muted= _('Name your individual key via a title')
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index ce20994b0f4..b9d73d89334 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -17,7 +17,8 @@
= key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : 'n/a'
.float-right
%span.key-created-at
- created #{time_ago_with_tooltip(key.created_at)}
- = link_to path_to_key(key, is_admin), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-transparent prepend-left-10" do
- %span.sr-only Remove
- = icon('trash')
+ = s_('Profiles|Created %{time_ago}'.html_safe) % { time_ago:time_ago_with_tooltip(key.created_at)}
+ - if key.can_delete?
+ = link_to path_to_key(key, is_admin), data: { confirm: _('Are you sure?')}, method: :delete, class: "btn btn-transparent prepend-left-10" do
+ %span.sr-only= _('Remove')
+ = icon('trash')
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index 88473c7f72d..0ef01dec493 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -3,25 +3,26 @@
.col-md-4
.card
.card-header
- SSH Key
+ = _('SSH Key')
%ul.content-list
%li
- %span.light Title:
+ %span.light= _('Title:')
%strong= @key.title
%li
- %span.light Created on:
+ %span.light= _('Created on:')
%strong= @key.created_at.to_s(:medium)
%li
- %span.light Last used on:
+ %span.light= _('Last used on:')
%strong= @key.last_used_at.try(:to_s, :medium) || 'N/A'
.col-md-8
= form_errors(@key, type: 'key') unless @key.valid?
%p
- %span.light Fingerprint:
+ %span.light= _('Fingerprint:')
%code.key-fingerprint= @key.fingerprint
%pre.well-pre
= @key.key
.col-md-12
.float-right
- = link_to 'Remove', path_to_key(@key, is_admin), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key qa-delete-key-button"
+ - if @key.can_delete?
+ = link_to _('Remove'), path_to_key(@key, is_admin), data: {confirm: _('Are you sure?')}, method: :delete, class: "btn btn-remove delete-key qa-delete-key-button"
diff --git a/app/views/profiles/keys/_key_table.html.haml b/app/views/profiles/keys/_key_table.html.haml
index e088140fdd2..4a6d8a1870d 100644
--- a/app/views/profiles/keys/_key_table.html.haml
+++ b/app/views/profiles/keys/_key_table.html.haml
@@ -6,6 +6,6 @@
- else
%p.settings-message.text-center
- if is_admin
- There are no SSH keys associated with this account.
+ = _('There are no SSH keys associated with this account.')
- else
- There are no SSH keys with access to your account.
+ = _('There are no SSH keys with access to your account.')
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 55ca8d0ebd4..da6aa0fce3a 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -1,4 +1,4 @@
-- page_title "SSH Keys"
+- page_title _('SSH Keys')
- @content_class = "limit-container-width" unless fluid_layout
.row.prepend-top-default
@@ -6,10 +6,10 @@
%h4.prepend-top-0
= page_title
%p
- SSH keys allow you to establish a secure connection between your computer and GitLab.
+ = _('SSH keys allow you to establish a secure connection between your computer and GitLab.')
.col-lg-8
%h5.prepend-top-0
- Add an SSH key
+ = _('Add an SSH key')
%p.profile-settings-content
- generate_link_url = help_page_path("ssh/README", anchor: 'generating-a-new-ssh-key-pair')
- existing_link_url = help_page_path("ssh/README", anchor: 'locating-an-existing-ssh-key-pair')
@@ -19,6 +19,6 @@
= render 'form'
%hr
%h5
- Your SSH keys (#{@keys.count})
+ = _('Your SSH keys (%{count})') % { count:@keys.count }
.append-bottom-default
= render 'key_table'
diff --git a/app/views/profiles/keys/show.html.haml b/app/views/profiles/keys/show.html.haml
index 28be6172219..360de7a0c11 100644
--- a/app/views/profiles/keys/show.html.haml
+++ b/app/views/profiles/keys/show.html.haml
@@ -1,5 +1,5 @@
- add_to_breadcrumbs "SSH Keys", profile_keys_path
- breadcrumb_title @key.title
-- page_title @key.title, "SSH Keys"
+- page_title @key.title, _('SSH Keys')
- @content_class = "limit-container-width" unless fluid_layout
= render "key_details"
diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml
index a12246bcdcc..cf17ee44145 100644
--- a/app/views/profiles/notifications/_group_settings.html.haml
+++ b/app/views/profiles/notifications/_group_settings.html.haml
@@ -1,12 +1,17 @@
-%li.notification-list-item
- %span.notification.fa.fa-holder.append-right-5
- - if setting.global?
- = notification_icon(current_user.global_notification_setting.level)
- - else
- = notification_icon(setting.level)
+.gl-responsive-table-row.notification-list-item
+ .table-section.section-40
+ %span.notification.fa.fa-holder.append-right-5
+ - if setting.global?
+ = notification_icon(current_user.global_notification_setting.level)
+ - else
+ = notification_icon(setting.level)
- %span.str-truncated
- = link_to group.name, group_path(group)
+ %span.str-truncated
+ = link_to group.name, group_path(group)
- .float-right
+ .table-section.section-30.text-right
= render 'shared/notifications/button', notification_setting: setting
+
+ .table-section.section-30
+ = form_for @user.notification_settings.find { |ns| ns.source == group }, url: profile_notifications_group_path(group), method: :put, html: { class: 'update-notifications' } do |f|
+ = f.select :notification_email, @user.all_emails, { include_blank: 'Global notification email' }, class: 'select2 js-group-notification-email'
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index fa35fbd3961..1f311e9a4a4 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -41,9 +41,8 @@
%h5
= _('Groups (%{count})') % { count: @group_notifications.count }
%div
- %ul.bordered-list
- - @group_notifications.each do |setting|
- = render 'group_settings', setting: setting, group: setting.source
+ - @group_notifications.each do |setting|
+ = render 'group_settings', setting: setting, group: setting.source
%h5
= _('Projects (%{count})') % { count: @project_notifications.count }
%p.account-well
diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml
index 0b4b9841ea1..ac8c31189d0 100644
--- a/app/views/profiles/passwords/edit.html.haml
+++ b/app/views/profiles/passwords/edit.html.haml
@@ -1,5 +1,5 @@
-- breadcrumb_title "Edit Password"
-- page_title "Password"
+- breadcrumb_title _('Edit Password')
+- page_title _('Password')
- @content_class = "limit-container-width" unless fluid_layout
.row.prepend-top-default
@@ -7,28 +7,29 @@
%h4.prepend-top-0
= page_title
%p
- After a successful password update, you will be redirected to the login page where you can log in with your new password.
+ = _('After a successful password update, you will be redirected to the login page where you can log in with your new password.')
.col-lg-8
%h5.prepend-top-0
- Change your password
- - unless @user.password_automatically_set?
- or recover your current one
+ - if @user.password_automatically_set
+ = _('Change your password')
+ - else
+ = _('Change your password or recover your current one')
= form_for @user, url: profile_password_path, method: :put, html: {class: "update-password"} do |f|
= form_errors(@user)
- unless @user.password_automatically_set?
.form-group
- = f.label :current_password, class: 'label-bold'
+ = f.label :current_password, _('Current password'), class: 'label-bold'
= f.password_field :current_password, required: true, class: 'form-control'
%p.form-text.text-muted
- You must provide your current password in order to change it.
+ = _('You must provide your current password in order to change it.')
.form-group
- = f.label :password, 'New password', class: 'label-bold'
+ = f.label :password, _('New password'), class: 'label-bold'
= f.password_field :password, required: true, class: 'form-control'
.form-group
- = f.label :password_confirmation, class: 'label-bold'
+ = f.label :password_confirmation, _('Password confirmation'), class: 'label-bold'
= f.password_field :password_confirmation, required: true, class: 'form-control'
.prepend-top-default.append-bottom-default
- = f.submit 'Save password', class: "btn btn-success append-right-10"
+ = f.submit _('Save password'), class: "btn btn-success append-right-10"
- unless @user.password_automatically_set?
- = link_to "I forgot my password", reset_profile_password_path, method: :put, class: "account-btn-link"
+ = link_to _('I forgot my password'), reset_profile_password_path, method: :put, class: "account-btn-link"
diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml
index 081166270ab..ce60455ab89 100644
--- a/app/views/profiles/passwords/new.html.haml
+++ b/app/views/profiles/passwords/new.html.haml
@@ -14,17 +14,17 @@
- unless @user.password_automatically_set?
.form-group.row
.col-sm-2.col-form-label
- = f.label :current_password
+ = f.label :current_password, _('Current password')
.col-sm-10
= f.password_field :current_password, required: true, class: 'form-control'
.form-group.row
.col-sm-2.col-form-label
- = f.label :password
+ = f.label :password, _('New password')
.col-sm-10
= f.password_field :password, required: true, class: 'form-control'
.form-group.row
.col-sm-2.col-form-label
- = f.label :password_confirmation
+ = f.label :password_confirmation, _('Password confirmation')
.col-sm-10
= f.password_field :password_confirmation, required: true, class: 'form-control'
.form-actions
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 46384bc28ef..4ebfaff0860 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -5,7 +5,8 @@
.col-lg-4.application-theme
%h4.prepend-top-0
= s_('Preferences|Navigation theme')
- %p= _('Customize the appearance of the application header and navigation sidebar.')
+ %p
+ = s_('Preferences|Customize the appearance of the application header and navigation sidebar.')
.col-lg-8.application-theme
- Gitlab::Themes.each do |theme|
= label_tag do
@@ -18,9 +19,9 @@
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
- = _('Syntax highlighting theme')
+ = s_('Preferences|Syntax highlighting theme')
%p
- = _('This setting allows you to customize the appearance of the syntax.')
+ = s_('Preferences|This setting allows you to customize the appearance of the syntax.')
= succeed '.' do
= link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'syntax-highlighting-theme'), target: '_blank'
.col-lg-8.syntax-theme
@@ -35,31 +36,31 @@
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
- = _('Behavior')
+ = s_('Preferences|Behavior')
%p
- = _('This setting allows you to customize the behavior of the system layout and default views.')
+ = s_('Preferences|This setting allows you to customize the behavior of the system layout and default views.')
= succeed '.' do
= link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'behavior'), target: '_blank'
.col-lg-8
.form-group
= f.label :layout, class: 'label-bold' do
- = _('Layout width')
+ = s_('Preferences|Layout width')
= f.select :layout, layout_choices, {}, class: 'form-control'
.form-text.text-muted
- = _('Choose between fixed (max. 1280px) and fluid (100%%) application layout.')
+ = s_('Preferences|Choose between fixed (max. 1280px) and fluid (100%%) application layout.')
.form-group
= f.label :dashboard, class: 'label-bold' do
- = _('Default dashboard')
+ = s_('Preferences|Default dashboard')
= f.select :dashboard, dashboard_choices, {}, class: 'form-control'
= render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific
.form-group
= f.label :project_view, class: 'label-bold' do
- = _('Project overview content')
+ = s_('Preferences|Project overview content')
= f.select :project_view, project_view_choices, {}, class: 'form-control'
.form-text.text-muted
- = _('Choose what content you want to see on a project’s overview page.')
+ = s_('Preferences|Choose what content you want to see on a project’s overview page.')
.col-sm-12
%hr
diff --git a/app/views/profiles/two_factor_auths/_codes.html.haml b/app/views/profiles/two_factor_auths/_codes.html.haml
index 759d39cf5f5..be0af977011 100644
--- a/app/views/profiles/two_factor_auths/_codes.html.haml
+++ b/app/views/profiles/two_factor_auths/_codes.html.haml
@@ -1,8 +1,6 @@
%p.slead
- Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one
- time each to regain access to your account. Please save them in a safe place, or you
- %b will
- lose access to your account.
+ - lose_2fa_message = _('Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account. Please save them in a safe place, or you %{b_start}will%{b_end} lose access to your account.') % { b_start:'<b>', b_end:'</b>' }
+ = lose_2fa_message.html_safe
.codes.card
%ul
@@ -11,5 +9,5 @@
%span.monospace= code
.d-flex
- = link_to 'Proceed', profile_account_path, class: 'btn btn-success append-right-10'
- = link_to 'Download codes', "data:text/plain;charset=utf-8,#{CGI.escape(@codes.join("\n"))}", download: "gitlab-recovery-codes.txt", class: 'btn btn-default'
+ = link_to _('Proceed'), profile_account_path, class: 'btn btn-success append-right-10'
+ = link_to _('Download codes'), "data:text/plain;charset=utf-8,#{CGI.escape(@codes.join("\n"))}", download: "gitlab-recovery-codes.txt", class: 'btn btn-default'
diff --git a/app/views/profiles/two_factor_auths/codes.html.haml b/app/views/profiles/two_factor_auths/codes.html.haml
index addf356697a..53907ebffab 100644
--- a/app/views/profiles/two_factor_auths/codes.html.haml
+++ b/app/views/profiles/two_factor_auths/codes.html.haml
@@ -1,5 +1,6 @@
-- page_title 'Recovery Codes', 'Two-factor Authentication'
+- page_title _('Recovery Codes'), _('Two-factor Authentication')
-%h3.page-title Two-factor Authentication Recovery codes
+%h3.page-title
+ = _('Two-factor Authentication Recovery codes')
%hr
= render 'codes'
diff --git a/app/views/profiles/two_factor_auths/create.html.haml b/app/views/profiles/two_factor_auths/create.html.haml
index e330aadac13..973eb8136c4 100644
--- a/app/views/profiles/two_factor_auths/create.html.haml
+++ b/app/views/profiles/two_factor_auths/create.html.haml
@@ -1,6 +1,6 @@
-- page_title 'Two-factor Authentication', 'Account'
+- page_title _('Two-factor Authentication'), _('Account')
.alert.alert-success
- Congratulations! You have enabled Two-factor Authentication!
+ = _('Congratulations! You have enabled Two-factor Authentication!')
= render 'codes'
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index d986c566928..5501e63e027 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -1,72 +1,68 @@
-- page_title 'Two-Factor Authentication', 'Account'
-- add_to_breadcrumbs("Two-Factor Authentication", profile_account_path)
+- page_title _('Two-Factor Authentication'), _('Account')
+- add_to_breadcrumbs(_('Two-Factor Authentication'), profile_account_path)
- @content_class = "limit-container-width" unless fluid_layout
.js-two-factor-auth{ 'data-two-factor-skippable' => "#{two_factor_skippable?}", 'data-two_factor_skip_url' => skip_profile_two_factor_auth_path }
.row.prepend-top-default
.col-lg-4
%h4.prepend-top-0
- Register Two-Factor Authenticator
+ = _('Register Two-Factor Authenticator')
%p
- Use an one time password authenticator on your mobile device or computer to enable two-factor authentication (2FA).
+ = _('Use an one time password authenticator on your mobile device or computer to enable two-factor authentication (2FA).')
.col-lg-8
- if current_user.two_factor_otp_enabled?
%p
- You've already enabled two-factor authentication using one time password authenticators. In order to register a different device, you must first disable two-factor authentication.
+ = _("You've already enabled two-factor authentication using one time password authenticators. In order to register a different device, you must first disable two-factor authentication.")
%p
- If you lose your recovery codes you can generate new ones, invalidating all previous codes.
+ = _('If you lose your recovery codes you can generate new ones, invalidating all previous codes.')
%div
- = link_to 'Disable two-factor authentication', profile_two_factor_auth_path,
+ = link_to _('Disable two-factor authentication'), profile_two_factor_auth_path,
method: :delete,
- data: { confirm: "Are you sure? This will invalidate your registered applications and U2F devices." },
+ data: { confirm: _('Are you sure? This will invalidate your registered applications and U2F devices.') },
class: 'btn btn-danger append-right-10'
= form_tag codes_profile_two_factor_auth_path, {style: 'display: inline-block', method: :post} do |f|
- = submit_tag 'Regenerate recovery codes', class: 'btn'
+ = submit_tag _('Regenerate recovery codes'), class: 'btn'
- else
%p
- Install a soft token authenticator like <a href="https://freeotp.github.io/">FreeOTP</a>
- or Google Authenticator from your application repository and scan this QR code.
- More information is available in the #{link_to('documentation', help_page_path('user/profile/account/two_factor_authentication'))}.
+ - help_link_start = '<a href="%{url}" target="_blank">' % { url: help_page_path('user/profile/account/two_factor_authentication') }
+ - register_2fa_token = _('Install a soft token authenticator like %{free_otp_link} or Google Authenticator from your application repository and scan this QR code. More information is available in the %{help_link_start}documentation%{help_link_end}.') % { free_otp_link:'<a href="https://freeotp.github.io/">FreeOTP</a>', help_link_start:help_link_start, help_link_end:'</a>' }
+ = register_2fa_token.html_safe
.row.append-bottom-10
.col-md-4
= raw @qr_code
.col-md-8
.account-well
%p.prepend-top-0.append-bottom-0
- Can't scan the code?
+ = _("Can't scan the code?")
%p.prepend-top-0.append-bottom-0
- To add the entry manually, provide the following details to the application on your phone.
+ = _('To add the entry manually, provide the following details to the application on your phone.')
%p.prepend-top-0.append-bottom-0
- Account:
- = @account_string
+ = _('Account: %{account}') % { account: @account_string }
%p.prepend-top-0.append-bottom-0
- Key:
- = current_user.otp_secret.scan(/.{4}/).join(' ')
+ = _('Key: %{key}') %{ key: current_user.otp_secret.scan(/.{4}/).join(' ') }
%p.two-factor-new-manual-content
- Time based: Yes
+ = _('Time based: Yes')
= form_tag profile_two_factor_auth_path, method: :post do |f|
- if @error
.alert.alert-danger
= @error
.form-group
- = label_tag :pin_code, nil, class: "label-bold"
+ = label_tag :pin_code, _('Pin code'), class: "label-bold"
= text_field_tag :pin_code, nil, class: "form-control", required: true
.prepend-top-default
- = submit_tag 'Register with two-factor app', class: 'btn btn-success'
+ = submit_tag _('Register with two-factor app'), class: 'btn btn-success'
%hr
.row.prepend-top-default
.col-lg-4
%h4.prepend-top-0
- Register Universal Two-Factor (U2F) Device
+ = _('Register Universal Two-Factor (U2F) Device')
%p
- Use a hardware device to add the second factor of authentication.
+ = _('Use a hardware device to add the second factor of authentication.')
%p
- As U2F devices are only supported by a few browsers, we require that you set up a
- two-factor authentication app before a U2F device. That way you'll always be able to
- log in - even when you're using an unsupported browser.
+ = _("As U2F devices are only supported by a few browsers, we require that you set up a two-factor authentication app before a U2F device. That way you'll always be able to log in - even when you're using an unsupported browser.")
.col-lg-8
- if @u2f_registration.errors.present?
= form_errors(@u2f_registration)
@@ -74,7 +70,8 @@
%hr
- %h5 U2F Devices (#{@u2f_registrations.length})
+ %h5
+ = _('U2F Devices (%{length})') % { length: @u2f_registrations.length }
- if @u2f_registrations.present?
.table-responsive
@@ -85,16 +82,16 @@
%col{ width: "20%" }
%thead
%tr
- %th Name
- %th Registered On
+ %th= _('Name')
+ %th= s_('2FADevice|Registered On')
%th
%tbody
- @u2f_registrations.each do |registration|
%tr
- %td= registration.name.presence || "<no name set>"
+ %td= registration.name.presence || _("<no name set>")
%td= registration.created_at.to_date.to_s(:medium)
- %td= link_to "Delete", profile_u2f_registration_path(registration), method: :delete, class: "btn btn-danger float-right", data: { confirm: "Are you sure you want to delete this device? This action cannot be undone." }
+ %td= link_to _('Delete'), profile_u2f_registration_path(registration), method: :delete, class: "btn btn-danger float-right", data: { confirm: _('Are you sure you want to delete this device? This action cannot be undone.') }
- else
.settings-message.text-center
- You don't have any U2F devices registered yet.
+ = _("You don't have any U2F devices registered yet.")
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
index 9c854369c93..28d4f8eb201 100644
--- a/app/views/projects/_import_project_pane.html.haml
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -8,61 +8,67 @@
.import-buttons
- if gitlab_project_import_enabled?
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
- = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitlab_export" } do
+ = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit', **tracking_attrs(track_label, 'click_button', 'gitlab_export') do
= icon('gitlab', text: 'GitLab export')
- if github_import_enabled?
%div
- = link_to new_import_github_path, class: 'btn js-import-github', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "github" } do
+ = link_to new_import_github_path, class: 'btn js-import-github', **tracking_attrs(track_label, 'click_button', 'github') do
= icon('github', text: 'GitHub')
- if bitbucket_import_enabled?
%div
= link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}",
- data: { track_label: "#{track_label}", track_event: "click_button", track_property: "bitbucket_cloud" } do
+ **tracking_attrs(track_label, 'click_button', 'bitbucket_cloud') do
= icon('bitbucket', text: 'Bitbucket Cloud')
- unless bitbucket_import_configured?
= render 'bitbucket_import_modal'
- if bitbucket_server_import_enabled?
%div
- = link_to status_import_bitbucket_server_path, class: "btn import_bitbucket",
- data: { track_label: "#{track_label}", track_event: "click_button", track_property: "bitbucket_server" } do
+ = link_to status_import_bitbucket_server_path, class: "btn import_bitbucket", **tracking_attrs(track_label, 'click_button', 'bitbucket_server') do
= icon('bitbucket-square', text: 'Bitbucket Server')
%div
- if gitlab_import_enabled?
%div
= link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}",
- data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitlab_com" } do
+ **tracking_attrs(track_label, 'click_button', 'gitlab_com') do
= icon('gitlab', text: 'GitLab.com')
- unless gitlab_import_configured?
= render 'gitlab_import_modal'
- if google_code_import_enabled?
%div
- = link_to new_import_google_code_path, class: 'btn import_google_code', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "google_code" } do
+ = link_to new_import_google_code_path, class: 'btn import_google_code', **tracking_attrs(track_label, 'click_button', 'google_code') do
= icon('google', text: 'Google Code')
- if fogbugz_import_enabled?
%div
- = link_to new_import_fogbugz_path, class: 'btn import_fogbugz', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "fogbugz" } do
+ = link_to new_import_fogbugz_path, class: 'btn import_fogbugz', **tracking_attrs(track_label, 'click_button', 'fogbugz') do
= icon('bug', text: 'Fogbugz')
- if gitea_import_enabled?
%div
- = link_to new_import_gitea_path, class: 'btn import_gitea', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitea" } do
+ = link_to new_import_gitea_path, class: 'btn import_gitea', **tracking_attrs(track_label, 'click_button', 'gitea') do
= custom_icon('gitea_logo')
Gitea
- if git_import_enabled?
%div
- %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active', data: { toggle_open_class: 'active', track_label: "#{track_label}" , track_event: "click_button", track_property: "repo_url" } } }
+ %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' }, **tracking_attrs(track_label, 'click_button', 'repo_url') }
= icon('git', text: 'Repo by URL')
- if manifest_import_enabled?
%div
- = link_to new_import_manifest_path, class: 'btn import_manifest', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "manifest_file" } do
+ = link_to new_import_manifest_path, class: 'btn import_manifest', **tracking_attrs(track_label, 'click_button', 'manifest_file') do
= icon('file-text-o', text: 'Manifest file')
+ - if phabricator_import_enabled?
+ %div
+ = link_to new_import_phabricator_path, class: 'btn import_phabricator', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "phabricator" } do
+ = custom_icon('issues')
+ = _("Phabricator Tasks")
+
+
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
= form_for @project, html: { class: 'new_project' } do |f|
%hr
diff --git a/app/views/projects/_merge_request_merge_checks_settings.html.haml b/app/views/projects/_merge_request_merge_checks_settings.html.haml
index 1ab467a3710..c21d333f21a 100644
--- a/app/views/projects/_merge_request_merge_checks_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_checks_settings.html.haml
@@ -3,7 +3,7 @@
.form-group
%b= s_('ProjectSettings|Merge checks')
%p.text-secondary= s_('ProjectSettings|These checks must pass before merge requests can be merged')
- .form-check.mb-2.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) }
+ .form-check.mb-2.builds-feature
= form.check_box :only_allow_merge_if_pipeline_succeeds, class: 'form-check-input'
= form.label :only_allow_merge_if_pipeline_succeeds, class: 'form-check-label' do
= s_('ProjectSettings|Pipelines must succeed')
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index 1c1c7d832bd..e423631ec99 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -1,4 +1,4 @@
-- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility
+- visibility_level = selected_visibility_level(@project, params.dig(:project, :visibility_level))
- ci_cd_only = local_assigns.fetch(:ci_cd_only, false)
- hide_init_with_readme = local_assigns.fetch(:hide_init_with_readme, false)
- track_label = local_assigns.fetch(:track_label, 'blank_project')
@@ -54,7 +54,7 @@
.form-group.row.initialize-with-readme-setting
%div{ :class => "col-sm-12" }
.form-check
- = check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input', data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "init_with_readme" }
+ = check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input qa-initialize-with-readme-checkbox', data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "init_with_readme" }
= label_tag 'project[initialize_with_readme]', class: 'form-check-label' do
.option-title
%strong Initialize repository with a README
diff --git a/app/views/projects/blob/_header_content.html.haml b/app/views/projects/blob/_header_content.html.haml
index 88fa31a73b0..7ed71a7d43c 100644
--- a/app/views/projects/blob/_header_content.html.haml
+++ b/app/views/projects/blob/_header_content.html.haml
@@ -6,7 +6,7 @@
= copy_file_path_button(blob.path)
- %small
+ %small.mr-1
= number_to_human_size(blob.raw_size)
- if blob.stored_externally? && blob.external_storage == :lfs
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index f4560404c03..bdf7b933ab8 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -53,9 +53,10 @@
%span.badge.badge-info= _('manual')
- if pipeline_link
- %td
- = link_to pipeline_path(pipeline) do
+ %td.pipeline-link
+ = link_to pipeline_path(pipeline), class: 'has-tooltip', title: _('Pipeline ID (IID)') do
%span.pipeline-id ##{pipeline.id}
+ %span.pipeline-iid (##{pipeline.iid})
%span by
- if pipeline.user
= user_avatar(user: pipeline.user, size: 20)
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index a0db48bf8ff..77ea2c04b28 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -28,7 +28,7 @@
= link_to project_tree_path(@project, @commit), class: "btn btn-default append-right-10 d-none d-sm-none d-md-inline" do
#{ _('Browse files') }
.dropdown.inline
- %a.btn.btn-default.dropdown-toggle.qa-options-button{ data: { toggle: "dropdown" } }
+ %a.btn.btn-default.dropdown-toggle.qa-options-button.d-md-inline{ data: { toggle: "dropdown" } }
%span= _('Options')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-right
@@ -81,7 +81,7 @@
= link_to project_pipeline_path(@project, last_pipeline.id), class: "ci-status-icon-#{last_pipeline.status}" do
= ci_icon_for_status(last_pipeline.status)
#{ _('Pipeline') }
- = link_to "##{last_pipeline.id}", project_pipeline_path(@project, last_pipeline.id)
+ = link_to "##{last_pipeline.id} (##{last_pipeline.iid})", project_pipeline_path(@project, last_pipeline.id), class: "has-tooltip", title: _('Pipeline ID (IID)')
= ci_label_for_status(last_pipeline.status)
- if last_pipeline.stages_count.nonzero?
#{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), last_pipeline.stages_count) }
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 771e1881e94..87b9920e8b4 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -15,7 +15,7 @@
%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
.avatar-cell.d-none.d-sm-block
- = author_avatar(commit, size: 36, has_tooltip: false)
+ = author_avatar(commit, size: 40, has_tooltip: false)
.commit-detail.flex-list
.commit-content.qa-commit-content
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 311b0be19ab..9587ea4696b 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -1,7 +1,7 @@
/ Side-by-side diff view
.text-file{ data: diff_view_data }
- %table.diff-wrap-lines.code.js-syntax-highlight
+ %table.diff-wrap-lines.code.code-commit.js-syntax-highlight
- diff_file.parallel_diff_lines.each do |line|
- left = line[:left]
- right = line[:right]
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index 018c5b38536..641a0689c26 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -3,7 +3,7 @@
.suppressed-container
%a.show-suppressed-diff.cursor-pointer.js-show-suppressed-diff= _("Changes suppressed. Click to show.")
-%table.text-file.diff-wrap-lines.code.js-syntax-highlight.commit-diff{ data: diff_view_data, class: too_big ? 'hide' : '' }
+%table.text-file.diff-wrap-lines.code.code-commit.js-syntax-highlight.commit-diff{ data: diff_view_data, class: too_big ? 'hide' : '' }
= render partial: "projects/diffs/line",
collection: diff_file.highlighted_diff_lines,
as: :line,
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index fbd70cd1906..457b2936278 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -8,18 +8,18 @@
- create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid)
- refs_path = refs_namespace_project_path(@project.namespace, @project, search: '')
- .create-mr-dropdown-wrap.d-inline-block{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } }
- .btn-group.unavailable
+ .create-mr-dropdown-wrap.d-inline-block.full-width-mobile{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } }
+ .btn-group.btn-group-sm.unavailable
%button.btn.btn-grouped{ type: 'button', disabled: 'disabled' }
= icon('spinner', class: 'fa-spin')
%span.text
Checking branch availability…
- .btn-group.available.hidden
+ .btn-group.btn-group-sm.available.hidden
%button.btn.js-create-merge-request.btn-success.btn-inverted{ type: 'button', data: { action: data_action } }
= value
- %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' }, display: 'static' } }
+ %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle.flex-grow-0{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' }, display: 'static' } }
= icon('caret-down')
.droplab-dropdown
diff --git a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
index 03226de120d..7bd5c437942 100644
--- a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
+++ b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
@@ -1,5 +1,5 @@
%inline-conflict-lines{ "inline-template" => "true", ":file" => "file" }
- %table.diff-wrap-lines.code.js-syntax-highlight
+ %table.diff-wrap-lines.code.code-commit.js-syntax-highlight
%tr.line_holder.diff-inline{ "v-for" => "line in file.inlineLines" }
%td.diff-line-num.new_line{ ":class" => "lineCssClass(line)", "v-if" => "!line.isHeader" }
%a {{line.new_line}}
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 05aeb5d972b..a201fafb949 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -31,29 +31,26 @@
.merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
.merge-request-tabs-container
- .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
- .fade-left= icon('angle-left')
- .fade-right= icon('angle-right')
- %ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs
- %li.notes-tab.qa-notes-tab
- = tab_link_for @merge_request, :show, force_link: @commit.present? do
- = _("Discussion")
- %span.badge.badge-pill= @merge_request.related_notes.user.count
- - if @merge_request.source_project
- %li.commits-tab
- = tab_link_for @merge_request, :commits do
- = _("Commits")
- %span.badge.badge-pill= @commits_count
- - if @pipelines.any?
- %li.pipelines-tab
- = tab_link_for @merge_request, :pipelines do
- = _("Pipelines")
- %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size
- %li.diffs-tab.qa-diffs-tab
- = tab_link_for @merge_request, :diffs do
- = _("Changes")
- %span.badge.badge-pill= @merge_request.diff_size
- .d-inline-flex.flex-wrap
+ %ul.merge-request-tabs.nav-tabs.nav.nav-links
+ %li.notes-tab.qa-notes-tab
+ = tab_link_for @merge_request, :show, force_link: @commit.present? do
+ = _("Discussion")
+ %span.badge.badge-pill= @merge_request.related_notes.user.count
+ - if @merge_request.source_project
+ %li.commits-tab
+ = tab_link_for @merge_request, :commits do
+ = _("Commits")
+ %span.badge.badge-pill= @commits_count
+ - if @pipelines.any?
+ %li.pipelines-tab
+ = tab_link_for @merge_request, :pipelines do
+ = _("Pipelines")
+ %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size
+ %li.diffs-tab.qa-diffs-tab
+ = tab_link_for @merge_request, :diffs do
+ = _("Changes")
+ %span.badge.badge-pill= @merge_request.diff_size
+ .d-flex.flex-wrap.align-items-center.justify-content-lg-end
#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@merge_request),
notes_filters: UserPreference.notes_filters.to_json } }
#js-vue-discussion-counter
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 78b416edd5c..1cee8be604a 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -59,7 +59,7 @@
= render_if_exists 'shared/milestones/burndown', milestone: @milestone, project: @project
- - if can?(current_user, :read_issue, @project) && @milestone.total_items_count(current_user).zero?
+ - if can?(current_user, :read_issue, @project) && @milestone.total_issues_count(current_user).zero?
.alert.alert-success.prepend-top-default
%span= _('Assign some issues to this milestone.')
- elsif @milestone.complete?(current_user) && @milestone.active?
diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml
index 8de84f82e9f..8a6e5fde99b 100644
--- a/app/views/projects/notes/_more_actions_dropdown.html.haml
+++ b/app/views/projects/notes/_more_actions_dropdown.html.haml
@@ -11,7 +11,7 @@
- unless is_current_user
%li
= link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
- = _('Report abuse to GitLab')
+ = _('Report abuse to admin')
- if note_editable
%li
= link_to note_url(note), method: :delete, data: { confirm: 'Are you sure you want to delete this comment?' }, remote: true, class: 'js-note-delete' do
diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml
index 1e50a101c1e..33f2166480b 100644
--- a/app/views/projects/pages_domains/_form.html.haml
+++ b/app/views/projects/pages_domains/_form.html.haml
@@ -1,29 +1,80 @@
- if @domain.errors.any?
- #error_explanation
- .alert.alert-danger
- - @domain.errors.full_messages.each do |msg|
- %p= msg
+ .alert.alert-danger
+ - @domain.errors.full_messages.each do |msg|
+ = msg
.form-group.row
.col-sm-2.col-form-label
= f.label :domain, _("Domain")
.col-sm-10
- = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control', disabled: @domain.persisted?
+ = f.text_field :domain, required: true, autocomplete: "off", class: "form-control", disabled: @domain.persisted?
- if Gitlab.config.pages.external_https
- .form-group.row
- .col-sm-2.col-form-label
- = f.label :certificate, _("Certificate (PEM)")
- .col-sm-10
- = f.text_area :certificate, rows: 5, class: 'form-control'
- %span.help-inline= _("Upload a certificate for your domain with all intermediates")
-
- .form-group.row
- .col-sm-2.col-form-label
- = f.label :key, _("Key (PEM)")
- .col-sm-10
- = f.text_area :key, rows: 5, class: 'form-control'
- %span.help-inline= _("Upload a private key for your certificate")
+
+ - auto_ssl_available = Feature.enabled?(:pages_auto_ssl)
+ - auto_ssl_enabled = @domain.auto_ssl_enabled?
+ - auto_ssl_available_and_enabled = auto_ssl_available && auto_ssl_enabled
+
+ - if auto_ssl_available
+ .form-group.row
+ .col-sm-2.col-form-label
+ %label{ for: "pages_domain_auto_ssl_enabled_button" }
+ - lets_encrypt_link_url = "https://letsencrypt.org/"
+ - lets_encrypt_link_start = "<a href=\"%{lets_encrypt_link_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-nowrap\">".html_safe % { lets_encrypt_link_url: lets_encrypt_link_url }
+ - lets_encrypt_link_end = "</a>".html_safe
+ = _("Automatic certificate management using %{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end}").html_safe % { lets_encrypt_link_start: lets_encrypt_link_start, lets_encrypt_link_end: lets_encrypt_link_end }
+
+ .col-sm-10.js-auto-ssl-toggle-container
+ %button{ type: "button", id: "pages_domain_auto_ssl_enabled_button",
+ class: "js-project-feature-toggle project-feature-toggle mt-2 #{"is-checked" if auto_ssl_available_and_enabled}",
+ "aria-label": _("Automatic certificate management using Let's Encrypt") }
+ = f.hidden_field :auto_ssl_enabled?, class: "js-project-feature-toggle-input"
+ %span.toggle-icon
+ = sprite_icon("status_success_borderless", size: 16, css_class: "toggle-icon-svg toggle-status-checked")
+ = sprite_icon("status_failed_borderless", size: 16, css_class: "toggle-icon-svg toggle-status-unchecked")
+ %p.text-secondary.mt-3
+ - docs_link_url = help_page_path("user/project/pages/lets_encrypt_for_gitlab_pages.md", anchor: "lets-encrypt-for-gitlab-pages")
+ - docs_link_start = "<a href=\"%{docs_link_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-nowrap\">".html_safe % { docs_link_url: docs_link_url }
+ - docs_link_end = "</a>".html_safe
+ = _("Let's Encrypt is a free, automated, and open certificate authority (CA) that gives digital certificates in order to enable HTTPS (SSL/TLS) for websites. Learn more about Let's Encrypt configuration by following the %{docs_link_start}documentation on GitLab Pages%{docs_link_end}.").html_safe % { docs_link_url: docs_link_url, docs_link_start: docs_link_start, docs_link_end: docs_link_end }
+
+ .js-shown-if-auto-ssl{ class: ("d-none" unless auto_ssl_available_and_enabled) }
+ .form-group.row
+ .col-sm-2.col-form-label
+ = f.label :certificate, _("Certificate (PEM)")
+ .col-sm-10
+ - if auto_ssl_available_and_enabled && !@domain.certificate.empty?
+ = f.text_area :certificate,
+ rows: 5,
+ class: "form-control",
+ disabled: true
+ %span.help-inline.text-muted= _("This certificate is automatically managed by Let's Encrypt")
+ - else
+ %p.text-secondary.form-control-plaintext= _("The certificate will be shown here once it has been obtained from Let's Encrypt. This process may take up to an hour to complete.")
+
+ .js-shown-unless-auto-ssl{ class: ("d-none" if auto_ssl_available_and_enabled) }
+ .form-group.row
+ .col-sm-2.col-form-label
+ = f.label :certificate, _("Certificate (PEM)")
+ .col-sm-10
+ = f.text_area :certificate,
+ rows: 5,
+ class: "form-control js-enabled-unless-auto-ssl",
+ value: (@domain.certificate unless auto_ssl_available_and_enabled),
+ disabled: auto_ssl_available_and_enabled
+ %span.help-inline.text-muted= _("Upload a certificate for your domain with all intermediates")
+
+ .form-group.row
+ .col-sm-2.col-form-label
+ = f.label :key, _("Key (PEM)")
+ .col-sm-10
+ = f.text_area :key,
+ rows: 5,
+ class: "form-control js-enabled-unless-auto-ssl",
+ value: (@domain.key unless auto_ssl_available_and_enabled),
+ disabled: auto_ssl_available_and_enabled
+ %span.help-inline.text-muted= _("Upload a private key for your certificate")
+
- else
.nothing-here-block
= _("Support for custom certificates is disabled. Ask your system's administrator to enable it.")
diff --git a/app/views/projects/pages_domains/_helper_text.html.haml b/app/views/projects/pages_domains/_helper_text.html.haml
new file mode 100644
index 00000000000..5a79fefabfc
--- /dev/null
+++ b/app/views/projects/pages_domains/_helper_text.html.haml
@@ -0,0 +1,9 @@
+- docs_link_url = help_page_path("user/project/pages/getting_started_part_three.md", anchor: "adding-certificates-to-your-project")
+- docs_link_start = "<a href=\"%{docs_link_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-nowrap\">".html_safe % { docs_link_url: docs_link_url }
+- docs_link_end = "</a>".html_safe
+
+-# Hiding behind a feature flag to avoid any changes to this feature's implemention
+-# when the :pages_auto_ssl feature flag is disabled. This check should be removed
+-# once the :pages_auto_ssl feature flag is removed.
+- if Feature.enabled?(:pages_auto_ssl)
+ %p= _("Learn more about adding certificates to your project by following the %{docs_link_start}documentation on GitLab Pages%{docs_link_end}.").html_safe % { docs_link_url: docs_link_url, docs_link_start: docs_link_start, docs_link_end: docs_link_end }
diff --git a/app/views/projects/pages_domains/edit.html.haml b/app/views/projects/pages_domains/edit.html.haml
index e11387ae742..7c0777e5496 100644
--- a/app/views/projects/pages_domains/edit.html.haml
+++ b/app/views/projects/pages_domains/edit.html.haml
@@ -3,6 +3,7 @@
- page_title @domain.domain
%h3.page-title
= @domain.domain
+= render 'projects/pages_domains/helper_text'
%hr.clearfix
%div
= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f|
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
index c7cefa87c76..e23ccb5d4c6 100644
--- a/app/views/projects/pages_domains/new.html.haml
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -2,6 +2,7 @@
- page_title _('New Pages Domain')
%h3.page-title
= _("New Pages Domain")
+= render 'projects/pages_domains/helper_text'
%hr.clearfix
%div
= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f|
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
index 9409418bbcc..82c1d57c97e 100644
--- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -18,22 +18,22 @@
.help-form
.form-group
- = label_tag :display_name, 'Display name', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :display_name, 'Display name', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :display_name, "GitLab / #{@project.full_name}", class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#display_name', class: 'input-group-text')
.form-group
- = label_tag :description, 'Description', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :description, 'Description', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#description', class: 'input-group-text')
.form-group
- = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block
+ = label_tag nil, 'Command trigger word', class: 'col-12 col-form-label label-bold'
+ .col-12
%p Fill in the word that works best for your team.
%p
Suggestions:
@@ -42,44 +42,44 @@
%code= @project.full_path
.form-group
- = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :request_url, 'Request URL', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :request_url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#request_url', class: 'input-group-text')
.form-group
- = label_tag nil, 'Request method', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block POST
+ = label_tag nil, 'Request method', class: 'col-12 col-form-label label-bold'
+ .col-12 POST
.form-group
- = label_tag :response_username, 'Response username', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :response_username, 'Response username', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :response_username, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#response_username', class: 'input-group-text')
.form-group
- = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :response_icon, 'Response icon', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#response_icon', class: 'input-group-text')
.form-group
- = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block Yes
+ = label_tag nil, 'Autocomplete', class: 'col-12 col-form-label label-bold'
+ .col-12 Yes
.form-group
- = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-12 col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :autocomplete_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#autocomplete_hint', class: 'input-group-text')
.form-group
- = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index 9a7004f89c0..9b7732abc62 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -27,8 +27,8 @@
.help-form
.form-group
- = label_tag nil, 'Command', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block
+ = label_tag nil, 'Command', class: 'col-12 col-form-label label-bold'
+ .col-12
%p Fill in the word that works best for your team.
%p
Suggestions:
@@ -37,50 +37,50 @@
%code= @project.full_path
.form-group
- = label_tag :url, 'URL', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :url, 'URL', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#url', class: 'input-group-text')
.form-group
- = label_tag nil, 'Method', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block POST
+ = label_tag nil, 'Method', class: 'col-12 col-form-label label-bold'
+ .col-12 POST
.form-group
- = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :customize_name, 'Customize name', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#customize_name', class: 'input-group-text')
.form-group
- = label_tag nil, 'Customize icon', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block
- = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
+ = label_tag nil, 'Customize icon', class: 'col-12 col-form-label label-bold'
+ .col-12
+ = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36, class: 'mr-3')
= link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
.form-group
- = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block Show this command in the autocomplete list
+ = label_tag nil, 'Autocomplete', class: 'col-12 col-form-label label-bold'
+ .col-12 Show this command in the autocomplete list
.form-group
- = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
.form-group
- = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text')
.form-group
- = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :descriptive_label, 'Descriptive label', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#descriptive_label', class: 'input-group-text')
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index b38b8e3f686..2d108a1cba5 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -26,6 +26,14 @@
%hr
.form-group
+ = f.fields_for :ci_cd_settings_attributes, @project.ci_cd_settings do |form|
+ = form.label :default_git_depth, _('Git shallow clone'), class: 'label-bold'
+ = form.number_field :default_git_depth, { class: 'form-control', min: 0, max: 1000 }
+ %p.form-text.text-muted
+ = _('The number of changes to be fetched from GitLab when cloning a repository. This can speed up Pipelines execution. Keep empty or set to 0 to disable shallow clone by default and make GitLab CI fetch all branches and tags each time.')
+
+ %hr
+ .form-group
= f.label :build_timeout_human_readable, _('Timeout'), class: 'label-bold'
= f.text_field :build_timeout_human_readable, class: 'form-control'
%p.form-text.text-muted
diff --git a/app/views/projects/settings/operations/_external_dashboard.html.haml b/app/views/projects/settings/operations/_external_dashboard.html.haml
index f049c35b38d..a124283921d 100644
--- a/app/views/projects/settings/operations/_external_dashboard.html.haml
+++ b/app/views/projects/settings/operations/_external_dashboard.html.haml
@@ -1,2 +1,3 @@
-.js-operation-settings{ data: { external_dashboard: { path: metrics_external_dashboard_url,
+.js-operation-settings{ data: { operations_settings_endpoint: project_settings_operations_path(@project),
+ external_dashboard: { url: metrics_external_dashboard_url,
help_page_path: help_page_path('user/project/operations/link_to_external_dashboard') } } }
diff --git a/app/views/projects/settings/repository/_protected_branches.html.haml b/app/views/projects/settings/repository/_protected_branches.html.haml
new file mode 100644
index 00000000000..31630828571
--- /dev/null
+++ b/app/views/projects/settings/repository/_protected_branches.html.haml
@@ -0,0 +1,2 @@
+= render "projects/protected_branches/index"
+= render "projects/protected_tags/index"
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index cb3a035c49e..ff30cc4f6db 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -3,14 +3,17 @@
- @content_class = "limit-container-width" unless fluid_layout
= render "projects/default_branch/show"
+= render_if_exists "projects/push_rules/index"
= render "projects/mirrors/mirror_repos"
-# Protected branches & tags use a lot of nested partials.
-# The shared parts of the views can be found in the `shared` directory.
-# Those are used throughout the actual views. These `shared` views are then
-# reused in EE.
-= render "projects/protected_branches/index"
-= render "projects/protected_tags/index"
+= render "projects/settings/repository/protected_branches"
+
= render @deploy_keys
= render "projects/deploy_tokens/index"
= render "projects/cleanup/show"
+
+= render_if_exists 'shared/promotions/promote_repository_features'
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 458096f9dd6..2e78b0bff3e 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -9,7 +9,7 @@
.nav-text.row-main-content
= s_('TagsPage|Tags give the ability to mark specific points in history as being important')
- .nav-controls.row-fixed-content
+ .nav-controls
= form_tag(filter_tags_path, method: :get) do
= search_field_tag :search, params[:search], { placeholder: s_('TagsPage|Filter by tag name'), id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 0be62bc5612..59232372150 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -18,7 +18,7 @@
- else
= s_("TagsPage|Can't find HEAD commit for this tag")
- .nav-controls.controls-flex
+ .nav-controls
- if can?(current_user, :push_code, @project)
= link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn btn-edit controls-item has-tooltip', title: s_('TagsPage|Edit release notes') do
= icon("pencil")
diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml
index e37fd7624be..065fef606d5 100644
--- a/app/views/projects/tree/_tree_commit_column.html.haml
+++ b/app/views/projects/tree/_tree_commit_column.html.haml
@@ -1,2 +1,3 @@
+- full_title = markdown_field(commit, :full_title)
%span.str-truncated
- = link_to_html commit.redacted_full_title_html, project_commit_path(@project, commit.id), title: commit.redacted_full_title_html, class: 'tree-commit-link'
+ = link_to_html full_title, project_commit_path(@project, commit.id), title: full_title, class: 'tree-commit-link'
diff --git a/app/views/repository_check_mailer/notify.html.haml b/app/views/repository_check_mailer/notify.html.haml
index d5327a2b4cc..dfcd1c6b19f 100644
--- a/app/views/repository_check_mailer/notify.html.haml
+++ b/app/views/repository_check_mailer/notify.html.haml
@@ -6,3 +6,5 @@
%p
= _("You are receiving this message because you are a GitLab administrator for %{url}.") % { url: Gitlab.config.gitlab.url }
+
+= render_if_exists 'repository_check_mailer/email_additional_text'
diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml
index 4af0c6bf84a..db0dcc8adfb 100644
--- a/app/views/search/_form.html.haml
+++ b/app/views/search/_form.html.haml
@@ -13,3 +13,4 @@
- unless params[:snippets].eql? 'true'
= render 'filter'
= button_tag _("Search"), class: "btn btn-success btn-search"
+ = render_if_exists 'search/form_elasticsearch'
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 8ae2807729b..12eb8d7fa81 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -1,5 +1,6 @@
- if @search_objects.to_a.empty?
= render partial: "search/results/empty"
+ = render_if_exists 'shared/promotions/promote_advanced_search'
- else
.row-content-block
- unless @search_objects.is_a?(Kaminari::PaginatableWithoutCount)
@@ -11,7 +12,7 @@
- elsif @group
- link_to_group = link_to(@group.name, @group)
= _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
-
+ = render_if_exists 'shared/promotions/promote_advanced_search'
.results.prepend-top-10
- if @scope == 'commits'
%ul.content-list.commit-list
@@ -20,7 +21,7 @@
.search-results
- if @scope == 'projects'
.term
- = render 'shared/projects/list', projects: @search_objects, pipeline_status: false
+ = render 'shared/projects/list', { projects: @search_objects, pipeline_status: false }.merge(@display_options)
- else
- locals = { projects: blob_projects(@search_objects) } if %w[blobs wiki_blobs].include?(@scope)
= render partial: "search/results/#{@scope.singularize}", collection: @search_objects, locals: locals
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index 796782035f2..1f055cdfa31 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -1,7 +1,7 @@
.search-result-row
%h4
= confidential_icon(issue)
- = link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do
+ = link_to namespace_project_issue_path(issue.project.namespace.becomes(Namespace), issue.project, issue) do
%span.term.str-truncated= issue.title
- if issue.closed?
%span.badge.badge-danger.prepend-left-5= _("Closed")
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index f0e0af11f27..074bb9bce8d 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -1,6 +1,6 @@
.search-result-row
%h4
- = link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do
+ = link_to namespace_project_merge_request_path(merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request) do
%span.term.str-truncated= merge_request.title
- if merge_request.merged?
%span.badge.badge-primary.prepend-left-5= _("Merged")
diff --git a/app/views/search/results/_milestone.html.haml b/app/views/search/results/_milestone.html.haml
index 2daa96e34d1..3201f1a7815 100644
--- a/app/views/search/results/_milestone.html.haml
+++ b/app/views/search/results/_milestone.html.haml
@@ -1,6 +1,6 @@
.search-result-row
%h4
- = link_to [milestone.project.namespace.becomes(Namespace), milestone.project, milestone] do
+ = link_to namespace_project_milestone_path(milestone.project.namespace.becomes(Namespace), milestone.project, milestone) do
%span.term.str-truncated= milestone.title
- if milestone.description.present?
diff --git a/app/views/sent_notifications/unsubscribe.html.haml b/app/views/sent_notifications/unsubscribe.html.haml
index ca392e1adfc..22fcfcda297 100644
--- a/app/views/sent_notifications/unsubscribe.html.haml
+++ b/app/views/sent_notifications/unsubscribe.html.haml
@@ -1,6 +1,6 @@
- noteable = @sent_notification.noteable
- noteable_type = @sent_notification.noteable_type.titleize.downcase
-- noteable_text = %(#{noteable.title} (#{noteable.to_reference}))
+- noteable_text = show_unsubscribe_title?(noteable) ? %(#{noteable.title} (#{noteable.to_reference})) : %(#{noteable.to_reference})
- page_title _("Unsubscribe"), noteable_text, noteable_type.pluralize, @sent_notification.project.full_name
%h3.page-title
diff --git a/app/views/shared/_email_with_badge.html.haml b/app/views/shared/_email_with_badge.html.haml
index ad863b1967d..294fe74a5ca 100644
--- a/app/views/shared/_email_with_badge.html.haml
+++ b/app/views/shared/_email_with_badge.html.haml
@@ -1,6 +1,6 @@
- css_classes = %w(badge badge-verification-status)
- css_classes << (verified ? 'verified': 'unverified')
-- text = verified ? 'Verified' : 'Unverified'
+- text = verified ? _('Verified') : _('Unverified')
.email-badge
.email-badge-email= email
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 3ee713cf499..d0f9374e832 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -1,11 +1,26 @@
- ci_cd_only = local_assigns.fetch(:ci_cd_only, false)
+- import_url = Gitlab::UrlSanitizer.new(f.object.import_url)
-.form-group.import-url-data
- = f.label :import_url, class: 'label-bold' do
- %span
- = _('Git repository URL')
+.import-url-data
+ .form-group
+ = f.label :import_url, class: 'label-bold' do
+ %span
+ = _('Git repository URL')
+ = f.text_field :import_url, value: import_url.sanitized_url,
+ autocomplete: 'off', class: 'form-control', placeholder: 'https://gitlab.company.com/group/project.git', required: true
- = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', required: true
+ .row
+ .form-group.col-md-6
+ = f.label :import_url_user, class: 'label-bold' do
+ %span
+ = _('Username (optional)')
+ = f.text_field :import_url_user, value: import_url.user, class: 'form-control', required: false, autocomplete: 'new-password'
+
+ .form-group.col-md-6
+ = f.label :import_url_password, class: 'label-bold' do
+ %span
+ = _('Password (optional)')
+ = f.password_field :import_url_password, class: 'form-control', required: false, autocomplete: 'new-password'
.info-well.prepend-top-20
.well-segment
@@ -13,7 +28,7 @@
%li
= _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe
%li
- = _('If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.').html_safe
+ = _('If your HTTP repository is not publicly accessible, add your credentials.')
%li
= import_will_timeout_message(ci_cd_only)
%li
diff --git a/app/views/shared/_old_visibility_level.html.haml b/app/views/shared/_old_visibility_level.html.haml
index fd576e4fbea..e8f3d888cce 100644
--- a/app/views/shared/_old_visibility_level.html.haml
+++ b/app/views/shared/_old_visibility_level.html.haml
@@ -1,6 +1,6 @@
.form-group.row
.col-sm-2.col-form-label
= _('Visibility level')
- = link_to icon('question-circle'), help_page_path("public_access/public_access")
+ = link_to icon('question-circle'), help_page_path("public_access/public_access"), target: '_blank'
.col-sm-10
= render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_visibility_level, form_model: form_model, with_label: with_label
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index 311dc69d213..c50826a7cda 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -32,7 +32,7 @@
%span.dropdown-toggle-text
{{ labelDropdownTitle }}
= icon('chevron-down')
- .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height
= render partial: "shared/issuable/label_page_default"
- if can?(current_user, :admin_label, current_board_parent)
= render partial: "shared/issuable/label_page_create", locals: { show_add_list: true }
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index f2c0c77a583..483652852b6 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -25,7 +25,7 @@
%span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) }
= multi_label_name(selected, label_name)
= icon('chevron-down')
- .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height
= render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create }
- if show_create && project && can?(current_user, :admin_label, project)
= render partial: "shared/issuable/label_page_create"
diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml
index d173e3c0192..a0d3bc64f1f 100644
--- a/app/views/shared/issuable/_label_page_create.html.haml
+++ b/app/views/shared/issuable/_label_page_create.html.haml
@@ -9,9 +9,7 @@
.dropdown-labels-error.js-label-error
%input#new_label_name.default-dropdown-input{ type: "text", placeholder: _('Name new label') }
.suggest-colors.suggest-colors-dropdown
- - suggested_colors.each do |color|
- = link_to '#', style: "background-color: #{color}", data: { color: color } do
- &nbsp
+ = render_suggested_colors
.dropdown-label-color-input
.dropdown-label-color-preview.js-dropdown-label-color-preview
%input#new_label_color.default-dropdown-input{ type: "text", placeholder: _('Assign custom color like #FF0000') }
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 63557c882f4..3a5adb34ad1 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -118,7 +118,7 @@
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true')
- .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height
= render partial: "shared/issuable/label_page_default"
- if issuable_sidebar.dig(:current_user, :can_admin_label)
= render partial: "shared/issuable/label_page_create"
@@ -158,7 +158,7 @@
%button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button',
data: { toggle: 'dropdown', display: 'static' } }
= _('Move issue')
- .dropdown-menu.dropdown-menu-selectable
+ .dropdown-menu.dropdown-menu-selectable.dropdown-extended-height
= dropdown_title(_('Move issue'))
= dropdown_filter(_('Search project'), search_id: 'sidebar-move-issue-dropdown-search')
= dropdown_content
diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml
index 967f31c8325..1dd97bc4ed1 100644
--- a/app/views/shared/issuable/_sort_dropdown.html.haml
+++ b/app/views/shared/issuable/_sort_dropdown.html.haml
@@ -10,12 +10,13 @@
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
%li
- = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority), sort_title)
- = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date), sort_title)
- = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated), sort_title)
- = sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone), sort_title)
- = sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date), sort_title) if viewing_issues
- = sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity), sort_title)
- = sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority), sort_title)
+ = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority), sort_title)
+ = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date), sort_title)
+ = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated), sort_title)
+ = sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone), sort_title)
+ = sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date), sort_title) if viewing_issues
+ = sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity), sort_title)
+ = sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority), sort_title)
+ = sortable_item(sort_title_relative_position, page_filter_path(sort: sort_value_relative_position), sort_title) if viewing_issues && Feature.enabled?(:manual_sorting)
= render_if_exists('shared/ee/issuable/sort_dropdown', viewing_issues: viewing_issues, sort_title: sort_title)
= issuable_sort_direction_button(sort_value)
diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml
index 4b88aff3313..78ff225daad 100644
--- a/app/views/shared/labels/_form.html.haml
+++ b/app/views/shared/labels/_form.html.haml
@@ -25,12 +25,7 @@
Choose any color.
%br
Or you can choose one of the suggested colors below
-
- .suggest-colors
- - suggested_colors.each do |color|
- = link_to '#', style: "background-color: #{color}", data: { color: color } do
- &nbsp;
-
+ = render_suggested_colors
.form-actions
- if @label.persisted?
= f.submit 'Save changes', class: 'btn btn-success js-save-button'
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index 9ec76d82d18..e83ca5eaab8 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -2,9 +2,12 @@
- group = group_link.group
- can_admin_member = can?(current_user, :admin_project_member, @project)
- dom_id = "group_member_#{group_link.id}"
-%li.member.group_member{ id: dom_id }
- %span.list-item-name
- = group_icon(group, class: "avatar s40", alt: '')
+
+-# Note this is just for groups. For individual members please see shared/members/_member
+
+%li.member.group_member.py-2.px-3.d-flex.flex-column.flex-md-row{ id: dom_id }
+ %span.list-item-name.mb-2.m-md-0
+ = group_icon(group, class: "avatar s40 flex-shrink-0 flex-grow-0", alt: '')
.user-info
= link_to group.full_name, group_path(group), class: 'member'
.cgray
@@ -13,10 +16,10 @@
·
%span{ class: ('text-warning' if group_link.expires_soon?) }
= _("Expires in %{expires_at}").html_safe % { expires_at: distance_of_time_in_words_to_now(group_link.expires_at) }
- .controls.member-controls
- = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form form-group row append-right-5' do
+ .controls.member-controls.align-items-center
+ = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form form-group d-sm-flex' do
= hidden_field_tag "group_link[group_access]", group_link.group_access
- .member-form-control.dropdown.append-right-5
+ .member-form-control.dropdown.mr-sm-2.d-sm-inline-block
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
disabled: !can_admin_member,
data: { toggle: "dropdown", field_name: "group_link[group_access]" } }
@@ -32,14 +35,14 @@
= link_to role, "javascript:void(0)",
class: ("is-active" if group_link.group_access == role_id),
data: { id: role_id, el_id: dom_id }
- .prepend-left-5.clearable-input.member-form-control
+ .clearable-input.member-form-control.d-sm-inline-block
= text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: _('Expiration date'), id: "member_expires_at_#{group.id}", disabled: !can_admin_member
%i.clear-icon.js-clear-input
- if can_admin_member
= link_to project_group_link_path(@project, group_link),
method: :delete,
data: { confirm: _("Are you sure you want to remove %{group_name}?") % { group_name: group.name } },
- class: 'btn btn-remove prepend-left-10' do
+ class: 'btn btn-remove m-0 ml-sm-2 align-self-center' do
%span.d-block.d-sm-none
= _("Delete")
= icon('trash', class: 'd-none d-sm-block')
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 2db1f67a793..331283f7eec 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -4,11 +4,14 @@
- member = local_assigns.fetch(:member)
- user = local_assigns.fetch(:user, member.user)
- source = member.source
+- override = member.try(:override)
-%li.member{ class: dom_class(member), id: dom_id(member) }
- %span.list-item-name
+-# Note this is just for individual members. For groups please see shared/members/_group
+
+%li.member.py-2.px-3.d-flex.flex-column{ class: [dom_class(member), ("is-overridden" if override), ("flex-md-row" unless force_mobile_view)], id: dom_id(member) }
+ %span.list-item-name.mb-2.m-md-0
- if user
- = image_tag avatar_icon_for_user(user, 40), class: "avatar s40", alt: ''
+ = image_tag avatar_icon_for_user(user, 40), class: "avatar s40 flex-shrink-0 flex-grow-0", alt: ''
.user-info
= link_to user.name, user_path(user), class: 'member js-user-link', data: { user_id: user.id }
= user_status(user)
@@ -42,7 +45,7 @@
= _("Expires in %{expires_at}").html_safe % { expires_at: distance_of_time_in_words_to_now(member.expires_at) }
- else
- = image_tag avatar_icon_for_email(member.invite_email, 40), class: "avatar s40", alt: ''
+ = image_tag avatar_icon_for_email(member.invite_email, 40), class: "avatar s40 flex-shrink-0 flex-grow-0", alt: ''
.user-info
.member= member.invite_email
.cgray
@@ -53,20 +56,22 @@
= time_ago_with_tooltip(member.created_at)
- if show_roles
- current_resource = @project || @group
- .controls.member-controls
+ .controls.member-controls.align-items-center
+ = render_if_exists 'shared/members/ee/ldap_tag', can_override: member.can_override?
- if show_controls && member.source == current_resource
- if member.can_resend_invite?
= link_to icon('paper-plane'), polymorphic_path([:resend_invite, member]),
method: :post,
- class: 'btn btn-default prepend-left-10 d-none d-sm-block',
+ class: 'btn btn-default align-self-center mr-sm-2',
title: _('Resend invite')
- if user != current_user && member.can_update?
- = form_for member, remote: true, html: { class: 'js-edit-member-form form-group row append-right-5' } do |f|
+ = form_for member, remote: true, html: { class: "js-edit-member-form form-group #{'d-sm-flex' unless force_mobile_view}" } do |f|
= f.hidden_field :access_level
- .member-form-control.dropdown.append-right-5
+ .member-form-control.dropdown{ class: [("mr-sm-2 d-sm-inline-block" unless force_mobile_view)] }
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
+ disabled: member.can_override? && !override,
data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } }
%span.dropdown-toggle-text
= member.human_access
@@ -80,20 +85,25 @@
= link_to role, "javascript:void(0)",
class: ("is-active" if member.access_level == role_id),
data: { id: role_id, el_id: dom_id(member) }
- .prepend-left-5.clearable-input.member-form-control
+ = render_if_exists 'shared/members/ee/revert_ldap_group_sync_option',
+ group: @group,
+ member: member,
+ can_override: member.can_override?
+ .clearable-input.member-form-control{ class: [("d-sm-inline-block" unless force_mobile_view)] }
= f.text_field :expires_at,
+ disabled: member.can_override? && !override,
class: 'form-control js-access-expiration-date js-member-update-control',
placeholder: _('Expiration date'),
id: "member_expires_at_#{member.id}",
data: { el_id: dom_id(member) }
%i.clear-icon.js-clear-input
- else
- %span.member-access-text= member.human_access
+ %span.member-access-text.user-access-role= member.human_access
- if member.can_approve?
= link_to polymorphic_path([:approve_access_request, member]),
method: :post,
- class: 'btn btn-success prepend-left-10',
+ class: "btn btn-success align-self-center m-0 mb-2 #{'mb-sm-0 ml-sm-2' unless force_mobile_view}",
title: _('Grant access') do
%span{ class: ('d-block d-sm-none' unless force_mobile_view) }
= _('Grant access')
@@ -105,16 +115,19 @@
= link_to icon('sign-out', text: _('Leave')), polymorphic_path([:leave, member.source, :members]),
method: :delete,
data: { confirm: leave_confirmation_message(member.source) },
- class: 'btn btn-remove prepend-left-10'
+ class: "btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}"
- else
= link_to member,
method: :delete,
data: { confirm: remove_member_message(member) },
- class: 'btn btn-remove prepend-left-10',
+ class: "btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}",
title: remove_member_title(member) do
%span{ class: ('d-block d-sm-none' unless force_mobile_view) }
= _("Delete")
- unless force_mobile_view
= icon('trash', class: 'd-none d-sm-block')
+ = render_if_exists 'shared/members/ee/override_member_buttons', group: @group, member: member, user: user, action: :edit, can_override: member.can_override?
- else
- %span.member-access-text= member.human_access
+ %span.member-access-text.user-access-role= member.human_access
+
+= render_if_exists 'shared/members/ee/override_member_buttons', group: @group, member: member, user: user, action: :confirm, can_override: member.can_override?
diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml
index 46f3f8428f1..fae7d6526e8 100644
--- a/app/views/shared/notes/_hints.html.haml
+++ b/app/views/shared/notes/_hints.html.haml
@@ -28,8 +28,9 @@
or
%button.attach-new-file.markdown-selector{ type: 'button' }= _("attach a new file")
- %button.markdown-selector.button-attach-file{ type: 'button', tabindex: '-1' }
+ %button.markdown-selector.button-attach-file.btn-link{ type: 'button', tabindex: '-1' }
= icon('file-image-o', class: 'toolbar-button-icon')
- = _("Attach a file")
+ %span.text-attach-file<>
+ = _("Attach a file")
%button.btn.btn-default.btn-sm.hide.button-cancel-uploading-files{ type: 'button' }= _("Cancel")
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 6fec435cc87..5c9dd72418e 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -31,7 +31,7 @@
.note-header
.note-header-info
%a{ href: user_path(note.author) }
- %span.note-header-author-name
+ %span.note-header-author-name.bold
= sanitize(note.author.name)
= user_status(note.author)
%span.note-headline-light
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index 31cc0c091dd..749aa258af6 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -1,14 +1,14 @@
- btn_class = local_assigns.fetch(:btn_class, nil)
- if notification_setting
- .js-notification-dropdown.notification-dropdown.home-panel-action-button.dropdown.inline
+ .js-notification-dropdown.notification-dropdown.mr-md-2.home-panel-action-button.dropdown.inline
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
= hidden_setting_source_input(notification_setting)
= f.hidden_field :level, class: "notification_setting_level"
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting"), class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: _("Notification setting"), class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
@@ -16,9 +16,11 @@
.sr-only Toggle dropdown
- else
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting"), class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
- = icon("bell", class: "js-notification-loading")
- = notification_title(notification_setting.level)
- = icon("caret-down")
+ .float-left
+ = icon("bell", class: "js-notification-loading")
+ = notification_title(notification_setting.level)
+ .float-right
+ = icon("caret-down")
= render "shared/notifications/notification_dropdown", notification_setting: notification_setting
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 6dc61088e65..a71bfd624e4 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -9,7 +9,7 @@
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
.user-profile
- .cover-block.user-cover-block.top-area
+ .cover-block.user-cover-block
.cover-controls
- if @user == current_user
= link_to profile_path, class: 'btn btn-default has-tooltip', title: s_('UserProfile|Edit profile'), 'aria-label': 'Edit profile' do
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index e4e85de93da..fd0cc5fb24e 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1,6 +1,8 @@
---
- auto_devops:auto_devops_disable
+- auto_merge:auto_merge_process
+
- cronjob:admin_email
- cronjob:expire_build_artifacts
- cronjob:gitlab_usage_ping
diff --git a/app/workers/auto_merge_process_worker.rb b/app/workers/auto_merge_process_worker.rb
new file mode 100644
index 00000000000..cd81cdbc60c
--- /dev/null
+++ b/app/workers/auto_merge_process_worker.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class AutoMergeProcessWorker
+ include ApplicationWorker
+
+ queue_namespace :auto_merge
+
+ def perform(merge_request_id)
+ MergeRequest.find_by_id(merge_request_id).try do |merge_request|
+ AutoMergeService.new(merge_request.project, merge_request.merge_user)
+ .process(merge_request)
+ end
+ end
+end
diff --git a/app/workers/pages_domain_verification_cron_worker.rb b/app/workers/pages_domain_verification_cron_worker.rb
index 92d62a15aee..60703c83e9e 100644
--- a/app/workers/pages_domain_verification_cron_worker.rb
+++ b/app/workers/pages_domain_verification_cron_worker.rb
@@ -5,6 +5,8 @@ class PagesDomainVerificationCronWorker
include CronjobQueue
def perform
+ return if Gitlab::Database.read_only?
+
PagesDomain.needs_verification.find_each do |domain|
PagesDomainVerificationWorker.perform_async(domain.id)
end
diff --git a/app/workers/pages_domain_verification_worker.rb b/app/workers/pages_domain_verification_worker.rb
index b3319ff5a13..7817b2ee5fc 100644
--- a/app/workers/pages_domain_verification_worker.rb
+++ b/app/workers/pages_domain_verification_worker.rb
@@ -5,6 +5,8 @@ class PagesDomainVerificationWorker
# rubocop: disable CodeReuse/ActiveRecord
def perform(domain_id)
+ return if Gitlab::Database.read_only?
+
domain = PagesDomain.find_by(id: domain_id)
return unless domain
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index 8a9ee7808e4..9410fd1a786 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -3,47 +3,12 @@
class PipelineScheduleWorker
include ApplicationWorker
include CronjobQueue
- include ::Gitlab::ExclusiveLeaseHelpers
- EXCLUSIVE_LOCK_KEY = 'pipeline_schedules:run:lock'
- LOCK_TIMEOUT = 50.minutes
-
- # rubocop: disable CodeReuse/ActiveRecord
def perform
- in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
- Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now)
- .preload(:owner, :project).find_each do |schedule|
-
- schedule.schedule_next_run!
-
- Ci::CreatePipelineService.new(schedule.project,
- schedule.owner,
- ref: schedule.ref)
- .execute!(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: schedule)
- rescue => e
- error(schedule, e)
+ Ci::PipelineSchedule.runnable_schedules.preloaded.find_in_batches do |schedules|
+ schedules.each do |schedule|
+ Ci::PipelineScheduleService.new(schedule.project, schedule.owner).execute(schedule)
end
end
end
- # rubocop: enable CodeReuse/ActiveRecord
-
- private
-
- def error(schedule, error)
- failed_creation_counter.increment
-
- Rails.logger.error "Failed to create a scheduled pipeline. " \
- "schedule_id: #{schedule.id} message: #{error.message}"
-
- Gitlab::Sentry
- .track_exception(error,
- issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
- extra: { schedule_id: schedule.id })
- end
-
- def failed_creation_counter
- @failed_creation_counter ||=
- Gitlab::Metrics.counter(:pipeline_schedule_creation_failed_total,
- "Counter of failed attempts of pipeline schedule creation")
- end
end
diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb
index 4f349ed922c..666331e6cd4 100644
--- a/app/workers/pipeline_success_worker.rb
+++ b/app/workers/pipeline_success_worker.rb
@@ -6,13 +6,7 @@ class PipelineSuccessWorker
queue_namespace :pipeline_processing
- # rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
- Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
- MergeRequests::MergeWhenPipelineSucceedsService
- .new(pipeline.project, nil)
- .trigger(pipeline)
- end
+ # no-op
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 9a9c0c9d803..3f1639ec2ed 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -74,6 +74,8 @@ class PostReceive
def process_wiki_changes(post_received)
post_received.project.touch(:last_activity_at, :last_repository_updated_at)
+ post_received.project.wiki.repository.expire_statistics_caches
+ ProjectCacheWorker.perform_async(post_received.project.id, [], [:wiki_size])
end
def log(message)
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index b2e0701008a..4e8ea903139 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -16,10 +16,12 @@ class ProjectCacheWorker
def perform(project_id, files = [], statistics = [])
project = Project.find_by(id: project_id)
- return unless project && project.repository.exists?
+ return unless project
update_statistics(project, statistics)
+ return unless project.repository.exists?
+
project.repository.refresh_method_caches(files.map(&:to_sym))
project.cleanup
diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb
index f72331c003a..43e0b9db22f 100644
--- a/app/workers/run_pipeline_schedule_worker.rb
+++ b/app/workers/run_pipeline_schedule_worker.rb
@@ -21,6 +21,30 @@ class RunPipelineScheduleWorker
Ci::CreatePipelineService.new(schedule.project,
user,
ref: schedule.ref)
- .execute(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule)
+ .execute!(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule)
+ rescue Ci::CreatePipelineService::CreateError
+ # no-op. This is a user operation error such as corrupted .gitlab-ci.yml.
+ rescue => e
+ error(schedule, e)
+ end
+
+ private
+
+ def error(schedule, error)
+ failed_creation_counter.increment
+
+ Rails.logger.error "Failed to create a scheduled pipeline. " \
+ "schedule_id: #{schedule.id} message: #{error.message}"
+
+ Gitlab::Sentry
+ .track_exception(error,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
+ extra: { schedule_id: schedule.id })
+ end
+
+ def failed_creation_counter
+ @failed_creation_counter ||=
+ Gitlab::Metrics.counter(:pipeline_schedule_creation_failed_total,
+ "Counter of failed attempts of pipeline schedule creation")
end
end
diff --git a/app/workers/todos_destroyer/confidential_issue_worker.rb b/app/workers/todos_destroyer/confidential_issue_worker.rb
index 481fde8c83d..240a5f98ad5 100644
--- a/app/workers/todos_destroyer/confidential_issue_worker.rb
+++ b/app/workers/todos_destroyer/confidential_issue_worker.rb
@@ -5,8 +5,8 @@ module TodosDestroyer
include ApplicationWorker
include TodosDestroyerQueue
- def perform(issue_id)
- ::Todos::Destroy::ConfidentialIssueService.new(issue_id).execute
+ def perform(issue_id = nil, project_id = nil)
+ ::Todos::Destroy::ConfidentialIssueService.new(issue_id: issue_id, project_id: project_id).execute
end
end
end
diff --git a/babel.config.js b/babel.config.js
index df30892731d..05554e8763e 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -39,7 +39,7 @@ if (BABEL_ENV === 'karma' || BABEL_ENV === 'coverage') {
}
// Jest is running in node environment, so we need additional plugins
-const isJest = !!process.env.JEST_WORKER_ID;
+const isJest = Boolean(process.env.JEST_WORKER_ID);
if (isJest) {
plugins.push('@babel/plugin-transform-modules-commonjs');
/*
diff --git a/changelogs/unreleased/10088-move-code-differences-EE-to-CE.yml b/changelogs/unreleased/10088-move-code-differences-EE-to-CE.yml
new file mode 100644
index 00000000000..1297e9712fa
--- /dev/null
+++ b/changelogs/unreleased/10088-move-code-differences-EE-to-CE.yml
@@ -0,0 +1,5 @@
+---
+title: "Added code differnces from EE in file 'app/assets/javascripts/pages/projects/project.js' to CE"
+merge_request: 29271
+author: Michel Engelen
+type: other
diff --git a/changelogs/unreleased/10795-add-epic-tree-BE-CE-epic-graphql-support.yml b/changelogs/unreleased/10795-add-epic-tree-BE-CE-epic-graphql-support.yml
new file mode 100644
index 00000000000..4c85d4f9acb
--- /dev/null
+++ b/changelogs/unreleased/10795-add-epic-tree-BE-CE-epic-graphql-support.yml
@@ -0,0 +1,5 @@
+---
+title: Added reference, web_path, and relative_position fields to GraphQL Issue
+merge_request: 28998
+author:
+type: changed
diff --git a/changelogs/unreleased/11204-turn-off-mask-by-default.yml b/changelogs/unreleased/11204-turn-off-mask-by-default.yml
new file mode 100644
index 00000000000..5c554e04d45
--- /dev/null
+++ b/changelogs/unreleased/11204-turn-off-mask-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Default masked to false for new variables
+merge_request: 28186
+author:
+type: changed
diff --git a/changelogs/unreleased/28996-create-mvc-ui-in-haml.yml b/changelogs/unreleased/28996-create-mvc-ui-in-haml.yml
new file mode 100644
index 00000000000..9c6897babb4
--- /dev/null
+++ b/changelogs/unreleased/28996-create-mvc-ui-in-haml.yml
@@ -0,0 +1,5 @@
+---
+title: Add auto SSL toggle option to Pages domain settings page
+merge_request: 26438
+author:
+type: added
diff --git a/changelogs/unreleased/37495.yml b/changelogs/unreleased/37495.yml
new file mode 100644
index 00000000000..f6d421fc45b
--- /dev/null
+++ b/changelogs/unreleased/37495.yml
@@ -0,0 +1,5 @@
+---
+title: Add documentation links for confidental and locked discussions
+merge_request: 29073
+author:
+type: changed
diff --git a/changelogs/unreleased/47846-position-is-off-when-visiting-files-with-anchors.yml b/changelogs/unreleased/47846-position-is-off-when-visiting-files-with-anchors.yml
new file mode 100644
index 00000000000..21dc170f1ca
--- /dev/null
+++ b/changelogs/unreleased/47846-position-is-off-when-visiting-files-with-anchors.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Position is off when visiting files with anchors
+merge_request: 28913
+author:
+type: fixed
diff --git a/changelogs/unreleased/50106-hide-whitespace-changes.yml b/changelogs/unreleased/50106-hide-whitespace-changes.yml
new file mode 100644
index 00000000000..e95953c8665
--- /dev/null
+++ b/changelogs/unreleased/50106-hide-whitespace-changes.yml
@@ -0,0 +1,5 @@
+---
+title: Fix whitespace changes visibility when the related file was initially collapsed
+merge_request: 28950
+author: Ondřej Budai
+type: fixed
diff --git a/changelogs/unreleased/50850-kerrizor-extend-api-to-accept-start_project-option.yml b/changelogs/unreleased/50850-kerrizor-extend-api-to-accept-start_project-option.yml
new file mode 100644
index 00000000000..45770e1012c
--- /dev/null
+++ b/changelogs/unreleased/50850-kerrizor-extend-api-to-accept-start_project-option.yml
@@ -0,0 +1,5 @@
+---
+title: Add API support for committing changes to different projects in same fork network
+merge_request: 27915
+author:
+type: added
diff --git a/changelogs/unreleased/51022-added-extended-height-to-labels-dropdown.yml b/changelogs/unreleased/51022-added-extended-height-to-labels-dropdown.yml
new file mode 100644
index 00000000000..07bf8b04bbe
--- /dev/null
+++ b/changelogs/unreleased/51022-added-extended-height-to-labels-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: "Added the `.extended-height` class to the labels-dropdown"
+merge_request: 28659
+author: Michel Engelen
+type: other
diff --git a/changelogs/unreleased/51636-task-list-api-pderichs.yml b/changelogs/unreleased/51636-task-list-api-pderichs.yml
new file mode 100644
index 00000000000..f18a0936ab2
--- /dev/null
+++ b/changelogs/unreleased/51636-task-list-api-pderichs.yml
@@ -0,0 +1,5 @@
+---
+title: Add task count and completed count to responses of Issue and MR
+merge_request: 28859
+author:
+type: added
diff --git a/changelogs/unreleased/53134-multiple-extendes-for-a-job.yml b/changelogs/unreleased/53134-multiple-extendes-for-a-job.yml
new file mode 100644
index 00000000000..e09de8ac8fc
--- /dev/null
+++ b/changelogs/unreleased/53134-multiple-extendes-for-a-job.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for multiple job parents in GitLab CI YAML.
+merge_request: 26801
+author: Wolphin (Nikita)
+type: added
diff --git a/changelogs/unreleased/54140-non-ar-cache-commit-markdown.yml b/changelogs/unreleased/54140-non-ar-cache-commit-markdown.yml
new file mode 100644
index 00000000000..efda07380a4
--- /dev/null
+++ b/changelogs/unreleased/54140-non-ar-cache-commit-markdown.yml
@@ -0,0 +1,5 @@
+---
+title: Use Redis for CacheMarkDownField on non AR models
+merge_request: 29054
+author:
+type: performance
diff --git a/changelogs/unreleased/54169-flash-warning-rebrush.yml b/changelogs/unreleased/54169-flash-warning-rebrush.yml
new file mode 100644
index 00000000000..420cc26a8cc
--- /dev/null
+++ b/changelogs/unreleased/54169-flash-warning-rebrush.yml
@@ -0,0 +1,5 @@
+---
+title: "Rebrush of flash-warning according to the new design (brighter background and darker font)"
+merge_request: 28916
+author: Michel Engelen
+type: changed
diff --git a/changelogs/unreleased/55033-discussion-system-note-alignment.yml b/changelogs/unreleased/55033-discussion-system-note-alignment.yml
new file mode 100644
index 00000000000..27072ec7e12
--- /dev/null
+++ b/changelogs/unreleased/55033-discussion-system-note-alignment.yml
@@ -0,0 +1,5 @@
+---
+title: Align system note within discussion with other notes
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/55125-mr-tab-scrolling.yml b/changelogs/unreleased/55125-mr-tab-scrolling.yml
new file mode 100644
index 00000000000..e03ff6c5060
--- /dev/null
+++ b/changelogs/unreleased/55125-mr-tab-scrolling.yml
@@ -0,0 +1,5 @@
+---
+title: Update merge request tabs so they no longer scroll
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/55253-activity-feed-ui-enhance-line-height.yml b/changelogs/unreleased/55253-activity-feed-ui-enhance-line-height.yml
new file mode 100644
index 00000000000..f7dd8c59a7c
--- /dev/null
+++ b/changelogs/unreleased/55253-activity-feed-ui-enhance-line-height.yml
@@ -0,0 +1,5 @@
+---
+title: Enhance line-height of Activity feed UI
+merge_request: 28856
+author: Jacopo Beschi @jacopo-beschi
+type: changed
diff --git a/changelogs/unreleased/56959-drop-project_auto_devops_domain.yml b/changelogs/unreleased/56959-drop-project_auto_devops_domain.yml
new file mode 100644
index 00000000000..c529749670d
--- /dev/null
+++ b/changelogs/unreleased/56959-drop-project_auto_devops_domain.yml
@@ -0,0 +1,5 @@
+---
+title: Removes project_auto_devops#domain column
+merge_request: 28574
+author:
+type: other
diff --git a/changelogs/unreleased/57037-fix-mr-checkboxes-mobile-alignment.yml b/changelogs/unreleased/57037-fix-mr-checkboxes-mobile-alignment.yml
new file mode 100644
index 00000000000..a2de6cd6d45
--- /dev/null
+++ b/changelogs/unreleased/57037-fix-mr-checkboxes-mobile-alignment.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Merge Request merge checkbox alignment on mobile view
+merge_request: 28845
+author:
+type: fixed
diff --git a/changelogs/unreleased/57414-show-pipeline-iid.yml b/changelogs/unreleased/57414-show-pipeline-iid.yml
new file mode 100644
index 00000000000..596ae00e5a3
--- /dev/null
+++ b/changelogs/unreleased/57414-show-pipeline-iid.yml
@@ -0,0 +1,5 @@
+---
+title: Show Pipeline IID everywhere Pipeline ID is shown
+merge_request: 57414
+author: Mike Scott
+type: added
diff --git a/changelogs/unreleased/58269-separate-update-patch.yml b/changelogs/unreleased/58269-separate-update-patch.yml
new file mode 100644
index 00000000000..e9b44257b07
--- /dev/null
+++ b/changelogs/unreleased/58269-separate-update-patch.yml
@@ -0,0 +1,5 @@
+---
+title: Do not display Update app button when saving Knative domain name
+merge_request: 28904
+author:
+type: changed
diff --git a/changelogs/unreleased/58297-remove-extraneous-gitaly-calls-from-md-rendering.yml b/changelogs/unreleased/58297-remove-extraneous-gitaly-calls-from-md-rendering.yml
new file mode 100644
index 00000000000..25cc973159f
--- /dev/null
+++ b/changelogs/unreleased/58297-remove-extraneous-gitaly-calls-from-md-rendering.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce Gitaly calls to improve performance when rendering suggestions
+merge_request: 29027
+author:
+type: performance
diff --git a/changelogs/unreleased/58941-use-gitlab-serverless-with-existing-knative-installation.yml b/changelogs/unreleased/58941-use-gitlab-serverless-with-existing-knative-installation.yml
new file mode 100644
index 00000000000..53be008816d
--- /dev/null
+++ b/changelogs/unreleased/58941-use-gitlab-serverless-with-existing-knative-installation.yml
@@ -0,0 +1,5 @@
+---
+title: Enable function features for external Knative installations
+merge_request: 27173
+author:
+type: changed
diff --git a/changelogs/unreleased/59376-Report-abuse-to-GitLab-should-be-Report-abuse-in-non-gitlab-com-instances.yml b/changelogs/unreleased/59376-Report-abuse-to-GitLab-should-be-Report-abuse-in-non-gitlab-com-instances.yml
new file mode 100644
index 00000000000..0904f788b6f
--- /dev/null
+++ b/changelogs/unreleased/59376-Report-abuse-to-GitLab-should-be-Report-abuse-in-non-gitlab-com-instances.yml
@@ -0,0 +1,5 @@
+---
+title: Change "Report abuse to GitLab" to more generic wording
+merge_request: 28884
+author: Marc Schwede
+type: other
diff --git a/changelogs/unreleased/59587-add-graphql-logging.yml b/changelogs/unreleased/59587-add-graphql-logging.yml
new file mode 100644
index 00000000000..74c2a734f37
--- /dev/null
+++ b/changelogs/unreleased/59587-add-graphql-logging.yml
@@ -0,0 +1,5 @@
+---
+title: Add dedicated logging for GraphQL queries
+merge_request: 27885
+author:
+type: other
diff --git a/changelogs/unreleased/60034-default-web-ide-s-merge-request-checkbox-to-true.yml b/changelogs/unreleased/60034-default-web-ide-s-merge-request-checkbox-to-true.yml
new file mode 100644
index 00000000000..fdf80c660f7
--- /dev/null
+++ b/changelogs/unreleased/60034-default-web-ide-s-merge-request-checkbox-to-true.yml
@@ -0,0 +1,5 @@
+---
+title: Default MR checkbox to true in most cases
+merge_request: !28665
+author:
+type: changed
diff --git a/changelogs/unreleased/60303-replace-sidekiq-mtail-metrics.yml b/changelogs/unreleased/60303-replace-sidekiq-mtail-metrics.yml
new file mode 100644
index 00000000000..90b72ec05c7
--- /dev/null
+++ b/changelogs/unreleased/60303-replace-sidekiq-mtail-metrics.yml
@@ -0,0 +1,5 @@
+---
+title: Replaces sidekiq mtail metrics with ruby instrumentation metrics
+merge_request: 29215
+author:
+type: changed
diff --git a/changelogs/unreleased/60323-inline-validation-for-users-name-and-username-length.yml b/changelogs/unreleased/60323-inline-validation-for-users-name-and-username-length.yml
new file mode 100644
index 00000000000..83b7bd3433e
--- /dev/null
+++ b/changelogs/unreleased/60323-inline-validation-for-users-name-and-username-length.yml
@@ -0,0 +1,5 @@
+---
+title: Update registration form to indicate invalid name or username length on input
+merge_request: 28095
+author: Jiaan Louw
+type: changed
diff --git a/changelogs/unreleased/60778-input-text-height.yml b/changelogs/unreleased/60778-input-text-height.yml
deleted file mode 100644
index c956ead5db2..00000000000
--- a/changelogs/unreleased/60778-input-text-height.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix input group height
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/60819_yamllint_gitlabci.yml b/changelogs/unreleased/60819_yamllint_gitlabci.yml
new file mode 100644
index 00000000000..aba3b206f7e
--- /dev/null
+++ b/changelogs/unreleased/60819_yamllint_gitlabci.yml
@@ -0,0 +1,5 @@
+---
+title: Fix yaml linting for GitLab CI inside project (.gitlab/ci) *.yml files and CI template files
+merge_request: 27576
+author: Will Hall
+type: fixed
diff --git a/changelogs/unreleased/609120-ref-link.yml b/changelogs/unreleased/609120-ref-link.yml
new file mode 100644
index 00000000000..97c93b7ff53
--- /dev/null
+++ b/changelogs/unreleased/609120-ref-link.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes Ref link being displayed as raw HTML in the Pipelines page
+merge_request: 28823
+author:
+type: fixed
diff --git a/changelogs/unreleased/60987-emoji-picker-popup.yml b/changelogs/unreleased/60987-emoji-picker-popup.yml
new file mode 100644
index 00000000000..3bccec8e164
--- /dev/null
+++ b/changelogs/unreleased/60987-emoji-picker-popup.yml
@@ -0,0 +1,5 @@
+---
+title: Fix emoji picker visibility issue
+merge_request: 28984
+author:
+type: fixed
diff --git a/changelogs/unreleased/61024-update-resolved-icon.yml b/changelogs/unreleased/61024-update-resolved-icon.yml
new file mode 100644
index 00000000000..4a4de9eb13a
--- /dev/null
+++ b/changelogs/unreleased/61024-update-resolved-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Add check circle filled icon for resolved comments
+merge_request: 28663
+author:
+type: changed
diff --git a/changelogs/unreleased/6104-ee-ce-difference.yml b/changelogs/unreleased/6104-ee-ce-difference.yml
new file mode 100644
index 00000000000..59d31daf0eb
--- /dev/null
+++ b/changelogs/unreleased/6104-ee-ce-difference.yml
@@ -0,0 +1,5 @@
+---
+title: Unified EE/CS differences in repository/show.html
+merge_request: 13562
+author:
+type: other
diff --git a/changelogs/unreleased/61045-charts-with-many-overlapping-series-display-incorrectly.yml b/changelogs/unreleased/61045-charts-with-many-overlapping-series-display-incorrectly.yml
new file mode 100644
index 00000000000..53cc0a15417
--- /dev/null
+++ b/changelogs/unreleased/61045-charts-with-many-overlapping-series-display-incorrectly.yml
@@ -0,0 +1,5 @@
+---
+title: Eliminate color inconsistencies in metric graphs
+merge_request: 29127
+author:
+type: fixed
diff --git a/changelogs/unreleased/61323-snippet-copy-icon-button-is-misaligned.yml b/changelogs/unreleased/61323-snippet-copy-icon-button-is-misaligned.yml
new file mode 100644
index 00000000000..94666ac12ec
--- /dev/null
+++ b/changelogs/unreleased/61323-snippet-copy-icon-button-is-misaligned.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Snippet icon button is misaligned
+merge_request: 28522
+author:
+type: other
diff --git a/changelogs/unreleased/61324-non-project-snippet-new-snippet-button-should-be-green-outline.yml b/changelogs/unreleased/61324-non-project-snippet-new-snippet-button-should-be-green-outline.yml
index 350fd525a30..a7f5706058d 100644
--- a/changelogs/unreleased/61324-non-project-snippet-new-snippet-button-should-be-green-outline.yml
+++ b/changelogs/unreleased/61324-non-project-snippet-new-snippet-button-should-be-green-outline.yml
@@ -1,5 +1,5 @@
---
title: Give New Snippet button green outline
merge_request: 28559
-author: Marcel van Remmerden
+author:
type: other
diff --git a/changelogs/unreleased/61339-Add-underline-to-attach-a-file.yml b/changelogs/unreleased/61339-Add-underline-to-attach-a-file.yml
new file mode 100644
index 00000000000..e446459ffc8
--- /dev/null
+++ b/changelogs/unreleased/61339-Add-underline-to-attach-a-file.yml
@@ -0,0 +1,5 @@
+---
+title: Add hover and focus to Attach a file
+merge_request: 28682
+author:
+type: fixed
diff --git a/changelogs/unreleased/61565-merge-request-discussion-text-jumps-when-resolved.yml b/changelogs/unreleased/61565-merge-request-discussion-text-jumps-when-resolved.yml
new file mode 100644
index 00000000000..718604c9ceb
--- /dev/null
+++ b/changelogs/unreleased/61565-merge-request-discussion-text-jumps-when-resolved.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Merge request discussion text jumps when resolved
+merge_request: 28995
+author:
+type: fixed
diff --git a/changelogs/unreleased/61788-predefined-colours-dont-have-descriptive-labels.yml b/changelogs/unreleased/61788-predefined-colours-dont-have-descriptive-labels.yml
new file mode 100644
index 00000000000..25c83d24007
--- /dev/null
+++ b/changelogs/unreleased/61788-predefined-colours-dont-have-descriptive-labels.yml
@@ -0,0 +1,5 @@
+---
+title: Adds a text label to color pickers to improve accessibility.
+merge_request: 28343
+author: Chris Toynbee
+type: changed
diff --git a/changelogs/unreleased/61821-tooltip-consistency.yml b/changelogs/unreleased/61821-tooltip-consistency.yml
new file mode 100644
index 00000000000..9b131907ebf
--- /dev/null
+++ b/changelogs/unreleased/61821-tooltip-consistency.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Tooltip Consistency
+merge_request: 28839
+author:
+type: fixed
diff --git a/changelogs/unreleased/61960-translatable-strings-in-issue-closure-emails.yml b/changelogs/unreleased/61960-translatable-strings-in-issue-closure-emails.yml
new file mode 100644
index 00000000000..50b3efba0a5
--- /dev/null
+++ b/changelogs/unreleased/61960-translatable-strings-in-issue-closure-emails.yml
@@ -0,0 +1,5 @@
+---
+title: I18n for issue closure reason in emails
+merge_request: 28489
+author: Michał Zając
+type: changed
diff --git a/changelogs/unreleased/61988-collapse-icon-on-merge-request-diff-larger-than-profile-picture.yml b/changelogs/unreleased/61988-collapse-icon-on-merge-request-diff-larger-than-profile-picture.yml
new file mode 100644
index 00000000000..4d2f73ce2ff
--- /dev/null
+++ b/changelogs/unreleased/61988-collapse-icon-on-merge-request-diff-larger-than-profile-picture.yml
@@ -0,0 +1,5 @@
+---
+title: Change collapse icon size to size of profile picture
+merge_request: 28512
+author:
+type: other
diff --git a/changelogs/unreleased/62091-remove-time-windows-flag.yml b/changelogs/unreleased/62091-remove-time-windows-flag.yml
new file mode 100644
index 00000000000..c6c11328312
--- /dev/null
+++ b/changelogs/unreleased/62091-remove-time-windows-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Allow users to specify a time range on metrics dashboard
+merge_request: 28670
+author:
+type: added
diff --git a/changelogs/unreleased/62092-missing-padding-next-to-time-windows-dropdown-on-metrics-dashboard.yml b/changelogs/unreleased/62092-missing-padding-next-to-time-windows-dropdown-on-metrics-dashboard.yml
new file mode 100644
index 00000000000..3317d505924
--- /dev/null
+++ b/changelogs/unreleased/62092-missing-padding-next-to-time-windows-dropdown-on-metrics-dashboard.yml
@@ -0,0 +1,5 @@
+---
+title: Added padding to time window dropdown in monitor dashboard
+merge_request: 28897
+author:
+type: fixed
diff --git a/changelogs/unreleased/62124-new-threaded-discussion-design.yml b/changelogs/unreleased/62124-new-threaded-discussion-design.yml
new file mode 100644
index 00000000000..6614e05be74
--- /dev/null
+++ b/changelogs/unreleased/62124-new-threaded-discussion-design.yml
@@ -0,0 +1,5 @@
+---
+title: Implement borderless discussion design with new reply field
+merge_request: 28580
+author:
+type: added
diff --git a/changelogs/unreleased/62144-fix-option-dropdown-button-size.yml b/changelogs/unreleased/62144-fix-option-dropdown-button-size.yml
new file mode 100644
index 00000000000..86d8f4536f9
--- /dev/null
+++ b/changelogs/unreleased/62144-fix-option-dropdown-button-size.yml
@@ -0,0 +1,5 @@
+---
+title: Fix inconsistent option dropdown button height to match adjacent button
+merge_request: 29096
+author:
+type: fixed
diff --git a/changelogs/unreleased/62418-project-default-git-depth.yml b/changelogs/unreleased/62418-project-default-git-depth.yml
new file mode 100644
index 00000000000..b5647cd0859
--- /dev/null
+++ b/changelogs/unreleased/62418-project-default-git-depth.yml
@@ -0,0 +1,5 @@
+---
+title: Add project level git depth CI/CD setting
+merge_request: 28919
+author:
+type: added
diff --git a/changelogs/unreleased/62432-fix-participants-wrapping.yml b/changelogs/unreleased/62432-fix-participants-wrapping.yml
new file mode 100644
index 00000000000..a7e4bd372de
--- /dev/null
+++ b/changelogs/unreleased/62432-fix-participants-wrapping.yml
@@ -0,0 +1,5 @@
+---
+title: Fix participants list wrapping
+merge_request: 28873
+author:
+type: fixed
diff --git a/changelogs/unreleased/62485-label-weights.yml b/changelogs/unreleased/62485-label-weights.yml
new file mode 100644
index 00000000000..354b18be11e
--- /dev/null
+++ b/changelogs/unreleased/62485-label-weights.yml
@@ -0,0 +1,5 @@
+---
+title: Give labels consistent weight
+merge_request: 28895
+author:
+type: fixed
diff --git a/changelogs/unreleased/62487-external-policy-desc.yml b/changelogs/unreleased/62487-external-policy-desc.yml
new file mode 100644
index 00000000000..2e787b89db1
--- /dev/null
+++ b/changelogs/unreleased/62487-external-policy-desc.yml
@@ -0,0 +1,5 @@
+---
+title: Move text under p tag
+merge_request: 28901
+author:
+type: fixed
diff --git a/changelogs/unreleased/62656-adjusted-dropdown-styles.yml b/changelogs/unreleased/62656-adjusted-dropdown-styles.yml
new file mode 100644
index 00000000000..36f14ae2741
--- /dev/null
+++ b/changelogs/unreleased/62656-adjusted-dropdown-styles.yml
@@ -0,0 +1,5 @@
+---
+title: "changed the styles on `Add List` dropdown to look more like the EE vesion"
+merge_request: 29338
+author: Michel Engelen
+type: changed
diff --git a/changelogs/unreleased/62847-url-for-the-next-request-with-pagination-is-missing-port.yml b/changelogs/unreleased/62847-url-for-the-next-request-with-pagination-is-missing-port.yml
new file mode 100644
index 00000000000..fca92a0d4dc
--- /dev/null
+++ b/changelogs/unreleased/62847-url-for-the-next-request-with-pagination-is-missing-port.yml
@@ -0,0 +1,5 @@
+---
+title: Include the port in the URLs of the API Link headers
+merge_request: 29267
+author:
+type: fixed
diff --git a/changelogs/unreleased/62974-follow-up-from-wip-align-merge-request-icons-and-text.yml b/changelogs/unreleased/62974-follow-up-from-wip-align-merge-request-icons-and-text.yml
new file mode 100644
index 00000000000..811986e6857
--- /dev/null
+++ b/changelogs/unreleased/62974-follow-up-from-wip-align-merge-request-icons-and-text.yml
@@ -0,0 +1,5 @@
+---
+title: 'Make margin between buttons consistent'
+merge_request: 29378
+author:
+type: other
diff --git a/changelogs/unreleased/dm-disable-two-step-rebase.yml b/changelogs/unreleased/9121-sort-relative-position.yml
index 342d61a20d6..adc9e87e5bb 100644
--- a/changelogs/unreleased/dm-disable-two-step-rebase.yml
+++ b/changelogs/unreleased/9121-sort-relative-position.yml
@@ -1,5 +1,5 @@
---
-title: Disable two-step rebase which could cause rebases to hang
-merge_request: 28778
+title: Allow issue list to be sorted by relative order
+merge_request: 28566
author:
-type: other
+type: added
diff --git a/changelogs/unreleased/9578-adjust-milestone-completion-rate.yml b/changelogs/unreleased/9578-adjust-milestone-completion-rate.yml
new file mode 100644
index 00000000000..0694e1462cf
--- /dev/null
+++ b/changelogs/unreleased/9578-adjust-milestone-completion-rate.yml
@@ -0,0 +1,5 @@
+---
+title: Adjust milestone completion rate to be based on issues count.
+merge_request: 28777
+author:
+type: changed
diff --git a/changelogs/unreleased/abstract-auto-merge.yml b/changelogs/unreleased/abstract-auto-merge.yml
new file mode 100644
index 00000000000..d3069a3e500
--- /dev/null
+++ b/changelogs/unreleased/abstract-auto-merge.yml
@@ -0,0 +1,5 @@
+---
+title: Refactor and abstract Auto Merge Processes
+merge_request: 28595
+author:
+type: other
diff --git a/changelogs/unreleased/ac-graphql-stats.yml b/changelogs/unreleased/ac-graphql-stats.yml
new file mode 100644
index 00000000000..8837dce4d89
--- /dev/null
+++ b/changelogs/unreleased/ac-graphql-stats.yml
@@ -0,0 +1,5 @@
+---
+title: Add Namespace and ProjectStatistics to GraphQL API
+merge_request: 28277
+author:
+type: added
diff --git a/changelogs/unreleased/ac-graphql-wikisize.yml b/changelogs/unreleased/ac-graphql-wikisize.yml
new file mode 100644
index 00000000000..be9c347ec21
--- /dev/null
+++ b/changelogs/unreleased/ac-graphql-wikisize.yml
@@ -0,0 +1,5 @@
+---
+title: Expose wiki_size on GraphQL API
+merge_request: 29123
+author:
+type: added
diff --git a/changelogs/unreleased/ac-namespaces-stats-no-coalesce.yml b/changelogs/unreleased/ac-namespaces-stats-no-coalesce.yml
new file mode 100644
index 00000000000..bd005206d4e
--- /dev/null
+++ b/changelogs/unreleased/ac-namespaces-stats-no-coalesce.yml
@@ -0,0 +1,5 @@
+---
+title: Forbid NULL in project_statistics.packages_size
+merge_request: 28400
+author:
+type: other
diff --git a/changelogs/unreleased/add-constraint-for-milestone-dates.yml b/changelogs/unreleased/add-constraint-for-milestone-dates.yml
new file mode 100644
index 00000000000..485149cf62e
--- /dev/null
+++ b/changelogs/unreleased/add-constraint-for-milestone-dates.yml
@@ -0,0 +1,5 @@
+---
+title: Limit milestone dates to before year 9999
+merge_request: 28742
+author: Luke Picciau
+type: fixed
diff --git a/changelogs/unreleased/add-wiki-size-to-statistics.yml b/changelogs/unreleased/add-wiki-size-to-statistics.yml
new file mode 100644
index 00000000000..85b6d7a1774
--- /dev/null
+++ b/changelogs/unreleased/add-wiki-size-to-statistics.yml
@@ -0,0 +1,5 @@
+---
+title: Add wiki size to project statistics
+merge_request: 25321
+author: Peter Marko
+type: added
diff --git a/changelogs/unreleased/always-show-pipelines-must-succeed-checkbox.yml b/changelogs/unreleased/always-show-pipelines-must-succeed-checkbox.yml
new file mode 100644
index 00000000000..d60dd65be8a
--- /dev/null
+++ b/changelogs/unreleased/always-show-pipelines-must-succeed-checkbox.yml
@@ -0,0 +1,5 @@
+---
+title: Always show "Pipelines must succeed" checkbox
+merge_request: 28651
+author:
+type: fixed
diff --git a/changelogs/unreleased/bump-auto-devops-helm-2-14-0.yml b/changelogs/unreleased/bump-auto-devops-helm-2-14-0.yml
new file mode 100644
index 00000000000..ecfbc97a8c5
--- /dev/null
+++ b/changelogs/unreleased/bump-auto-devops-helm-2-14-0.yml
@@ -0,0 +1,5 @@
+---
+title: Bump Helm version in Auto-DevOps.gitlab-ci.yml to 2.14.0
+merge_request: 28527
+author:
+type: other
diff --git a/changelogs/unreleased/bvl-use-global-ids-graphql.yml b/changelogs/unreleased/bvl-use-global-ids-graphql.yml
new file mode 100644
index 00000000000..34cb65e6001
--- /dev/null
+++ b/changelogs/unreleased/bvl-use-global-ids-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Use global IDs when exposing GraphQL resources
+merge_request: 29080
+author:
+type: added
diff --git a/changelogs/unreleased/cancel-auto-merge-when-merge-request-is-closed.yml b/changelogs/unreleased/cancel-auto-merge-when-merge-request-is-closed.yml
new file mode 100644
index 00000000000..d38046ebcbf
--- /dev/null
+++ b/changelogs/unreleased/cancel-auto-merge-when-merge-request-is-closed.yml
@@ -0,0 +1,5 @@
+---
+title: Cancel auto merge when merge request is closed
+merge_request: 28782
+author:
+type: fixed
diff --git a/changelogs/unreleased/ce-jej-fix-git-http-with-sso-enforcement.yml b/changelogs/unreleased/ce-jej-fix-git-http-with-sso-enforcement.yml
new file mode 100644
index 00000000000..a795e33b00d
--- /dev/null
+++ b/changelogs/unreleased/ce-jej-fix-git-http-with-sso-enforcement.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid setting Gitlab::Session on sessionless requests and Git HTTP
+merge_request: 29146
+author:
+type: fixed
diff --git a/changelogs/unreleased/ci-variable-conjunction.yml b/changelogs/unreleased/ci-variable-conjunction.yml
new file mode 100644
index 00000000000..839c4285f3a
--- /dev/null
+++ b/changelogs/unreleased/ci-variable-conjunction.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for && and || to CI Pipeline Expressions. Change CI variable expression matching for Lexeme::Pattern to eagerly return tokens.
+merge_request: 27925
+author: Martin Manelli
+type: added
diff --git a/changelogs/unreleased/copy-button-in-modals.yml b/changelogs/unreleased/copy-button-in-modals.yml
new file mode 100644
index 00000000000..bc18eb9ab26
--- /dev/null
+++ b/changelogs/unreleased/copy-button-in-modals.yml
@@ -0,0 +1,5 @@
+---
+title: Add a New Copy Button That Works in Modals
+merge_request: 28676
+author:
+type: added
diff --git a/changelogs/unreleased/dm-http-hostname-override.yml b/changelogs/unreleased/dm-http-hostname-override.yml
new file mode 100644
index 00000000000..f84f36a0010
--- /dev/null
+++ b/changelogs/unreleased/dm-http-hostname-override.yml
@@ -0,0 +1,5 @@
+---
+title: Protect Gitlab::HTTP against DNS rebinding attack
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/docs-add-chatops-request-doc.yml b/changelogs/unreleased/docs-add-chatops-request-doc.yml
new file mode 100644
index 00000000000..85ba86a73af
--- /dev/null
+++ b/changelogs/unreleased/docs-add-chatops-request-doc.yml
@@ -0,0 +1,5 @@
+---
+title: Add section to dev docs on accessing chatops
+merge_request: 28623
+author:
+type: other
diff --git a/changelogs/unreleased/ee-11040-added-conditional-rendering.yml b/changelogs/unreleased/ee-11040-added-conditional-rendering.yml
new file mode 100644
index 00000000000..7b06e43830f
--- /dev/null
+++ b/changelogs/unreleased/ee-11040-added-conditional-rendering.yml
@@ -0,0 +1,5 @@
+---
+title: "Added conditional rendering to `app/views/search/_form.html.haml` for CE/EE code base consistency"
+merge_request: 28883
+author: Michel Engelen
+type: other
diff --git a/changelogs/unreleased/feature-gb-use-gitlabktl-to-build-serverless-applications.yml b/changelogs/unreleased/feature-gb-use-gitlabktl-to-build-serverless-applications.yml
new file mode 100644
index 00000000000..443fff92f55
--- /dev/null
+++ b/changelogs/unreleased/feature-gb-use-gitlabktl-to-build-serverless-applications.yml
@@ -0,0 +1,5 @@
+---
+title: Use to 'gitlabktl' build serverless applications
+merge_request: 29258
+author:
+type: added
diff --git a/changelogs/unreleased/fix-allow-lower-case-issue-ids.yml b/changelogs/unreleased/fix-allow-lower-case-issue-ids.yml
new file mode 100644
index 00000000000..46fa90ccda0
--- /dev/null
+++ b/changelogs/unreleased/fix-allow-lower-case-issue-ids.yml
@@ -0,0 +1,5 @@
+---
+title: Allow lowercase prefix for Youtrack issue ids
+merge_request: 29057
+author: Matthias Baur
+type: fixed
diff --git a/changelogs/unreleased/fix-gb-fix-serverless-apps-deployment-template.yml b/changelogs/unreleased/fix-gb-fix-serverless-apps-deployment-template.yml
new file mode 100644
index 00000000000..88656b7ef4c
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-fix-serverless-apps-deployment-template.yml
@@ -0,0 +1,5 @@
+---
+title: Fix serverless apps deployments by bumping 'tm' version
+merge_request: 29254
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-gb-remove-serverless-app-build-policies-from-template.yml b/changelogs/unreleased/fix-gb-remove-serverless-app-build-policies-from-template.yml
new file mode 100644
index 00000000000..f51ec273a57
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-remove-serverless-app-build-policies-from-template.yml
@@ -0,0 +1,5 @@
+---
+title: Remove build policies from serverless app template
+merge_request: 29253
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-search-dropdown-blur-close.yml b/changelogs/unreleased/fix-search-dropdown-blur-close.yml
new file mode 100644
index 00000000000..1ac9dc674fe
--- /dev/null
+++ b/changelogs/unreleased/fix-search-dropdown-blur-close.yml
@@ -0,0 +1,5 @@
+---
+title: Fix search dropdown not closing on blur if empty
+merge_request: 28730
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-time-window-default.yml b/changelogs/unreleased/fix-time-window-default.yml
new file mode 100644
index 00000000000..147f82eb6c9
--- /dev/null
+++ b/changelogs/unreleased/fix-time-window-default.yml
@@ -0,0 +1,5 @@
+---
+title: Use the selected time window for metrics dashboard
+merge_request: 29152
+author:
+type: fixed
diff --git a/changelogs/unreleased/gitaly-version-v1.43.0.yml b/changelogs/unreleased/gitaly-version-v1.43.0.yml
new file mode 100644
index 00000000000..67acd2725e1
--- /dev/null
+++ b/changelogs/unreleased/gitaly-version-v1.43.0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Gitaly v1.43.0
+merge_request: 28867
+author:
+type: changed
diff --git a/changelogs/unreleased/gt-externalize-profiles-preferences.yml b/changelogs/unreleased/gt-externalize-profiles-preferences.yml
new file mode 100644
index 00000000000..1a72e92a241
--- /dev/null
+++ b/changelogs/unreleased/gt-externalize-profiles-preferences.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize profiles preferences
+merge_request: 28470
+author: George Tsiolis
+type: other
diff --git a/changelogs/unreleased/gt-open-visibility-help-link-in-a-new-tab.yml b/changelogs/unreleased/gt-open-visibility-help-link-in-a-new-tab.yml
new file mode 100644
index 00000000000..35515c9d639
--- /dev/null
+++ b/changelogs/unreleased/gt-open-visibility-help-link-in-a-new-tab.yml
@@ -0,0 +1,5 @@
+---
+title: Open visibility help link in a new tab
+merge_request: 28603
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/i18n-active_sessions-in-user-profile.yml b/changelogs/unreleased/i18n-active_sessions-in-user-profile.yml
new file mode 100644
index 00000000000..fe6eb3a2bf7
--- /dev/null
+++ b/changelogs/unreleased/i18n-active_sessions-in-user-profile.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings of active sessions page in user profile
+merge_request: 28590
+author: antony liu
+type: other
diff --git a/changelogs/unreleased/i18n-email-of-user-profile.yml b/changelogs/unreleased/i18n-email-of-user-profile.yml
new file mode 100644
index 00000000000..6cb718843d5
--- /dev/null
+++ b/changelogs/unreleased/i18n-email-of-user-profile.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings of email page in user profile
+merge_request: 28587
+author: antony liu
+type: other
diff --git a/changelogs/unreleased/i18n-pgp_ssh_keys-of-user-profile.yml b/changelogs/unreleased/i18n-pgp_ssh_keys-of-user-profile.yml
new file mode 100644
index 00000000000..4dc45b35976
--- /dev/null
+++ b/changelogs/unreleased/i18n-pgp_ssh_keys-of-user-profile.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings of PGP Keys and SSH Keys page in user profile
+merge_request: 28653
+author: Antony Liu
+type: other
diff --git a/changelogs/unreleased/improve-email-text-part.yml b/changelogs/unreleased/improve-email-text-part.yml
new file mode 100644
index 00000000000..ce506cb1507
--- /dev/null
+++ b/changelogs/unreleased/improve-email-text-part.yml
@@ -0,0 +1,5 @@
+---
+title: Improve new user email markup unconsistency between text and html parts
+merge_request: 29111
+author: Haunui Saint-sevin
+type: fixed
diff --git a/changelogs/unreleased/increase-move-issue-dropdown-height.yml b/changelogs/unreleased/increase-move-issue-dropdown-height.yml
new file mode 100644
index 00000000000..bb67e9341b2
--- /dev/null
+++ b/changelogs/unreleased/increase-move-issue-dropdown-height.yml
@@ -0,0 +1,5 @@
+---
+title: Increase height of move issue dropdown
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/issue_49897.yml b/changelogs/unreleased/issue_49897.yml
new file mode 100644
index 00000000000..b630b5143c6
--- /dev/null
+++ b/changelogs/unreleased/issue_49897.yml
@@ -0,0 +1,5 @@
+---
+title: Delete unauthorized Todos when project is made private
+merge_request: 28560
+author:
+type: fixed
diff --git a/changelogs/unreleased/jp-label-fix.yml b/changelogs/unreleased/jp-label-fix.yml
deleted file mode 100644
index de64286cc1f..00000000000
--- a/changelogs/unreleased/jp-label-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix display of 'Promote to group label' button.
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/osw-avoid-500-on-suggestions-check.yml b/changelogs/unreleased/osw-avoid-500-on-suggestions-check.yml
new file mode 100644
index 00000000000..d0a09385d4c
--- /dev/null
+++ b/changelogs/unreleased/osw-avoid-500-on-suggestions-check.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid 500 when rendering users ATOM data
+merge_request: 25408
+author:
+type: fixed
diff --git a/changelogs/unreleased/osw-reset-merge-status-from-mergeable-mrs.yml b/changelogs/unreleased/osw-reset-merge-status-from-mergeable-mrs.yml
new file mode 100644
index 00000000000..6b5f97f24b3
--- /dev/null
+++ b/changelogs/unreleased/osw-reset-merge-status-from-mergeable-mrs.yml
@@ -0,0 +1,5 @@
+---
+title: Reset merge status from mergeable MRs
+merge_request: 28843
+author:
+type: other
diff --git a/changelogs/unreleased/osw-sync-merge-ref-upon-mergeability-check.yml b/changelogs/unreleased/osw-sync-merge-ref-upon-mergeability-check.yml
new file mode 100644
index 00000000000..1f40089adb8
--- /dev/null
+++ b/changelogs/unreleased/osw-sync-merge-ref-upon-mergeability-check.yml
@@ -0,0 +1,5 @@
+---
+title: Sync merge ref upon mergeability check
+merge_request: 28513
+author:
+type: added
diff --git a/changelogs/unreleased/patch-64.yml b/changelogs/unreleased/patch-64.yml
deleted file mode 100644
index 1bf022e7e41..00000000000
--- a/changelogs/unreleased/patch-64.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update SAST.gitlab-ci.yml - Add SAST_GITLEAKS_ENTROPY_LEVEL
-merge_request: 28607
-author:
-type: fixed
diff --git a/changelogs/unreleased/pb-update-gitaly-1-45-0.yml b/changelogs/unreleased/pb-update-gitaly-1-45-0.yml
new file mode 100644
index 00000000000..eaad7a8378b
--- /dev/null
+++ b/changelogs/unreleased/pb-update-gitaly-1-45-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update GITALY_SERVER_VERSION to 1.45.0
+merge_request: 29109
+author:
+type: changed
diff --git a/changelogs/unreleased/referenced-labels.yml b/changelogs/unreleased/referenced-labels.yml
new file mode 100644
index 00000000000..c39ef4c2478
--- /dev/null
+++ b/changelogs/unreleased/referenced-labels.yml
@@ -0,0 +1,5 @@
+---
+title: Add referenced-commands in no overflow list
+merge_request: 28858
+author:
+type: fixed
diff --git a/changelogs/unreleased/remove-legacy-artifacts-related-code.yml b/changelogs/unreleased/remove-legacy-artifacts-related-code.yml
new file mode 100644
index 00000000000..acde65af2d4
--- /dev/null
+++ b/changelogs/unreleased/remove-legacy-artifacts-related-code.yml
@@ -0,0 +1,5 @@
+---
+title: Remove legacy artifact related code
+merge_request: 26475
+author:
+type: other
diff --git a/changelogs/unreleased/remove-mr-diff-header-height.yml b/changelogs/unreleased/remove-mr-diff-header-height.yml
new file mode 100644
index 00000000000..c06c7281c58
--- /dev/null
+++ b/changelogs/unreleased/remove-mr-diff-header-height.yml
@@ -0,0 +1,5 @@
+---
+title: Remove fixed height from MR diff headers
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/security-58856-persistent-xss-in-note-objects.yml b/changelogs/unreleased/security-58856-persistent-xss-in-note-objects.yml
new file mode 100644
index 00000000000..d9ad5af256a
--- /dev/null
+++ b/changelogs/unreleased/security-58856-persistent-xss-in-note-objects.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent XSS injection in note imports
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-60039.yml b/changelogs/unreleased/security-60039.yml
new file mode 100644
index 00000000000..5edbf32ec97
--- /dev/null
+++ b/changelogs/unreleased/security-60039.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent invalid branch for merge request
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-60143-address-xss-issue-in-wiki-links.yml b/changelogs/unreleased/security-60143-address-xss-issue-in-wiki-links.yml
new file mode 100644
index 00000000000..5b79258af54
--- /dev/null
+++ b/changelogs/unreleased/security-60143-address-xss-issue-in-wiki-links.yml
@@ -0,0 +1,5 @@
+---
+title: Filter relative links in wiki for XSS
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-fix-confidential-issue-label-visibility-master.yml b/changelogs/unreleased/security-fix-confidential-issue-label-visibility-master.yml
new file mode 100644
index 00000000000..adfd8e1298f
--- /dev/null
+++ b/changelogs/unreleased/security-fix-confidential-issue-label-visibility-master.yml
@@ -0,0 +1,5 @@
+---
+title: Fix confidential issue label disclosure on milestone view
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-fix-project-existence-disclosure-master.yml b/changelogs/unreleased/security-fix-project-existence-disclosure-master.yml
new file mode 100644
index 00000000000..084439c71d9
--- /dev/null
+++ b/changelogs/unreleased/security-fix-project-existence-disclosure-master.yml
@@ -0,0 +1,5 @@
+---
+title: Fix url redaction for issue links
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-fix_milestones_search_api_leak.yml b/changelogs/unreleased/security-fix_milestones_search_api_leak.yml
new file mode 100644
index 00000000000..5691550b602
--- /dev/null
+++ b/changelogs/unreleased/security-fix_milestones_search_api_leak.yml
@@ -0,0 +1,5 @@
+---
+title: 'Resolve: Milestones leaked via search API'
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-id-leaked-password-in-import-url-frontend.yml b/changelogs/unreleased/security-id-leaked-password-in-import-url-frontend.yml
new file mode 100644
index 00000000000..df636ec37fb
--- /dev/null
+++ b/changelogs/unreleased/security-id-leaked-password-in-import-url-frontend.yml
@@ -0,0 +1,5 @@
+---
+title: Add extra fields for handling basic auth on import by url page
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-jej-prevent-web-sign-in-bypass.yml b/changelogs/unreleased/security-jej-prevent-web-sign-in-bypass.yml
new file mode 100644
index 00000000000..02773fa1d7c
--- /dev/null
+++ b/changelogs/unreleased/security-jej-prevent-web-sign-in-bypass.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent bypass of restriction disabling web password sign in
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-unsubscribing-from-issue.yml b/changelogs/unreleased/security-unsubscribing-from-issue.yml
new file mode 100644
index 00000000000..3a33a457c69
--- /dev/null
+++ b/changelogs/unreleased/security-unsubscribing-from-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Hide confidential issue title on unsubscribe for anonymous users
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/set-real-next-run-at-for-preventing-duplciate-pipeline-creations.yml b/changelogs/unreleased/set-real-next-run-at-for-preventing-duplciate-pipeline-creations.yml
new file mode 100644
index 00000000000..04eb035b157
--- /dev/null
+++ b/changelogs/unreleased/set-real-next-run-at-for-preventing-duplciate-pipeline-creations.yml
@@ -0,0 +1,5 @@
+---
+title: Make pipeline schedule worker resilient
+merge_request: 28407
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-add-backtrace-to-sql-queries.yml b/changelogs/unreleased/sh-add-backtrace-to-sql-queries.yml
new file mode 100644
index 00000000000..d4ca027d1b9
--- /dev/null
+++ b/changelogs/unreleased/sh-add-backtrace-to-sql-queries.yml
@@ -0,0 +1,5 @@
+---
+title: Add backtraces to Peek performance bar for SQL calls
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/sh-default-visibility-fix.yml b/changelogs/unreleased/sh-default-visibility-fix.yml
new file mode 100644
index 00000000000..8308f310150
--- /dev/null
+++ b/changelogs/unreleased/sh-default-visibility-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Set project default visibility to max allowed
+merge_request: 28754
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-fogbugz-import.yml b/changelogs/unreleased/sh-fix-fogbugz-import.yml
new file mode 100644
index 00000000000..1ac730fca24
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-fogbugz-import.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Fogbugz Importer not working
+merge_request: 29383
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-issue-55869.yml b/changelogs/unreleased/sh-fix-issue-55869.yml
new file mode 100644
index 00000000000..7935cffc13b
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-issue-55869.yml
@@ -0,0 +1,5 @@
+---
+title: Fix alignment of resend button in members page
+merge_request: 28202
+author:
+type: other
diff --git a/changelogs/unreleased/sh-fix-omniauth-generic-strategy.yml b/changelogs/unreleased/sh-fix-omniauth-generic-strategy.yml
deleted file mode 100644
index 561c19c9685..00000000000
--- a/changelogs/unreleased/sh-fix-omniauth-generic-strategy.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix OmniAuth OAuth2Generic strategy not loading
-merge_request: 28680
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-project-import-visibility-error.yml b/changelogs/unreleased/sh-project-import-visibility-error.yml
deleted file mode 100644
index eb7e001c6aa..00000000000
--- a/changelogs/unreleased/sh-project-import-visibility-error.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix invalid visibility string comparison in project import
-merge_request: 28612
-author:
-type: fixed
diff --git a/changelogs/unreleased/t1.yml b/changelogs/unreleased/t1.yml
new file mode 100644
index 00000000000..6740375485d
--- /dev/null
+++ b/changelogs/unreleased/t1.yml
@@ -0,0 +1,5 @@
+---
+title: Set up git client in Jupyter installtion
+merge_request: 28783
+author: Amit Rathi
+type: added
diff --git a/changelogs/unreleased/thomas-nilsson-irfu-gitlab-ce-thomas-nilsson-irfu-master-patch-13137.yml b/changelogs/unreleased/thomas-nilsson-irfu-gitlab-ce-thomas-nilsson-irfu-master-patch-13137.yml
new file mode 100644
index 00000000000..3391fcc9537
--- /dev/null
+++ b/changelogs/unreleased/thomas-nilsson-irfu-gitlab-ce-thomas-nilsson-irfu-master-patch-13137.yml
@@ -0,0 +1,5 @@
+---
+title: Allow masking if 8 or more characters in base64.
+merge_request: 29143
+author: thomas-nilsson-irfu
+type: changed
diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-5-2.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-5-2.yml
new file mode 100644
index 00000000000..9ca6d18c2a8
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-5-2.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Runner Helm Chart to 0.5.2
+merge_request: 29050
+author:
+type: other
diff --git a/changelogs/unreleased/update-gitlab-shell-9-3-0.yml b/changelogs/unreleased/update-gitlab-shell-9-3-0.yml
new file mode 100644
index 00000000000..781ff31c7d8
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-shell-9-3-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update to GitLab Shell v9.3.0
+merge_request: 29283
+author:
+type: other
diff --git a/changelogs/unreleased/update-pages.yml b/changelogs/unreleased/update-pages.yml
new file mode 100644
index 00000000000..97a20b6b8fa
--- /dev/null
+++ b/changelogs/unreleased/update-pages.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Pages to v1.6.0
+merge_request: 29048
+author:
+type: other
diff --git a/changelogs/unreleased/update-psd-doc.yml b/changelogs/unreleased/update-psd-doc.yml
new file mode 100644
index 00000000000..38c8d1c0d68
--- /dev/null
+++ b/changelogs/unreleased/update-psd-doc.yml
@@ -0,0 +1,5 @@
+---
+title: Update project security dashboard documentation
+merge_request: 28681
+author:
+type: changed
diff --git a/changelogs/unreleased/use-source-ref-name-in-webhook.yml b/changelogs/unreleased/use-source-ref-name-in-webhook.yml
deleted file mode 100644
index 1a5c56d79ca..00000000000
--- a/changelogs/unreleased/use-source-ref-name-in-webhook.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use source ref in pipeline webhook
-merge_request: 28772
-author:
-type: fixed
diff --git a/changelogs/unreleased/weimeng-email-routing.yml b/changelogs/unreleased/weimeng-email-routing.yml
new file mode 100644
index 00000000000..6536433bd03
--- /dev/null
+++ b/changelogs/unreleased/weimeng-email-routing.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to define notification email addresses for groups you belong to.
+merge_request: 25299
+author:
+type: added
diff --git a/changelogs/unreleased/zj-remove-delta-island-feature-flag.yml b/changelogs/unreleased/zj-remove-delta-island-feature-flag.yml
new file mode 100644
index 00000000000..e752e01b701
--- /dev/null
+++ b/changelogs/unreleased/zj-remove-delta-island-feature-flag.yml
@@ -0,0 +1,3 @@
+merge_request: 28871
+title: Improve clone performance by using delta islands
+type: performance
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 494ddd72556..ac9b02b08d5 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -47,4 +47,7 @@ Rails.application.configure do
config.assets.quiet = true
config.allow_concurrency = defined?(::Puma)
+
+ # BetterErrors live shell (REPL) on every stack frame
+ BetterErrors::Middleware.allow_ip!("127.0.0.1/0")
end
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index 68f8487d377..4da683014d4 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -19,12 +19,6 @@ Gitlab::Application.configure do |config|
config.middleware.insert(1, Gitlab::Metrics::RequestsRackMiddleware)
end
-Sidekiq.configure_server do |config|
- config.on(:startup) do
- Gitlab::Metrics::SidekiqMetricsExporter.instance.start
- end
-end
-
if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled?
Gitlab::Cluster::LifecycleEvents.on_worker_start do
defined?(::Prometheus::Client.reinitialize_on_pid_change) && Prometheus::Client.reinitialize_on_pid_change
diff --git a/config/initializers/hipchat_client_patch.rb b/config/initializers/hipchat_client_patch.rb
index 1879ecb15fb..51bd48af320 100644
--- a/config/initializers/hipchat_client_patch.rb
+++ b/config/initializers/hipchat_client_patch.rb
@@ -2,14 +2,14 @@
# This monkey patches the HTTParty used in https://github.com/hipchat/hipchat-rb.
module HipChat
class Client
- connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter
+ connection_adapter ::Gitlab::HTTPConnectionAdapter
end
class Room
- connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter
+ connection_adapter ::Gitlab::HTTPConnectionAdapter
end
class User
- connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter
+ connection_adapter ::Gitlab::HTTPConnectionAdapter
end
end
diff --git a/config/initializers/http_hostname_override.rb b/config/initializers/http_hostname_override.rb
new file mode 100644
index 00000000000..58dd380326f
--- /dev/null
+++ b/config/initializers/http_hostname_override.rb
@@ -0,0 +1,49 @@
+# This override allows passing `@hostname_override` to the SNI protocol,
+# which is used to lookup the correct SSL certificate in the
+# request handshake process.
+#
+# Given we've forced the HTTP request to be sent to the resolved
+# IP address in a few scenarios (e.g.: `Gitlab::HTTP` through
+# `Gitlab::UrlBlocker.validate!`), we need to provide the _original_
+# hostname via SNI in order to have a clean connection setup.
+#
+# This is ultimately needed in order to avoid DNS rebinding attacks
+# through HTTP requests.
+#
+class OpenSSL::SSL::SSLContext
+ attr_accessor :hostname_override
+end
+
+class OpenSSL::SSL::SSLSocket
+ module HostnameOverride
+ # rubocop: disable Gitlab/ModuleWithInstanceVariables
+ def hostname=(hostname)
+ super(@context.hostname_override || hostname)
+ end
+
+ def post_connection_check(hostname)
+ super(@context.hostname_override || hostname)
+ end
+ # rubocop: enable Gitlab/ModuleWithInstanceVariables
+ end
+
+ prepend HostnameOverride
+end
+
+class Net::HTTP
+ attr_accessor :hostname_override
+ SSL_IVNAMES << :@hostname_override
+ SSL_ATTRIBUTES << :hostname_override
+
+ module HostnameOverride
+ def addr_port
+ return super unless hostname_override
+
+ addr = hostname_override
+ default_port = use_ssl? ? Net::HTTP.https_default_port : Net::HTTP.http_default_port
+ default_port == port ? addr : "#{addr}:#{port}"
+ end
+ end
+
+ prepend HostnameOverride
+end
diff --git a/config/routes.rb b/config/routes.rb
index f5957f43655..cb90a0134c4 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -75,6 +75,8 @@ Rails.application.routes.draw do
resources :issues, module: :boards, only: [:index, :update]
end
+ get 'acme-challenge/' => 'acme_challenges#show'
+
# UserCallouts
resources :user_callouts, only: [:create]
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index bc19219a0b8..ae79beb1dba 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -83,7 +83,7 @@ namespace :admin do
resources(:projects,
path: '/',
constraints: { id: Gitlab::PathRegex.project_route_regex },
- only: [:show]) do
+ only: [:show, :destroy]) do
member do
put :transfer
diff --git a/config/routes/import.rb b/config/routes/import.rb
index 24013eb2c88..9fe2688de1e 100644
--- a/config/routes/import.rb
+++ b/config/routes/import.rb
@@ -67,4 +67,6 @@ namespace :import do
get :jobs
post :upload
end
+
+ resource :phabricator, only: [:create, :new], controller: :phabricator
end
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index c1cac3905f1..0e213b0b989 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -17,7 +17,11 @@ resource :profile, only: [:show, :update] do
delete :unlink
end
end
- resource :notifications, only: [:show, :update]
+
+ resource :notifications, only: [:show, :update] do
+ resources :groups, only: :update
+ end
+
resource :password, only: [:new, :create, :edit, :update] do
member do
put :reset
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 1e94bdc245f..d44ff62bc2a 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -124,6 +124,44 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
put :revoke
end
end
+
+ resources :milestones, constraints: { id: /\d+/ } do
+ member do
+ post :promote
+ put :sort_issues
+ put :sort_merge_requests
+ get :merge_requests
+ get :participants
+ get :labels
+ end
+ end
+
+ resources :labels, except: [:show], constraints: { id: /\d+/ } do
+ collection do
+ post :generate
+ post :set_priorities
+ end
+
+ member do
+ post :promote
+ post :toggle_subscription
+ delete :remove_priority
+ end
+ end
+
+ resources :services, constraints: { id: %r{[^/]+} }, only: [:edit, :update] do
+ member do
+ put :test
+ end
+ end
+
+ resources :boards, only: [:index, :show], constraints: { id: /\d+/ }
+ resources :releases, only: [:index]
+ resources :forks, only: [:index, :new, :create]
+ resources :group_links, only: [:index, :create, :update, :destroy], constraints: { id: /\d+/ }
+
+ resource :import, only: [:new, :create, :show]
+ resource :avatar, only: [:show, :destroy]
end
# End of the /-/ scope.
@@ -132,7 +170,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
#
get '/templates/:template_type/:key' => 'templates#show', as: :template, constraints: { key: %r{[^/]+} }
- resource :avatar, only: [:show, :destroy]
resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
member do
get :branches
@@ -159,12 +196,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resources :services, constraints: { id: %r{[^/]+} }, only: [:edit, :update] do
- member do
- put :test
- end
- end
-
resource :mattermost, only: [:new, :create]
namespace :prometheus do
@@ -173,15 +204,11 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resources :releases, only: [:index]
- resources :forks, only: [:index, :new, :create]
- resource :import, only: [:new, :create, :show]
-
resources :merge_requests, concerns: :awardable, except: [:new, :create], constraints: { id: /\d+/ } do
member do
get :commit_change_content
post :merge
- post :cancel_merge_when_pipeline_succeeds
+ post :cancel_auto_merge
get :pipeline_status
get :ci_environments_status
post :toggle_subscription
@@ -372,31 +399,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resources :milestones, constraints: { id: /\d+/ } do
- member do
- post :promote
- put :sort_issues
- put :sort_merge_requests
- get :merge_requests
- get :participants
- get :labels
- end
- end
-
- resources :labels, except: [:show], constraints: { id: /\d+/ } do
- collection do
- post :generate
- post :set_priorities
- end
-
- member do
- post :promote
- post :toggle_subscription
- delete :remove_priority
- end
- end
-
get :issues, to: 'issues#calendar', constraints: lambda { |req| req.format == :ics }
+
resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
member do
post :toggle_subscription
@@ -408,14 +412,13 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :create_merge_request
get :discussions, format: :json
end
+
collection do
post :bulk_update
post :import_csv
end
end
- resources :group_links, only: [:index, :create, :update, :destroy], constraints: { id: /\d+/ }
-
resources :notes, only: [:create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
member do
delete :delete_attachment
@@ -426,8 +429,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get 'noteable/:target_type/:target_id/notes' => 'notes#index', as: 'noteable_notes'
- resources :boards, only: [:index, :show], constraints: { id: /\d+/ }
-
resources :todos, only: [:create]
resources :uploads, only: [:create] do
@@ -510,7 +511,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
as: :project) do
Gitlab::Routing.redirect_legacy_paths(self, :settings, :branches, :tags,
:network, :graphs, :autocomplete_sources,
- :project_members, :deploy_keys, :deploy_tokens)
+ :project_members, :deploy_keys, :deploy_tokens,
+ :labels, :milestones, :services, :boards, :releases,
+ :forks, :group_links, :import, :avatar)
end
end
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 8bc2426ec4c..fd9ce4d3374 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -30,6 +30,7 @@
- [pipeline_default, 3]
- [pipeline_cache, 3]
- [deployment, 3]
+ - [auto_merge, 3]
- [pipeline_hooks, 2]
- [gitlab_shell, 2]
- [email_receiver, 2]
@@ -91,3 +92,4 @@
- [chat_notification, 2]
- [migrate_external_diffs, 1]
- [update_project_statistics, 1]
+ - [phabricator_import_import_tasks, 1]
diff --git a/config/webpack.config.review_toolbar.js b/config/webpack.config.review_toolbar.js
new file mode 100644
index 00000000000..baaba7ed387
--- /dev/null
+++ b/config/webpack.config.review_toolbar.js
@@ -0,0 +1,58 @@
+const path = require('path');
+const CompressionPlugin = require('compression-webpack-plugin');
+
+const ROOT_PATH = path.resolve(__dirname, '..');
+const CACHE_PATH = process.env.WEBPACK_CACHE_PATH || path.join(ROOT_PATH, 'tmp/cache');
+const NO_SOURCEMAPS = process.env.NO_SOURCEMAPS;
+const IS_PRODUCTION = process.env.NODE_ENV === 'production';
+
+const devtool = IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map';
+
+const alias = {
+ vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'),
+ spec: path.join(ROOT_PATH, 'spec/javascripts'),
+};
+
+module.exports = {
+ mode: IS_PRODUCTION ? 'production' : 'development',
+
+ context: path.join(ROOT_PATH, 'app/assets/javascripts'),
+
+ name: 'visual_review_toolbar',
+
+ entry: './visual_review_toolbar',
+
+ output: {
+ path: path.join(ROOT_PATH, 'public/assets/webpack'),
+ filename: 'visual_review_toolbar.js',
+ library: 'VisualReviewToolbar',
+ libraryTarget: 'var',
+ },
+
+ resolve: {
+ alias,
+ },
+
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ loader: 'babel-loader',
+ options: {
+ cacheDirectory: path.join(CACHE_PATH, 'babel-loader'),
+ },
+ },
+ {
+ test: /\.css$/,
+ use: ['style-loader', 'css-loader'],
+ },
+ ],
+ },
+
+ plugins: [
+ // compression can require a lot of compute time and is disabled in CI
+ new CompressionPlugin(),
+ ].filter(Boolean),
+
+ devtool: NO_SOURCEMAPS ? false : devtool,
+};
diff --git a/danger/commit_messages/Dangerfile b/danger/commit_messages/Dangerfile
index 048c539bcf9..bdb4343b1d6 100644
--- a/danger/commit_messages/Dangerfile
+++ b/danger/commit_messages/Dangerfile
@@ -163,10 +163,10 @@ def lint_commit(commit)
end
if emoji_checker.includes_emoji?(commit.message)
- fail_commit(
+ warn_commit(
commit,
'Avoid the use of Markdown Emoji such as `:+1:`. ' \
- 'These add no value to the commit message, ' \
+ 'These add limited value to the commit message, ' \
'and are displayed as plain text outside of GitLab'
)
diff --git a/danger/plugins/helper.rb b/danger/plugins/helper.rb
index 581c0720083..2d7a933e801 100644
--- a/danger/plugins/helper.rb
+++ b/danger/plugins/helper.rb
@@ -1,8 +1,5 @@
# frozen_string_literal: true
-require 'net/http'
-require 'yaml'
-
require_relative '../../lib/gitlab/danger/helper'
module Danger
diff --git a/danger/plugins/roulette.rb b/danger/plugins/roulette.rb
new file mode 100644
index 00000000000..7c62cff0c92
--- /dev/null
+++ b/danger/plugins/roulette.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require_relative '../../lib/gitlab/danger/roulette'
+
+module Danger
+ class Roulette < Plugin
+ # Put the helper code somewhere it can be tested
+ include Gitlab::Danger::Roulette
+ end
+end
diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile
index 62e5526c02b..a0f1447e76a 100644
--- a/danger/roulette/Dangerfile
+++ b/danger/roulette/Dangerfile
@@ -32,53 +32,26 @@ for them.
MARKDOWN
def spin_for_category(team, project, category, branch_name)
- rng = Random.new(Digest::MD5.hexdigest(branch_name).to_i(16))
-
- reviewers = team.select { |member| member.reviewer?(project, category) }
- traintainers = team.select { |member| member.traintainer?(project, category) }
- maintainers = team.select { |member| member.maintainer?(project, category) }
+ random = roulette.new_random(branch_name)
+ labels = gitlab.mr_labels
+
+ reviewers, traintainers, maintainers =
+ %i[reviewer? traintainer? maintainer?].map do |kind|
+ team.select do |member|
+ member.public_send(kind, project, category, labels) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
# TODO: take CODEOWNERS into account?
# https://gitlab.com/gitlab-org/gitlab-ce/issues/57653
# Make traintainers have triple the chance to be picked as a reviewer
- reviewer = spin_for_person(reviewers + traintainers + traintainers, random: rng)
- maintainer = spin_for_person(maintainers, random: rng)
+ reviewer = roulette.spin_for_person(reviewers + traintainers + traintainers, random: random)
+ maintainer = roulette.spin_for_person(maintainers, random: random)
"| #{helper.label_for_category(category)} | #{reviewer&.markdown_name} | #{maintainer&.markdown_name} |"
end
-# Known issue: If someone is rejected due to OOO, and then becomes not OOO, the
-# selection will change on next spin
-def spin_for_person(people, random:)
- person = nil
- people = people.dup
-
- people.size.times do
- person = people.sample(random: random)
-
- break person unless out_of_office?(person)
-
- people -= [person]
- end
-
- person
-end
-
-def out_of_office?(person)
- username = CGI.escape(person.username)
- api_endpoint = "https://gitlab.com/api/v4/users/#{username}/status"
- response = HTTParty.get(api_endpoint) # rubocop:disable Gitlab/HTTParty
-
- if response.code == 200
- response["message"]&.match(/OOO/i)
- else
- false # this is no worse than not checking for OOO
- end
-rescue
- false
-end
-
def build_list(items)
list = items.map { |filename| "* `#{filename}`" }.join("\n")
@@ -101,14 +74,12 @@ categories = changes.keys - [:unknown]
# disable the review roulette for such MRs.
if changes.any? && !gitlab.mr_labels.include?('single codebase') && !gitlab.mr_labels.include?('CSS cleanup')
# Strip leading and trailing CE/EE markers
- canonical_branch_name = gitlab
- .mr_json['source_branch']
- .gsub(/^[ce]e-/, '')
- .gsub(/-[ce]e$/, '')
+ canonical_branch_name =
+ roulette.canonical_branch_name(gitlab.mr_json['source_branch'])
team =
begin
- helper.project_team
+ roulette.project_team(helper.project_name)
rescue => err
warn("Reviewer roulette failed to load team data: #{err.message}")
[]
diff --git a/danger/single_codebase/Dangerfile b/danger/single_codebase/Dangerfile
index a5938cd6783..d1f538bec7f 100644
--- a/danger/single_codebase/Dangerfile
+++ b/danger/single_codebase/Dangerfile
@@ -1,29 +1,36 @@
+def new_teammates(usernames)
+ usernames.map { |u| ::Gitlab::Danger::Teammate.new('username' => u) }
+end
+
def mention_single_codebase_approvers
- frontend_maintainers = %w(@filipa @iamphill)
- backend_maintainers = %w(@rspeicher @rymai @yorickpeterse @godfat)
+ canonical_branch_name =
+ roulette.canonical_branch_name(gitlab.mr_json['source_branch'])
+
+ random = roulette.new_random(canonical_branch_name)
+
+ frontend_maintainers = new_teammates(%w[filipa iamphill])
+ backend_maintainers = new_teammates(%w[rspeicher rymai yorickpeterse godfat])
rows = []
- users = []
if gitlab.mr_labels.include?('frontend')
- frontend_maintainer = frontend_maintainers.sample
+ frontend_maintainer =
+ roulette.spin_for_person(frontend_maintainers, random: random)
- rows << "| ~frontend | `#{frontend_maintainer}`"
- users << frontend_maintainer
+ rows << "| ~frontend | #{frontend_maintainer.markdown_name}"
end
if gitlab.mr_labels.include?('backend')
- backend_maintainer = backend_maintainers.sample
+ backend_maintainer =
+ roulette.spin_for_person(backend_maintainers, random: random)
- rows << "| ~backend | `#{backend_maintainer}`"
- users << backend_maintainer
+ rows << "| ~backend | #{backend_maintainer.markdown_name}"
end
if rows.empty?
backup_maintainer = backend_maintainers.sample
- rows << "| ~frontend / ~backend | `#{backup_maintainer}`"
- users << backup_maintainer
+ rows << "| ~frontend / ~backend | #{backup_maintainer.markdown_name}"
end
markdown(<<~MARKDOWN.strip)
diff --git a/db/migrate/20180702134423_generate_missing_routes.rb b/db/migrate/20180702134423_generate_missing_routes.rb
index a440bc3179c..dd1106c9e6a 100644
--- a/db/migrate/20180702134423_generate_missing_routes.rb
+++ b/db/migrate/20180702134423_generate_missing_routes.rb
@@ -98,6 +98,7 @@ class GenerateMissingRoutes < ActiveRecord::Migration[4.2]
class Namespace < ActiveRecord::Base
self.table_name = 'namespaces'
+ self.inheritance_column = :_type_disabled
include EachBatch
include GenerateMissingRoutes::Routable
diff --git a/db/migrate/20190327163904_add_notification_email_to_notification_settings.rb b/db/migrate/20190327163904_add_notification_email_to_notification_settings.rb
new file mode 100644
index 00000000000..2f3069032a1
--- /dev/null
+++ b/db/migrate/20190327163904_add_notification_email_to_notification_settings.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddNotificationEmailToNotificationSettings < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :notification_settings, :notification_email, :string
+ end
+end
diff --git a/db/migrate/20190429082448_create_pages_domain_acme_orders.rb b/db/migrate/20190429082448_create_pages_domain_acme_orders.rb
new file mode 100644
index 00000000000..af811e83518
--- /dev/null
+++ b/db/migrate/20190429082448_create_pages_domain_acme_orders.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CreatePagesDomainAcmeOrders < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ create_table :pages_domain_acme_orders do |t|
+ t.references :pages_domain, null: false, index: true, foreign_key: { on_delete: :cascade }, type: :integer
+
+ t.datetime_with_timezone :expires_at, null: false
+ t.timestamps_with_timezone null: false
+
+ t.string :url, null: false
+
+ t.string :challenge_token, null: false, index: true
+ t.text :challenge_file_content, null: false
+
+ t.text :encrypted_private_key, null: false
+ t.text :encrypted_private_key_iv, null: false
+ end
+ end
+end
diff --git a/db/migrate/20190516155724_change_packages_size_defaults_in_project_statistics.rb b/db/migrate/20190516155724_change_packages_size_defaults_in_project_statistics.rb
new file mode 100644
index 00000000000..eba154df496
--- /dev/null
+++ b/db/migrate/20190516155724_change_packages_size_defaults_in_project_statistics.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class ChangePackagesSizeDefaultsInProjectStatistics < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ change_column_default :project_statistics, :packages_size, 0
+
+ update_column_in_batches(:project_statistics, :packages_size, 0) do |table, query|
+ query.where(table[:packages_size].eq(nil))
+ end
+
+ change_column_null :project_statistics, :packages_size, false
+ end
+
+ def down
+ change_column_null :project_statistics, :packages_size, true
+ change_column_default :project_statistics, :packages_size, nil
+ end
+end
diff --git a/db/migrate/20190523112344_limit_milestone_date_years_to_4_digits.rb b/db/migrate/20190523112344_limit_milestone_date_years_to_4_digits.rb
new file mode 100644
index 00000000000..86fe09d7573
--- /dev/null
+++ b/db/migrate/20190523112344_limit_milestone_date_years_to_4_digits.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class LimitMilestoneDateYearsTo4Digits < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ Milestone.where("start_date > '9999-12-31'").update_all(
+ "start_date = '9999-12-31'"
+ )
+ Milestone.where("due_date > '9999-12-31'").update_all(
+ "due_date = '9999-12-31'"
+ )
+ end
+end
diff --git a/db/migrate/20190524062810_generate_lets_encrypt_private_key.rb b/db/migrate/20190524062810_generate_lets_encrypt_private_key.rb
index 21d7049b998..ae93a76575a 100644
--- a/db/migrate/20190524062810_generate_lets_encrypt_private_key.rb
+++ b/db/migrate/20190524062810_generate_lets_encrypt_private_key.rb
@@ -9,23 +9,8 @@ class GenerateLetsEncryptPrivateKey < ActiveRecord::Migration[5.1]
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
- class ApplicationSetting < ActiveRecord::Base
- self.table_name = 'application_settings'
-
- attr_encrypted :lets_encrypt_private_key,
- mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
- algorithm: 'aes-256-gcm',
- encode: true
- end
-
+ # we now generate this key on the fly, but since this migration was merged to master, we don't remove it
def up
- ApplicationSetting.reset_column_information
-
- private_key = OpenSSL::PKey::RSA.new(4096).to_pem
- ApplicationSetting.find_each do |setting|
- setting.update!(lets_encrypt_private_key: private_key)
- end
end
def down
diff --git a/db/migrate/20190524071727_add_ssl_valid_period_to_pages_domain.rb b/db/migrate/20190524071727_add_ssl_valid_period_to_pages_domain.rb
new file mode 100644
index 00000000000..18544dcb6d3
--- /dev/null
+++ b/db/migrate/20190524071727_add_ssl_valid_period_to_pages_domain.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddSslValidPeriodToPagesDomain < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ add_column :pages_domains, :certificate_valid_not_before, :datetime_with_timezone
+ add_column :pages_domains, :certificate_valid_not_after, :datetime_with_timezone
+ end
+end
diff --git a/db/migrate/20190527194830_add_wiki_size_to_statistics.rb b/db/migrate/20190527194830_add_wiki_size_to_statistics.rb
new file mode 100644
index 00000000000..d4f16cdec18
--- /dev/null
+++ b/db/migrate/20190527194830_add_wiki_size_to_statistics.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddWikiSizeToStatistics < ActiveRecord::Migration[5.0]
+ DOWNTIME = false
+
+ def change
+ add_column :project_statistics, :wiki_size, :bigint
+ end
+end
diff --git a/db/migrate/20190529142545_add_dns_rebinding_protection_enabled_to_application_settings.rb b/db/migrate/20190529142545_add_dns_rebinding_protection_enabled_to_application_settings.rb
new file mode 100644
index 00000000000..8835dc8b7ba
--- /dev/null
+++ b/db/migrate/20190529142545_add_dns_rebinding_protection_enabled_to_application_settings.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddDnsRebindingProtectionEnabledToApplicationSettings < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:application_settings, :dns_rebinding_protection_enabled,
+ :boolean,
+ default: true,
+ allow_null: false)
+ end
+
+ def down
+ remove_column(:application_settings, :dns_rebinding_protection_enabled)
+ end
+end
diff --git a/db/migrate/20190530042141_add_default_git_depth_to_ci_cd_settings.rb b/db/migrate/20190530042141_add_default_git_depth_to_ci_cd_settings.rb
new file mode 100644
index 00000000000..8abea05def4
--- /dev/null
+++ b/db/migrate/20190530042141_add_default_git_depth_to_ci_cd_settings.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddDefaultGitDepthToCiCdSettings < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :project_ci_cd_settings, :default_git_depth, :integer
+ end
+end
diff --git a/db/migrate/20190530154715_add_index_to_merge_requests_state_and_merge_status.rb b/db/migrate/20190530154715_add_index_to_merge_requests_state_and_merge_status.rb
new file mode 100644
index 00000000000..e669f81ca35
--- /dev/null
+++ b/db/migrate/20190530154715_add_index_to_merge_requests_state_and_merge_status.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexToMergeRequestsStateAndMergeStatus < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :merge_requests, [:state, :merge_status],
+ where: "state = 'opened' AND merge_status = 'can_be_merged'"
+ end
+
+ def down
+ remove_concurrent_index :merge_requests, [:state, :merge_status]
+ end
+end
diff --git a/db/post_migrate/20190522143720_drop_project_auto_devops_domain.rb b/db/post_migrate/20190522143720_drop_project_auto_devops_domain.rb
new file mode 100644
index 00000000000..36278d83927
--- /dev/null
+++ b/db/post_migrate/20190522143720_drop_project_auto_devops_domain.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class DropProjectAutoDevopsDomain < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ remove_column :project_auto_devops, :domain, :string
+ end
+end
diff --git a/db/post_migrate/20190524073827_schedule_fill_valid_time_for_pages_domain_certificates.rb b/db/post_migrate/20190524073827_schedule_fill_valid_time_for_pages_domain_certificates.rb
new file mode 100644
index 00000000000..1d8510e4514
--- /dev/null
+++ b/db/post_migrate/20190524073827_schedule_fill_valid_time_for_pages_domain_certificates.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ScheduleFillValidTimeForPagesDomainCertificates < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ MIGRATION = 'FillValidTimeForPagesDomainCertificate'
+ BATCH_SIZE = 500
+ BATCH_TIME = 5.minutes
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class PagesDomain < ActiveRecord::Base
+ include ::EachBatch
+
+ self.table_name = 'pages_domains'
+ end
+
+ def up
+ queue_background_migration_jobs_by_range_at_intervals(
+ PagesDomain.where.not(certificate: [nil, '']),
+ MIGRATION,
+ BATCH_TIME,
+ batch_size: BATCH_SIZE)
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20190527194900_schedule_calculate_wiki_sizes.rb b/db/post_migrate/20190527194900_schedule_calculate_wiki_sizes.rb
new file mode 100644
index 00000000000..04cf5906b61
--- /dev/null
+++ b/db/post_migrate/20190527194900_schedule_calculate_wiki_sizes.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class ScheduleCalculateWikiSizes < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ MIGRATION = 'CalculateWikiSizes'
+ BATCH_SIZE = 100000
+ BATCH_TIME = 5.minutes
+
+ class ProjectStatistics < ActiveRecord::Base
+ self.table_name = 'project_statistics'
+
+ scope :without_wiki_size, -> { where(wiki_size: nil) }
+
+ include ::EachBatch
+ end
+
+ disable_ddl_transaction!
+
+ def up
+ queue_background_migration_jobs_by_range_at_intervals(
+ ::ScheduleCalculateWikiSizes::ProjectStatistics.without_wiki_size,
+ MIGRATION,
+ BATCH_TIME,
+ batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/post_migrate/20190528180441_enqueue_reset_merge_status.rb b/db/post_migrate/20190528180441_enqueue_reset_merge_status.rb
new file mode 100644
index 00000000000..1b668d85bac
--- /dev/null
+++ b/db/post_migrate/20190528180441_enqueue_reset_merge_status.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class EnqueueResetMergeStatus < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 10_000
+ MIGRATION = 'ResetMergeStatus'
+ DELAY_INTERVAL = 5.minutes.to_i
+
+ disable_ddl_transaction!
+
+ def up
+ say 'Scheduling `ResetMergeStatus` jobs'
+
+ # We currently have around 135_000 opened, mergeable MRs in GitLab.com. This iteration
+ # will schedule around 13 batches of 10_000 MRs, which should take around 1 hour to
+ # complete.
+ relation = MergeRequest.where(state: 'opened', merge_status: 'can_be_merged')
+
+ relation.each_batch(of: BATCH_SIZE) do |batch, index|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+
+ BackgroundMigrationWorker.perform_in(index * DELAY_INTERVAL, MIGRATION, range)
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index bb59af540fe..22d0cddb7a0 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20190524062810) do
+ActiveRecord::Schema.define(version: 20190530154715) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -193,6 +193,7 @@ ActiveRecord::Schema.define(version: 20190524062810) do
t.integer "elasticsearch_replicas", default: 1, null: false
t.text "encrypted_lets_encrypt_private_key"
t.text "encrypted_lets_encrypt_private_key_iv"
+ t.boolean "dns_rebinding_protection_enabled", default: true, null: false
t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
end
@@ -1361,6 +1362,7 @@ ActiveRecord::Schema.define(version: 20190524062810) do
t.index ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
t.index ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_and_branch_state_opened", where: "((state)::text = 'opened'::text)", using: :btree
t.index ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree
+ t.index ["state", "merge_status"], name: "index_merge_requests_on_state_and_merge_status", where: "(((state)::text = 'opened'::text) AND ((merge_status)::text = 'can_be_merged'::text))", using: :btree
t.index ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
t.index ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree
t.index ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid_opened", where: "((state)::text = 'opened'::text)", using: :btree
@@ -1516,6 +1518,7 @@ ActiveRecord::Schema.define(version: 20190524062810) do
t.boolean "success_pipeline"
t.boolean "push_to_merge_request"
t.boolean "issue_due"
+ t.string "notification_email"
t.index ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree
t.index ["user_id", "source_id", "source_type"], name: "index_notifications_on_user_id_and_source_id_and_source_type", unique: true, using: :btree
t.index ["user_id"], name: "index_notification_settings_on_user_id", using: :btree
@@ -1568,6 +1571,20 @@ ActiveRecord::Schema.define(version: 20190524062810) do
t.index ["access_grant_id"], name: "index_oauth_openid_requests_on_access_grant_id", using: :btree
end
+ create_table "pages_domain_acme_orders", force: :cascade do |t|
+ t.integer "pages_domain_id", null: false
+ t.datetime_with_timezone "expires_at", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.string "url", null: false
+ t.string "challenge_token", null: false
+ t.text "challenge_file_content", null: false
+ t.text "encrypted_private_key", null: false
+ t.text "encrypted_private_key_iv", null: false
+ t.index ["challenge_token"], name: "index_pages_domain_acme_orders_on_challenge_token", using: :btree
+ t.index ["pages_domain_id"], name: "index_pages_domain_acme_orders_on_pages_domain_id", using: :btree
+ end
+
create_table "pages_domains", id: :serial, force: :cascade do |t|
t.integer "project_id"
t.text "certificate"
@@ -1580,6 +1597,8 @@ ActiveRecord::Schema.define(version: 20190524062810) do
t.datetime_with_timezone "enabled_until"
t.datetime_with_timezone "remove_at"
t.boolean "auto_ssl_enabled", default: false, null: false
+ t.datetime_with_timezone "certificate_valid_not_before"
+ t.datetime_with_timezone "certificate_valid_not_after"
t.index ["domain"], name: "index_pages_domains_on_domain", unique: true, using: :btree
t.index ["project_id", "enabled_until"], name: "index_pages_domains_on_project_id_and_enabled_until", using: :btree
t.index ["project_id"], name: "index_pages_domains_on_project_id", using: :btree
@@ -1632,7 +1651,6 @@ ActiveRecord::Schema.define(version: 20190524062810) do
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.boolean "enabled"
- t.string "domain"
t.integer "deploy_strategy", default: 0, null: false
t.index ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree
end
@@ -1642,6 +1660,7 @@ ActiveRecord::Schema.define(version: 20190524062810) do
t.boolean "group_runners_enabled", default: true, null: false
t.boolean "merge_pipelines_enabled"
t.boolean "merge_trains_enabled", default: false, null: false
+ t.integer "default_git_depth"
t.index ["project_id"], name: "index_project_ci_cd_settings_on_project_id", unique: true, using: :btree
end
@@ -1744,7 +1763,8 @@ ActiveRecord::Schema.define(version: 20190524062810) do
t.bigint "repository_size", default: 0, null: false
t.bigint "lfs_objects_size", default: 0, null: false
t.bigint "build_artifacts_size", default: 0, null: false
- t.bigint "packages_size"
+ t.bigint "packages_size", default: 0, null: false
+ t.bigint "wiki_size"
t.index ["namespace_id"], name: "index_project_statistics_on_namespace_id", using: :btree
t.index ["project_id"], name: "index_project_statistics_on_project_id", unique: true, using: :btree
end
@@ -2557,6 +2577,7 @@ ActiveRecord::Schema.define(version: 20190524062810) do
add_foreign_key "notes", "projects", name: "fk_99e097b079", on_delete: :cascade
add_foreign_key "notification_settings", "users", name: "fk_0c95e91db7", on_delete: :cascade
add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"
+ add_foreign_key "pages_domain_acme_orders", "pages_domains", on_delete: :cascade
add_foreign_key "pages_domains", "projects", name: "fk_ea2f6dfc6f", on_delete: :cascade
add_foreign_key "personal_access_tokens", "users"
add_foreign_key "pool_repositories", "projects", column: "source_project_id", on_delete: :nullify
diff --git a/doc/administration/auth/google_secure_ldap.md b/doc/administration/auth/google_secure_ldap.md
index 65a51fc4aa0..760af0cfd1a 100644
--- a/doc/administration/auth/google_secure_ldap.md
+++ b/doc/administration/auth/google_secure_ldap.md
@@ -27,7 +27,7 @@ The steps below cover:
'Entire domain (GitLab)' or 'Selected organizational units' for both 'Verify user
credentials' and 'Read user information'. Select 'Add LDAP Client'
- TIP: **Tip:** If you plan to use GitLab [LDAP Group Sync](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html#group-sync)
+ TIP: **Tip:** If you plan to use GitLab [LDAP Group Sync](ldap-ee.md#group-sync)
, turn on 'Read group information'.
![Add LDAP Client Step 2](img/google_secure_ldap_add_step_2.png)
diff --git a/doc/administration/auth/oidc.md b/doc/administration/auth/oidc.md
index e55f7dbb4df..00422ec347c 100644
--- a/doc/administration/auth/oidc.md
+++ b/doc/administration/auth/oidc.md
@@ -31,6 +31,7 @@ The OpenID Connect will provide you with a client details and secret for you to
{ 'name' => 'openid_connect',
'label' => '<your_oidc_label>',
'args' => {
+ "name' => 'openid_connect',
'scope' => ['openid','profile'],
'response_type' => 'code',
'issuer' => '<your_oidc_url>',
@@ -53,6 +54,7 @@ The OpenID Connect will provide you with a client details and secret for you to
- { name: 'openid_connect',
label: '<your_oidc_label>',
args: {
+ name: 'openid_connect',
scope: ['openid','profile'],
response_type: 'code',
issuer: '<your_oidc_url>',
@@ -103,3 +105,59 @@ On the sign in page, there should now be an OpenID Connect icon below the regula
Click the icon to begin the authentication process. The OpenID Connect provider will ask the user to
sign in and authorize the GitLab application (if confirmation required by the client). If everything goes well, the user
will be redirected to GitLab and will be signed in.
+
+## Example configurations
+
+The following configurations illustrate how to set up OpenID with
+different providers with Omnibus GitLab.
+
+### Google
+
+See the [Google
+documentation](https://developers.google.com/identity/protocols/OpenIDConnect)
+for more details:
+
+```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ 'name' => 'openid_connect',
+ 'label' => 'Google OpenID',
+ 'args' => {
+ 'name' => 'openid_connect',
+ 'scope' => ['openid', 'profile', 'email'],
+ 'response_type' => 'code',
+ 'issuer' => 'https://accounts.google.com',
+ 'client_auth_method' => 'query',
+ 'discovery' => true,
+ 'uid_field' => 'preferred_username',
+ 'client_options' => {
+ 'identifier' => '<YOUR PROJECT CLIENT ID>',
+ 'secret' => '<YOUR PROJECT CLIENT SECRET>',
+ 'redirect_uri' => 'https://example.com/users/auth/openid_connect/callback',
+ }
+ }
+ }
+```
+
+### Troubleshooting
+
+If you're having trouble, here are some tips:
+
+1. Ensure `discovery` is set to `true`. Setting it to `false` requires
+specifying all the URLs and keys required to make OpenID work.
+
+1. Check your system clock to ensure the time is synchronized properly.
+
+1. As mentioned in [the
+documentation](https://github.com/m0n9oose/omniauth_openid_connect),
+make sure `issuer` corresponds to the base URL of the Discovery URL. For
+example, `https://accounts.google.com` is used for the URL
+`https://accounts.google.com/.well-known/openid-configuration`.
+
+1. The OpenID Connect client uses HTTP Basic Authentication to send the
+OAuth2 access token. For example, if you are seeing 401 errors upon
+retrieving the `userinfo` endpoint, you may want to check your OpenID
+Web server configuration. For example, for
+[oauth2-server-php](https://github.com/bshaffer/oauth2-server-php), you
+may need to [add a configuration parameter to
+Apache](https://github.com/bshaffer/oauth2-server-php/issues/926#issuecomment-387502778).
diff --git a/doc/administration/compliance.md b/doc/administration/compliance.md
index 9c13ff772b3..246addb6dc9 100644
--- a/doc/administration/compliance.md
+++ b/doc/administration/compliance.md
@@ -12,7 +12,7 @@ GitLab’s [security features](../security/README.md) may also help you meet rel
|**[Email all users of a project, group, or entire server](../user/admin_area/settings/terms.md)**<br>An admin can email groups of users based on project or group membership, or email everyone using the GitLab instance. This is great for scheduled maintenance or upgrades.|Starter+||
|**[Omnibus package supports log forwarding](https://docs.gitlab.com/omnibus/settings/logs.html#udp-log-forwarding)**<br>Forward your logs to a central system.|Starter+||
|**[Lock project membership to group](../user/group/index.md#member-lock-starter)**<br>Group owners can prevent new members from being added to projects within a group.|Starter+|✓|
-|**[LDAP group sync](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html#group-sync)**<br>GitLab Enterprise Edition gives admins the ability to automatically sync groups and manage SSH keys, permissions, and authentication, so you can focus on building your product, not configuring your tools.|Starter+||
-|**[LDAP group sync filters](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html#group-sync)**<br>GitLab Enterprise Edition Premium gives more flexibility to synchronize with LDAP based on filters, meaning you can leverage LDAP attributes to map GitLab permissions.|Premium+||
+|**[LDAP group sync](auth/ldap-ee.md#group-sync)**<br>GitLab Enterprise Edition gives admins the ability to automatically sync groups and manage SSH keys, permissions, and authentication, so you can focus on building your product, not configuring your tools.|Starter+||
+|**[LDAP group sync filters](auth/ldap-ee.md#group-sync)**<br>GitLab Enterprise Edition Premium gives more flexibility to synchronize with LDAP based on filters, meaning you can leverage LDAP attributes to map GitLab permissions.|Premium+||
|**[Audit logs](audit_events.md)**<br>To maintain the integrity of your code, GitLab Enterprise Edition Premium gives admins the ability to view any modifications made within the GitLab server in an advanced audit log system, so you can control, analyze and track every change.|Premium+||
|**[Auditor users](auditor_users.md)**<br>Auditor users are users who are given read-only access to all projects, groups, and other resources on the GitLab instance.|Premium+||
diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md
index 288cb1bf0bb..113514e1ee8 100644
--- a/doc/administration/custom_hooks.md
+++ b/doc/administration/custom_hooks.md
@@ -5,11 +5,11 @@ Custom Git hooks must be configured on the filesystem of the GitLab
server. Only GitLab server administrators will be able to complete these tasks.
Please explore [webhooks] and [CI] as an option if you do not
have filesystem access. For a user configurable Git hook interface, see
-[Push Rules](https://docs.gitlab.com/ee/push_rules/push_rules.html),
+[Push Rules](../push_rules/push_rules.md),
available in GitLab Enterprise Edition.
NOTE: **Note:**
-Custom Git hooks won't be replicated to secondary nodes if you use [GitLab Geo][gitlab-geo]
+Custom Git hooks won't be replicated to secondary nodes if you use [GitLab Geo](geo/replication/index.md)
Git natively supports hooks that are executed on different actions.
Examples of server-side git hooks include pre-receive, post-receive, and update.
@@ -123,6 +123,5 @@ exit 1
[CI]: ../ci/README.md
[hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks
[webhooks]: ../user/project/integrations/webhooks.md
-[gitlab-geo]: https://docs.gitlab.com/ee/administration/geo/replication/index.html
[5073]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5073
[93]: https://gitlab.com/gitlab-org/gitlab-shell/merge_requests/93
diff --git a/doc/administration/geo/disaster_recovery/planned_failover.md b/doc/administration/geo/disaster_recovery/planned_failover.md
index 88ab12d910a..b8071b5993f 100644
--- a/doc/administration/geo/disaster_recovery/planned_failover.md
+++ b/doc/administration/geo/disaster_recovery/planned_failover.md
@@ -113,7 +113,7 @@ If any objects are failing to replicate, this should be investigated before
scheduling the maintenance window. Following a planned failover, anything that
failed to replicate will be **lost**.
-You can use the [Geo status API](https://docs.gitlab.com/ee/api/geo_nodes.html#retrieve-project-sync-or-verification-failures-that-occurred-on-the-current-node) to review failed objects and
+You can use the [Geo status API](../../../api/geo_nodes.md#retrieve-project-sync-or-verification-failures-that-occurred-on-the-current-node) to review failed objects and
the reasons for failure.
A common cause of replication failures is the data being missing on the
diff --git a/doc/administration/geo/replication/database.md b/doc/administration/geo/replication/database.md
index a0c2cf0eced..1e5a56c3f4e 100644
--- a/doc/administration/geo/replication/database.md
+++ b/doc/administration/geo/replication/database.md
@@ -445,8 +445,7 @@ The replication process is now complete.
PostgreSQL connections. We recommend using PGBouncer if you use GitLab in a
high-availability configuration with a cluster of nodes supporting a Geo
**primary** node and another cluster of nodes supporting a Geo **secondary** node. For more
-information, see the [Omnibus HA](https://docs.gitlab.com/ee/administration/high_availability/database.html#configure-using-omnibus-for-high-availability)
-documentation.
+information, see [High Availability with GitLab Omnibus](../../high_availability/database.md#high-availability-with-gitlab-omnibus-premium-only).
For a Geo **secondary** node to work properly with PGBouncer in front of the database,
it will need a separate read-only user to make [PostgreSQL FDW queries][FDW]
diff --git a/doc/administration/geo/replication/external_database.md b/doc/administration/geo/replication/external_database.md
index dae5ed911b0..177ca68613e 100644
--- a/doc/administration/geo/replication/external_database.md
+++ b/doc/administration/geo/replication/external_database.md
@@ -129,7 +129,7 @@ To configure the connection to the external read-replica database and enable Log
database to keep track of replication status and automatically recover from
potential replication issues. Omnibus automatically configures a tracking database
when `roles ['geo_secondary_role']` is set. For high availability,
-refer to [Geo High Availability](https://docs.gitlab.com/ee/administration/high_availability).
+refer to [Geo High Availability](../../high_availability/README.md).
If you want to run this database external to Omnibus, please follow the instructions below.
The tracking database requires an [FDW](https://www.postgresql.org/docs/9.6/static/postgres-fdw.html)
diff --git a/doc/administration/geo/replication/high_availability.md b/doc/administration/geo/replication/high_availability.md
index 715a83a9ff3..921a3ef1c7a 100644
--- a/doc/administration/geo/replication/high_availability.md
+++ b/doc/administration/geo/replication/high_availability.md
@@ -6,7 +6,7 @@ described, it is possible to adapt these instructions to your needs.
## Architecture overview
-![Geo HA Diagram](https://docs.gitlab.com/ee/administration/img/high_availability/geo-ha-diagram.png)
+![Geo HA Diagram](../../high_availability/img/geo-ha-diagram.png)
_[diagram source - gitlab employees only][diagram-source]_
@@ -68,7 +68,7 @@ NOTE: **Note:** PostgreSQL and Redis should have already been disabled on the
application servers, and connections from the application servers to those
services on the backend servers configured, during normal GitLab HA set up. See
high availability configuration documentation for
-[PostgreSQL](https://docs.gitlab.com/ee/administration/high_availability/database.html#configuring-the-application-nodes)
+[PostgreSQL](../../high_availability/database.md#configuring-the-application-nodes)
and [Redis](../../high_availability/redis.md#example-configuration-for-the-gitlab-application).
The **primary** database will require modification later, as part of
diff --git a/doc/administration/geo/replication/index.md b/doc/administration/geo/replication/index.md
index b2f71d82cfc..54377f7ae4e 100644
--- a/doc/administration/geo/replication/index.md
+++ b/doc/administration/geo/replication/index.md
@@ -179,7 +179,7 @@ The steps below should be followed in the order they appear. **Make sure the Git
If you installed GitLab using the Omnibus packages (highly recommended):
1. [Install GitLab Enterprise Edition](https://about.gitlab.com/installation/) on the server that will serve as the **secondary** node. Do not create an account or log in to the new **secondary** node.
-1. [Upload the GitLab License](https://docs.gitlab.com/ee/user/admin_area/license.html) on the **primary** node to unlock Geo. The license must be for [GitLab Premium](https://about.gitlab.com/pricing/) or higher.
+1. [Upload the GitLab License](../../../user/admin_area/license.md) on the **primary** node to unlock Geo. The license must be for [GitLab Premium](https://about.gitlab.com/pricing/) or higher.
1. [Set up the database replication](database.md) (`primary (read-write) <-> secondary (read-only)` topology).
1. [Configure fast lookup of authorized SSH keys in the database](../../operations/fast_ssh_key_lookup.md). This step is required and needs to be done on **both** the **primary** and **secondary** nodes.
1. [Configure GitLab](configuration.md) to set the **primary** and **secondary** nodes.
@@ -261,7 +261,7 @@ Take special note that these examples of GitLab features are both:
Examples include:
-- [Elasticsearch integration](https://docs.gitlab.com/ee/integration/elasticsearch.html).
+- [Elasticsearch integration](../../../integration/elasticsearch.md).
- [Container Registry](../../container_registry.md). [Object Storage](object_storage.md) can mitigate this.
- [GitLab Pages](../../pages/index.md).
- [Mattermost integration](https://docs.gitlab.com/omnibus/gitlab-mattermost/).
diff --git a/doc/administration/geo/replication/tuning.md b/doc/administration/geo/replication/tuning.md
index b9921b2e69f..1943f2230df 100644
--- a/doc/administration/geo/replication/tuning.md
+++ b/doc/administration/geo/replication/tuning.md
@@ -13,5 +13,5 @@ However, this may not lead to more downloads in parallel unless the number of
available Sidekiq threads is also increased. For example, if repository sync
capacity is increased from 25 to 50, you may also want to increase the number
of Sidekiq threads from 25 to 50. See the
-[Sidekiq concurrency documentation](https://docs.gitlab.com/ee/administration/operations/extra_sidekiq_processes.html#number-of-threads)
+[Sidekiq concurrency documentation](../../operations/extra_sidekiq_processes.md#number-of-threads)
for more details.
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index f1cedb85455..53a85dfad6c 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -53,6 +53,10 @@ But since 11.8 the indexer uses Gitaly for data access as well. NFS can still
be leveraged for redudancy on block level of the Git data. But only has to
be mounted on the Gitaly server.
+NOTE: **Note:** While Gitaly can be used as a replacement for NFS, we do not recommend
+using EFS as it may impact GitLab's performance. Please review the [relevant documentation](../high_availability/nfs.md#avoid-using-awss-elastic-file-system-efs)
+for more details.
+
### Network architecture
- gitlab-rails shards repositories into "repository storages"
@@ -73,18 +77,29 @@ be mounted on the Gitaly server.
- Gitaly servers must not be exposed to the public internet
Gitaly network traffic is unencrypted by default, but supports
-[TLS](#tls-support). Authentication is done through a static token. For
-security in depth, its recommended to use a firewall to restrict access
-to your Gitaly server.
+[TLS](#tls-support). Authentication is done through a static token.
+
+NOTE: **Note:** Gitaly network traffic is unencrypted so we recommend a firewall to
+restrict access to your Gitaly server.
Below we describe how to configure a Gitaly server at address
`gitaly.internal:8075` with secret token `abc123secret`. We assume
your GitLab installation has two repository storages, `default` and
`storage1`.
+### Installation
+
+First install Gitaly using either Omnibus or from source.
+
+Omnibus: [Download/install](https://about.gitlab.com/installation) the Omnibus GitLab
+package you want using **steps 1 and 2** from the GitLab downloads page but
+**_do not_** provide the `EXTERNAL_URL=` value.
+
+Source: [Install Gitaly](../../install/installation.md#install-gitaly)
+
### Client side token configuration
-Start by configuring a token on the client side.
+Configure a token on the client side.
Omnibus installations:
@@ -110,7 +125,7 @@ changes to be picked up.
Next, on the Gitaly server, we need to configure storage paths, enable
the network listener and configure the token.
-Note: if you want to reduce the risk of downtime when you enable
+NOTE: **Note:** if you want to reduce the risk of downtime when you enable
authentication you can temporarily disable enforcement, see [the
documentation on configuring Gitaly
authentication](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/configuration/README.md#authentication)
@@ -122,12 +137,17 @@ the Gitaly server. The easiest way to accomplish this is to copy `/etc/gitlab/gi
from an existing GitLab server to the Gitaly server. Without this shared secret,
Git operations in GitLab will result in an API error.
-> **NOTE:** In most or all cases the storage paths below end in `/repositories` which is
+NOTE: **Note:** In most or all cases the storage paths below end in `/repositories` which is
different than `path` in `git_data_dirs` of Omnibus installations. Check the
directory layout on your Gitaly server to be sure.
Omnibus installations:
+<!--
+updates to following example must also be made at
+https://gitlab.com/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab
+-->
+
```ruby
# /etc/gitlab/gitlab.rb
@@ -147,6 +167,7 @@ gitlab_rails['auto_migrate'] = false
# Configure the gitlab-shell API callback URL. Without this, `git push` will
# fail. This can be your 'front door' GitLab URL or an internal load
# balancer.
+# Don't forget to copy `/etc/gitlab/gitlab-secrets.json` from web server to Gitaly server.
gitlab_rails['internal_api_url'] = 'https://gitlab.example.com'
# Make Gitaly accept connections on all network interfaces. You must use
@@ -199,6 +220,9 @@ network, firewall, or name resolution problem preventing your GitLab
server from reaching the Gitaly server then all Gitaly requests will
fail.
+Additionally, you need to
+[disable Rugged if previously manually enabled](../high_availability/nfs.md#improving-nfs-performance-with-gitlab).
+
We assume that your Gitaly server can be reached at
`gitaly.internal:8075` from your GitLab server, and that Gitaly can read and
write to `/mnt/gitlab/default` and `/mnt/gitlab/storage1` respectively.
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index e5701525077..002deeaf945 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -73,10 +73,10 @@ Complete the following installation steps in order. A link at the end of each
section will bring you back to the Scalable Architecture Examples section so
you can continue with the next step.
-1. [PostgreSQL](./database.md#postgresql-in-a-scaled-environment)
-1. [Redis](./redis.md#redis-in-a-scaled-environment)
-1. [Gitaly](./gitaly.md) (recommended) or [NFS](./nfs.md)
-1. [GitLab application nodes](./gitlab.md)
+1. [PostgreSQL](database.md#postgresql-in-a-scaled-environment)
+1. [Redis](redis.md#redis-in-a-scaled-environment)
+1. [Gitaly](gitaly.md) (recommended) or [NFS](nfs.md)
+1. [GitLab application nodes](gitlab.md)
### Full Scaling
diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md
index a446ed9637c..3b874e5d312 100644
--- a/doc/administration/high_availability/database.md
+++ b/doc/administration/high_availability/database.md
@@ -17,16 +17,16 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
## PostgreSQL in a Scaled Environment
-This section is relevant for [Scaled Architecture](./README.md#scalable-architecture-examples)
-environments including [Basic Scaling](./README.md#basic-scaling) and
-[Full Scaling](./README.md#full-scaling).
+This section is relevant for [Scaled Architecture](README.md#scalable-architecture-examples)
+environments including [Basic Scaling](README.md#basic-scaling) and
+[Full Scaling](README.md#full-scaling).
### Provide your own PostgreSQL instance **[CORE ONLY]**
-If you want to use your own deployed PostgreSQL instance(s),
+If you want to use your own deployed PostgreSQL instance(s),
see [Provide your own PostgreSQL instance](#provide-your-own-postgresql-instance-core-only)
-for more details. However, you can use the GitLab Omnibus package to easily
-deploy the bundled PostgreSQL.
+for more details. However, you can use the GitLab Omnibus package to easily
+deploy the bundled PostgreSQL.
### Standalone PostgreSQL using GitLab Omnibus **[CORE ONLY]**
@@ -36,19 +36,19 @@ deploy the bundled PostgreSQL.
- Do not complete any other steps on the download page.
1. Generate a password hash for PostgreSQL. This assumes you will use the default
username of `gitlab` (recommended). The command will request a password
- and confirmation. Use the value that is output by this command in the next
+ and confirmation. Use the value that is output by this command in the next
step as the value of `POSTGRESQL_PASSWORD_HASH`.
```sh
sudo gitlab-ctl pg-password-md5 gitlab
```
-
+
1. Edit `/etc/gitlab/gitlab.rb` and add the contents below, updating placeholder
- values appropriately.
-
+ values appropriately.
+
- `POSTGRESQL_PASSWORD_HASH` - The value output from the previous step
- `APPLICATION_SERVER_IP_BLOCKS` - A space delimited list of IP subnets or IP
- addresses of the GitLab application servers that will connect to the
+ addresses of the GitLab application servers that will connect to the
database. Example: `%w(123.123.123.123/32 123.123.123.234/32)`
```ruby
@@ -65,11 +65,11 @@ deploy the bundled PostgreSQL.
postgresql['listen_address'] = '0.0.0.0'
postgresql['port'] = 5432
- # Replace POSTGRESQL_PASSWORD_HASH with a generated md5 value
+ # Replace POSTGRESQL_PASSWORD_HASH with a generated md5 value
postgresql['sql_user_password'] = 'POSTGRESQL_PASSWORD_HASH'
# Replace XXX.XXX.XXX.XXX/YY with Network Address
- # ????
+ # ????
postgresql['trust_auth_cidr_addresses'] = %w(APPLICATION_SERVER_IP_BLOCKS)
# Disable automatic database migrations
@@ -77,42 +77,43 @@ deploy the bundled PostgreSQL.
```
NOTE: **Note:** The role `postgres_role` was introduced with GitLab 10.3
-
+
1. [Reconfigure GitLab] for the changes to take effect.
1. Note the PostgreSQL node's IP address or hostname, port, and
plain text password. These will be necessary when configuring the GitLab
application servers later.
-
+
Advanced configuration options are supported and can be added if
needed.
Continue configuration of other components by going
-[back to Scaled Architectures](./README.md#scalable-architecture-examples)
+[back to Scaled Architectures](README.md#scalable-architecture-examples)
## PostgreSQL with High Availability
-This section is relevant for [High Availability Architecture](./README.md#high-availability-architecture-examples)
-environments including [Horizontal](./README.md#horizontal),
-[Hybrid](./README.md#hybrid), and
-[Fully Distributed](./README.md#fully-distributed).
+This section is relevant for [High Availability Architecture](README.md#high-availability-architecture-examples)
+environments including [Horizontal](README.md#horizontal),
+[Hybrid](README.md#hybrid), and
+[Fully Distributed](README.md#fully-distributed).
### Provide your own PostgreSQL instance **[CORE ONLY]**
If you want to use your own deployed PostgreSQL instance(s),
see [Provide your own PostgreSQL instance](#provide-your-own-postgresql-instance-core-only)
for more details. However, you can use the GitLab Omnibus package to easily
-deploy the bundled PostgreSQL.
+deploy the bundled PostgreSQL.
### High Availability with GitLab Omnibus **[PREMIUM ONLY]**
> Important notes:
+>
> - This document will focus only on configuration supported with [GitLab Premium](https://about.gitlab.com/pricing/), using the Omnibus GitLab package.
> - If you are a Community Edition or Starter user, consider using a cloud hosted solution.
> - This document will not cover installations from source.
>
> - If HA setup is not what you were looking for, see the [database configuration document](http://docs.gitlab.com/omnibus/settings/database.html)
> for the Omnibus GitLab packages.
-
+>
> Please read this document fully before attempting to configure PostgreSQL HA
> for GitLab.
>
@@ -122,9 +123,9 @@ The recommended configuration for a PostgreSQL HA requires:
- A minimum of three database nodes
- Each node will run the following services:
- - `PostgreSQL` - The database itself
- - `repmgrd` - A service to monitor, and handle failover in case of a failure
- - `Consul` agent - Used for service discovery, to alert other nodes when failover occurs
+ - `PostgreSQL` - The database itself
+ - `repmgrd` - A service to monitor, and handle failover in case of a failure
+ - `Consul` agent - Used for service discovery, to alert other nodes when failover occurs
- A minimum of three `Consul` server nodes
- A minimum of one `pgbouncer` service node
@@ -142,7 +143,7 @@ Database nodes run two services with PostgreSQL:
- Selecting a new master for the cluster.
- Promoting the new node to master.
- Instructing remaining servers to follow the new master node.
-
+
On failure, the old master node is automatically evicted from the cluster, and should be rejoined manually once recovered.
- Consul. Monitors the status of each node in the database cluster and tracks its health in a service definition on the consul cluster.
@@ -171,13 +172,10 @@ Similarly, PostgreSQL access is controlled based on the network source.
This is why you will need:
-> IP address of each nodes network interface
-> - This can be set to `0.0.0.0` to listen on all interfaces. It cannot
-> be set to the loopack address `127.0.0.1`
->
-> Network Address
-> - This can be in subnet (i.e. `192.168.0.0/255.255.255.0`) or CIDR (i.e.
-> `192.168.0.0/24`) form.
+- IP address of each nodes network interface. This can be set to `0.0.0.0` to
+ listen on all interfaces. It cannot be set to the loopack address `127.0.0.1`.
+- Network Address. This can be in subnet (i.e. `192.168.0.0/255.255.255.0`)
+ or CIDR (i.e. `192.168.0.0/24`) form.
##### User information
@@ -199,7 +197,7 @@ When using default setup, minimum configuration requires:
sudo gitlab-ctl pg-password-md5 CONSUL_USERNAME
```
-- `CONSUL_SERVER_NODES`. The IP addresses or DNS records of the Consul server nodes.
+- `CONSUL_SERVER_NODES`. The IP addresses or DNS records of the Consul server nodes.
Few notes on the service itself:
@@ -220,8 +218,7 @@ the number of database nodes in the cluster.
This is used to prevent replication from using up all of the
available database connections.
-> Note:
-> - In this document we are assuming 3 database nodes, which makes this configuration:
+In this document we are assuming 3 database nodes, which makes this configuration:
```
postgresql['max_wal_senders'] = 4
@@ -277,7 +274,7 @@ be allowed to authenticate with the service.
Few notes on the service itself:
- The service runs under the same system account as the database
- - In the package, this is by default `gitlab-psql`
+ - In the package, this is by default `gitlab-psql`
- The service will have a superuser database user account generated for it
- This defaults to `gitlab_repmgr`
@@ -327,7 +324,7 @@ On each Consul node perform the following:
Before moving on, make sure Consul is configured correctly. Run the following
command to verify all server nodes are communicating:
-```
+```sh
/opt/gitlab/embedded/bin/consul members
```
@@ -401,14 +398,15 @@ check the [Troubleshooting section](#troubleshooting) before proceeding.
repmgr['master_on_initialization'] = false
```
-1. [Reconfigure GitLab] for te changes to take effect.
+1. [Reconfigure GitLab] for the changes to take effect.
> Please note:
+>
> - If you want your database to listen on a specific interface, change the config:
-> `postgresql['listen_address'] = '0.0.0.0'`
+> `postgresql['listen_address'] = '0.0.0.0'`.
> - If your Pgbouncer service runs under a different user account,
> you also need to specify: `postgresql['pgbouncer_user'] = PGBOUNCER_USERNAME` in
-> your configuration
+> your configuration.
##### Database nodes post-configuration
@@ -449,7 +447,6 @@ Select one node as a primary node.
is not an IP address, it will need to be a resolvable name (via DNS or
`/etc/hosts`)
-
###### Secondary nodes
1. Set up the repmgr standby:
@@ -500,7 +497,7 @@ Before moving on, make sure the databases are configured correctly. Run the
following command on the **primary** node to verify that replication is working
properly:
-```
+```sh
gitlab-ctl repmgr cluster show
```
@@ -518,7 +515,7 @@ If the 'Role' column for any node says "FAILED", check the
Also, check that the check master command works successfully on each node:
-```
+```sh
su - gitlab-consul
gitlab-ctl repmgr-check-master || echo 'This node is a standby repmgr node'
```
@@ -649,7 +646,7 @@ in the Troubleshooting section before proceeding.
##### Ensure GitLab is running
At this point, your GitLab instance should be up and running. Verify you are
-able to login, and create issues and merge requests. If you have troubles check
+able to login, and create issues and merge requests. If you have troubles check
the [Troubleshooting section](#troubleshooting).
#### Example configuration
@@ -665,13 +662,13 @@ can connect to each freely other on those addresses.
Here is a list and description of each machine and the assigned IP:
-* `10.6.0.11`: Consul 1
-* `10.6.0.12`: Consul 2
-* `10.6.0.13`: Consul 3
-* `10.6.0.21`: PostgreSQL master
-* `10.6.0.22`: PostgreSQL secondary
-* `10.6.0.23`: PostgreSQL secondary
-* `10.6.0.31`: GitLab application
+- `10.6.0.11`: Consul 1
+- `10.6.0.12`: Consul 2
+- `10.6.0.13`: Consul 3
+- `10.6.0.21`: PostgreSQL master
+- `10.6.0.22`: PostgreSQL secondary
+- `10.6.0.23`: PostgreSQL secondary
+- `10.6.0.31`: GitLab application
All passwords are set to `toomanysecrets`, please do not use this password or derived hashes.
@@ -735,7 +732,7 @@ consul['configuration'] = {
On secondary nodes, edit `/etc/gitlab/gitlab.rb` and add all the configuration
added to primary node, noted above. In addition, append the following
-configuration
+configuration:
```
# HA setting to specify if a node should attempt to be master on initialization
@@ -839,10 +836,10 @@ In this example we start with all servers on the same 10.6.0.0/16 private networ
Here is a list and description of each machine and the assigned IP:
-* `10.6.0.21`: PostgreSQL master
-* `10.6.0.22`: PostgreSQL secondary
-* `10.6.0.23`: PostgreSQL secondary
-* `10.6.0.31`: GitLab application
+- `10.6.0.21`: PostgreSQL master
+- `10.6.0.22`: PostgreSQL secondary
+- `10.6.0.23`: PostgreSQL secondary
+- `10.6.0.31`: GitLab application
All passwords are set to `toomanysecrets`, please do not use this password or derived hashes.
@@ -853,6 +850,7 @@ Please note that after the initial configuration, if a failover occurs, the Post
##### Example minimal configuration for database servers
##### Primary node
+
On primary database node edit `/etc/gitlab/gitlab.rb`:
```ruby
@@ -1047,7 +1045,6 @@ For example:
repmgr['trust_auth_cidr_addresses'] = %w(192.168.1.44/32 db2.example.com)
```
-
##### MD5 Authentication
If you are running on an untrusted network, repmgr can use md5 authentication
@@ -1114,7 +1111,7 @@ steps to fix the problem:
1. Change to the `gitlab-consul` user - `su - gitlab-consul`
1. Try the check command again - `gitlab-ctl repmgr-check-master`.
-Now there should not be errors. If errors still occur then there is another problem.
+Now there should not be errors. If errors still occur then there is another problem.
#### PGBouncer error `ERROR: pgbouncer cannot connect to server`
@@ -1157,8 +1154,6 @@ If you're running into an issue with a component not outlined here, be sure to c
**Note**: We recommend that you follow the instructions here for a full [PostgreSQL cluster](#high-availability-with-gitlab-omnibus-premium-only).
If you are reading this section due to an old bookmark, you can find that old documentation [in the repository](https://gitlab.com/gitlab-org/gitlab-ce/blob/v10.1.4/doc/administration/high_availability/database.md#configure-using-omnibus).
----
-
Read more on high-availability configuration:
1. [Configure Redis](redis.md)
diff --git a/doc/administration/high_availability/gitaly.md b/doc/administration/high_availability/gitaly.md
index d44744f2af8..1d8e6c999cb 100644
--- a/doc/administration/high_availability/gitaly.md
+++ b/doc/administration/high_availability/gitaly.md
@@ -7,84 +7,15 @@ should consider using Gitaly on a separate node.
See the [Gitaly HA Epic](https://gitlab.com/groups/gitlab-org/-/epics/289) to
track plans and progress toward high availability support.
-This document is relevant for [Scaled Architecture](./README.md#scalable-architecture-examples)
-environments and [High Availability Architecture](./README.md#high-availability-architecture-examples).
+This document is relevant for [Scaled Architecture](README.md#scalable-architecture-examples)
+environments and [High Availability Architecture](README.md#high-availability-architecture-examples).
## Running Gitaly on its own server
-Starting with GitLab 11.4, Gitaly is a replacement for NFS except
-when the [Elastic Search indexer](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer)
-is used.
-
-NOTE: **Note:** While Gitaly can be used as a replacement for NFS, we do not recommend using EFS as it may impact GitLab's performance. Please review the [relevant documentation](nfs.md#avoid-using-awss-elastic-file-system-efs) for more details.
-
-NOTE: **Note:** Gitaly network traffic is unencrypted so we recommend a firewall to
-restrict access to your Gitaly server.
-
-The steps below are the minimum necessary to configure a Gitaly server with
-Omnibus:
-
-1. SSH into the Gitaly server.
-1. [Download/install](https://about.gitlab.com/installation) the Omnibus GitLab
- package you want using **steps 1 and 2** from the GitLab downloads page.
- - Do not complete any other steps on the download page.
-
-1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
-
- Gitaly must trigger some callbacks to GitLab via GitLab Shell. As a result,
- the GitLab Shell secret must be the same between the other GitLab servers and
- the Gitaly server. The easiest way to accomplish this is to copy `/etc/gitlab/gitlab-secrets.json`
- from an existing GitLab server to the Gitaly server. Without this shared secret,
- Git operations in GitLab will result in an API error.
-
- > **NOTE:** In most or all cases the storage paths below end in `repositories` which is
- different than `path` in `git_data_dirs` of Omnibus installations. Check the
- directory layout on your Gitaly server to be sure.
-
- ```ruby
- # Enable Gitaly
- gitaly['enable'] = true
-
- ## Disable all other services
- sidekiq['enable'] = false
- gitlab_workhorse['enable'] = false
- unicorn['enable'] = false
- postgresql['enable'] = false
- nginx['enable'] = false
- prometheus['enable'] = false
- alertmanager['enable'] = false
- pgbouncer_exporter['enable'] = false
- redis_exporter['enable'] = false
- gitlab_monitor['enable'] = false
-
- # Prevent database connections during 'gitlab-ctl reconfigure'
- gitlab_rails['rake_cache_clear'] = false
- gitlab_rails['auto_migrate'] = false
-
- # Configure the gitlab-shell API callback URL. Without this, `git push` will
- # fail. This can be your 'front door' GitLab URL or an internal load
- # balancer.
- gitlab_rails['internal_api_url'] = 'https://gitlab.example.com'
-
- # Make Gitaly accept connections on all network interfaces. You must use
- # firewalls to restrict access to this address/port.
- gitaly['listen_addr'] = "0.0.0.0:8075"
- gitaly['auth_token'] = 'abc123secret'
-
- gitaly['storage'] = [
- { 'name' => 'default', 'path' => '/mnt/gitlab/default/repositories' },
- { 'name' => 'storage1', 'path' => '/mnt/gitlab/storage1/repositories' },
- ]
-
- # To use tls for gitaly you need to add
- gitaly['tls_listen_addr'] = "0.0.0.0:9999"
- gitaly['certificate_path'] = "path/to/cert.pem"
- gitaly['key_path'] = "path/to/key.pem"
- ```
-
-Again, reconfigure (Omnibus) or restart (source).
+See [Running Gitaly on its own server](../gitaly/index.md#running-gitaly-on-its-own-server)
+in Gitaly documentation.
Continue configuration of other components by going back to:
-- [Scaled Architectures](./README.md#scalable-architecture-examples)
-- [High Availability Architectures](./README.md#high-availability-architecture-examples)
+- [Scaled Architectures](README.md#scalable-architecture-examples)
+- [High Availability Architectures](README.md#high-availability-architecture-examples)
diff --git a/doc/administration/high_availability/img/geo-ha-diagram.png b/doc/administration/high_availability/img/geo-ha-diagram.png
new file mode 100644
index 00000000000..da5d612827c
--- /dev/null
+++ b/doc/administration/high_availability/img/geo-ha-diagram.png
Binary files differ
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index d1233d815ed..490c2699458 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -56,7 +56,8 @@ To do this, run the Rake task:
sudo gitlab-rake gitlab:features:enable_rugged
```
-If you need to undo this setting for some reason, run:
+If you need to undo this setting for some reason such as switching to [Gitaly without NFS](gitaly.md)
+(recommended), run:
```sh
sudo gitlab-rake gitlab:features:disable_rugged
@@ -122,7 +123,7 @@ Additionally, this configuration is specifically warned against in the
>to the NFS server can cause data corruption problems.
For supported database architecture, please see our documentation on
-[Configuring a Database for GitLab HA](https://docs.gitlab.com/ee/administration/high_availability/database.html).
+[Configuring a Database for GitLab HA](database.md).
## NFS Client mount options
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index 46ad3ecd9bb..1aaa709fc8f 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -16,9 +16,9 @@ These will be necessary when configuring the GitLab application servers later.
## Redis in a Scaled Environment
-This section is relevant for [Scaled Architecture](./README.md#scalable-architecture-examples)
-environments including [Basic Scaling](./README.md#basic-scaling) and
-[Full Scaling](./README.md#full-scaling).
+This section is relevant for [Scaled Architecture](README.md#scalable-architecture-examples)
+environments including [Basic Scaling](README.md#basic-scaling) and
+[Full Scaling](README.md#full-scaling).
### Provide your own Redis instance **[CORE ONLY]**
@@ -34,7 +34,7 @@ In this configuration Redis is not highly available, and represents a single
point of failure. However, in a scaled environment the objective is to allow
the environment to handle more users or to increase throughput. Redis itself
is generally stable and can handle many requests so it is an acceptable
-trade off to have only a single instance. See [Scaling and High Availability](./README.md)
+trade off to have only a single instance. See [Scaling and High Availability](README.md)
for an overview of GitLab scaling and high availability options.
The steps below are the minimum necessary to configure a Redis server with
@@ -79,14 +79,14 @@ Advanced configuration options are supported and can be added if
needed.
Continue configuration of other components by going
-[back to Scaled Architectures](./README.md#scalable-architecture-examples)
+[back to Scaled Architectures](README.md#scalable-architecture-examples)
## Redis with High Availability
-This section is relevant for [High Availability Architecture](./README.md#high-availability-architecture-examples)
-environments including [Horizontal](./README.md#horizontal),
-[Hybrid](./README.md#hybrid), and
-[Fully Distributed](./README.md#fully-distributed).
+This section is relevant for [High Availability Architecture](README.md#high-availability-architecture-examples)
+environments including [Horizontal](README.md#horizontal),
+[Hybrid](README.md#hybrid), and
+[Fully Distributed](README.md#fully-distributed).
### Provide your own Redis instance **[CORE ONLY]**
@@ -863,7 +863,7 @@ You can check if everything is correct by connecting to each server using
`redis-cli` application, and sending the `info replication` command as below.
```
-/opt/gitlab/embedded/bin/redis-cli -a <redis-password> info replication
+/opt/gitlab/embedded/bin/redis-cli -h <redis-host-or-ip> -a '<redis-password>' info replication
```
When connected to a `master` redis, you will see the number of connected
diff --git a/doc/administration/incoming_email.md b/doc/administration/incoming_email.md
index 9ac310af248..8271c579f5b 100644
--- a/doc/administration/incoming_email.md
+++ b/doc/administration/incoming_email.md
@@ -10,7 +10,7 @@ GitLab has several features based on receiving incoming emails:
- [New merge request by email](../user/project/merge_requests/index.md#create-new-merge-requests-by-email):
allow GitLab users to create a new merge request by sending an email to a
user-specific email address.
-- [Service Desk](https://docs.gitlab.com/ee/user/project/service_desk.html): provide e-mail support to
+- [Service Desk](../user/project/service_desk.md): provide e-mail support to
your customers through GitLab. **[PREMIUM]**
## Requirements
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 797a7242bd0..95a0e84deb6 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -20,7 +20,7 @@ GitLab Community Edition installations only have access to Core features.
GitLab.com is administered by GitLab, Inc., therefore, only GitLab team members have
access to its admin configurations. If you're a GitLab.com user, please check the
-[user documentation](../user/index.html).
+[user documentation](../user/index.md).
NOTE: **Note:**
Non-administrator users don’t have access to GitLab administration tools and settings.
@@ -37,9 +37,9 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Omnibus support for log forwarding](https://docs.gitlab.com/omnibus/settings/logs.html#udp-log-shipping-gitlab-enterprise-edition-only) **[STARTER ONLY]**
- [High Availability](high_availability/README.md): Configure multiple servers for scaling or high availability.
- [High Availability on AWS](../university/high-availability/aws/README.md): Set up GitLab HA on Amazon AWS.
-- [Geo](https://docs.gitlab.com/ee/administration/geo/replication/index.html): Replicate your GitLab instance to other geographic locations as a read-only fully operational version. **[PREMIUM ONLY]**
-- [Disaster Recovery](https://docs.gitlab.com/ee/administration/geo/disaster_recovery/index.html): Quickly fail-over to a different site with minimal effort in a disaster situation. **[PREMIUM ONLY]**
-- [Pivotal Tile](https://docs.gitlab.com/ee/install/pivotal/index.html): Deploy GitLab as a pre-configured appliance using Ops Manager (BOSH) for Pivotal Cloud Foundry. **[PREMIUM ONLY]**
+- [Geo](geo/replication/index.md): Replicate your GitLab instance to other geographic locations as a read-only fully operational version. **[PREMIUM ONLY]**
+- [Disaster Recovery](geo/disaster_recovery/index.md): Quickly fail-over to a different site with minimal effort in a disaster situation. **[PREMIUM ONLY]**
+- [Pivotal Tile](../install/pivotal/index.md): Deploy GitLab as a pre-configured appliance using Ops Manager (BOSH) for Pivotal Cloud Foundry. **[PREMIUM ONLY]**
- [Add License](../user/admin_area/license.md): Upload a license at install time to unlock features that are in paid tiers of GitLab. **[STARTER ONLY]**
### Configuring GitLab
@@ -61,9 +61,9 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Diff limits](../user/admin_area/diff_limits.md): Configure the diff rendering size limits of branch comparison pages.
- [Merge request diffs storage](merge_request_diffs.md): Configure merge requests diffs external storage.
- [Broadcast Messages](../user/admin_area/broadcast_messages.md): Send messages to GitLab users through the UI.
-- [Elasticsearch](https://docs.gitlab.com/ee/integration/elasticsearch.html): Enable Elasticsearch to empower GitLab's Advanced Global Search. Useful when you deal with a huge amount of data. **[STARTER ONLY]**
+- [Elasticsearch](../integration/elasticsearch.md): Enable Elasticsearch to empower GitLab's Advanced Global Search. Useful when you deal with a huge amount of data. **[STARTER ONLY]**
- [External Classification Policy Authorization](../user/admin_area/settings/external_authorization.md) **[PREMIUM ONLY]**
-- [Upload a license](https://docs.gitlab.com/ee/user/admin_area/license.html): Upload a license to unlock features that are in paid tiers of GitLab. **[STARTER ONLY]**
+- [Upload a license](../user/admin_area/license.md): Upload a license to unlock features that are in paid tiers of GitLab. **[STARTER ONLY]**
- [Admin Area](../user/admin_area/index.md): for self-managed instance-wide configuration and maintenance.
#### Customizing GitLab's appearance
@@ -73,7 +73,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Branded login page](../customization/branded_login_page.md): Customize the login page with your own logo, title, and description.
- [Welcome message](../customization/welcome_message.md): Add a custom welcome message to the sign-in page.
- ["New Project" page](../customization/new_project_page.md): Customize the text to be displayed on the page that opens whenever your users create a new project.
-- [Additional custom email text](https://docs.gitlab.com/ee/user/admin_area/settings/email.html#custom-additional-text-premium-only): Add additional custom text to emails sent from GitLab. **[PREMIUM ONLY]**
+- [Additional custom email text](../user/admin_area/settings/email.md#custom-additional-text-premium-only): Add additional custom text to emails sent from GitLab. **[PREMIUM ONLY]**
### Maintaining GitLab
@@ -108,34 +108,29 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Sign-up restrictions](../user/admin_area/settings/sign_up_restrictions.md): block email addresses of specific domains, or whitelist only specific domains.
- [Access restrictions](../user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols): Define which Git access protocols can be used to talk to GitLab (SSH, HTTP, HTTPS).
- [Authentication and Authorization](auth/README.md): Configure external authentication with LDAP, SAML, CAS and additional providers.
- - [Sync LDAP](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html) **[STARTER ONLY]**
- - [Kerberos authentication](https://docs.gitlab.com/ee/integration/kerberos.html) **[STARTER ONLY]**
+ - [Sync LDAP](auth/ldap-ee.md) **[STARTER ONLY]**
+ - [Kerberos authentication](../integration/kerberos.md) **[STARTER ONLY]**
- See also other [authentication](../topics/authentication/index.md#gitlab-administrators) topics (for example, enforcing 2FA).
-- [Email users](https://docs.gitlab.com/ee/tools/email.html): Email GitLab users from within GitLab. **[STARTER ONLY]**
+- [Email users](../tools/email.md): Email GitLab users from within GitLab. **[STARTER ONLY]**
- [User Cohorts](../user/admin_area/user_cohorts.md): Display the monthly cohorts of new users and their activities over time.
- [Audit logs and events](audit_events.md): View the changes made within the GitLab server for:
- Groups and projects. **[STARTER]**
- Instances. **[PREMIUM ONLY]**
- [Auditor users](auditor_users.md): Users with read-only access to all projects, groups, and other resources on the GitLab instance. **[PREMIUM ONLY]**
- [Incoming email](incoming_email.md): Configure incoming emails to allow
- users to [reply by email], create [issues by email] and
- [merge requests by email], and to enable [Service Desk].
+ users to [reply by email](reply_by_email.md), create [issues by email](../user/project/issues/create_new_issue.md#new-issue-via-email) and
+ [merge requests by email](../user/project/merge_requests/index.md#create-new-merge-requests-by-email), and to enable [Service Desk](../user/project/service_desk.md).
- [Postfix for incoming email](reply_by_email_postfix_setup.md): Set up a
basic Postfix mail server with IMAP authentication on Ubuntu for incoming
emails.
- [Abuse reports](../user/admin_area/abuse_reports.md): View and resolve abuse reports from your users.
-[reply by email]: reply_by_email.md
-[issues by email]: ../user/project/issues/create_new_issue.md#new-issue-via-email
-[merge requests by email]: ../user/project/merge_requests/index.md#create-new-merge-requests-by-email
-[Service Desk]: https://docs.gitlab.com/ee/user/project/service_desk.html
-
## Project settings
- [Container Registry](container_registry.md): Configure Container Registry with GitLab.
- [Issue closing pattern](issue_closing_pattern.md): Customize how to close an issue from commit messages.
- [Gitaly](gitaly/index.md): Configuring Gitaly, GitLab's Git repository storage service.
-- [Default labels](../user/admin_area/labels.html): Create labels that will be automatically added to every new project.
+- [Default labels](../user/admin_area/labels.md): Create labels that will be automatically added to every new project.
- [Restrict the use of public or internal projects](../public_access/public_access.md#restricting-the-use-of-public-or-internal-projects): Restrict the use of visibility levels for users when they create a project or a snippet.
- [Custom project templates](../user/admin_area/custom_project_templates.md): Configure a set of projects to be used as custom templates when creating a new project. **[PREMIUM ONLY]**
- [Packages](packages.md): Enable GitLab to act as a Maven repository or NPM registry. **[PREMIUM ONLY]**
@@ -145,7 +140,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Repository checks](repository_checks.md): Periodic Git repository checks.
- [Repository storage paths](repository_storage_paths.md): Manage the paths used to store repositories.
- [Repository storage rake tasks](raketasks/storage.md): A collection of rake tasks to list and migrate existing projects and attachments associated with it from Legacy storage to Hashed storage.
-- [Limit repository size](https://docs.gitlab.com/ee/user/admin_area/settings/account_and_limit_settings.html): Set a hard limit for your repositories' size. **[STARTER ONLY]**
+- [Limit repository size](../user/admin_area/settings/account_and_limit_settings.md): Set a hard limit for your repositories' size. **[STARTER ONLY]**
## Continuous Integration settings
@@ -154,7 +149,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Job artifacts](job_artifacts.md): Enable, disable, and configure job artifacts (a set of files and directories which are outputted by a job when it completes successfully).
- [Job traces](job_traces.md): Information about the job traces (logs).
- [Register Shared and specific Runners](../ci/runners/README.md#registering-a-shared-runner): Learn how to register and configure Shared and specific Runners to your own instance.
-- [Shared Runners pipelines quota](https://docs.gitlab.com/ee/user/admin_area/settings/continuous_integration.html#shared-runners-pipeline-minutes-quota-starter-only): Limit the usage of pipeline minutes for Shared Runners. **[STARTER ONLY]**
+- [Shared Runners pipelines quota](../user/admin_area/settings/continuous_integration.md#shared-runners-pipeline-minutes-quota-starter-only): Limit the usage of pipeline minutes for Shared Runners. **[STARTER ONLY]**
- [Enable/disable Auto DevOps](../topics/autodevops/index.md#enablingdisabling-auto-devops): Enable or disable Auto DevOps for your instance.
## Git configuration options
diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md
index 2596e3fe68b..c34858cd0db 100644
--- a/doc/administration/integration/terminal.md
+++ b/doc/administration/integration/terminal.md
@@ -43,6 +43,11 @@ detail below.
## Enabling and disabling terminal support
+NOTE: **Note:** AWS Elastic Load Balancers (ELBs) do not support web sockets.
+AWS Application Load Balancers (ALBs) must be used if you want web terminals
+to work. See [AWS Elastic Load Balancing Product Comparison](https://aws.amazon.com/elasticloadbalancing/features/#compare)
+for more information.
+
As web terminals use WebSockets, every HTTP/HTTPS reverse proxy in front of
Workhorse needs to be configured to pass the `Connection` and `Upgrade` headers
through to the next one in the chain. If you installed GitLab using Omnibus, or
diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md
index ef370573a98..05e15fc303b 100644
--- a/doc/administration/job_artifacts.md
+++ b/doc/administration/job_artifacts.md
@@ -7,9 +7,9 @@
> - Starting with GitLab 8.17, builds are renamed to jobs.
> - This is the administration documentation. For the user guide see [pipelines/job_artifacts](../user/project/pipelines/job_artifacts.md).
-Artifacts is a list of files and directories which are attached to a job
-after it completes successfully. This feature is enabled by default in all
-GitLab installations. Keep reading if you want to know how to disable it.
+Artifacts is a list of files and directories which are attached to a job after it
+finishes. This feature is enabled by default in all GitLab installations. Keep reading
+if you want to know how to disable it.
## Disabling job artifacts
@@ -42,8 +42,9 @@ To disable artifacts site-wide, follow the steps below.
## Storing job artifacts
-After a successful job, GitLab Runner uploads an archive containing the job
-artifacts to GitLab.
+GitLab Runner can upload an archive containing the job artifacts to GitLab. By default,
+this is done when the job succeeds, but can also be done on failure, or always, via the
+[`artifacts:when`](../ci/yaml/README.md#artifactswhen) parameter.
### Using local storage
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index a7e57e44e86..c5cfb8d5016 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -129,7 +129,7 @@ It contains information about [integrations](../user/project/integrations/projec
``` json
{"severity":"ERROR","time":"2018-09-06T14:56:20.439Z","service_class":"JiraService","project_id":8,"project_path":"h5bp/html5-boilerplate","message":"Error sending message","client_url":"http://jira.gitlap.com:8080","error":"execution expired"}
-{"severity":"INFO","time":"2018-09-06T17:15:16.365Z","service_class":"JiraService","project_id":3,"project_path":"namespace2/project2","message":"Successfully posted","client_url":"http://jira.example.net"}
+{"severity":"INFO","time":"2018-09-06T17:15:16.365Z","service_class":"JiraService","project_id":3,"project_path":"namespace2/project2","message":"Successfully posted","client_url":"http://jira.example.com"}
```
## `kubernetes.log`
@@ -288,6 +288,20 @@ installations from source.
It logs information whenever [Rack Attack] registers an abusive request.
+## `graphql_json.log`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/59587) in GitLab 12.0.
+
+This file lives in `/var/log/gitlab/gitlab-rails/graphql_json.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/graphql_json.log` for
+installations from source.
+
+GraphQL queries are recorded in that file. For example:
+
+```json
+{"query_string":"query IntrospectionQuery{__schema {queryType { name },mutationType { name }}}...(etc)","variables":{"a":1,"b":2},"complexity":181,"depth":1,"duration":7}
+```
+
## Reconfigure Logs
Reconfigure log files live in `/var/log/gitlab/reconfigure` for Omnibus GitLab
diff --git a/doc/administration/monitoring/performance/grafana_configuration.md b/doc/administration/monitoring/performance/grafana_configuration.md
index ab43ec2cc4f..187fb2f73a1 100644
--- a/doc/administration/monitoring/performance/grafana_configuration.md
+++ b/doc/administration/monitoring/performance/grafana_configuration.md
@@ -3,7 +3,7 @@
[Grafana](http://grafana.org/) is a tool that allows you to visualize time
series metrics through graphs and dashboards. It supports several backend
data stores, including InfluxDB. GitLab writes performance data to InfluxDB
-and Grafana will allow you to query InfluxDB to display useful graphs.
+and Grafana will allow you to query to display useful graphs.
For the easiest installation and configuration, install Grafana on the same
server as InfluxDB. For larger installations, you may want to split out these
@@ -11,11 +11,13 @@ services.
## Installation
-Grafana supplies package repositories (Yum/Apt) for easy installation.
+[GitLab Omnibus can help you install Grafana (recommended)](https://docs.gitlab.com/omnibus/settings/grafana.html)
+or Grafana supplies package repositories (Yum/Apt) for easy installation.
See [Grafana installation documentation](http://docs.grafana.org/installation/)
for detailed steps.
-> **Note**: Before starting Grafana for the first time, set the admin user
+NOTE: **Note:**
+Before starting Grafana for the first time, set the admin user
and password in `/etc/grafana/grafana.ini`. Otherwise, the default password
will be `admin`.
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 4529bd3bee3..3dcd1593099 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -43,10 +43,11 @@ The following metrics are available:
| redis_ping_latency_seconds | Gauge | 9.4 | Round trip time of the redis ping |
| user_session_logins_total | Counter | 9.4 | Counter of how many users have logged in |
| upload_file_does_not_exist | Counter | 10.7 in EE, 11.5 in CE | Number of times an upload record could not find its file |
-| failed_login_captcha_total | Gauge | 11.0 | Counter of failed CAPTCHA attempts during login |
-| successful_login_captcha_total | Gauge | 11.0 | Counter of successful CAPTCHA attempts during login |
-| unicorn_active_connections | Gauge | 11.0 | The number of active Unicorn connections (workers) |
-| unicorn_queued_connections | Gauge | 11.0 | The number of queued Unicorn connections |
+| failed_login_captcha_total | Gauge | 11.0 | Counter of failed CAPTCHA attempts during login |
+| successful_login_captcha_total | Gauge | 11.0 | Counter of successful CAPTCHA attempts during login |
+| unicorn_active_connections | Gauge | 11.0 | The number of active Unicorn connections (workers) |
+| unicorn_queued_connections | Gauge | 11.0 | The number of queued Unicorn connections |
+| unicorn_workers | Gauge | 12.0 | The number of Unicorn workers |
## Sidekiq Metrics available for Geo **[PREMIUM]**
@@ -86,8 +87,8 @@ the `monitoring.sidekiq_exporter` configuration option in `gitlab.yml`.
| geo_wikis_checksum_mismatch_count | Gauge | 10.7 | Number of wikis that checksum mismatch on secondary | url
| geo_repositories_checked_count | Gauge | 11.1 | Number of repositories that have been checked via `git fsck` | url
| geo_repositories_checked_failed_count | Gauge | 11.1 | Number of repositories that have a failure from `git fsck` | url
-| geo_repositories_retrying_verification_count | Gauge | 11.2 | Number of repositories verification failures that Geo is actively trying to correct on secondary | url
-| geo_wikis_retrying_verification_count | Gauge | 11.2 | Number of wikis verification failures that Geo is actively trying to correct on secondary | url
+| geo_repositories_retrying_verification_count | Gauge | 11.2 | Number of repositories verification failures that Geo is actively trying to correct on secondary | url
+| geo_wikis_retrying_verification_count | Gauge | 11.2 | Number of wikis verification failures that Geo is actively trying to correct on secondary | url
### Ruby metrics
@@ -100,6 +101,10 @@ Some basic Ruby runtime metrics are available:
| ruby_file_descriptors | Gauge | 11.1 | File descriptors per process |
| ruby_memory_bytes | Gauge | 11.1 | Memory usage by process |
| ruby_sampler_duration_seconds_total | Counter | 11.1 | Time spent collecting stats |
+| ruby_process_cpu_seconds_total | Gauge | 12.0 | Total amount of CPU time per process |
+| ruby_process_max_fds | Gauge | 12.0 | Maximum number of open file descriptors per process |
+| ruby_process_resident_memory_bytes | Gauge | 12.0 | Memory usage by process, measured in bytes |
+| ruby_process_start_time_seconds | Gauge | 12.0 | The elapsed time between system boot and the process started, measured in seconds |
[GC.stat]: https://ruby-doc.org/core-2.3.0/GC.html#method-c-stat
diff --git a/doc/administration/operations/fast_ssh_key_lookup.md b/doc/administration/operations/fast_ssh_key_lookup.md
index 69f110805b7..3631ea0822f 100644
--- a/doc/administration/operations/fast_ssh_key_lookup.md
+++ b/doc/administration/operations/fast_ssh_key_lookup.md
@@ -34,10 +34,10 @@ feature for CentOS 6, follow [the instructions on how to build and install a cus
By default, GitLab manages an `authorized_keys` file, which contains all the
public SSH keys for users allowed to access GitLab. However, to maintain a
-single source of truth, [Geo](https://docs.gitlab.com/ee/administration/geo/replication/index.html) needs to be configured to perform SSH fingerprint
+single source of truth, [Geo](../geo/replication/index.md) needs to be configured to perform SSH fingerprint
lookups via database lookup.
-As part of [setting up Geo](https://docs.gitlab.com/ee/administration/geo/replication/index.html#setup-instructions),
+As part of [setting up Geo](../geo/replication/index.md#setup-instructions),
you will be required to follow the steps outlined below for both the primary and
secondary nodes, but note that the `Write to "authorized keys" file` checkbox
only needs to be unchecked on the primary node since it will be reflected
diff --git a/doc/administration/packages.md b/doc/administration/packages.md
index 5b9a13e3859..0d5f784b71e 100644
--- a/doc/administration/packages.md
+++ b/doc/administration/packages.md
@@ -8,12 +8,12 @@ The Packages feature allows GitLab to act as a repository for the following:
| Software repository | Description | Available in GitLab version |
| ------------------- | ----------- | --------------------------- |
-| [Maven Repository](https://docs.gitlab.com/ee/user/project/packages/maven_repository.html) | The GitLab Maven Repository enables every project in GitLab to have its own space to store [Maven](https://maven.apache.org/) packages. | 11.3+ |
-| [NPM Registry](https://docs.gitlab.com/ee/user/project/packages/npm_registry.html) | The GitLab NPM Registry enables every project in GitLab to have its own space to store [NPM](https://www.npmjs.com/) packages. | 11.7+ |
+| [Maven Repository](../user/project/packages/maven_repository.md) | The GitLab Maven Repository enables every project in GitLab to have its own space to store [Maven](https://maven.apache.org/) packages. | 11.3+ |
+| [NPM Registry](../user/project/packages/npm_registry.md) | The GitLab NPM Registry enables every project in GitLab to have its own space to store [NPM](https://www.npmjs.com/) packages. | 11.7+ |
Don't you see your package management system supported yet?
Please consider contributing
-to GitLab. This [development documentation](https://docs.gitlab.com/ee/development/packages.html) will guide you through the process.
+to GitLab. This [development documentation](../development/packages.md) will guide you through the process.
## Enabling the Packages feature
diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md
index b295b7d5dc4..0b4c1ae15d6 100644
--- a/doc/administration/raketasks/maintenance.md
+++ b/doc/administration/raketasks/maintenance.md
@@ -205,25 +205,6 @@ cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:track_deployment RAILS_ENV=production
```
-## Create or repair repository hooks symlink
-
-If the GitLab shell hooks directory location changes or another circumstance
-leads to the hooks symlink becoming missing or invalid, run this Rake task
-to create or repair the symlinks.
-
-**Omnibus Installation**
-
-```
-sudo gitlab-rake gitlab:shell:create_hooks
-```
-
-**Source Installation**
-
-```
-cd /home/git/gitlab
-sudo -u git -H bundle exec rake gitlab:shell:create_hooks RAILS_ENV=production
-```
-
## Check TCP connectivity to a remote site
Sometimes you need to know if your GitLab installation can connect to a TCP
diff --git a/doc/administration/raketasks/project_import_export.md b/doc/administration/raketasks/project_import_export.md
index f43bba0a7a7..6ca23aabdec 100644
--- a/doc/administration/raketasks/project_import_export.md
+++ b/doc/administration/raketasks/project_import_export.md
@@ -32,5 +32,4 @@ bundle exec rake gitlab:import_export:data RAILS_ENV=production
```
[ce-3050]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3050
-[feature-flags]: https://docs.gitlab.com/ee/api/features.html
[tmp]: ../../development/shared_files.md
diff --git a/doc/administration/repository_storage_paths.md b/doc/administration/repository_storage_paths.md
index 1689b0a57d6..4aafc06cfdc 100644
--- a/doc/administration/repository_storage_paths.md
+++ b/doc/administration/repository_storage_paths.md
@@ -62,7 +62,7 @@ files and add the full paths of the alternative repository storage paths. In
the example below, we add two more mountpoints that are named `nfs` and `cephfs`
respectively.
-NOTE: **Note:** This example uses NFS and CephFS. We do not recommend using EFS for storage as it may impact GitLab's performance. See the [relevant documentation](./high_availability/nfs.md#avoid-using-awss-elastic-file-system-efs) for more details.
+NOTE: **Note:** This example uses NFS and CephFS. We do not recommend using EFS for storage as it may impact GitLab's performance. See the [relevant documentation](high_availability/nfs.md#avoid-using-awss-elastic-file-system-efs) for more details.
**For installations from source**
diff --git a/doc/analytics/README.md b/doc/analytics/README.md
index 6b63edb5174..bfb15f6c4f3 100644
--- a/doc/analytics/README.md
+++ b/doc/analytics/README.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'https://docs.gitlab.com/ee/user/group/index.html#user-contribution-analysis-starter'
+redirect_to: '../user/group/index.md#user-contribution-analysis-starter'
---
-This document was moved to [another location](https://docs.gitlab.com/ee/user/group/index.html#user-contribution-analysis-starter)
+This document was moved to [another location](../user/group/index.md#user-contribution-analysis-starter)
diff --git a/doc/analytics/contribution_analytics.md b/doc/analytics/contribution_analytics.md
index 38d71263bc1..e36f55071a4 100644
--- a/doc/analytics/contribution_analytics.md
+++ b/doc/analytics/contribution_analytics.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'https://docs.gitlab.com/ee/user/group/contribution_analytics/index.html'
+redirect_to: '../user/group/contribution_analytics/index.md'
---
-This document was moved to [another location](https://docs.gitlab.com/ee/user/group/contribution_analytics/index.html).
+This document was moved to [another location](../user/group/contribution_analytics/index.md).
diff --git a/doc/api/README.md b/doc/api/README.md
index 439f58c1253..3a1064b787e 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -21,72 +21,83 @@ See also:
The following API resources are available in the project context:
-| Resource | Available endpoints |
-|:------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| [Access requests](access_requests.md) | `/projects/:id/access_requests` (also available for groups) |
-| [Award emoji](award_emoji.md) | `/projects/:id/issues/.../award_emoji`, `/projects/:id/merge_requests/.../award_emoji`, `/projects/:id/snippets/.../award_emoji` |
-| [Branches](branches.md) | `/projects/:id/repository/branches/`, `/projects/:id/repository/merged_branches` |
-| [Commits](commits.md) | `/projects/:id/repository/commits`, `/projects/:id/statuses` |
-| [Container Registry](container_registry.md) | `/projects/:id/registry/repositories` |
-| [Custom attributes](custom_attributes.md) | `/projects/:id/custom_attributes` (also available for groups and users) |
-| [Deploy keys](deploy_keys.md) | `/projects/:id/deploy_keys` (also available standalone) |
-| [Deployments](deployments.md) | `/projects/:id/deployments` |
-| [Discussions](discussions.md) (threaded comments) | `/projects/:id/issues/.../discussions`, `/projects/:id/snippets/.../discussions`, `/projects/:id/merge_requests/.../discussions`, `/projects/:id/commits/.../discussions` |
-| [Environments](environments.md) | `/projects/:id/environments` |
-| [Events](events.md) | `/projects/:id/events` (also available for users and standalone) |
-| [Issues](issues.md) | `/projects/:id/issues` (also available for groups and standalone) |
-| [Issue boards](boards.md) | `/projects/:id/boards` |
-| [Jobs](jobs.md) | `/projects/:id/jobs`, `/projects/:id/pipelines/.../jobs` |
-| [Labels](labels.md) | `/projects/:id/labels` |
-| [Members](members.md) | `/projects/:id/members` (also available for groups) |
-| [Merge requests](merge_requests.md) | `/projects/:id/merge_requests` (also available for groups and standalone) |
-| [Notes](notes.md) (comments) | `/projects/:id/issues/.../notes`, `/projects/:id/snippets/.../notes`, `/projects/:id/merge_requests/.../notes` |
-| [Notification settings](notification_settings.md) | `/projects/:id/notification_settings` (also available for groups and standalone) |
-| [Pages domains](pages_domains.md) | `/projects/:id/pages` (also available standalone) |
-| [Pipelines](pipelines.md) | `/projects/:id/pipelines` |
-| [Pipeline schedules](pipeline_schedules.md) | `/projects/:id/pipeline_schedules` |
-| [Pipeline triggers](pipeline_triggers.md) | `/projects/:id/triggers` |
-| [Projects](projects.md) including setting Webhooks | `/projects`, `/projects/:id/hooks` (also available for users) |
-| [Project badges](project_badges.md) | `/projects/:id/badges` |
-| [Project clusters](project_clusters.md) | `/projects/:id/clusters` |
-| [Project-level variables](project_level_variables.md) | `/projects/:id/variables` |
-| [Project import/export](project_import_export.md) | `/projects/:id/export`, `/projects/import`, `/projects/:id/import` |
-| [Project milestones](milestones.md) | `/projects/:id/milestones` |
-| [Project snippets](project_snippets.md) | `/projects/:id/snippets` |
-| [Project templates](project_templates.md) | `/projects/:id/templates` |
-| [Protected branches](protected_branches.md) | `/projects/:id/protected_branches` |
-| [Protected tags](protected_tags.md) | `/projects/:id/protected_tags` |
-| [Releases](releases/index.md) | `/projects/:id/releases` |
-| [Release links](releases/links.md) | `/projects/:id/releases/.../assets/links` |
-| [Repositories](repositories.md) | `/projects/:id/repository` |
-| [Repository files](repository_files.md) | `/projects/:id/repository/files` |
-| [Repository submodules](repository_submodules.md) | `/projects/:id/repository/submodules` |
-| [Resource label events](resource_label_events.md) | `/projects/:id/issues/.../resource_label_events`, `/projects/:id/merge_requests/.../resource_label_events` |
-| [Runners](runners.md) | `/projects/:id/runners` (also available standalone) |
-| [Search](search.md) | `/projects/:id/search` (also available for groups and standalone) |
-| [Services](services.md) | `/projects/:id/services` |
-| [Tags](tags.md) | `/projects/:id/repository/tags` |
-| [Wikis](wikis.md) | `/projects/:id/wikis` |
+| Resource | Available endpoints |
+|:--------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [Access requests](access_requests.md) | `/projects/:id/access_requests` (also available for groups) |
+| [Award emoji](award_emoji.md) | `/projects/:id/issues/.../award_emoji`, `/projects/:id/merge_requests/.../award_emoji`, `/projects/:id/snippets/.../award_emoji` |
+| [Branches](branches.md) | `/projects/:id/repository/branches/`, `/projects/:id/repository/merged_branches` |
+| [Commits](commits.md) | `/projects/:id/repository/commits`, `/projects/:id/statuses` |
+| [Container Registry](container_registry.md) | `/projects/:id/registry/repositories` |
+| [Custom attributes](custom_attributes.md) | `/projects/:id/custom_attributes` (also available for groups and users) |
+| [Deploy keys](deploy_keys.md) | `/projects/:id/deploy_keys` (also available standalone) |
+| [Deployments](deployments.md) | `/projects/:id/deployments` |
+| [Discussions](discussions.md) (threaded comments) | `/projects/:id/issues/.../discussions`, `/projects/:id/snippets/.../discussions`, `/projects/:id/merge_requests/.../discussions`, `/projects/:id/commits/.../discussions` (also available for groups) |
+| [Environments](environments.md) | `/projects/:id/environments` |
+| [Events](events.md) | `/projects/:id/events` (also available for users and standalone) |
+| [Issues](issues.md) | `/projects/:id/issues` (also available for groups and standalone) |
+| [Issue boards](boards.md) | `/projects/:id/boards` |
+| [Issue links](issue_links.md) **[STARTER]** | `/projects/:id/issues/.../links` |
+| [Jobs](jobs.md) | `/projects/:id/jobs`, `/projects/:id/pipelines/.../jobs` |
+| [Labels](labels.md) | `/projects/:id/labels` |
+| [Managed licenses](managed_licenses.md) **[ULTIMATE]** | `/projects/:id/managed_licenses` |
+| [Members](members.md) | `/projects/:id/members` (also available for groups) |
+| [Merge request approvals](merge_request_approvals.md) **[STARTER]** | `/projects/:id/approvals`, `/projects/:id/merge_requests/.../approvals` |
+| [Merge requests](merge_requests.md) | `/projects/:id/merge_requests` (also available for groups and standalone) |
+| [Notes](notes.md) (comments) | `/projects/:id/issues/.../notes`, `/projects/:id/snippets/.../notes`, `/projects/:id/merge_requests/.../notes` (also available for groups) |
+| [Notification settings](notification_settings.md) | `/projects/:id/notification_settings` (also available for groups and standalone) |
+| [Packages](packages.md) **[PREMIUM]** | `/projects/:id/packages` |
+| [Pages domains](pages_domains.md) | `/projects/:id/pages` (also available standalone) |
+| [Pipelines](pipelines.md) | `/projects/:id/pipelines` |
+| [Pipeline schedules](pipeline_schedules.md) | `/projects/:id/pipeline_schedules` |
+| [Pipeline triggers](pipeline_triggers.md) | `/projects/:id/triggers` |
+| [Projects](projects.md) including setting Webhooks | `/projects`, `/projects/:id/hooks` (also available for users) |
+| [Project badges](project_badges.md) | `/projects/:id/badges` |
+| [Project clusters](project_clusters.md) | `/projects/:id/clusters` |
+| [Project-level variables](project_level_variables.md) | `/projects/:id/variables` |
+| [Project import/export](project_import_export.md) | `/projects/:id/export`, `/projects/import`, `/projects/:id/import` |
+| [Project milestones](milestones.md) | `/projects/:id/milestones` |
+| [Project snippets](project_snippets.md) | `/projects/:id/snippets` |
+| [Project templates](project_templates.md) | `/projects/:id/templates` |
+| [Protected branches](protected_branches.md) | `/projects/:id/protected_branches` |
+| [Protected tags](protected_tags.md) | `/projects/:id/protected_tags` |
+| [Releases](releases/index.md) | `/projects/:id/releases` |
+| [Release links](releases/links.md) | `/projects/:id/releases/.../assets/links` |
+| [Repositories](repositories.md) | `/projects/:id/repository` |
+| [Repository files](repository_files.md) | `/projects/:id/repository/files` |
+| [Repository submodules](repository_submodules.md) | `/projects/:id/repository/submodules` |
+| [Resource label events](resource_label_events.md) | `/projects/:id/issues/.../resource_label_events`, `/projects/:id/merge_requests/.../resource_label_events` (also available for groups) |
+| [Runners](runners.md) | `/projects/:id/runners` (also available standalone) |
+| [Search](search.md) | `/projects/:id/search` (also available for groups and standalone) |
+| [Services](services.md) | `/projects/:id/services` |
+| [Tags](tags.md) | `/projects/:id/repository/tags` |
+| [Vulnerabilities](vulnerabilities.md) **[ULTIMATE]** | `/projects/:id/vulnerabilities` (also available for groups) |
+| [Wikis](wikis.md) | `/projects/:id/wikis` |
### Group resources
The following API resources are available in the group context:
-| Resource | Available endpoints |
-|:--------------------------------------------------|:---------------------------------------------------------------------------------|
-| [Access requests](access_requests.md) | `/groups/:id/access_requests/` (also available for projects) |
-| [Custom attributes](custom_attributes.md) | `/groups/:id/custom_attributes` (also available for projects and users) |
-| [Groups](groups.md) | `/groups`, `/groups/.../subgroups` |
-| [Group badges](group_badges.md) | `/groups/:id/badges` |
-| [Group issue boards](group_boards.md) | `/groups/:id/boards` |
-| [Group labels](group_labels.md) | `/groups/:id/labels` |
-| [Group-level variables](group_level_variables.md) | `/groups/:id/variables` |
-| [Group milestones](group_milestones.md) | `/groups/:id/milestones` |
-| [Issues](issues.md) | `/groups/:id/issues` (also available for projects and standalone) |
-| [Members](members.md) | `/groups/:id/members` (also available for projects) |
-| [Merge requests](merge_requests.md) | `/groups/:id/merge_requests` (also available for projects and standalone) |
-| [Notification settings](notification_settings.md) | `/groups/:id/notification_settings` (also available for projects and standalone) |
-| [Search](search.md) | `/groups/:id/search` (also available for projects and standalone) |
+| Resource | Available endpoints |
+|:-----------------------------------------------------------------|:---------------------------------------------------------------------------------|
+| [Access requests](access_requests.md) | `/groups/:id/access_requests/` (also available for projects) |
+| [Custom attributes](custom_attributes.md) | `/groups/:id/custom_attributes` (also available for projects and users) |
+| [Discussions](discussions.md) (threaded comments) **[ULTIMATE]** | `/groups/:id/epics/.../discussions` (also available for projects) |
+| [Epic issues](epic_issues.md) **[ULTIMATE]** | `/groups/:id/epics/.../issues` |
+| [Epic links](epic_links.md) **[ULTIMATE]** | `/groups/:id/epics/.../epics` |
+| [Epics](epics.md) **[ULTIMATE]** | `/groups/:id/epics` |
+| [Groups](groups.md) | `/groups`, `/groups/.../subgroups` |
+| [Group badges](group_badges.md) | `/groups/:id/badges` |
+| [Group issue boards](group_boards.md) | `/groups/:id/boards` |
+| [Group labels](group_labels.md) | `/groups/:id/labels` |
+| [Group-level variables](group_level_variables.md) | `/groups/:id/variables` |
+| [Group milestones](group_milestones.md) | `/groups/:id/milestones` |
+| [Issues](issues.md) | `/groups/:id/issues` (also available for projects and standalone) |
+| [Members](members.md) | `/groups/:id/members` (also available for projects) |
+| [Merge requests](merge_requests.md) | `/groups/:id/merge_requests` (also available for projects and standalone) |
+| [Notes](notes.md) (comments) | `/groups/:id/epics/.../notes` (also available for projects) |
+| [Notification settings](notification_settings.md) | `/groups/:id/notification_settings` (also available for projects and standalone) |
+| [Resource label events](resource_label_events.md) | `/groups/:id/epics/.../resource_label_events` (also available for projects) |
+| [Search](search.md) | `/groups/:id/search` (also available for projects and standalone) |
### Standalone resources
@@ -102,9 +113,11 @@ The following API resources are available outside of project and group contexts
| [Deploy keys](deploy_keys.md) | `/deploy_keys` (also available for projects) |
| [Events](events.md) | `/events`, `/users/:id/events` (also available for projects) |
| [Feature flags](features.md) | `/features` |
+| [Geo Nodes](geo_nodes.md) **[PREMIUM ONLY]** | `/geo_nodes` |
| [Import repository from GitHub](import.md) | `/import/github` |
| [Issues](issues.md) | `/issues` (also available for groups and projects) |
| [Keys](keys.md) | `/keys` |
+| [License](license.md) **[CORE ONLY]** | `/license` |
| [Markdown](markdown.md) | `/markdown` |
| [Merge requests](merge_requests.md) | `/merge_requests` (also available for groups and projects) |
| [Namespaces](namespaces.md) | `/namespaces` |
@@ -131,6 +144,11 @@ Endpoints are available for:
- [GitLab CI YAML templates](templates/gitlab_ci_ymls.md).
- [Open source license templates](templates/licenses.md).
+## SCIM **[SILVER ONLY]**
+
+[GitLab.com Silver and above](https://about.gitlab.com/pricing/) provides an [SCIM API](scim.md) that implements [the RFC7644 protocol](https://tools.ietf.org/html/rfc7644) and provides
+the `/Users` endpoint. The base URL is: `/api/scim/v2/groups/:group_path/Users/`.
+
## Road to GraphQL
Going forward, we will start on moving to
@@ -186,8 +204,6 @@ curl "https://gitlab.example.com/api/v4/projects"
The API uses JSON to serialize data. You don't need to specify `.json` at the
end of an API URL.
-All of the API endpoints that use the `POST`, `PUT` or `PATCH` method support params in the request body, with `Content-Type` `application/x-www-form-urlencoded`, `multipart/form-data` or `application/json`.
-
## Authentication
Most API requests require authentication, or will only return public data when
diff --git a/doc/api/boards.md b/doc/api/boards.md
index 28c73db6b98..a96206f5df3 100644
--- a/doc/api/boards.md
+++ b/doc/api/boards.md
@@ -141,6 +141,173 @@ Example response:
}
```
+## Create a board **[STARTER]**
+
+Creates a board.
+
+```
+POST /projects/:id/boards
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `name` | string | yes | The name of the new board |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/boards?name=newboard
+```
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "project": {
+ "id": 5,
+ "name": "Diaspora Project Site",
+ "name_with_namespace": "Diaspora / Diaspora Project Site",
+ "path": "diaspora-project-site",
+ "path_with_namespace": "diaspora/diaspora-project-site",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
+ "web_url": "http://example.com/diaspora/diaspora-project-site"
+ },
+ "name": "newboard",
+ "milestone": {
+ "id": 12
+ "title": "10.0"
+ },
+ "lists" : [
+ {
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+ },
+ {
+ "id" : 2,
+ "label" : {
+ "name" : "Ready",
+ "color" : "#FF0000",
+ "description" : null
+ },
+ "position" : 2
+ },
+ {
+ "id" : 3,
+ "label" : {
+ "name" : "Production",
+ "color" : "#FF5F00",
+ "description" : null
+ },
+ "position" : 3
+ }
+ ]
+ }
+```
+
+## Update a board **[STARTER]**
+
+> [Introduced][ee-5954] in [GitLab Starter](https://about.gitlab.com/pricing/) 11.1.
+
+Updates a board.
+
+```
+PUT /projects/:id/boards/:board_id
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
+| `name` | string | no | The new name of the board |
+| `assignee_id` | integer | no | The assignee the board should be scoped to |
+| `milestone_id` | integer | no | The milestone the board should be scoped to |
+| `labels` | string | no | Comma-separated list of label names which the board should be scoped to |
+| `weight` | integer | no | The weight range from 0 to 9, to which the board should be scoped to |
+
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/boards/1?name=new_name&milestone_id=43&assignee_id=1&labels=Doing&weight=4
+```
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "project": {
+ "id": 5,
+ "name": "Diaspora Project Site",
+ "name_with_namespace": "Diaspora / Diaspora Project Site",
+ "path": "diaspora-project-site",
+ "path_with_namespace": "diaspora/diaspora-project-site",
+ "created_at": "2018-07-03T05:48:49.982Z",
+ "default_branch": null,
+ "tag_list": [],
+ "ssh_url_to_repo": "ssh://user@example.com/diaspora/diaspora-project-site.git",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
+ "web_url": "http://example.com/diaspora/diaspora-project-site",
+ "readme_url": null,
+ "avatar_url": null,
+ "star_count": 0,
+ "forks_count": 0,
+ "last_activity_at": "2018-07-03T05:48:49.982Z"
+ },
+ "lists": [],
+ "name": "new_name",
+ "group": null,
+ "milestone": {
+ "id": 43,
+ "iid": 1,
+ "project_id": 15,
+ "title": "Milestone 1",
+ "description": "Milestone 1 desc",
+ "state": "active",
+ "created_at": "2018-07-03T06:36:42.618Z",
+ "updated_at": "2018-07-03T06:36:42.618Z",
+ "due_date": null,
+ "start_date": null,
+ "web_url": "http://example.com/root/board1/milestones/1"
+ },
+ "assignee": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://example.com/root"
+ },
+ "labels": [{
+ "id": 10,
+ "name": "Doing",
+ "color": "#5CB85C",
+ "description": null
+ }],
+ "weight": 4
+ }
+```
+
+## Delete a board **[STARTER]**
+
+Deletes a board.
+
+```
+DELETE /projects/:id/boards/:board_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/boards/1
+```
+
## List board lists
Get a list of the board's lists.
@@ -237,7 +404,15 @@ POST /projects/:id/boards/:board_id/lists
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
-| `label_id` | integer | yes | The ID of a label |
+| `label_id` | integer | no | The ID of a label |
+| `assignee_id` **[PREMIUM]** | integer | no | The ID of a user |
+| `milestone_id` **[PREMIUM]** | integer | no | The ID of a milestone |
+
+NOTE: **Note**:
+Label, assignee and milestone arguments are mutually exclusive,
+that is, only one of them are accepted in a request.
+Check the [Issue Board docs](../user/project/issue_board.md#summary-of-features-per-tier)
+for more information regarding the required license for each list type.
```bash
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/boards/1/lists?label_id=5
@@ -307,3 +482,5 @@ DELETE /projects/:id/boards/:board_id/lists/:list_id
```bash
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1
```
+
+[ee-5954]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/5954
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 92f53c7b5e6..25015fad9e3 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -75,6 +75,7 @@ POST /projects/:id/repository/commits
| `branch` | string | yes | Name of the branch to commit into. To create a new branch, also provide `start_branch`. |
| `commit_message` | string | yes | Commit message |
| `start_branch` | string | no | Name of the branch to start the new commit from |
+| `start_project` | integer/string | no | The project ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) to start the commit from. Defaults to the value of `id`. |
| `actions[]` | array | yes | An array of action hashes to commit as a batch. See the next table for what attributes it can take. |
| `author_email` | string | no | Specify the commit author's email address |
| `author_name` | string | no | Specify the commit author's name |
diff --git a/doc/api/discussions.md b/doc/api/discussions.md
index 07a6201b10b..9defef4fd53 100644
--- a/doc/api/discussions.md
+++ b/doc/api/discussions.md
@@ -1,6 +1,12 @@
# Discussions API
-Discussions are set of related notes on snippets, issues, merge requests or commits.
+Discussions are a set of related notes on:
+
+- Snippets
+- Issues
+- Epics **[ULTIMATE]**
+- Merge requests
+- Commits
This includes system notes, which are notes about changes to the object (for example, when a milestone changes, there will be a corresponding system note). Label notes are not part of this API, but recorded as separate events in [resource label events](resource_label_events.md).
@@ -424,6 +430,214 @@ Parameters:
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/636
```
+## Epics **[ULTIMATE]**
+
+### List group epic discussions
+
+Gets a list of all discussions for a single epic.
+
+```
+GET /groups/:id/epics/:epic_id/discussions
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ------------ |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+
+```json
+[
+ {
+ "id": "6a9c1750b37d513a43987b574953fceb50b03ce7",
+ "individual_note": false,
+ "notes": [
+ {
+ "id": 1126,
+ "type": "DiscussionNote",
+ "body": "discussion text",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-03T21:54:39.668Z",
+ "updated_at": "2018-03-03T21:54:39.668Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Epic",
+ "noteable_id": null,
+ "resolvable": false
+ },
+ {
+ "id": 1129,
+ "type": "DiscussionNote",
+ "body": "reply to the discussion",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-04T13:38:02.127Z",
+ "updated_at": "2018-03-04T13:38:02.127Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Epic",
+ "noteable_id": null,
+ "resolvable": false
+ }
+ ]
+ },
+ {
+ "id": "87805b7c09016a7058e91bdbe7b29d1f284a39e6",
+ "individual_note": true,
+ "notes": [
+ {
+ "id": 1128,
+ "type": null,
+ "body": "a single comment",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-04T09:17:22.520Z",
+ "updated_at": "2018-03-04T09:17:22.520Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Epic",
+ "noteable_id": null,
+ "resolvable": false
+ }
+ ]
+ }
+]
+```
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/discussions
+```
+
+### Get single epic discussion
+
+Returns a single discussion for a specific group epic
+
+```
+GET /groups/:id/epics/:epic_id/discussions/:discussion_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `discussion_id` | integer | yes | The ID of a discussion |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7
+```
+
+### Create new epic discussion
+
+Creates a new discussion to a single group epic. This is similar to creating
+a note but but another comments (replies) can be added to it later.
+
+```
+POST /groups/:id/epics/:epic_id/discussions
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `body` | string | yes | The content of a discussion |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/discussions?body=comment
+```
+
+### Add note to existing epic discussion
+
+Adds a new note to the discussion. This can also
+[create a discussion from a single comment](../user/discussions/#start-a-discussion-by-replying-to-a-standard-comment).
+
+```
+POST /groups/:id/epics/:epic_id/discussions/:discussion_id/notes
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+| `body` | string | yes | The content of a discussion |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment
+```
+
+### Modify existing epic discussion note
+
+Modify existing discussion note of an epic.
+
+```
+PUT /groups/:id/epics/:epic_id/discussions/:discussion_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+| `body` | string | yes | The content of a discussion |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes/1108?body=comment
+```
+
+### Delete an epic discussion note
+
+Deletes an existing discussion note of an epic.
+
+```
+DELETE /groups/:id/epics/:epic_id/discussions/:discussion_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/636
+```
+
## Merge requests
### List project merge request discussions
diff --git a/doc/api/epic_issues.md b/doc/api/epic_issues.md
index 438a3361dcc..ec59ea7068e 100644
--- a/doc/api/epic_issues.md
+++ b/doc/api/epic_issues.md
@@ -7,6 +7,7 @@ If a user is not a member of a group and the group is private, a `GET` request o
Epics are available only in Ultimate. If epics feature is not available a `403` status code will be returned.
## List issues for an epic
+
Gets all issues that are assigned to an epic and the authenticated user has access to.
```
diff --git a/doc/api/epic_links.md b/doc/api/epic_links.md
index d6e43ae7074..9ad90a6d0f1 100644
--- a/doc/api/epic_links.md
+++ b/doc/api/epic_links.md
@@ -12,6 +12,7 @@ If a user is not a member of a group and the group is private, a `GET` request o
Epics are available only in the [Ultimate/Gold tier](https://about.gitlab.com/pricing/). If the epics feature is not available, a `403` status code will be returned.
## List epics related to a given epic
+
Gets all child epics of an epic.
```
diff --git a/doc/api/geo_nodes.md b/doc/api/geo_nodes.md
index a1cb524499f..ea31abdd87e 100644
--- a/doc/api/geo_nodes.md
+++ b/doc/api/geo_nodes.md
@@ -192,12 +192,10 @@ Example response:
"job_artifacts_failed_count": nil,
"job_artifacts_synced_missing_on_primary_count": 0,
"job_artifacts_synced_in_percentage": "0.00%",
- "repositories_count": 41,
"projects_count": 41,
"repositories_failed_count": nil,
"repositories_synced_count": nil,
"repositories_synced_in_percentage": "0.00%",
- "wikis_count": 41,
"wikis_failed_count": nil,
"wikis_synced_count": nil,
"wikis_synced_in_percentage": "0.00%",
@@ -257,12 +255,10 @@ Example response:
"job_artifacts_failed_count": 1,
"job_artifacts_synced_missing_on_primary_count": 0,
"job_artifacts_synced_in_percentage": "50.00%",
- "repositories_count": 41,
"projects_count": 41,
"repositories_failed_count": 1,
"repositories_synced_count": 40,
"repositories_synced_in_percentage": "97.56%",
- "wikis_count": 41,
"wikis_failed_count": 0,
"wikis_synced_count": 41,
"wikis_synced_in_percentage": "100.00%",
@@ -300,7 +296,8 @@ Example response:
]
```
-Note: fields `wikis_count` and `repositories_count` are deprecated and will be deleted soon. Please use `projects_count` instead.
+NOTE: **Note:**
+In GitLab 12.0, deprecated fields `wikis_count` and `repositories_count` were removed. Use `projects_count` instead.
## Retrieve status about a specific Geo node
@@ -337,12 +334,10 @@ Example response:
"job_artifacts_failed_count": 1,
"job_artifacts_synced_missing_on_primary_count": 0,
"job_artifacts_synced_in_percentage": "50.00%",
- "repositories_count": 41,
"projects_count": 41,
"repositories_failed_count": 1,
"repositories_synced_count": 40,
"repositories_synced_in_percentage": "97.56%",
- "wikis_count": 41,
"wikis_failed_count": 0,
"wikis_synced_count": 41,
"wikis_synced_in_percentage": "100.00%",
@@ -362,7 +357,8 @@ Example response:
Note: The `health_status` parameter can only be in an "Healthy" or "Unhealthy" state, while the `health` parameter can be empty, "Healthy", or contain the actual error message.
-Note: Fields `wikis_count` and `repositories_count` are deprecated and will be deleted soon. Please use `projects_count` instead.
+NOTE: **Note:**
+In GitLab 12.0, deprecated fields `wikis_count` and `repositories_count` were removed. Use `projects_count` instead.
## Retrieve project sync or verification failures that occurred on the current node
diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md
index 9195ba4cdf1..88e657a5d2f 100644
--- a/doc/api/graphql/index.md
+++ b/doc/api/graphql/index.md
@@ -47,6 +47,7 @@ A first iteration of a GraphQL API includes the following queries
1. `project` : Within a project it is also possible to fetch a `mergeRequest` by IID.
1. `group` : Only basic group information is currently supported.
+1. `namespace` : Within a namespace it is also possible to fetch `projects`.
### Multiplex queries
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 907b443d355..20789a1d4a4 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -68,6 +68,7 @@ GET /groups?statistics=true
"statistics": {
"storage_size" : 212,
"repository_size" : 33,
+ "wiki_size" : 100,
"lfs_objects_size" : 123,
"job_artifacts_size" : 57
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 4fb3626f637..0d96cfa1b21 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -135,7 +135,11 @@ Example response:
"award_emoji":"http://example.com/api/v4/projects/1/issues/76/award_emoji",
"project":"http://example.com/api/v4/projects/1"
},
- "subscribed": false
+ "subscribed": false,
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
+ }
}
]
```
@@ -265,7 +269,11 @@ Example response:
"award_emoji":"http://example.com/api/v4/projects/4/issues/41/award_emoji",
"project":"http://example.com/api/v4/projects/4"
},
- "subscribed": false
+ "subscribed": false,
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
+ }
}
]
```
@@ -403,7 +411,11 @@ Example response:
"award_emoji":"http://example.com/api/v4/projects/4/issues/41/award_emoji",
"project":"http://example.com/api/v4/projects/4"
},
- "subscribed": false
+ "subscribed": false,
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
+ }
}
]
```
@@ -500,6 +512,10 @@ Example response:
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
"award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
"project": "http://example.com/api/v4/projects/1"
+ },
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
}
}
```
@@ -583,6 +599,10 @@ Example response:
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
"award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
"project": "http://example.com/api/v4/projects/1"
+ },
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
}
}
```
@@ -674,6 +694,10 @@ Example response:
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
"award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
"project": "http://example.com/api/v4/projects/1"
+ },
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
}
}
```
@@ -780,6 +804,10 @@ Example response:
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
"award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
"project": "http://example.com/api/v4/projects/1"
+ },
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
}
}
```
@@ -865,6 +893,10 @@ Example response:
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
"award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
"project": "http://example.com/api/v4/projects/1"
+ },
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
}
}
```
@@ -931,7 +963,11 @@ Example response:
"due_date": null,
"web_url": "http://example.com/example/example/issues/12",
"confidential": false,
- "discussion_locked": false
+ "discussion_locked": false,
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
+ }
}
```
@@ -1029,7 +1065,11 @@ Example response:
"due_date": null,
"web_url": "http://example.com/example/example/issues/110",
"confidential": false,
- "discussion_locked": false
+ "discussion_locked": false,
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
+ }
},
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/issues/10",
"body": "Vel voluptas atque dicta mollitia adipisci qui at.",
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index d51451b9c83..72973b69117 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -346,30 +346,58 @@ Example of response
> **Notes**:
>
> - [Introduced][ce-2893] in GitLab 8.5.
+> - The use of `CI_JOB_TOKEN` in the artifacts download API was [introduced][ee-2346]
+> in [GitLab Premium][ee] 9.5.
-Get job artifacts of a project.
+Get the job's artifacts zipped archive of a project.
```
GET /projects/:id/jobs/:job_id/artifacts
```
-| Attribute | Type | Required | Description |
-|-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------|
-| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
-| `job_id` | integer | yes | ID of a job. |
+| Attribute | Type | Required | Description |
+|-------------|----------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `job_id` | integer | yes | ID of a job. |
+| `job_token` **[PREMIUM]** | string | no | To be used with [triggers] for multi-project pipelines. It should be invoked only inside `.gitlab-ci.yml`. Its value is always `$CI_JOB_TOKEN`. |
-Example requests:
+Example request using the `PRIVATE-TOKEN` header:
```sh
-curl --location --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/8/artifacts"
+curl --output artifacts.zip --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/42/artifacts"
```
+To use this in a [`script` definition](../ci/yaml/README.md#script) inside
+`.gitlab-ci.yml` **[PREMIUM]**, you can use either:
+
+- The `JOB-TOKEN` header with the GitLab-provided `CI_JOB_TOKEN` variable.
+ For example, the following job will download the artifacts of the job with ID
+ `42`. Note that the command is wrapped into single quotes since it contains a
+ colon (`:`):
+
+ ```yaml
+ artifact_download:
+ stage: test
+ script:
+ - 'curl --location --output artifacts.zip --header "JOB-TOKEN: $CI_JOB_TOKEN" "https://gitlab.example.com/api/v4/projects/1/jobs/42/artifacts"'
+ ```
+
+- Or the `job_token` attribute with the GitLab-provided `CI_JOB_TOKEN` variable.
+ For example, the following job will download the artifacts of the job with ID `42`:
+
+ ```yaml
+ artifact_download:
+ stage: test
+ script:
+ - 'curl --location --output artifacts.zip "https://gitlab.example.com/api/v4/projects/1/jobs/42/artifacts?job_token=$CI_JOB_TOKEN"'
+ ```
+
Possible response status codes:
| Status | Description |
|-----------|---------------------------------|
-| 200 | Serves the artifacts file |
-| 404 | Build not found or no artifacts |
+| 200 | Serves the artifacts file. |
+| 404 | Build not found or no artifacts.|
[ce-2893]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2893
@@ -378,9 +406,13 @@ Possible response status codes:
> **Notes**:
>
> - [Introduced][ce-5347] in GitLab 8.10.
+> - The use of `CI_JOB_TOKEN` in the artifacts download API was [introduced][ee-2346]
+> in [GitLab Premium][ee] 9.5.
-Download the artifacts archive from the given reference name and job provided the
-job finished successfully.
+Download the artifacts zipped archive from the given reference name and job,
+provided the job finished successfully. This is the same as
+[getting the job's artifacts](#get-job-artifacts), but by defining the job's
+name instead of its ID.
```
GET /projects/:id/jobs/artifacts/:ref_name/download?job=name
@@ -388,24 +420,51 @@ GET /projects/:id/jobs/artifacts/:ref_name/download?job=name
Parameters
-| Attribute | Type | Required | Description |
-|------------|----------------|----------|------------------------------------------------------------------------------------------------------------------|
-| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
-| `ref_name` | string | yes | Branch or tag name in repository. HEAD or SHA references are not supported. |
-| `job` | string | yes | The name of the job. |
+| Attribute | Type | Required | Description |
+|-------------|----------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `ref_name` | string | yes | Branch or tag name in repository. HEAD or SHA references are not supported. |
+| `job` | string | yes | The name of the job. |
+| `job_token` **[PREMIUM]** | string | no | To be used with [triggers] for multi-project pipelines. It should be invoked only inside `.gitlab-ci.yml`. Its value is always `$CI_JOB_TOKEN`. |
-Example requests:
+Example request using the `PRIVATE-TOKEN` header:
```sh
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/download?job=test"
```
+To use this in a [`script` definition](../ci/yaml/README.md#script) inside
+`.gitlab-ci.yml` **[PREMIUM]**, you can use either:
+
+- The `JOB-TOKEN` header with the GitLab-provided `CI_JOB_TOKEN` variable.
+ For example, the following job will download the artifacts of the `test` job
+ of the `master` branch. Note that the command is wrapped into single quotes
+ since it contains a colon (`:`):
+
+ ```yaml
+ artifact_download:
+ stage: test
+ script:
+ - 'curl --location --output artifacts.zip --header "JOB-TOKEN: $CI_JOB_TOKEN" "https://gitlab.example.com/api/v4/projects/$CI_PROJECT_ID/jobs/artifacts/master/download?job=test"'
+ ```
+
+- Or the `job_token` attribute with the GitLab-provided `CI_JOB_TOKEN` variable.
+ For example, the following job will download the artifacts of the `test` job
+ of the `master` branch:
+
+ ```yaml
+ artifact_download:
+ stage: test
+ script:
+ - 'curl --location --output artifacts.zip "https://gitlab.example.com/api/v4/projects/$CI_PROJECT_ID/jobs/artifacts/master/download?job=test&job_token=$CI_JOB_TOKEN"'
+ ```
+
Possible response status codes:
| Status | Description |
|-----------|---------------------------------|
-| 200 | Serves the artifacts file |
-| 404 | Build not found or no artifacts |
+| 200 | Serves the artifacts file. |
+| 404 | Build not found or no artifacts.|
[ce-5347]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5347
@@ -414,7 +473,7 @@ Possible response status codes:
> Introduced in GitLab 10.0
Download a single artifact file from a job with a specified ID from within
-the job's artifacts archive. The file is extracted from the archive and
+the job's artifacts zipped archive. The file is extracted from the archive and
streamed to the client.
```
@@ -783,3 +842,7 @@ Example of response
"user": null
}
```
+
+[ee]: https://about.gitlab.com/pricing/
+[ee-2346]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2346
+[triggers]: ../ci/triggers/README.md#when-a-pipeline-depends-on-the-artifacts-of-another-pipeline-premium
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 7992af15448..96a956ad03a 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -138,7 +138,11 @@ Parameters:
"human_time_estimate": null,
"human_total_time_spent": null
},
- "squash": false
+ "squash": false,
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
+ }
}
]
```
@@ -280,7 +284,11 @@ Parameters:
"human_time_estimate": null,
"human_total_time_spent": null
},
- "squash": false
+ "squash": false,
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
+ }
}
]
```
@@ -410,7 +418,11 @@ Parameters:
"human_time_estimate": null,
"human_total_time_spent": null
},
- "squash": false
+ "squash": false,
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
+ }
}
]
```
@@ -545,7 +557,11 @@ Parameters:
"start_sha": "c380d3acebd181f13629a25d2e2acca46ffe1e00"
},
"diverged_commits_count": 2,
- "rebase_in_progress": false
+ "rebase_in_progress": false,
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
+ }
}
```
@@ -579,7 +595,7 @@ Parameters:
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/10fc7f102be8de7657fb4d80898bbfe3?s=80&d=identicon",
"web_url": "http://localhost/user2"
- },
+ }
]
```
@@ -702,7 +718,11 @@ Parameters:
"total_time_spent": 0,
"human_time_estimate": null,
"human_total_time_spent": null
- }
+ },
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
+ },
"changes": [
{
"old_path": "VERSION",
@@ -865,7 +885,11 @@ POST /projects/:id/merge_requests
"head_sha": "2be7ddb704c7b6b83732fdd5b9f09d5a397b5f8f",
"start_sha": "c380d3acebd181f13629a25d2e2acca46ffe1e00"
},
- "diverged_commits_count": 2
+ "diverged_commits_count": 2,
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
+ }
}
```
@@ -1002,7 +1026,11 @@ Must include at least one non-required attribute from above.
"head_sha": "2be7ddb704c7b6b83732fdd5b9f09d5a397b5f8f",
"start_sha": "c380d3acebd181f13629a25d2e2acca46ffe1e00"
},
- "diverged_commits_count": 2
+ "diverged_commits_count": 2,
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
+ }
}
```
@@ -1155,37 +1183,37 @@ Parameters:
"head_sha": "2be7ddb704c7b6b83732fdd5b9f09d5a397b5f8f",
"start_sha": "c380d3acebd181f13629a25d2e2acca46ffe1e00"
},
- "diverged_commits_count": 2
+ "diverged_commits_count": 2,
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
+ }
}
```
-## Merge to default merge ref path
+## Returns the up to date merge-ref HEAD commit
-Merge the changes between the merge request source and target branches into `refs/merge-requests/:iid/merge`
-ref, of the target project repository. This ref will have the state the target branch would have if
+Merge the changes between the merge request source and target branches into `refs/merge-requests/:iid/merge`
+ref, of the target project repository, if possible. This ref will have the state the target branch would have if
a regular merge action was taken.
-This is not a regular merge action given it doesn't change the merge request state in any manner.
+This is not a regular merge action given it doesn't change the merge request target branch state in any manner.
-This ref (`refs/merge-requests/:iid/merge`) is **always** overwritten when submitting
-requests to this API, so none of its state is kept or used in the process.
+This ref (`refs/merge-requests/:iid/merge`) isn't necessarily overwritten when submitting
+requests to this API, though it'll make sure the ref has the latest possible state.
-If the merge request has conflicts, is empty or already merged,
-you'll get a `400` and a descriptive error message. If you don't have permissions to do so,
-you'll get a `403`.
+If the merge request has conflicts, is empty or already merged, you'll get a `400` and a descriptive error message.
-It returns the HEAD commit of `refs/merge-requests/:iid/merge` in the response body in
-case of `200`.
+It returns the HEAD commit of `refs/merge-requests/:iid/merge` in the response body in case of `200`.
```
-PUT /projects/:id/merge_requests/:merge_request_iid/merge_to_ref
+GET /projects/:id/merge_requests/:merge_request_iid/merge_ref
```
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - Internal ID of MR
-- `merge_commit_message` (optional) - Custom merge commit message
```json
{
@@ -1313,7 +1341,11 @@ Parameters:
"head_sha": "2be7ddb704c7b6b83732fdd5b9f09d5a397b5f8f",
"start_sha": "c380d3acebd181f13629a25d2e2acca46ffe1e00"
},
- "diverged_commits_count": 2
+ "diverged_commits_count": 2,
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
+ }
}
```
@@ -1349,7 +1381,7 @@ If the rebase operation is ongoing, the response will include the following:
```json
{
- "rebase_in_progress": true
+ "rebase_in_progress": true,
"merge_error": null
}
```
@@ -1360,7 +1392,7 @@ the following:
```json
{
"rebase_in_progress": false,
- "merge_error": null,
+ "merge_error": null
}
```
@@ -1369,7 +1401,7 @@ If the rebase operation fails, the response will include the following:
```json
{
"rebase_in_progress": false,
- "merge_error": "Rebase failed. Please rebase locally",
+ "merge_error": "Rebase failed. Please rebase locally"
}
```
@@ -1576,7 +1608,11 @@ Example response:
"head_sha": "2be7ddb704c7b6b83732fdd5b9f09d5a397b5f8f",
"start_sha": "c380d3acebd181f13629a25d2e2acca46ffe1e00"
},
- "diverged_commits_count": 2
+ "diverged_commits_count": 2,
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
+ }
}
```
@@ -1705,7 +1741,11 @@ Example response:
"head_sha": "2be7ddb704c7b6b83732fdd5b9f09d5a397b5f8f",
"start_sha": "c380d3acebd181f13629a25d2e2acca46ffe1e00"
},
- "diverged_commits_count": 2
+ "diverged_commits_count": 2,
+ "task_completion_status":{
+ "count":0,
+ "completed_count":0
+ }
}
```
diff --git a/doc/api/notes.md b/doc/api/notes.md
index dfde80c6441..c09129c22d4 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -1,6 +1,11 @@
# Notes API
-Notes are comments on snippets, issues or merge requests.
+Notes are comments on:
+
+- Snippets
+- Issues
+- Merge requests
+- Epics **[ULTIMATE]**
This includes system notes, which are notes about changes to the object (for example, when a milestone changes, there will be a corresponding system note). Label notes are not part of this API, but recorded as separate events in [resource label events](resource_label_events.md).
@@ -390,3 +395,126 @@ Parameters:
```bash
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/merge_requests/7/notes/1602
```
+
+## Epics **[ULTIMATE]**
+
+### List all epic notes
+
+Gets a list of all notes for a single epic. Epic notes are comments users can post to an epic.
+
+```
+GET /groups/:id/epics/:epic_id/notes
+GET /groups/:id/epics/:epic_id/notes?sort=asc&order_by=updated_at
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of a group epic |
+| `sort` | string | no | Return epic notes sorted in `asc` or `desc` order. Default is `desc` |
+| `order_by` | string | no | Return epic notes ordered by `created_at` or `updated_at` fields. Default is `created_at` |
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/notes
+```
+
+### Get single epic note
+
+Returns a single note for a given epic.
+
+```
+GET /groups/:id/epics/:epic_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `note_id` | integer | yes | The ID of a note |
+
+```json
+{
+ "id": 52,
+ "title": "Epic",
+ "file_name": "epic.rb",
+ "author": {
+ "id": 1,
+ "username": "pipin",
+ "email": "admin@example.com",
+ "name": "Pip",
+ "state": "active",
+ "created_at": "2013-09-30T13:46:01Z"
+ },
+ "expires_at": null,
+ "updated_at": "2013-10-02T07:34:20Z",
+ "created_at": "2013-10-02T07:34:20Z"
+}
+```
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/notes/1
+```
+
+### Create new epic note
+
+Creates a new note for a single epic. Epic notes are comments users can post to an epic.
+If you create a note where the body only contains an Award Emoji, you'll receive this object back.
+
+```
+POST /groups/:id/epics/:epic_id/notes
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `body` | string | yes | The content of a note |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippet/11/notes?body=note
+```
+
+### Modify existing epic note
+
+Modify existing note of an epic.
+
+```
+PUT /groups/:id/epics/:epic_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `note_id` | integer | yes | The ID of a note |
+| `body` | string | yes | The content of a note |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippet/11/notes?body=note
+```
+
+### Delete an epic note
+
+Deletes an existing note of an epic.
+
+```
+DELETE /groups/:id/epics/:epic_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `note_id` | integer | yes | The ID of a note |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/52/notes/1659
+```
diff --git a/doc/api/project_clusters.md b/doc/api/project_clusters.md
index c831cc52a93..327781f6c93 100644
--- a/doc/api/project_clusters.md
+++ b/doc/api/project_clusters.md
@@ -167,6 +167,7 @@ Parameters:
| `platform_kubernetes_attributes[ca_cert]` | String | no | TLS certificate (needed if API is using a self-signed TLS certificate |
| `platform_kubernetes_attributes[namespace]` | String | no | The unique namespace related to the project |
| `platform_kubernetes_attributes[authorization_type]` | String | no | The cluster authorization type: `rbac`, `abac` or `unknown_authorization`. Defaults to `rbac`. |
+| `environment_scope` | String | no | The associated environment to the cluster. Defaults to `*` **[PREMIUM]** |
Example request:
@@ -256,6 +257,7 @@ Parameters:
| `platform_kubernetes_attributes[token]` | String | no | The token to authenticate against Kubernetes |
| `platform_kubernetes_attributes[ca_cert]` | String | no | TLS certificate (needed if API is using a self-signed TLS certificate |
| `platform_kubernetes_attributes[namespace]` | String | no | The unique namespace related to the project |
+| `environment_scope` | String | no | The associated environment to the cluster **[PREMIUM]** |
NOTE: **Note:**
`name`, `api_url`, `ca_cert` and `token` can only be updated if the cluster was added
diff --git a/doc/api/project_level_variables.md b/doc/api/project_level_variables.md
index 3b00f6f140e..66a749e4811 100644
--- a/doc/api/project_level_variables.md
+++ b/doc/api/project_level_variables.md
@@ -1,4 +1,4 @@
-# Project-level Variables API
+# Project-level Variables API
## List project variables
@@ -66,14 +66,15 @@ Create a new variable.
POST /projects/:id/variables
```
-| Attribute | Type | required | Description |
-|-----------------|---------|----------|-----------------------|
-| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
-| `value` | string | yes | The `value` of a variable |
-| `variable_type` | string | no | The type of a variable. Available types are: `env_var` (default) and `file` |
-| `protected` | boolean | no | Whether the variable is protected |
-| `masked` | boolean | no | Whether the variable is masked |
+| Attribute | Type | required | Description |
+|---------------------|---------|----------|-----------------------|
+| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
+| `value` | string | yes | The `value` of a variable |
+| `variable_type` | string | no | The type of a variable. Available types are: `env_var` (default) and `file` |
+| `protected` | boolean | no | Whether the variable is protected |
+| `masked` | boolean | no | Whether the variable is masked |
+| `environment_scope` | string | no | The `environment_scope` of the variable **[PREMIUM]** |
```
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value"
@@ -83,9 +84,11 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitla
{
"key": "NEW_VARIABLE",
"value": "new value",
+ "protected": false,
"variable_type": "env_var",
"protected": false,
- "masked": false
+ "masked": false,
+ "environment_scope": "*"
}
```
@@ -97,14 +100,15 @@ Update a project's variable.
PUT /projects/:id/variables/:key
```
-| Attribute | Type | required | Description |
-|-----------------|---------|----------|-------------------------|
-| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `key` | string | yes | The `key` of a variable |
-| `value` | string | yes | The `value` of a variable |
-| `variable_type` | string | no | The type of a variable. Available types are: `env_var` (default) and `file` |
-| `protected` | boolean | no | Whether the variable is protected |
-| `masked` | boolean | no | Whether the variable is masked |
+| Attribute | Type | required | Description |
+|---------------------|---------|----------|-------------------------|
+| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `key` | string | yes | The `key` of a variable |
+| `value` | string | yes | The `value` of a variable |
+| `variable_type` | string | no | The type of a variable. Available types are: `env_var` (default) and `file` |
+| `protected` | boolean | no | Whether the variable is protected |
+| `masked` | boolean | no | Whether the variable is masked |
+| `environment_scope` | string | no | The `environment_scope` of the variable **[PREMIUM]** |
```
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/variables/NEW_VARIABLE" --form "value=updated value"
@@ -116,7 +120,8 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab
"value": "updated value",
"variable_type": "env_var",
"protected": true,
- "masked": false
+ "masked": false,
+ "environment_scope": "*"
}
```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 951961e45ff..75669d85803 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -153,6 +153,7 @@ When the user is authenticated and `simple` is not set this returns something li
"commit_count": 37,
"storage_size": 1038090,
"repository_size": 1038090,
+ "wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
@@ -234,6 +235,7 @@ When the user is authenticated and `simple` is not set this returns something li
"commit_count": 12,
"storage_size": 2066080,
"repository_size": 2066080,
+ "wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
@@ -342,6 +344,7 @@ GET /users/:user_id/projects
"commit_count": 37,
"storage_size": 1038090,
"repository_size": 1038090,
+ "wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
@@ -423,6 +426,7 @@ GET /users/:user_id/projects
"commit_count": 12,
"storage_size": 2066080,
"repository_size": 2066080,
+ "wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
@@ -548,6 +552,7 @@ GET /projects/:id
"commit_count": 37,
"storage_size": 1038090,
"repository_size": 1038090,
+ "wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
diff --git a/doc/api/resource_label_events.md b/doc/api/resource_label_events.md
index e1f9ffa9472..f0a7ac4e41d 100644
--- a/doc/api/resource_label_events.md
+++ b/doc/api/resource_label_events.md
@@ -88,6 +88,92 @@ Parameters:
curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/issues/11/resource_label_events/1
```
+## Epics **[ULTIMATE]**
+
+### List group epic label events
+
+Gets a list of all label events for a single epic.
+
+```
+GET /groups/:id/epics/:epic_id/resource_label_events
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ------------ |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+
+```json
+[
+ {
+ "id": 106,
+ "user": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/root"
+ },
+ "created_at": "2018-08-19T11:43:01.746Z",
+ "resource_type": "Epic",
+ "resource_id": 33,
+ "label": {
+ "id": 73,
+ "name": "a1",
+ "color": "#34495E",
+ "description": ""
+ },
+ "action": "add"
+ },
+ {
+ "id": 107,
+ "user": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/root"
+ },
+ "created_at": "2018-08-19T11:43:01.746Z",
+ "resource_type": "Epic",
+ "resource_id": 33,
+ "label": {
+ "id": 37,
+ "name": "glabel2",
+ "color": "#A8D695",
+ "description": ""
+ },
+ "action": "add"
+ }
+]
+```
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/11/resource_label_events
+```
+
+### Get single epic label event
+
+Returns a single label event for a specific group epic
+
+```
+GET /groups/:id/epics/:epic_id/resource_label_events/:resource_label_event_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `resource_label_event_id` | integer | yes | The ID of a label event |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/11/resource_label_events/107
+```
+
## Merge requests
### List project merge request label events
diff --git a/doc/api/scim.md b/doc/api/scim.md
index 4595c6f2ed3..3870ea788e7 100644
--- a/doc/api/scim.md
+++ b/doc/api/scim.md
@@ -1,4 +1,4 @@
-# SCIM API
+# SCIM API **[SILVER ONLY]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9388) in [GitLab Silver](https://about.gitlab.com/pricing/) 11.10.
diff --git a/doc/api/search.md b/doc/api/search.md
index c2dd72479c1..da81c8321c9 100644
--- a/doc/api/search.md
+++ b/doc/api/search.md
@@ -19,6 +19,8 @@ GET /search
Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, snippet_titles, snippet_blobs, users.
+If Elasticsearch is enabled additional scopes available are blobs, wiki_blobs and commits. Find more about [the feature](../integration/elasticsearch.md). **[STARTER]**
+
The response depends on the requested scope.
### Scope: projects
@@ -281,6 +283,98 @@ Example response:
]
```
+### Scope: wiki_blobs **[STARTER]**
+
+This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled.
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/search?scope=wiki_blobs&search=bye
+```
+
+Example response:
+
+```json
+
+[
+ {
+ "basename": "home",
+ "data": "hello\n\nand bye\n\nend",
+ "filename": "home.md",
+ "id": null,
+ "ref": "master",
+ "startline": 5,
+ "project_id": 6
+ }
+]
+```
+
+### Scope: commits **[STARTER]**
+
+This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled.
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/search?scope=commits&search=bye
+```
+
+Example response:
+
+```json
+
+[
+ {
+ "id": "4109c2d872d5fdb1ed057400d103766aaea97f98",
+ "short_id": "4109c2d8",
+ "title": "goodbye $.browser",
+ "created_at": "2013-02-18T22:02:54.000Z",
+ "parent_ids": [
+ "59d05353ab575bcc2aa958fe1782e93297de64c9"
+ ],
+ "message": "goodbye $.browser\n",
+ "author_name": "angus croll",
+ "author_email": "anguscroll@gmail.com",
+ "authored_date": "2013-02-18T22:02:54.000Z",
+ "committer_name": "angus croll",
+ "committer_email": "anguscroll@gmail.com",
+ "committed_date": "2013-02-18T22:02:54.000Z",
+ "project_id": 6
+ }
+]
+```
+
+### Scope: blobs **[STARTER]**
+
+This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled.
+
+Filters are available for this scope:
+- filename
+- path
+- extension
+
+to use a filter simply include it in your query like so: `a query filename:some_name*`.
+
+You may use wildcards (`*`) to use glob matching.
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/search?scope=blobs&search=installation
+```
+
+Example response:
+
+```json
+
+[
+ {
+ "basename": "README",
+ "data": "```\n\n## Installation\n\nQuick start using the [pre-built",
+ "filename": "README.md",
+ "id": null,
+ "ref": "master",
+ "startline": 46,
+ "project_id": 6
+ }
+]
+```
+
### Scope: users
```bash
@@ -320,6 +414,8 @@ GET /groups/:id/search
Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, users.
+If Elasticsearch is enabled additional scopes available are blobs, wiki_blobs and commits. Find more about [the feature](../integration/elasticsearch.md). **[STARTER]**
+
The response depends on the requested scope.
### Scope: projects
@@ -520,6 +616,98 @@ Example response:
]
```
+### Scope: wiki_blobs **[STARTER]**
+
+This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled.
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/6/search?scope=wiki_blobs&search=bye
+```
+
+Example response:
+
+```json
+
+[
+ {
+ "basename": "home",
+ "data": "hello\n\nand bye\n\nend",
+ "filename": "home.md",
+ "id": null,
+ "ref": "master",
+ "startline": 5,
+ "project_id": 6
+ }
+]
+```
+
+### Scope: commits **[STARTER]**
+
+This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled.
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/6/search?scope=commits&search=bye
+```
+
+Example response:
+
+```json
+
+[
+ {
+ "id": "4109c2d872d5fdb1ed057400d103766aaea97f98",
+ "short_id": "4109c2d8",
+ "title": "goodbye $.browser",
+ "created_at": "2013-02-18T22:02:54.000Z",
+ "parent_ids": [
+ "59d05353ab575bcc2aa958fe1782e93297de64c9"
+ ],
+ "message": "goodbye $.browser\n",
+ "author_name": "angus croll",
+ "author_email": "anguscroll@gmail.com",
+ "authored_date": "2013-02-18T22:02:54.000Z",
+ "committer_name": "angus croll",
+ "committer_email": "anguscroll@gmail.com",
+ "committed_date": "2013-02-18T22:02:54.000Z",
+ "project_id": 6
+ }
+]
+```
+
+### Scope: blobs **[STARTER]**
+
+This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled.
+
+Filters are available for this scope:
+- filename
+- path
+- extension
+
+to use a filter simply include it in your query like so: `a query filename:some_name*`.
+
+You may use wildcards (`*`) to use glob matching.
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/6/search?scope=blobs&search=installation
+```
+
+Example response:
+
+```json
+
+[
+ {
+ "basename": "README",
+ "data": "```\n\n## Installation\n\nQuick start using the [pre-built",
+ "filename": "README.md",
+ "id": null,
+ "ref": "master",
+ "startline": 46,
+ "project_id": 6
+ }
+]
+```
+
### Scope: users
```bash
@@ -556,7 +744,6 @@ GET /projects/:id/search
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `scope` | string | yes | The scope to search in |
| `search` | string | yes | The search query |
-| `ref` | string | no | The name of a repository branch or tag to search on. The project's default branch is used by default. This is only applicable for scopes: commits, blobs, and wiki_blobs. |
Search the expression within the specified scope. Currently these scopes are supported: issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs, users.
@@ -851,7 +1038,7 @@ Blobs searches are performed on both filenames and contents. Search results:
times in the content.
```bash
-curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/6/search?scope=blobs&search=installation&ref=feature
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/6/search?scope=blobs&search=installation
```
Example response:
@@ -864,7 +1051,7 @@ Example response:
"data": "```\n\n## Installation\n\nQuick start using the [pre-built",
"filename": "README.md",
"id": null,
- "ref": "feature",
+ "ref": "master",
"startline": 46,
"project_id": 6
}
diff --git a/doc/api/services.md b/doc/api/services.md
index 01df2a50198..898cfad7254 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -1101,6 +1101,75 @@ Get JetBrains TeamCity CI service settings for a project.
GET /projects/:id/services/teamcity
```
+## Jenkins CI **[STARTER]**
+
+A continuous integration and build server
+
+### Create/Edit Jenkins CI service
+
+Set Jenkins CI service for a project.
+
+```
+PUT /projects/:id/services/jenkins
+```
+
+Parameters:
+
+- `jenkins_url` (**required**) - Jenkins URL like http://jenkins.example.com
+- `project_name` (**required**) - The URL-friendly project name. Example: my_project_name
+- `username` (optional) - A user with access to the Jenkins server, if applicable
+- `password` (optional) - The password of the user
+
+### Delete Jenkins CI service
+
+Delete Jenkins CI service for a project.
+
+```
+DELETE /projects/:id/services/jenkins
+```
+
+### Get Jenkins CI service settings
+
+Get Jenkins CI service settings for a project.
+
+```
+GET /projects/:id/services/jenkins
+```
+
+## Jenkins CI (Deprecated) Service
+
+A continuous integration and build server
+
+### Create/Edit Jenkins CI (Deprecated) service
+
+Set Jenkins CI (Deprecated) service for a project.
+
+```
+PUT /projects/:id/services/jenkins-deprecated
+```
+
+Parameters:
+
+- `project_url` (**required**) - Jenkins project URL like http://jenkins.example.com/job/my-project/
+- `multiproject_enabled` (optional) - Multi-project mode is configured in Jenkins GitLab Hook plugin
+- `pass_unstable` (optional) - Unstable builds will be treated as passing
+
+### Delete Jenkins CI (Deprecated) service
+
+Delete Jenkins CI (Deprecated) service for a project.
+
+```
+DELETE /projects/:id/services/jenkins-deprecated
+```
+
+### Get Jenkins CI (Deprecated) service settings
+
+Get Jenkins CI (Deprecated) service settings for a project.
+
+```
+GET /projects/:id/services/jenkins-deprecated
+```
+
[jira-doc]: ../user/project/integrations/jira.md
[old-jira-api]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/api/services.md#jira
diff --git a/doc/api/snippets.md b/doc/api/snippets.md
index f90447e124e..1ce0b1e7a62 100644
--- a/doc/api/snippets.md
+++ b/doc/api/snippets.md
@@ -165,15 +165,15 @@ Parameters:
|:--------------|:-------|:---------|:---------------------------------------------------|
| `title` | string | yes | Title of a snippet. |
| `file_name` | string | yes | Name of a snippet file. |
-| `content` | string | yes | Content of a snippet. |
+| `code` | string | yes | Content of a snippet. |
| `description` | string | no | Description of a snippet. |
-| `visibility` | string | no | Snippet's [visibility](#snippet-visibility-level). |
+| `visibility` | string | yes | Snippet's [visibility](#snippet-visibility-level). |
Example request:
```sh
curl --request POST \
- --data '{"title": "This is a snippet", "content": "Hello world", "description": "Hello World snippet", "file_name": "test.txt", "visibility": "internal" }' \
+ --data '{"title": "This is a snippet", "code": "Hello world", "description": "Hello World snippet", "file_name": "test.txt", "visibility": "internal" }' \
--header 'Content-Type: application/json' \
--header "PRIVATE-TOKEN: valid_api_token" \
https://gitlab.example.com/api/v4/snippets
@@ -222,14 +222,14 @@ Parameters:
| `title` | string | no | Title of a snippet. |
| `file_name` | string | no | Name of a snippet file. |
| `description` | string | no | Description of a snippet. |
-| `content` | string | no | Content of a snippet. |
+| `code` | string | no | Content of a snippet. |
| `visibility` | string | no | Snippet's [visibility](#snippet-visibility-level). |
Example request:
```sh
curl --request PUT \
- --data '{"title": "foo", "content": "bar"}' \
+ --data '{"title": "foo", "code": "bar"}' \
--header 'Content-Type: application/json' \
--header "PRIVATE-TOKEN: valid_api_token" \
https://gitlab.example.com/api/v4/snippets/1
diff --git a/doc/api/users.md b/doc/api/users.md
index d3e67d3d510..47028c679b8 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -140,8 +140,7 @@ GET /users
"can_create_project": true,
"two_factor_enabled": true,
"external": false,
- "private_profile": false,
- "highest_role":10
+ "private_profile": false
}
]
```
@@ -257,7 +256,8 @@ Parameters:
"can_create_project": true,
"two_factor_enabled": true,
"external": false,
- "private_profile": false
+ "private_profile": false,
+ "highest_role":10
}
```
diff --git a/doc/api/vulnerabilities.md b/doc/api/vulnerabilities.md
index 87f77613ad3..390d0966244 100644
--- a/doc/api/vulnerabilities.md
+++ b/doc/api/vulnerabilities.md
@@ -1,4 +1,4 @@
-# Vulnerabilities API
+# Vulnerabilities API **[ULTIMATE]**
Every API call to vulnerabilities must be authenticated.
diff --git a/doc/articles/how_to_configure_ldap_gitlab_ee/index.md b/doc/articles/how_to_configure_ldap_gitlab_ee/index.md
index 4ce96fcb230..3e6f3130437 100644
--- a/doc/articles/how_to_configure_ldap_gitlab_ee/index.md
+++ b/doc/articles/how_to_configure_ldap_gitlab_ee/index.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'https://docs.gitlab.com/ee/administration/auth/how_to_configure_ldap_gitlab_ee/index.html'
+redirect_to: '../../administration/auth/how_to_configure_ldap_gitlab_ee/index.md'
---
-This document was moved to [another location](https://docs.gitlab.com/ee/administration/auth/how_to_configure_ldap_gitlab_ee/index.html).
+This document was moved to [another location](../../administration/auth/how_to_configure_ldap_gitlab_ee/index.md).
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 27bde2ac0f1..635cce13b4e 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -1,6 +1,7 @@
---
comments: false
description: "Learn how to use GitLab CI/CD, the GitLab built-in Continuous Integration, Continuous Deployment, and Continuous Delivery toolset to build, test, and deploy your application."
+type: index
---
# GitLab CI/CD
@@ -160,6 +161,33 @@ See also the [Why CI/CD?](https://docs.google.com/presentation/d/1OGgk2Tcxbpl7DJ
As GitLab CI/CD has evolved, certain breaking changes have
been necessary. These are:
+#### 12.0
+
+- [Use refspec to clone/fetch git
+ repository](https://gitlab.com/gitlab-org/gitlab-runner/issues/4069).
+- [Old cache
+ configuration](https://gitlab.com/gitlab-org/gitlab-runner/issues/4070).
+- [Old metrics server
+ configuration](https://gitlab.com/gitlab-org/gitlab-runner/issues/4072).
+- [Remove
+ `FF_K8S_USE_ENTRYPOINT_OVER_COMMAND`](https://gitlab.com/gitlab-org/gitlab-runner/issues/4073).
+- [Remove Linux distributions that reach
+ EOL](https://gitlab.com/gitlab-org/gitlab-runner/merge_requests/1130).
+- [Update command line API for helper
+ images](https://gitlab.com/gitlab-org/gitlab-runner/issues/4013).
+- [Remove old `git clean`
+ flow](https://gitlab.com/gitlab-org/gitlab-runner/issues/4175).
+
+#### 11.0
+
+- No breaking changes.
+
+#### 10.0
+
+- No breaking changes.
+
+#### 9.0
+
- [CI variables renaming for GitLab 9.0](variables/deprecated_variables.md#gitlab-90-renamed-variables). Read about the
deprecated CI variables and what you should use for GitLab 9.0+.
- [New CI job permissions model](../user/project/new_ci_build_permissions_model.md).
diff --git a/doc/ci/caching/index.md b/doc/ci/caching/index.md
index d703e106e76..9a5a3624c73 100644
--- a/doc/ci/caching/index.md
+++ b/doc/ci/caching/index.md
@@ -1,3 +1,7 @@
+---
+type: index, concepts, howto
+---
+
# Cache dependencies in GitLab CI/CD
GitLab CI/CD provides a caching mechanism that can be used to save time
@@ -60,7 +64,7 @@ In summary:
- Caches are disabled if not defined globally or per job (using `cache:`).
- Caches are available for all jobs in your `.gitlab-ci.yml` if enabled globally.
-- Caches can be used by subsequent pipelines of that very same job (a script in
+- Caches can be used by subsequent pipelines of that same job (a script in
a stage) in which the cache was created (if not defined globally).
- Caches are stored where the Runner is installed **and** uploaded to S3 if
[distributed cache is enabled](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching).
@@ -87,7 +91,7 @@ cache, when declaring `cache` in your jobs, use one or a mix of the following:
that share their cache.
- [Use sticky Runners](../runners/README.md#locking-a-specific-runner-from-being-enabled-for-other-projects)
that will be only available to a particular project.
-- [Use a `key`](../yaml/README.md#cachekey) that fits your workflow (e.g.,
+- [Use a `key`](../yaml/README.md#cachekey) that fits your workflow (for example,
different caches on each branch). For that, you can take advantage of the
[CI/CD predefined variables](../variables/README.md#predefined-environment-variables).
@@ -420,7 +424,7 @@ mismatch and a few ideas how to fix it.
Let's explore some examples.
----
+#### Examples
Let's assume you have only one Runner assigned to your project, so the cache
will be stored in the Runner's machine by default. If two jobs, A and B,
@@ -462,8 +466,6 @@ job B:
To fix that, use different `keys` for each job.
----
-
In another case, let's assume you have more than one Runners assigned to your
project, but the distributed cache is not enabled. We want the second time the
pipeline is run, `job A` and `job B` to re-use their cache (which in this case
@@ -526,3 +528,15 @@ Behind the scenes, this works by increasing a counter in the database, and the
value of that counter is used to create the key for the cache by appending an
integer to it: `-1`, `-2`, etc. After a push, a new key is generated and the
old cache is not valid anymore.
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/ci/chatops/README.md b/doc/ci/chatops/README.md
index 5ecdf0f8c54..241134783da 100644
--- a/doc/ci/chatops/README.md
+++ b/doc/ci/chatops/README.md
@@ -1,3 +1,7 @@
+---
+type: index, concepts, howto
+---
+
# GitLab ChatOps
> **Notes:**
@@ -10,7 +14,10 @@ GitLab ChatOps provides a method to interact with CI/CD jobs through chat servic
## How it works
-GitLab ChatOps is built upon two existing features, [GitLab CI/CD](../README.md) and [Slack Slash Commmands](../../user/project/integrations/slack_slash_commands.md).
+GitLab ChatOps is built upon two existing features:
+
+- [GitLab CI/CD](../README.md).
+- [Slack Slash Commands](../../user/project/integrations/slack_slash_commands.md).
A new `run` action has been added to the [slash commands](../../integration/slash_commands.md), which takes two arguments: a `<job name>` to execute and the `<job arguments>`. When executed, ChatOps will look up the specified job name and attempt to match it to a corresponding job in [.gitlab-ci.yml](../yaml/README.md). If a matching job is found on `master`, a pipeline containing just that job is scheduled. Two additional [CI/CD variables](../variables/README.md#predefined-environment-variables) are passed to the job: `CHAT_INPUT` contains any additional arguments, and `CHAT_CHANNEL` is set to the name of channel the action was triggered in.
@@ -22,9 +29,9 @@ After the job has finished, its output is sent back to Slack provided it has com
Since ChatOps is built upon GitLab CI/CD, the job has all the same features and functions available. There a few best practices to consider however when creating ChatOps jobs:
-* It is strongly recommended to set `only: [chat]` so the job does not run as part of the standard CI pipeline.
-* If the job is set to `when: manual`, the pipeline will be created however the job will wait to be started.
-* It is important to keep in mind that there is very limited support for access control. If the user who triggered the slash command is a developer in the project, the job will run. The job itself can utilize existing [CI/CD variables](../variables/README.html#predefined-environment-variables) like `GITLAB_USER_ID` to perform additional rights validation, however these variables can be [overridden](../variables/README.html#priority-of-environment-variables).
+- It is strongly recommended to set `only: [chat]` so the job does not run as part of the standard CI pipeline.
+- If the job is set to `when: manual`, the pipeline will be created however the job will wait to be started.
+- It is important to keep in mind that there is limited support for access control. If the user who triggered the slash command is a developer in the project, the job will run. The job itself can utilize existing [CI/CD variables](../variables/README.html#predefined-environment-variables) like `GITLAB_USER_ID` to perform additional rights validation, however these variables can be [overridden](../variables/README.html#priority-of-environment-variables).
### Controlling the ChatOps reply
@@ -59,3 +66,15 @@ You can find and download the official GitLab ChatOps icon here.
![GitLab ChatOps bot icon](img/gitlab-chatops-icon-small.png)
[Download bigger image](img/gitlab-chatops-icon.png)
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md b/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md
index 1c8e12d229c..9126eb65f07 100644
--- a/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md
+++ b/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md
@@ -1,8 +1,15 @@
+---
+type: howto
+---
+
# Using GitLab CI/CD with a Bitbucket Cloud repository **[PREMIUM]**
-GitLab CI/CD can be used with Bitbucket Cloud by creating a
-[CI/CD project](https://docs.gitlab.com/ee/user/project/ci_cd_for_external_repo.html) and connecting
-your Git repository via URL.
+GitLab CI/CD can be used with Bitbucket Cloud by:
+
+1. Creating a [CI/CD project](https://docs.gitlab.com/ee/user/project/ci_cd_for_external_repo.html).
+1. Connecting your Git repository via URL.
+
+To use GitLab CI/CD with a Bitbucket Cloud repository:
1. In GitLab create a **CI/CD for external repo**, select **Repo by URL** and
create the project.
@@ -16,13 +23,13 @@ your Git repository via URL.
with `api` scope. This will be used to authenticate requests from the web
hook that will be created in Bitbucket to notify GitLab of new commits.
-1. In Bitbucket from **Settings > Webhooks** create a new web hook to notify
+1. In Bitbucket, from **Settings > Webhooks**, create a new web hook to notify
GitLab of new commits.
The web hook URL should be set to the GitLab API to trigger pull mirroring,
using the Personal Access Token we just generated for authentication.
- ```
+ ```text
https://gitlab.com/api/v4/projects/<NAMESPACE>%2F<PROJECT>/mirror/pull?private_token=<PERSONAL_ACCESS_TOKEN>
```
@@ -33,27 +40,27 @@ your Git repository via URL.
After saving, test the web hook by pushing a change to your Bitbucket
repository.
-1. In Bitbucket create an **App Password** from **Bitbucket Settings > App
+1. In Bitbucket, create an **App Password** from **Bitbucket Settings > App
Passwords** to authenticate the build status script setting commit build
statuses in Bitbucket. Repository write permissions are required.
![Bitbucket Cloud webhook](img/bitbucket_app_password.png)
-1. In GitLab from **Settings > CI/CD > Environment variables** add variables to allow
- communication with Bitbucket via the Bitbucket API.
+1. In GitLab, from **Settings > CI/CD > Environment variables**, add variables to allow
+ communication with Bitbucket via the Bitbucket API:
- `BITBUCKET_ACCESS_TOKEN`: the Bitbucket app password created above
+ `BITBUCKET_ACCESS_TOKEN`: the Bitbucket app password created above.
- `BITBUCKET_USERNAME`: the username of the Bitbucket account
+ `BITBUCKET_USERNAME`: the username of the Bitbucket account.
- `BITBUCKET_NAMESPACE`: set this if your GitLab and Bitbucket namespaces differ
+ `BITBUCKET_NAMESPACE`: set this if your GitLab and Bitbucket namespaces differ.
- `BITBUCKET_REPOSITORY`: set this if your GitLab and Bitbucket project names differ
+ `BITBUCKET_REPOSITORY`: set this if your GitLab and Bitbucket project names differ.
-1. In Bitbucket add a script to push the pipeline status to Bitbucket.
+1. In Bitbucket, add a script to push the pipeline status to Bitbucket.
> Note: changes made in GitLab will be overwritten by any changes made
- upstream in Bitbucket.
+ > upstream in Bitbucket.
Create a file `build_status` and insert the script below and run
`chmod +x build_status` in your terminal to make the script executable.
@@ -111,7 +118,7 @@ your Git repository via URL.
1. Still in Bitbucket, create a `.gitlab-ci.yml` file to use the script to push
pipeline success and failures to Bitbucket.
- ```
+ ```yaml
stages:
- test
- ci_status
@@ -145,3 +152,15 @@ GitLab is now configured to mirror changes from Bitbucket, run CI/CD pipelines
configured in `.gitlab-ci.yml` and push the status to Bitbucket.
[pull-mirroring]: ../../workflow/repository_mirroring.md#pulling-from-a-remote-repository-starter
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/ci/ci_cd_for_external_repos/github_integration.md b/doc/ci/ci_cd_for_external_repos/github_integration.md
index 0e2acf957e0..612dcc93bc1 100644
--- a/doc/ci/ci_cd_for_external_repos/github_integration.md
+++ b/doc/ci/ci_cd_for_external_repos/github_integration.md
@@ -1,3 +1,7 @@
+---
+type: howto
+---
+
# Using GitLab CI/CD with a GitHub repository **[PREMIUM]**
GitLab CI/CD can be used with **GitHub.com** and **GitHub Enterprise** by
@@ -15,7 +19,7 @@ administrator:
NOTE: **Note:**
Due to a 10-token limitation on the [GitHub OAuth Implementation](https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#creating-multiple-tokens-for-oauth-apps),
-if you import more than 10 times, your oldest imported project's token will be
+if you import more than 10 times, your oldest imported project's token will be
revoked. See issue [#9147](https://gitlab.com/gitlab-org/gitlab-ee/issues/9147)
for more information.
@@ -31,23 +35,27 @@ for more information.
1. In GitHub, add a `.gitlab-ci.yml` to [configure GitLab CI/CD](../quick_start/README.md).
-GitLab will import the project, enable [Pull Mirroring](../../workflow/repository_mirroring.md#pulling-from-a-remote-repository-starter), enable
-[GitHub project integration](https://docs.gitlab.com/ee/user/project/integrations/github.html), and create a web hook
-on GitHub to notify GitLab of new commits.
+GitLab will:
+
+1. Import the project.
+1. Enable [Pull Mirroring](../../workflow/repository_mirroring.md#pulling-from-a-remote-repository-starter).
+1. Enable [GitHub project integration](https://docs.gitlab.com/ee/user/project/integrations/github.html).
+1. Create a web hook on GitHub to notify GitLab of new commits.
## Connect with Personal Access Token
-NOTE: **Note:** Personal access tokens can only be used to connect GitHub.com
+NOTE: **Note:**
+Personal access tokens can only be used to connect GitHub.com
repositories to GitLab.
If you are not using the [GitHub integration](../../integration/github.md), you can
still perform a one-off authorization with GitHub to grant GitLab access your
repositories:
-1. Open https://github.com/settings/tokens/new to create a **Personal Access
+1. Open <https://github.com/settings/tokens/new> to create a **Personal Access
Token**. This token with be used to access your repository and push commit
statuses to GitHub.
-
+
The `repo` and `admin:repo_hook` should be enable to allow GitLab access to
your project, update commit statuses, and create a web hook to notify
GitLab of new commits.
@@ -62,20 +70,23 @@ repositories:
1. In GitHub, add a `.gitlab-ci.yml` to [configure GitLab CI/CD](../quick_start/README.md).
-GitLab will import the project, enable [Pull Mirroring](../../workflow/repository_mirroring.md#pulling-from-a-remote-repository-starter), enable
-[GitHub project integration](https://docs.gitlab.com/ee/user/project/integrations/github.html), and create a web hook
-on GitHub to notify GitLab of new commits.
+GitLab will:
+
+1. Import the project.
+1. Enable [Pull Mirroring](../../workflow/repository_mirroring.md#pulling-from-a-remote-repository-starter).
+1. Enable [GitHub project integration](https://docs.gitlab.com/ee/user/project/integrations/github.html).
+1. Create a web hook on GitHub to notify GitLab of new commits.
## Connect manually
If the [GitHub integration](../../integration/github.md) is not enabled, or is enabled
for a different GitHub instance, you GitLab CI/CD can be manually enabled for
-your repository.
+your repository:
-1. In GitHub open https://github.com/settings/tokens/new create a **Personal
+1. In GitHub open <https://github.com/settings/tokens/new> create a **Personal
Access Token.** GitLab will use this token to access your repository and
push commit statuses.
-
+
Enter a **Token description** and update the scope to allow:
`repo` so that GitLab can access your project and update commit statuses
@@ -109,3 +120,15 @@ your repository.
![Create web hook](img/github_push_webhook.png)
1. In GitHub add a `.gitlab-ci.yml` to configure GitLab CI/CD.
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/ci/ci_cd_for_external_repos/index.md b/doc/ci/ci_cd_for_external_repos/index.md
index 450ffe512dc..5de88412121 100644
--- a/doc/ci/ci_cd_for_external_repos/index.md
+++ b/doc/ci/ci_cd_for_external_repos/index.md
@@ -1,3 +1,7 @@
+---
+type: index, howto
+---
+
# GitLab CI/CD for external repositories **[PREMIUM]**
NOTE: **Note:**
@@ -6,22 +10,26 @@ GitLab.com users until September 22nd, 2019.
>[Introduced][ee-4642] in [GitLab Premium][eep] 10.6.
-GitLab CI/CD can be used with GitHub or any other Git server.
+GitLab CI/CD can be used with:
+
+- [GitHub](github_integration.md).
+- [Bitbucket Cloud](bitbucket_integration.md).
+- Any other Git server.
+
Instead of moving your entire project to GitLab, you can connect your
external repository to get the benefits of GitLab CI/CD.
-- [GitHub](github_integration.md)
-- [Bitbucket Cloud](bitbucket_integration.md)
-
Connecting an external repository will set up [repository mirroring][mirroring]
and create a lightweight project where issues, merge requests, wiki, and
snippets disabled. These features
[can be re-enabled later][settings].
-1. From your GitLab dashboard click **New project**
-1. Switch to the **CI/CD for external repo** tab
-1. Choose **GitHub** or **Repo by URL**
-1. The next steps are similar to the [import flow](../../user/project/import/index.md)
+To connect to an external repository:
+
+1. From your GitLab dashboard, click **New project**.
+1. Switch to the **CI/CD for external repo** tab.
+1. Choose **GitHub** or **Repo by URL**.
+1. The next steps are similar to the [import flow](../../user/project/import/index.md).
![CI/CD for external repository project creation](img/ci_cd_for_external_repo.png)
diff --git a/doc/ci/docker/README.md b/doc/ci/docker/README.md
index 446f5b54f0c..f76471b50f2 100644
--- a/doc/ci/docker/README.md
+++ b/doc/ci/docker/README.md
@@ -1,9 +1,13 @@
---
comments: false
+type: index
---
# Docker integration
+GitLab CI/CD can be combined with [Docker](https://www.docker.com) to enable
+integration between the two.
+
The following documentation is available for using GitLab CI/CD with Docker:
- [Using Docker images](using_docker_images.md).
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index dd112dadc40..d8068bbb7f0 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -1,16 +1,18 @@
+---
+type: concepts, howto
+---
+
# Building Docker images with GitLab CI/CD
GitLab CI/CD allows you to use Docker Engine to build and test docker-based projects.
-TIP: **Tip:**
-This also allows to you to use `docker-compose` and other docker-enabled tools.
One of the new trends in Continuous Integration/Deployment is to:
-1. Create an application image
-1. Run tests against the created image
-1. Push image to a remote registry
-1. Deploy to a server from the pushed image
+1. Create an application image.
+1. Run tests against the created image.
+1. Push image to a remote registry.
+1. Deploy to a server from the pushed image.
It's also useful when your application already has the `Dockerfile` that can be
used to create and test an image:
@@ -75,7 +77,7 @@ GitLab Runner then executes job scripts as the `gitlab-runner` user.
- docker run my-docker-image /script/to/run/tests
```
-1. You can now use `docker` command and install `docker-compose` if needed.
+1. You can now use `docker` command (and **install** `docker-compose` if needed).
NOTE: **Note:**
By adding `gitlab-runner` to the `docker` group you are effectively granting `gitlab-runner` full root permissions.
@@ -85,8 +87,10 @@ For more information please read [On Docker security: `docker` group considered
The second approach is to use the special docker-in-docker (dind)
[Docker image](https://hub.docker.com/_/docker/) with all tools installed
-(`docker` and `docker-compose`) and run the job script in context of that
-image in privileged mode.
+(`docker`) and run the job script in context of that
+image in privileged mode.
+
+NOTE: **Note:** `docker-compose` is not part of docker-in-docker (dind). In case you'd like to use `docker-compose` in your CI builds, please follow the (installation instructions for docker-compose)[https://docs.docker.com/compose/install/] provided by docker.
In order to do that, follow the steps:
@@ -113,7 +117,7 @@ In order to do that, follow the steps:
The above command will create a `config.toml` entry similar to this:
- ```
+ ```toml
[[runners]]
url = "https://gitlab.com/"
token = TOKEN
@@ -227,7 +231,7 @@ In order to do that, follow the steps:
The above command will create a `config.toml` entry similar to this:
- ```
+ ```toml
[[runners]]
url = "https://gitlab.com/"
token = REGISTRATION_TOKEN
@@ -270,9 +274,9 @@ aware of the following implications:
create containers with specific names, they may conflict with each other.
- Sharing files and directories from the source repo into containers may not
work as expected since volume mounting is done in the context of the host
- machine, not the build container, e.g.:
+ machine, not the build container. For example:
- ```
+ ```sh
docker run --rm -t -i -v $(pwd)/src:/home/app/src test-image:latest run_app_tests
```
@@ -337,7 +341,7 @@ NOTE: **Note:**
The shared Runners on GitLab.com use the `overlay2` driver by default.
By default, when using `docker:dind`, Docker uses the `vfs` storage driver which
-copies the filesystem on every run. This is a very disk-intensive operation
+copies the filesystem on every run. This is a disk-intensive operation
which can be avoided if a different driver is used, for example `overlay2`.
### Requirements
@@ -345,13 +349,13 @@ which can be avoided if a different driver is used, for example `overlay2`.
1. Make sure a recent kernel is used, preferably `>= 4.2`.
1. Check whether the `overlay` module is loaded:
- ```
+ ```sh
sudo lsmod | grep overlay
```
If you see no result, then it isn't loaded. To load it use:
- ```
+ ```sh
sudo modprobe overlay
```
@@ -359,7 +363,7 @@ which can be avoided if a different driver is used, for example `overlay2`.
On Ubuntu systems, this is done by editing `/etc/modules`. Just add the
following line into it:
- ```
+ ```text
overlay
```
@@ -367,7 +371,7 @@ which can be avoided if a different driver is used, for example `overlay2`.
You can enable the driver for each project individually by editing the project's `.gitlab-ci.yml`:
-```
+```yaml
variables:
DOCKER_DRIVER: overlay2
```
@@ -571,3 +575,15 @@ deploy:
[docker-cap]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
[2fa]: ../../user/profile/account/two_factor_authentication.md
[pat]: ../../user/profile/personal_access_tokens.md
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 13c26bc5f47..d09efce7cc1 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -1,3 +1,7 @@
+---
+type: concepts, howto
+---
+
# Using Docker images
GitLab CI in conjunction with [GitLab Runner](../runners/README.md) can use
@@ -45,10 +49,10 @@ The `image` keyword is the name of the Docker image the Docker executor
will run to perform the CI tasks.
By default, the executor will only pull images from [Docker Hub][hub],
-but this can be configured in the `gitlab-runner/config.toml` by setting
+however this can be configured in the `gitlab-runner/config.toml` by setting
the [Docker pull policy][] to allow using local images.
-For more information about images and Docker Hub please read
+For more information about images and Docker Hub, please read
the [Docker Fundamentals][] documentation.
## What is a service
@@ -95,8 +99,8 @@ required for the CI/CD job to proceed and is accessed by network.
To make sure this works, the Runner:
-1. checks which ports are exposed from the container by default
-1. starts a special container that waits for these ports to be accessible
+1. Checks which ports are exposed from the container by default.
+1. Starts a special container that waits for these ports to be accessible.
When the second stage of the check fails, either because there is no opened port in the
service, or the service was not started properly before the timeout and the port is not
@@ -106,7 +110,7 @@ In most cases it will affect the job, but there may be situations when the job
will still succeed even if that warning was printed. For example:
- The service was started a little after the warning was raised, and the job is
- not using the linked service from the very beginning. In that case, when the
+ not using the linked service from the beginning. In that case, when the
job needed to access the service, it may have been already there waiting for
connections.
- The service container is not providing any networking service, but it's doing
@@ -143,9 +147,9 @@ job:
If you need to have `php`, `node` and `go` available for your script, you should
either:
-- choose an existing Docker image that contains all required tools, or
-- create your own Docker image, which will have all the required tools included
- and use that in your job
+- Choose an existing Docker image that contains all required tools.
+- Create your own Docker image, which will have all the required tools included
+ and use that in your job.
### Accessing the services
@@ -167,18 +171,18 @@ access to it from your build container under two hostnames to choose from:
- `tutum-wordpress`
- `tutum__wordpress`
->**Note:**
+NOTE: **Note:**
Hostnames with underscores are not RFC valid and may cause problems in 3rd party
applications.
The default aliases for the service's hostname are created from its image name
following these rules:
-- Everything after the colon (`:`) is stripped
+- Everything after the colon (`:`) is stripped.
- Slash (`/`) is replaced with double underscores (`__`) and the primary alias
- is created
+ is created.
- Slash (`/`) is replaced with a single dash (`-`) and the secondary alias is
- created (requires GitLab Runner v1.1.0 or higher)
+ created (requires GitLab Runner v1.1.0 or higher).
To override the default behavior, you can
[specify a service alias](#available-settings-for-services).
@@ -333,7 +337,7 @@ services:
```
The Runner will still start two containers using the `mysql:latest` image,
-but now each of them will also be accessible with the alias configured
+however now each of them will also be accessible with the alias configured
in `.gitlab-ci.yml` file.
### Setting a command for the service
@@ -408,8 +412,6 @@ you should check which one your Runner is using. Specifically:
The syntax of `image:entrypoint` is similar to [Dockerfile's `ENTRYPOINT`][entrypoint].
-----
-
Let's assume you have a `super/sql:experimental` image with some SQL database
inside it and you would like to use it as a base image for your job because you
want to execute some tests with this database binary. Let's also assume that
@@ -443,7 +445,7 @@ image:
Look for the `[runners.docker]` section:
-```
+```toml
[runners.docker]
image = "ruby:2.1"
services = ["mysql:latest", "postgres:latest"]
@@ -469,11 +471,11 @@ image which is private and requires you to login into a private container regist
Let's also assume that these are the login credentials:
-| Key | Value |
-|----------|---------------------------|
-| registry | registry.example.com:5000 |
-| username | my_username |
-| password | my_password |
+| Key | Value |
+|:---------|:----------------------------|
+| registry | `registry.example.com:5000` |
+| username | `my_username` |
+| password | `my_password` |
To configure access for `registry.example.com:5000`, follow these steps:
@@ -534,7 +536,8 @@ To configure access for `registry.example.com:5000`, follow these steps:
You can add configuration for as many registries as you want, adding more
registries to the `"auths"` hash as described above.
-NOTE: **Note:** The full `hostname:port` combination is required everywhere
+NOTE: **Note:**
+The full `hostname:port` combination is required everywhere
for the Runner to match the `DOCKER_AUTH_CONFIG`. For example, if
`registry.example.com:5000/namespace/image:tag` is specified in `.gitlab-ci.yml`,
then the `DOCKER_AUTH_CONFIG` must also specify `registry.example.com:5000`.
@@ -551,8 +554,9 @@ service containers.
For all possible configuration variables check the documentation of each image
provided in their corresponding Docker hub page.
-*Note: All variables will be passed to all services containers. It's not
-designed to distinguish which variable should go where.*
+NOTE: **Note:**
+All variables will be passed to all services containers. It's not
+designed to distinguish which variable should go where.
### PostgreSQL service example
@@ -582,8 +586,9 @@ time.
## How to debug a job locally
-*Note: The following commands are run without root privileges. You should be
-able to run Docker with your regular user account.*
+NOTE: **Note:**
+The following commands are run without root privileges. You should be
+able to run Docker with your regular user account.
First start with creating a file named `build_script`:
@@ -602,7 +607,7 @@ is specific to your project.
Then create some service containers:
-```
+```sh
docker run -d --name service-mysql mysql:latest
docker run -d --name service-postgres postgres:latest
```
@@ -614,7 +619,7 @@ respectively. They will both run in the background (`-d`).
Finally, create a build container by executing the `build_script` file we
created earlier:
-```
+```sh
docker run --name build -i --link=service-mysql:mysql --link=service-postgres:postgres ruby:2.1 /bin/bash < build_script
```
@@ -626,7 +631,7 @@ piped using STDIN to the bash interpreter which in turn executes the
When you finish testing and no longer need the containers, you can remove them
with:
-```
+```sh
docker rm -f -v build service-mysql service-postgres
```
diff --git a/doc/ci/docker/using_kaniko.md b/doc/ci/docker/using_kaniko.md
index f354cdb398e..50f1ac3d54a 100644
--- a/doc/ci/docker/using_kaniko.md
+++ b/doc/ci/docker/using_kaniko.md
@@ -1,3 +1,7 @@
+---
+type: howto
+---
+
# Building images with kaniko and GitLab CI/CD
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/45512) in GitLab 11.2.
@@ -9,17 +13,18 @@ container images from a Dockerfile, inside a container or Kubernetes cluster.
kaniko solves two problems with using the
[docker-in-docker build](using_docker_build.md#use-docker-in-docker-executor) method:
-1. Docker-in-docker requires [privileged mode](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities)
- in order to function, which is a significant security concern.
-1. Docker-in-docker generally incurs a performance penalty and can be quite slow.
+- Docker-in-docker requires [privileged mode](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities)
+ in order to function, which is a significant security concern.
+- Docker-in-docker generally incurs a performance penalty and can be quite slow.
## Requirements
In order to utilize kaniko with GitLab, a [GitLab Runner](https://docs.gitlab.com/runner/)
-using either the [Kubernetes](https://docs.gitlab.com/runner/executors/kubernetes.html),
-[Docker](https://docs.gitlab.com/runner/executors/docker.html), or
-[Docker Machine](https://docs.gitlab.com/runner/executors/docker_machine.html)
-executors is required.
+using one of the following executors is required:
+
+- [Kubernetes](https://docs.gitlab.com/runner/executors/kubernetes.html).
+- [Docker](https://docs.gitlab.com/runner/executors/docker.html).
+- [Docker Machine](https://docs.gitlab.com/runner/executors/docker_machine.html).
## Building a Docker image with kaniko
@@ -34,14 +39,17 @@ few important details:
- A Docker `config.json` file needs to be created with the authentication
information for the desired container registry.
----
+In the following example, kaniko is used to:
+
+1. Build a Docker image.
+1. Then push it to [GitLab Container Registry](../../user/project/container_registry.md).
-In the following example, kaniko is used to build a Docker image and then push
-it to [GitLab Container Registry](../../user/project/container_registry.md).
The job will run only when a tag is pushed. A `config.json` file is created under
`/kaniko/.docker` with the needed GitLab Container Registry credentials taken from the
[environment variables](../variables/README.md#predefined-environment-variables)
-GitLab CI/CD provides. In the last step, kaniko uses the `Dockerfile` under the
+GitLab CI/CD provides.
+
+In the last step, kaniko uses the `Dockerfile` under the
root directory of the project, builds the Docker image and pushes it to the
project's Container Registry while tagging it with the Git tag:
@@ -80,3 +88,15 @@ store:
...
-----END CERTIFICATE-----" >> /kaniko/ssl/certs/ca-certificates.crt
```
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md
index 7aa7de97c43..56200142055 100644
--- a/doc/ci/enable_or_disable_ci.md
+++ b/doc/ci/enable_or_disable_ci.md
@@ -1,25 +1,32 @@
+---
+type: howto
+---
+
# How to enable or disable GitLab CI/CD
-To effectively use GitLab CI/CD, you need a valid [`.gitlab-ci.yml`](yaml/README.md)
-file present at the root directory of your project and a
-[runner](runners/README.md) properly set up. You can read our
-[quick start guide](quick_start/README.md) to get you started.
+To effectively use GitLab CI/CD, you need:
+
+- A valid [`.gitlab-ci.yml`](yaml/README.md) file present at the root directory
+ of your project.
+- A [runner](runners/README.md) properly set up.
+
+You can read our [quick start guide](quick_start/README.md) to get you started.
If you are using an external CI/CD server like Jenkins or Drone CI, it is advised
to disable GitLab CI/CD in order to not have any conflicts with the commits status
API.
----
-
GitLab CI/CD is exposed via the `/pipelines` and `/jobs` pages of a project.
Disabling GitLab CI/CD in a project does not delete any previous jobs.
In fact, the `/pipelines` and `/jobs` pages can still be accessed, although
it's hidden from the left sidebar menu.
-GitLab CI/CD is enabled by default on new installations and can be disabled either
-individually under each project's settings, or site-wide by modifying the
-settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations
-respectively.
+GitLab CI/CD is enabled by default on new installations and can be disabled
+either:
+
+- Individually under each project's settings.
+- Site-wide by modifying the settings in `gitlab.yml` and `gitlab.rb` for source
+ and Omnibus installations respectively.
## Per-project user setting
@@ -36,10 +43,10 @@ and `gitlab.rb` for source and Omnibus installations respectively.
Two things to note:
-1. Disabling GitLab CI/CD, will affect only newly-created projects. Projects that
- had it enabled prior to this modification, will work as before.
-1. Even if you disable GitLab CI/CD, users will still be able to enable it in the
- project's settings.
+- Disabling GitLab CI/CD, will affect only newly-created projects. Projects that
+ had it enabled prior to this modification, will work as before.
+- Even if you disable GitLab CI/CD, users will still be able to enable it in the
+ project's settings.
For installations from source, open `gitlab.yml` with your editor and set
`builds` to `false`:
@@ -54,12 +61,32 @@ default_projects_features:
builds: false
```
-Save the file and restart GitLab: `sudo service gitlab restart`.
+Save the file and restart GitLab:
+
+```sh
+sudo service gitlab restart
+```
For Omnibus installations, edit `/etc/gitlab/gitlab.rb` and add the line:
-```
+```ruby
gitlab_rails['gitlab_default_projects_features_builds'] = false
```
-Save the file and reconfigure GitLab: `sudo gitlab-ctl reconfigure`.
+Save the file and reconfigure GitLab:
+
+```sh
+sudo gitlab-ctl reconfigure
+```
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 5a14ac17aec..f2661c4bafd 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Environments and deployments
> Introduced in GitLab 8.9.
@@ -88,7 +92,7 @@ deploy_staging:
- master
```
-We have defined 3 [stages](yaml/README.md#stages):
+We have defined three [stages](yaml/README.md#stages):
- `test`
- `build`
@@ -131,7 +135,7 @@ In summary, with the above `.gitlab-ci.yml` we have achieved the following:
> the name given in `.gitlab-ci.yml` (with any variables expanded), while the
> second is a "cleaned-up" version of the name, suitable for use in URLs, DNS,
> etc.
-
+>
> Starting with GitLab 9.3, the environment URL is exposed to the Runner via
> `$CI_ENVIRONMENT_URL`. The URL is expanded from `.gitlab-ci.yml`, or if
> the URL was not defined there, the external URL from the environment is used.
@@ -667,9 +671,24 @@ fetch = +refs/environments/*:refs/remotes/origin/environments/*
### Scoping environments with specs **[PREMIUM]**
-Some GitLab [Enterprise Edition](https://about.gitlab.com/pricing/) features can
-behave differently for each environment. For example, you can
-[create a secret variable to be injected only into a production environment](variables/README.md#limiting-environment-scopes-of-environment-variables-premium).
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2112) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.4.
+
+You can limit the environment scope of a variable by
+defining which environments it can be available for.
+
+Wildcards can be used, and the default environment scope is `*`, which means
+any jobs will have this variable, not matter if an environment is defined or
+not.
+
+For example, if the environment scope is `production`, then only the jobs
+having the environment `production` defined would have this specific variable.
+Wildcards (`*`) can be used along with the environment name, therefore if the
+environment scope is `review/*` then any jobs with environment names starting
+with `review/` would have that particular variable.
+
+Some GitLab features can behave differently for each environment.
+For example, you can
+[create a secret variable to be injected only into a production environment](variables/README.md#limiting-environment-scopes-of-environment-variables-premium). **[PREMIUM]**
In most cases, these features use the _environment specs_ mechanism, which offers
an efficient way to implement scoping within each environment group.
@@ -693,7 +712,7 @@ Each environment can be matched with the following environment spec:
As you can see, you can use specific matching for selecting a particular environment,
and also use wildcard matching (`*`) for selecting a particular environment group,
-such as [Review apps](review_apps/index.md) (`review/*`).
+such as [Review Apps](review_apps/index.md) (`review/*`).
NOTE: **Note:**
The most _specific_ spec takes precedence over the other wildcard matching.
@@ -712,3 +731,15 @@ Below are some links you may find interesting:
- [A blog post on Deployments & Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
- [Review Apps - Use dynamic environments to deploy your code for every branch](review_apps/index.md)
- [Deploy Boards for your applications running on Kubernetes](https://docs.gitlab.com/ee/user/project/deploy_boards.html) **[PREMIUM]**
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/ci/environments/protected_environments.md b/doc/ci/environments/protected_environments.md
index ab5c0e2dbad..b72ebe838b8 100644
--- a/doc/ci/environments/protected_environments.md
+++ b/doc/ci/environments/protected_environments.md
@@ -1,3 +1,7 @@
+---
+type: concepts, howto
+---
+
# Protected Environments **[PREMIUM]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6303) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.3.
@@ -47,3 +51,15 @@ Maintainers can:
- Update existing protected environments at any time by changing the access in the
**Allowed to Deploy** dropdown menu.
- Unprotect a protected environment by clicking the **Unprotect** button for that environment.
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/ci/git_submodules.md b/doc/ci/git_submodules.md
index 37078230b34..551044dd76f 100644
--- a/doc/ci/git_submodules.md
+++ b/doc/ci/git_submodules.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Using Git submodules with GitLab CI
> **Notes:**
diff --git a/doc/ci/interactive_web_terminal/index.md b/doc/ci/interactive_web_terminal/index.md
index 7109b2ec583..1387d4df500 100644
--- a/doc/ci/interactive_web_terminal/index.md
+++ b/doc/ci/interactive_web_terminal/index.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Interactive Web Terminals
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/50144) in GitLab 11.3.
diff --git a/doc/ci/introduction/index.md b/doc/ci/introduction/index.md
index 0045fa2fb1f..bd2b9b099f2 100644
--- a/doc/ci/introduction/index.md
+++ b/doc/ci/introduction/index.md
@@ -1,5 +1,6 @@
---
description: "An overview of Continuous Integration, Continuous Delivery, and Continuous Deployment, as well as an introduction to GitLab CI/CD."
+type: concepts
---
# Introduction to CI/CD with GitLab
diff --git a/doc/ci/junit_test_reports.md b/doc/ci/junit_test_reports.md
index 799217c9a08..fa78f53f563 100644
--- a/doc/ci/junit_test_reports.md
+++ b/doc/ci/junit_test_reports.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# JUnit test reports
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/45318) in GitLab 11.2.
diff --git a/doc/ci/large_repositories/index.md b/doc/ci/large_repositories/index.md
index 244ccbb92b0..29d649ad717 100644
--- a/doc/ci/large_repositories/index.md
+++ b/doc/ci/large_repositories/index.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Optimizing GitLab for large repositories
Large repositories consisting of more than 50k files in a worktree
diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md
index b3ff55daea2..fe2fc790505 100644
--- a/doc/ci/merge_request_pipelines/index.md
+++ b/doc/ci/merge_request_pipelines/index.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Pipelines for merge requests
NOTE: **Note**:
diff --git a/doc/ci/metrics_reports.md b/doc/ci/metrics_reports.md
index 83a7094faaa..b7824402d45 100644
--- a/doc/ci/metrics_reports.md
+++ b/doc/ci/metrics_reports.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Metrics Reports **[PREMIUM]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9788) in [GitLab Premium](https://about.gitlab.com/pricing) 11.10.
diff --git a/doc/ci/multi_project_pipelines.md b/doc/ci/multi_project_pipelines.md
index e9deabf27f8..50c8d82602b 100644
--- a/doc/ci/multi_project_pipelines.md
+++ b/doc/ci/multi_project_pipelines.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Multi-project pipelines **[PREMIUM]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/2121) in
@@ -22,6 +26,10 @@ and when hovering or tapping (on touchscreen devices) they will expand and be sh
Multi-project pipelines are useful for larger products that require cross-project inter-dependencies, such as those
adopting a [microservices architecture](https://about.gitlab.com/2016/08/16/trends-in-version-control-land-microservices/).
+For a demonstration of how cross-functional development teams can use cross-pipeline
+triggering to trigger multiple pipelines for different microservices projects, see
+[Cross-project Pipeline Triggering and Visualization](https://about.gitlab.com/handbook/marketing/product-marketing/demo/#cross-project-pipeline-triggering-and-visualization-may-2019---1110).
+
## Use cases
Let's assume you deploy your web app from different projects in GitLab:
@@ -134,14 +142,15 @@ staging:
The `ENVIRONMENT` variable will be passed to every job defined in a downstream
pipeline. It will be available as an environment variable when GitLab Runner picks a job.
-In the following configuration, the `MY_VARIABLE` variable will be passed
-downstream, because jobs inherit variables declared in top-level `variables`:
+In the following configuration, the `MY_VARIABLE` variable will be passed to the downstream pipeline
+that is created when the `trigger-downstream` job is queued. This is because `trigger-downstream`
+job inherits variables declared in global variables blocks, and then we pass these variables to a downstream pipeline.
```yaml
variables:
MY_VARIABLE: my-value
-my-pipeline:
+trigger-downstream:
variables:
ENVIRONMENT: something
trigger: my/project
@@ -152,14 +161,14 @@ example, predefined variables. In order to do that, you can use interpolation
to pass any variable. For example:
```yaml
-my-pipeline:
+downstream-job:
variables:
UPSTREAM_BRANCH: $CI_COMMIT_REF_NAME
trigger: my/project
```
In this scenario, the `UPSTREAM_BRANCH` variable with a value related to the
-upstream pipeline will be passed to a `downstream` job, and will be available
+upstream pipeline will be passed to the `downstream-job` job, and will be available
within the context of all downstream builds.
### Limitations
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 4dbe1a85588..1ad3d516df9 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Creating and using CI/CD pipelines
> Introduced in GitLab 8.8.
@@ -266,9 +270,6 @@ Clicking on an individual job will show you its job trace, and allow you to:
- Retry the job.
- Erase the job trace.
-NOTE: **Note:**
-To prevent jobs from being bypassed or run out of order, canceled jobs can only be retried when the whole pipeline they belong to is retried.
-
### Seeing the failure reason for jobs
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17782) in GitLab 10.7.
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 015f1c0dc0f..11bcfd5dc2c 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -1,9 +1,18 @@
+---
+type: reference
+---
+
# Getting started with GitLab CI/CD
->**Note:** Starting from version 8.0, GitLab [Continuous Integration][ci] (CI)
+NOTE: **Note:**
+Starting from version 8.0, GitLab [Continuous Integration][ci] (CI)
is fully integrated into GitLab itself and is [enabled] by default on all
projects.
+NOTE: **Note:**
+Please keep in mind that only project Maintainers and Admin users have
+the permissions to access a project's settings.
+
GitLab offers a [continuous integration][ci] service. If you
[add a `.gitlab-ci.yml` file][yaml] to the root directory of your repository,
and configure your GitLab project to use a [Runner], then each commit or
@@ -35,11 +44,12 @@ project's **Pipelines** page.
---
-This guide assumes that you:
+This guide assumes that you have:
-- have a working GitLab instance of version 8.0+r or are using
- [GitLab.com](https://gitlab.com)
-- have a project in GitLab that you would like to use CI for
+- A working GitLab instance of version 8.0+r or are using
+ [GitLab.com](https://gitlab.com).
+- A project in GitLab that you would like to use CI for.
+- Maintainer or owner access to the project
Let's break it down to pieces and work on solving the GitLab CI puzzle.
@@ -73,6 +83,8 @@ You need to create a file named `.gitlab-ci.yml` in the root directory of your
repository. Below is an example for a Ruby on Rails project.
```yaml
+image: "ruby:2.5"
+
before_script:
- apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs
- ruby -v
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
index 1a71c5fd258..7b039fe6654 100644
--- a/doc/ci/review_apps/index.md
+++ b/doc/ci/review_apps/index.md
@@ -1,9 +1,13 @@
+---
+type: reference
+---
+
# Review Apps
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/21971) in GitLab 8.12. Further additions were made in GitLab 8.13 and 8.14.
> - Inspired by [Heroku's Review Apps](https://devcenter.heroku.com/articles/github-integration-review-apps), which itself was inspired by [Fourchette](https://github.com/rainforestapp/fourchette).
-Review Apps are a collaboration tool that takes the hard work out of providing an environment to showcase product changes.
+Review Apps is a collaboration tool that takes the hard work out of providing an environment to showcase product changes.
## Introduction
@@ -18,7 +22,7 @@ Review Apps:
In the above example:
-- A Review App is built every time a commit is pushed to`topic branch`.
+- A Review App is built every time a commit is pushed to `topic branch`.
- The reviewer fails two reviews before passing the third review.
- Once the review as passed, `topic branch` is merged into `master` where it's deploy to staging.
- After been approved in staging, the changes that were merged into `master` are deployed in to production.
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index ce55b231666..b089229ab58 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Configuring GitLab Runners
In GitLab CI, Runners run the code defined in [`.gitlab-ci.yml`](../yaml/README.md).
diff --git a/doc/ci/services/README.md b/doc/ci/services/README.md
index 2eda5d23976..7fe12eb53e7 100644
--- a/doc/ci/services/README.md
+++ b/doc/ci/services/README.md
@@ -1,13 +1,18 @@
---
comments: false
+type: index
---
-# GitLab CI Services
+# GitLab CI services examples
-GitLab CI uses the `services` keyword to define what docker containers should
-be linked with your base image. Below is a list of examples you may use.
+The [`services`](../docker/using_docker_images.md#what-is-a-service)
+keyword defines a Docker image that runs during a `job` linked to the
+Docker image that the image keyword defines. This allows you to access
+the service image during build time.
+
+The service image can run any application, but the most common use
+case is to run a database container, for example:
- [Using MySQL](mysql.md)
- [Using PostgreSQL](postgres.md)
- [Using Redis](redis.md)
-- [Using Other Services](../docker/using_docker_images.md#what-is-a-service)
diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md
index 5fa378fc4c2..697452cee83 100644
--- a/doc/ci/services/mysql.md
+++ b/doc/ci/services/mysql.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Using MySQL
As many applications depend on MySQL as their database, you will eventually
diff --git a/doc/ci/services/postgres.md b/doc/ci/services/postgres.md
index 2e6d7ae94d2..211eea26eb0 100644
--- a/doc/ci/services/postgres.md
+++ b/doc/ci/services/postgres.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Using PostgreSQL
As many applications depend on PostgreSQL as their database, you will
diff --git a/doc/ci/services/redis.md b/doc/ci/services/redis.md
index 36f71427ae7..8b227154b06 100644
--- a/doc/ci/services/redis.md
+++ b/doc/ci/services/redis.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Using Redis
As many applications depend on Redis as their key-value store, you will
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index 9ed1ec5aa5c..69591ed605c 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -1,5 +1,6 @@
---
last_updated: 2017-12-13
+type: tutorial
---
# Using SSH keys with GitLab CI/CD
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index ad80b5d8818..04c541fefe7 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -1,3 +1,7 @@
+---
+type: tutorial
+---
+
# Triggering pipelines through the API
> **Notes**:
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index d33275cae4f..df455857dee 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -1,5 +1,6 @@
---
table_display_block: true
+type: reference
---
# GitLab CI/CD environment variables
@@ -86,15 +87,14 @@ Variable types can be set via the [UI](#via-the-ui) or the [API](../../api/proje
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/13784) in GitLab 11.10
-By default, variables will be created as masked variables.
+Variables can be created as masked variables.
This means that the value of the variable will be hidden in job logs,
though it must match certain requirements to do so:
- The value must be in a single line.
-- The value must not have escape characters.
-- The value must not use variables.
-- The value must not have any whitespace.
+- The value must only consist of characters from the Base64 alphabet, defined in [RFC4648](https://tools.ietf.org/html/rfc4648).
- The value must be at least 8 characters long.
+- The value must not use variables.
If the value does not meet the requirements above, then the CI variable will fail to save.
In order to save, either alter the value to meet the masking requirements
@@ -389,21 +389,9 @@ Once you set them, they will be available for all subsequent pipelines.
### Limiting environment scopes of environment variables **[PREMIUM]**
-> [Introduced][ee-2112] in [GitLab Premium](https://about.gitlab.com/pricing/) 9.4.
-
You can limit the environment scope of a variable by
[defining which environments][envs] it can be available for.
-Wildcards can be used, and the default environment scope is `*` which means
-any jobs will have this variable, not matter if an environment is defined or
-not.
-
-For example, if the environment scope is `production`, then only the jobs
-having the environment `production` defined would have this specific variable.
-Wildcards (`*`) can be used along with the environment name, therefore if the
-environment scope is `review/*` then any jobs with environment names starting
-with `review/` would have that particular variable.
-
To learn more about about scoping environments, see [Scoping environments with specs](../environments.md#scoping-environments-with-specs-premium).
### Deployment environment variables
@@ -491,6 +479,7 @@ Below you can find supported syntax reference:
1. Equality matching using a string
> Example: `$VARIABLE == "some value"`
+
> Example: `$VARIABLE != "some value"` _(added in 11.11)_
You can use equality operator `==` or `!=` to compare a variable content to a
@@ -501,6 +490,7 @@ Below you can find supported syntax reference:
1. Checking for an undefined value
> Example: `$VARIABLE == null`
+
> Example: `$VARIABLE != null` _(added in 11.11)_
It sometimes happens that you want to check whether a variable is defined
@@ -511,6 +501,7 @@ Below you can find supported syntax reference:
1. Checking for an empty variable
> Example: `$VARIABLE == ""`
+
> Example: `$VARIABLE != ""` _(added in 11.11)_
If you want to check whether a variable is defined, but is empty, you can
@@ -520,6 +511,7 @@ Below you can find supported syntax reference:
1. Comparing two variables
> Example: `$VARIABLE_1 == $VARIABLE_2`
+
> Example: `$VARIABLE_1 != $VARIABLE_2` _(added in 11.11)_
It is possible to compare two variables. This is going to compare values
@@ -539,6 +531,7 @@ Below you can find supported syntax reference:
1. Pattern matching _(added in 11.0)_
> Example: `$VARIABLE =~ /^content.*/`
+
> Example: `$VARIABLE_1 !~ /^content.*/` _(added in 11.11)_
It is possible perform pattern matching against a variable and regular
@@ -548,6 +541,19 @@ Below you can find supported syntax reference:
Pattern matching is case-sensitive by default. Use `i` flag modifier, like
`/pattern/i` to make a pattern case-insensitive.
+1. Conjunction / Disjunction
+
+ > Example: `$VARIABLE1 =~ /^content.*/ && $VARIABLE2 == "something"`
+
+ > Example: `$VARIABLE1 =~ /^content.*/ && $VARIABLE2 =~ /thing$/ && $VARIABLE3`
+
+ > Example: `$VARIABLE1 =~ /^content.*/ || $VARIABLE2 =~ /thing$/ && $VARIABLE3`
+
+ It is possible to join multiple conditions using `&&` or `||`. Any of the otherwise
+ supported syntax may be used in a conjunctive or disjunctive statement.
+ Precedence of operators follows standard Ruby 2.5 operation
+ [precedence](https://ruby-doc.org/core-2.5.0/doc/syntax/precedence_rdoc.html).
+
## Debug tracing
> Introduced in GitLab Runner 1.7.
@@ -717,7 +723,6 @@ MIIFQzCCBCugAwIBAgIRAL/ElDjuf15xwja1ZnCocWAwDQYJKoZIhvcNAQELBQAw'
...
```
-[ee-2112]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2112
[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI variables"
[envs]: ../environments.md
[protected branches]: ../../user/project/protected_branches.md
diff --git a/doc/ci/variables/deprecated_variables.md b/doc/ci/variables/deprecated_variables.md
index 2642c9b0eb4..cdca5bf27fc 100644
--- a/doc/ci/variables/deprecated_variables.md
+++ b/doc/ci/variables/deprecated_variables.md
@@ -1,5 +1,12 @@
+---
+type: reference
+---
+
# Deprecated GitLab CI/CD variables
+Read through this document to learn what predefined variables
+were deprecated and their new references.
+
## GitLab 9.0 renamed variables
To follow conventions of naming across GitLab, and to further move away from the
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index 4e902c042e6..4655eec51de 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Predefined environment variables reference
For an introduction on this subject, read through the
diff --git a/doc/ci/variables/where_variables_can_be_used.md b/doc/ci/variables/where_variables_can_be_used.md
index 0470cf52654..8009b1d5e8a 100644
--- a/doc/ci/variables/where_variables_can_be_used.md
+++ b/doc/ci/variables/where_variables_can_be_used.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Where variables can be used
As it's described in the [CI/CD variables](README.md) docs, you can
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 18c85618b1b..fa4b0378f61 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# GitLab CI/CD Pipeline Configuration Reference
GitLab CI/CD [pipelines](../pipelines.md) are configured using a YAML file called `.gitlab-ci.yml` within each project.
@@ -108,7 +112,7 @@ The following table lists available parameters for jobs:
| [`parallel`](#parallel) | How many instances of a job should be run in parallel. |
| [`trigger`](#trigger-premium) | Defines a downstream pipeline trigger. |
| [`include`](#include) | Allows this job to include external YAML files. Also available: `include:local`, `include:file`, `include:template`, and `include:remote`. |
-| [`extends`](#extends) | Configuration entry that this job is going to inherit from. |
+| [`extends`](#extends) | Configuration entries that this job is going to inherit from. |
| [`pages`](#pages) | Upload the result of a job to use with GitLab Pages. |
| [`variables`](#variables) | Define job variables on a job level. |
@@ -1185,9 +1189,9 @@ skip the download step.
> - 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.
+attached to the job when it [succeeds, fails, or always](#artifactswhen).
-The artifacts will be sent to GitLab after the job finishes successfully and will
+The artifacts will be sent to GitLab after the job finishes and will
be available for download in the GitLab UI.
[Read more about artifacts](../../user/project/pipelines/job_artifacts.md).
@@ -2117,7 +2121,7 @@ docker-test:
> Introduced in GitLab 11.3.
-`extends` defines an entry name that a job that uses `extends` is going to
+`extends` defines entry names that a job that uses `extends` is going to
inherit from.
It is an alternative to using [YAML anchors](#anchors) and is a little
@@ -2194,6 +2198,46 @@ spinach:
script: rake spinach
```
+It's also possible to use multiple parents for `extends`.
+The algorithm used for merge is "closest scope wins", so keys
+from the last member will always shadow anything defined on other levels.
+For example:
+
+```yaml
+.only-important:
+ only:
+ - master
+ - stable
+ tags:
+ - production
+
+.in-docker:
+ tags:
+ - docker
+ image: alpine
+
+rspec:
+ extends:
+ - .only-important
+ - .in-docker
+ script:
+ - rake rspec
+```
+
+This results in the following `rspec` job:
+
+```yaml
+rspec:
+ only:
+ - master
+ - stable
+ tags:
+ - docker
+ image: alpine
+ script:
+ - rake rspec
+```
+
### Using `extends` and `include` together
`extends` works across configuration files combined with `include`.
@@ -2747,6 +2791,18 @@ using Git 2.10 or newer:
git push -o ci.skip
```
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
+
[ce-6323]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6323
[ce-6669]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6669
[ce-7983]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7983
diff --git a/doc/customization/issue_and_merge_request_template.md b/doc/customization/issue_and_merge_request_template.md
index 01c31728c21..adaa120a37e 100644
--- a/doc/customization/issue_and_merge_request_template.md
+++ b/doc/customization/issue_and_merge_request_template.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'https://docs.gitlab.com/ee/user/project/description_templates.html#setting-a-default-template-for-issues-and-merge-requests--starter'
+redirect_to: '../user/project/description_templates.md#setting-a-default-template-for-issues-and-merge-requests--starter'
---
-This document was moved to [description_templates](https://docs.gitlab.com/ee/user/project/description_templates.html#setting-a-default-template-for-issues-and-merge-requests--starter).
+This document was moved to [description_templates](../user/project/description_templates.md#setting-a-default-template-for-issues-and-merge-requests--starter).
diff --git a/doc/development/README.md b/doc/development/README.md
index 624665a42d1..d2f09fc01de 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -20,6 +20,7 @@ description: 'Learn how to contribute to GitLab.'
- [Automatic CE->EE merge](automatic_ce_ee_merge.md)
- [Guidelines for implementing Enterprise Edition features](ee_features.md)
- [Security process for developers](https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#security-releases-critical-non-critical-as-a-developer)
+- [Requesting access to Chatops on GitLab.com](chatops_on_gitlabcom.md#requesting-access) (for GitLabbers)
## UX and frontend guides
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index 8d2bfff3a5d..38270af682e 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -32,6 +32,21 @@ a new presenter specifically for GraphQL.
The presenter is initialized using the object resolved by a field, and
the context.
+### Exposing Global ids
+
+When exposing an `id` field on a type, we will by default try to
+expose a global id by calling `to_global_id` on the resource being
+rendered.
+
+To override this behaviour, you can implement an `id` method on the
+type for which you are exposing an id. Please make sure that when
+exposing a `GraphQL::ID_TYPE` using a custom method that it is
+globally unique.
+
+The records that are exposing a `full_path` as an `ID_TYPE` are one of
+these exceptions. Since the full path is a unique identifier for a
+`Project` or `Namespace`.
+
### Connection Types
GraphQL uses [cursor based
@@ -79,14 +94,14 @@ look like this:
{
"cursor": "Nzc=",
"node": {
- "id": "77",
+ "id": "gid://gitlab/Pipeline/77",
"status": "FAILED"
}
},
{
"cursor": "Njc=",
"node": {
- "id": "67",
+ "id": "gid://gitlab/Pipeline/67",
"status": "FAILED"
}
}
@@ -330,7 +345,7 @@ argument :project_path, GraphQL::ID_TYPE,
required: true,
description: "The project the merge request to mutate is in"
-argument :iid, GraphQL::ID_TYPE,
+argument :iid, GraphQL::STRING_TYPE,
required: true,
description: "The iid of the merge request to mutate"
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index 4b76d5f9c5b..650325121b2 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -59,7 +59,7 @@ graph TB
PgBouncerExporter[PgBouncer Exporter] --> PgBouncer
Prometheus -- TCP 9187 --> PostgreSQLExporter
Prometheus -- TCP 9100 --> NodeExporter[Node Exporter]
- Prometheus -- TCP 9168 --> GitLabMonito[GitLab Monitor]
+ Prometheus -- TCP 9168 --> GitLabMonitor[GitLab Monitor]
Prometheus -- TCP 9127 --> PgBouncerExporter
GitLabMonitor --> PostgreSQL
GitLabMonitor --> GitLabShell
@@ -142,7 +142,7 @@ Component statuses are linked to configuration documentation for each component.
| [ElasticSearch](#elasticsearch) | Improved search within GitLab | [⤓][elasticsearch-omnibus] | [⤓][elasticsearch-charts] | [⤓][elasticsearch-charts] | [❌](https://gitlab.com/groups/gitlab-org/-/epics/153) | [⤓][elasticsearch-source] | [⤓][elasticsearch-gdk] | EE Only |
| [Sentry integration](#sentry) | Error tracking for deployed apps | [⤓][sentry-integration] | [⤓][sentry-integration] | [⤓][sentry-integration] | [⤓][sentry-integration] | [⤓][sentry-integration] | [⤓][sentry-integration] | CE & EE |
| [Jaeger integration](#jaeger) | Distributed tracing for deployed apps | [⤓][jaeger-integration] | [⤓][jaeger-integration] | [⤓][jaeger-integration] | [⤓][jaeger-integration] | [⤓][jaeger-integration] | [⤓][jaeger-integration] | EE Only |
-| [Kubernetes cluster apps](#kubernetes-cluster-apps) | Deploy [Helm](https://docs.helm.sh/), [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/), [Cert-Manager](https://docs.cert-manager.io/en/latest/), [Prometheus](https://prometheus.io/docs/introduction/overview/), a [Runner](https://docs.gitlab.com/runner/), [JupyterHub](http://jupyter.org/), [Knative](https://cloud.google.com/knative) to a cluster | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | CE & EE |
+| [GitLab Managed Apps](#gitlab-managed-apps) | Deploy [Helm](https://docs.helm.sh/), [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/), [Cert-Manager](https://docs.cert-manager.io/en/latest/), [Prometheus](https://prometheus.io/docs/introduction/overview/), a [Runner](https://docs.gitlab.com/runner/), [JupyterHub](http://jupyter.org/), [Knative](https://cloud.google.com/knative) to a cluster | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | CE & EE |
### Component details
@@ -435,7 +435,7 @@ Sidekiq is a Ruby background job processor that pulls jobs from the redis queue
- Configuration: [Omnibus][inbound-email-omnibus], [Charts][inbound-email-charts], [Source][gitlab-yml], [GDK][gitlab-yml]
- Layer: Core Service (Processor)
-#### Kubernetes Cluster Apps
+#### GitLab Managed Apps
- Configuration: [Omnibus][managed-k8s-apps], [Charts][managed-k8s-apps], [Source][managed-k8s-apps], [GDK][managed-k8s-apps]
- Layer: Core Service (Processor)
@@ -676,7 +676,7 @@ We've also detailed [our architecture of GitLab.com](https://about.gitlab.com/ha
[runner-gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/runner.md
[database-migrations-omnibus]: https://docs.gitlab.com/omnibus/settings/database.html#disabling-automatic-database-migration
[database-migrations-charts]: https://docs.gitlab.com/charts/charts/gitlab/migrations/
-[database-migrations-source]: ../update/upgrading_from_source.md#13-install-libs-migrations-etc
+[database-migrations-source]: ../update/upgrading_from_source.md#14-install-libs-migrations-etc
[certificate-management-omnibus]: https://docs.gitlab.com/omnibus/settings/ssl.html
[certificate-management-charts]: https://docs.gitlab.com/charts/installation/tls.html
[certificate-management-source]: ../install/installation.md#using-https
diff --git a/doc/development/chatops_on_gitlabcom.md b/doc/development/chatops_on_gitlabcom.md
new file mode 100644
index 00000000000..c63ec53414c
--- /dev/null
+++ b/doc/development/chatops_on_gitlabcom.md
@@ -0,0 +1,21 @@
+# Chatops on GitLab.com
+
+Chatops on GitLab.com allows GitLabbers to run various automation tasks on GitLab.com using Slack.
+
+## Requesting access
+
+GitLabbers may need access to Chatops on GitLab.com for administration tasks such as:
+
+- Configuring feature flags on staging.
+- Running `EXPLAIN` queries against the GitLab.com production replica.
+
+To request access to Chatops on GitLab.com:
+
+1. Log into <https://ops.gitlab.net/users/sign_in> using the same username as for GitLab.com.
+1. Ask [anyone in the `chatops` project](https://gitlab.com/gitlab-com/chatops/project_members) to add you by running `/chatops run member add <username> gitlab-com/chatops --ops`.
+
+## See also
+
+ - [Chatops Usage](https://docs.gitlab.com/ee/ci/chatops/README.html)
+ - [Understanding EXPLAIN plans](understanding_explain_plans.md)
+ - [Feature Groups](feature_flags.md#feature-groups)
diff --git a/doc/development/database_debugging.md b/doc/development/database_debugging.md
index f00c5ccb9e9..68d33a9d8e0 100644
--- a/doc/development/database_debugging.md
+++ b/doc/development/database_debugging.md
@@ -56,7 +56,7 @@ bundle exec rails db RAILS_ENV=development
### `ActiveRecord::PendingMigrationError` with Spring
-When running specs with the [Spring preloader](./rake_tasks.md#speed-up-tests-rake-tasks-and-migrations),
+When running specs with the [Spring preloader](rake_tasks.md#speed-up-tests-rake-tasks-and-migrations),
the test database can get into a corrupted state. Trying to run the migration or
dropping/resetting the test database has no effect.
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index 6dfd3b2a690..c7fa40af930 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -74,9 +74,14 @@ Here are some links to get you up to speed with the current effort:
After a given documentation path is aligned across CE and EE, all merge requests
affecting that path must be submitted to CE, regardless of the content it has.
-This means that for EE-only features which are being added only to the EE codebase,
-you have to submit a separate merge request in CE that contains the docs.
-This might seem like a duplicate effort, but it's for the short term.
+This means that:
+
+* For **EE-only docs changes**, you only have to submit a CE MR.
+* For **EE-only features** that touch both the code and the docs, you have to submit
+an EE MR containing all changes, and a CE MR containing only the docs changes
+and without a changelog entry.
+
+This might seem like a duplicate effort, but it's only for the short term.
A list of the already aligned docs can be found in
[the epic description](https://gitlab.com/groups/gitlab-org/-/epics/199#ee-specific-lines-check).
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 857595330aa..cca52706ddc 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -459,15 +459,6 @@ Resolving an EE template path that is relative to the CE view path will not work
= render_if_exists 'projects/button' # Will render `ee/app/views/projects/_button`
```
-You should not explicitly set render options like `partial` or provide a `locals` hash.
-The first argument should be a path string and the second can be a hash replacing `locals`.
-
-```ruby
-render partial: 'projects/button', locals: { project: project }
-# becomes
-render_if_exists 'projects/button', project: project
-```
-
#### Using `render_ce`
For `render` and `render_if_exists`, they search for the EE partial first,
@@ -915,7 +906,7 @@ import bundle from 'ee/protected_branches/protected_branches_bundle.js';
import bundle from 'ee_else_ce/protected_branches/protected_branches_bundle.js';
```
-See the frontend guide [performance section](./fe_guide/performance.md) for
+See the frontend guide [performance section](fe_guide/performance.md) for
information on managing page-specific javascript within EE.
diff --git a/doc/development/fe_guide/frontend_faq.md b/doc/development/fe_guide/frontend_faq.md
index 77f064a21a9..e4225f2bc39 100644
--- a/doc/development/fe_guide/frontend_faq.md
+++ b/doc/development/fe_guide/frontend_faq.md
@@ -25,3 +25,17 @@ document.body.dataset.page
```
Find here the [source code setting the attribute](https://gitlab.com/gitlab-org/gitlab-ce/blob/cc5095edfce2b4d4083a4fb1cdc7c0a1898b9921/app/views/layouts/application.html.haml#L4).
+
+### `modal_copy_button` vs `clipboard_button`
+
+The `clipboard_button` uses the `copy_to_clipboard.js` behaviour, which is
+initialized on page load, so if there are vue-based clipboard buttons that
+don't exist at page load (such as ones in a `GlModal`), they do not have the
+click handlers associated with the clipboard package.
+
+`modal_copy_button` was added that manages an instance of the
+[`clipboard` plugin](https://www.npmjs.com/package/clipboard) specific to
+the instance of that component, which means that clipboard events are
+bound on mounting and destroyed when the button is, mitigating the above
+issue. It also has bindings to a particular container or modal ID
+available, to work with the focus trap created by our GlModal.
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index 94dfdbdd073..b50159c2b75 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -95,6 +95,7 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/
#### Modules, Imports, and Exports
1. Use ES module syntax to import modules
+
```javascript
// bad
const SomeClass = require('some_class');
@@ -168,6 +169,7 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/
Do not use them anymore and feel free to remove them when refactoring legacy code.
1. Avoid adding to the global namespace.
+
```javascript
// bad
window.MyClass = class { /* ... */ };
@@ -176,7 +178,8 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/
export default class MyClass { /* ... */ }
```
-1. Side effects are forbidden in any script which contains exports
+1. Side effects are forbidden in any script which contains export
+
```javascript
// bad
export default class MyClass { /* ... */ }
@@ -449,6 +452,7 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
#### Props
1. Props should be declared as an object
+
```javascript
// bad
props: ['foo']
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index 437ce9abc7d..8c6a73c6824 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -234,7 +234,7 @@ One should apply to be a Vue.js expert by opening an MR when the Merge Request's
- Deep understanding of Vue and Vuex reactivy
- Vue and Vuex code are structured according to both official and our guidelines
- Full understanding of testing a Vue and Vuex application
-- Vuex code follows the [documented pattern](./vuex.md#actions-pattern-request-and-receive-namespaces)
+- Vuex code follows the [documented pattern](vuex.md#actions-pattern-request-and-receive-namespaces)
- Knowledge about the existing Vue and Vuex applications and existing reusable components
[vue-docs]: http://vuejs.org/guide/index.html
diff --git a/doc/development/feature_flags.md b/doc/development/feature_flags.md
index c871015aaf6..13f0c5cc33e 100644
--- a/doc/development/feature_flags.md
+++ b/doc/development/feature_flags.md
@@ -20,7 +20,7 @@ dynamic (querying the DB etc.).
Once defined in `lib/feature.rb`, you will be able to activate a
feature for a given feature group via the [`feature_group` param of the features API](../api/features.md#set-or-create-a-feature)
-For GitLab.com, team members have access to feature flags through chatops. Only
+For GitLab.com, [team members have access to feature flags through Chatops](chatops_on_gitlabcom.md). Only
percentage gates are supported at this time. Setting a feature to be used 50% of
the time, you should execute `/chatops run feature set my_feature_flag 50`.
diff --git a/doc/development/geo.md b/doc/development/geo.md
index 87ec34ec5c4..6e59fab34c7 100644
--- a/doc/development/geo.md
+++ b/doc/development/geo.md
@@ -10,6 +10,7 @@ the diagram below and are described in more detail within this document.
## Replication layer
Geo handles replication for different components:
+
- [Database](#database-replication): includes the entire application, except cache and jobs.
- [Git repositories](#repository-replication): includes both projects and wikis.
- [Uploaded blobs](#uploads-replication): includes anything from images attached on issues
@@ -209,20 +210,38 @@ bundle exec rake geo:db:migrate
### Foreign Data Wrapper
-The use of [FDW](#fdw) was introduced in GitLab 10.1.
+> Introduced in GitLab 10.1.
+
+Foreign Data Wrapper ([FDW](#fdw)) is used by the [Geo Log Cursor](#geo-log-cursor) and improves
+the performance of many synchronization operations.
-This is useful for the [Geo Log Cursor](#geo-log-cursor) and improves
-the performance of some synchronization operations.
+FDW is a PostgreSQL extension ([`postgres_fdw`](https://www.postgresql.org/docs/current/postgres-fdw.html)) that is enabled within
+the Geo Tracking Database (on a **secondary** node), which allows it
+to connect to the readonly database replica and perform queries and filter
+data from both instances.
While FDW is available in older versions of PostgreSQL, we needed to
raise the minimum required version to 9.6 as this includes many
performance improvements to the FDW implementation.
+This persistent connection is configured as an FDW server
+named `gitlab_secondary`. This configuration exists within the database's user
+context only. To access the `gitlab_secondary`, GitLab needs to use the
+same database user that had previously been configured.
+
+The Geo Tracking Database accesses the readonly database replica via FDW as a regular user,
+limited by its own restrictions. The credentials are configured as a
+`USER MAPPING` associated with the `SERVER` mapped previously
+(`gitlab_secondary`).
+
+FDW configuration and credentials definition are managed automatically by the
+Omnibus GitLab `gitlab-ctl reconfigure` command.
+
#### Refeshing the Foreign Tables
-Whenever the database schema changes on the **primary** node, the
-**secondary** node will need to refresh its foreign tables by running
-the following:
+Whenever a new Geo node is configured or the database schema changes on the
+**primary** node, you must refresh the foreign tables on the **secondary** node
+by running the following:
```sh
bundle exec rake geo:db:refresh_foreign_tables
@@ -243,6 +262,53 @@ STATEMENT: SELECT a.attname, format_type(a.atttypid, a.atttypmod)
ORDER BY a.attnum
```
+#### Accessing data from a Foreign Table
+
+At the SQL level, all you have to do is `SELECT` data from `gitlab_secondary.*`.
+
+Here's an example of how to access all projects from the Geo Tracking Database's FDW:
+
+```sql
+SELECT * FROM gitlab_secondary.projects;
+```
+
+As a more real-world example, this is how you filter for unarchived projects
+on the Tracking Database:
+
+```sql
+SELECT project_registry.*
+ FROM project_registry
+ JOIN gitlab_secondary.projects
+ ON (project_registry.project_id = gitlab_secondary.projects.id
+ AND gitlab_secondary.projects.archived IS FALSE)
+```
+
+At the ActiveRecord level, we have additional Models that represent the
+foreign tables. They must be mapped in a slightly different way, and they are read-only.
+
+Check the existing FDW models in `ee/app/models/geo/fdw` for reference.
+
+From a developer's perspective, it's no different than creating a model that
+represents a Database View.
+
+With the examples above, you can access the projects with:
+
+```ruby
+Geo::Fdw::Project.all
+```
+
+and to access the `ProjectRegistry` filtering by unarchived projects:
+
+```ruby
+# We have to use Arel here:
+project_registry_table = Geo::ProjectRegistry.arel_table
+fdw_project_table = Geo::Fdw::Project.arel_table
+
+project_registry_table.join(fdw_project_table)
+ .on(project_registry_table[:project_id].eq(fdw_project_table[:id]))
+ .where((fdw_project_table[:archived]).eq(true)) # if you append `.to_sql` you can check generated query
+```
+
## Finders
Geo uses [Finders](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/app/finders),
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index 38425674567..9fb8ea542d9 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -174,7 +174,7 @@ For example use `%{created_at}` in Ruby but `%{createdAt}` in JavaScript.
# => When size == 2: 'There are 2 mice.'
```
- Avoid using `%d` or count variables in sigular strings. This allows more natural translation in some languages.
+ Avoid using `%d` or count variables in singular strings. This allows more natural translation in some languages.
- In JavaScript:
@@ -212,7 +212,7 @@ Namespaces should be PascalCase.
```
Note: The namespace should be removed from the translation. See the [translation
-guidelines for more details](./translation.md#namespaced-strings).
+guidelines for more details](translation.md#namespaced-strings).
### Dates / times
@@ -332,7 +332,7 @@ Errors in `locale/zh_HK/gitlab.po`:
Syntax error in msgstr
Syntax error in message_line
There should be only whitespace until the end of line after the double quote character of a message text.
- Parseing result before error: '{:msgid=>["", "You are going to remove %{project_name_with_namespace}.\\n", "Removed project CANNOT be restored!\\n", "Are you ABSOLUTELY sure?"]}'
+ Parsing result before error: '{:msgid=>["", "You are going to remove %{project_name_with_namespace}.\\n", "Removed project CANNOT be restored!\\n", "Are you ABSOLUTELY sure?"]}'
SimplePoParser filtered backtrace: SimplePoParser::ParserError
Errors in `locale/zh_TW/gitlab.po`:
1 pipeline
diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index fb5cfb6c157..35c5b155594 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -130,7 +130,7 @@ are very appreciative of the work done by translators and proofreaders!
your previous translations by [GitLab team members](https://about.gitlab.com/team/)
or [Core team members](https://about.gitlab.com/core-team/) who are fluent in
the language or current proofreaders.
- - When a request is made for the first proofreader for a lanuguage and there are no [GitLab team members](https://about.gitlab.com/team/)
+ - When a request is made for the first proofreader for a language and there are no [GitLab team members](https://about.gitlab.com/team/)
or [Core team members](https://about.gitlab.com/core-team/) who speak the language, we will request links to previous translation work in other communities or projects.
diff --git a/doc/development/i18n/translation.md b/doc/development/i18n/translation.md
index 99c0fe6db1d..62be3786549 100644
--- a/doc/development/i18n/translation.md
+++ b/doc/development/i18n/translation.md
@@ -89,9 +89,7 @@ To propose additions to the glossary please
### Inclusive language in French
-In French, we should follow the guidelines from [ecriture-inclusive.fr]. For
-instance:
+In French, the "écriture inclusive" is now over (see on [Legifrance](https://www.legifrance.gouv.fr/affichTexte.do?cidTexte=JORFTEXT000036068906&categorieLien=id)).
+So, to include both genders, write “Utilisateurs et utilisatrices” instead of “Utilisateur·rice·s”.
+When space is missing, the male gender should be used alone.
-- Utilisateur•rice•s
-
-[ecriture-inclusive.fr]: http://www.ecriture-inclusive.fr/
diff --git a/doc/development/licensing.md b/doc/development/licensing.md
index 0e71cd47481..0db90d2872f 100644
--- a/doc/development/licensing.md
+++ b/doc/development/licensing.md
@@ -88,9 +88,11 @@ Definitions
GitLab means GitLab Inc. and its affiliates and subsidiaries.
-## Requesting Approval for Licenses
+## Requesting Approval for Licenses or any other Intellectual Property
-Libraries that are not listed in the [Acceptable Licenses][Acceptable-Licenses] or [Unacceptable Licenses][Unacceptable-Licenses] list can be submitted to the legal team for review. Please email `legal@gitlab.com` with the details. After a decision has been made, the original requestor is responsible for updating this document.
+Libraries that are not already approved and listed on the [Acceptable Licenses][Acceptable-Licenses] list or that may be listed on the [Unacceptable Licenses][Unacceptable-Licenses] list may be submitted to the legal team for review and use on a case-by-case basis. Please email `legal@gitlab.com` with the details of how the software will be used, whether or not it will be modified, and how it will be distributed (if at all). After a decision has been made, the original requestor is responsible for updating this document, if applicable. Not all approvals will be approved for universal use and may continue to remain on the Unacceptable License list.
+
+All inquiries relating to patents should be directed to the Legal team.
## Notes
diff --git a/doc/development/rolling_out_changes_using_feature_flags.md b/doc/development/rolling_out_changes_using_feature_flags.md
index 1b4c89ba2a8..84028b1b342 100644
--- a/doc/development/rolling_out_changes_using_feature_flags.md
+++ b/doc/development/rolling_out_changes_using_feature_flags.md
@@ -200,10 +200,9 @@ isn't gated by a License or Plan.
### Undefined feature flags default to "on"
-An important side-effect of the [implicit feature
-flags][#implicit-feature-flags] mentioned above is that unless the feature is
-explicitly disabled or limited to a percentage of users, the feature flag check
-will default to `true`.
+An important side-effect of the [implicit feature flags](#implicit-feature-flags)
+mentioned above is that unless the feature is explicitly disabled or limited to a
+percentage of users, the feature flag check will default to `true`.
As an example, if you were to ship the backend half of a feature behind a flag,
you'd want to explicitly disable that flag until the frontend half is also ready
diff --git a/qa/docs/best_practices.md b/doc/development/testing_guide/end_to_end/best_practices.md
index d6e5350b0c8..89500ef9a90 100644
--- a/qa/docs/best_practices.md
+++ b/doc/development/testing_guide/end_to_end/best_practices.md
@@ -4,7 +4,7 @@ The majority of the end-to-end tests require some state to be built in the appli
A good example is a user being logged in as a pre-condition for testing the feature.
-But if the login feature is already covered with end-to-end tests through the GUI, there is no reason to perform such an expensive task to test the functionality of creating a project, or importing a repo, even if this features depend on a user being logged in. Let's see an example to make things clear.
+But if the login feature is already covered with end-to-end tests through the GUI, there is no reason to perform such an expensive task to test the functionality of creating a project, or importing a repo, even if these features depend on a user being logged in. Let's see an example to make things clear.
Let's say that, on average, the process to perform a successful login through the GUI takes 2 seconds.
@@ -33,6 +33,6 @@ Finally, interacting with the application only by its GUI generates a higher rat
**The takeaways here are:**
- Building state through the GUI is time consuming and it's not sustainable as the test suite grows.
-- When depending only on the GUI to create the application's state and tests fail due to front-end issues, we can't rely on the test failures rate, and we generates a higher rate of test flakiness.
+- When depending only on the GUI to create the application's state and tests fail due to front-end issues, we can't rely on the test failures rate, and we generate a higher rate of test flakiness.
-Now that we are aware of all of it, [let's go create some tests](writing_tests_from_scratch.md).
+Now that we are aware of all of it, [let's go create some tests](quick_start_guide.md).
diff --git a/doc/development/testing_guide/end_to_end/dynamic_element_validation.md b/doc/development/testing_guide/end_to_end/dynamic_element_validation.md
new file mode 100644
index 00000000000..f7b3ca8bc89
--- /dev/null
+++ b/doc/development/testing_guide/end_to_end/dynamic_element_validation.md
@@ -0,0 +1,113 @@
+# Dynamic Element Validation
+
+We devised a solution to solve common test automation problems such as the dreaded `NoSuchElementException`.
+
+Other problems that dynamic element validations solve are...
+
+- When we perform an action with the mouse, we expect something to occur.
+- When our test is navigating to (or from) a page, we ensure that we are on the page we expect before
+test continuation.
+
+## How it works
+
+We interpret user actions on the page to have some sort of effect. These actions are
+
+- [Navigation](#navigation)
+- [Clicks](#clicks)
+
+### Navigation
+
+When a page is navigated to, there are elements that will always appear on the page unconditionally.
+
+Dynamic element validation is instituted when using
+
+```ruby
+Runtime::Browser.visit(:gitlab, Some::Page)
+```
+
+### Clicks
+
+When we perform a click within our tests, we expect something to occur. That something could be a component to now
+appear on the webpage, or the test to navigate away from the page entirely.
+
+Dynamic element validation is instituted when using
+
+```ruby
+click_element :my_element, Some::Page
+```
+
+### Required Elements
+
+#### Definition
+
+First it is important to define what a "required element" is.
+
+Simply put, a required element is a visible HTML element that appears on a UI component without any user input.
+
+"Visible" can be defined as
+
+- Not having any CSS preventing its display. E.g.: `display: none` or `width: 0px; height: 0px;`
+- Being able to be interacted with by the user
+
+"UI component" can be defined as
+
+- Anything the user sees
+- A button, a text field
+- A layer that sits atop the page
+
+#### Application
+
+Requiring elements is very easy. By adding `required: true` as a parameter to an `element`, you've now made it
+a requirement that the element appear on the page upon navigation.
+
+## Examples
+
+Given ...
+
+```ruby
+class MyPage < Page::Base
+ view 'app/views/view.html.haml' do
+ element :my_element, required: true
+ element :another_element, required: true
+ element :conditional_element
+ end
+
+ def open_layer
+ click_element :my_element, Layer::MyLayer
+ end
+end
+
+class Layer < Page::Component
+ view 'app/views/mylayer/layer.html.haml' do
+ element :message_content, required: true
+ end
+end
+```
+
+### Navigating
+
+Given the [source](#examples) ...
+
+```ruby
+Runtime::Browser.visit(:gitlab, Page::MyPage)
+
+execute_stuff
+```
+
+will invoke GitLab QA to scan `MyPage` for `my_element` and `another_element` to be on the page before continuing to
+`execute_stuff`
+
+### Clicking
+
+Given the [source](#examples) ...
+
+```ruby
+def open_layer
+ click_element :my_element, Layer::MyLayer
+end
+```
+
+will invoke GitLab QA to ensure that `message_content` appears on
+the Layer upon clicking `my_element`.
+
+This will imply that the Layer is indeed rendered before we continue our test.
diff --git a/doc/development/testing_guide/end_to_end_tests.md b/doc/development/testing_guide/end_to_end/index.md
index fc7aaedca29..afd81ff00b2 100644
--- a/doc/development/testing_guide/end_to_end_tests.md
+++ b/doc/development/testing_guide/end_to_end/index.md
@@ -65,7 +65,7 @@ Below you can read more about how to use it and how does it work.
Currently, we are using _multi-project pipeline_-like approach to run QA
pipelines.
-![QA on merge requests CI/CD architecture](img/qa_on_merge_requests_cicd_architecture.png)
+![QA on merge requests CI/CD architecture](../img/qa_on_merge_requests_cicd_architecture.png)
<details>
<summary>Show mermaid source</summary>
@@ -136,6 +136,12 @@ Once you decided where to put [test environment orchestration scenarios] and
the [GitLab QA orchestrator README][gitlab-qa-readme], and [the already existing
instance-level scenarios][instance-level scenarios].
+Continued reading:
+
+- [Quick Start Guide](quick_start_guide.md)
+- [Style Guide](style_guide.md)
+- [Best Practices](best_practices.md)
+
## Where can I ask for help?
You can ask question in the `#quality` channel on Slack (GitLab internal) or
@@ -149,7 +155,7 @@ you can find an issue you would like to work on in
[gitlab-qa-readme]: https://gitlab.com/gitlab-org/gitlab-qa/tree/master/README.md
[quality-nightly-pipelines]: https://gitlab.com/gitlab-org/quality/nightly/pipelines
[quality-staging-pipelines]: https://gitlab.com/gitlab-org/quality/staging/pipelines
-[review-apps]: ./review_apps.md
+[review-apps]: ../review_apps.md
[gitlab-qa-architecture]: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/architecture.md
[gitlab-qa-issues]: https://gitlab.com/gitlab-org/gitlab-qa/issues?label_name%5B%5D=new+scenario
[gitlab-ce-issues]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name[]=QA&label_name[]=test
diff --git a/qa/qa/page/README.md b/doc/development/testing_guide/end_to_end/page_objects.md
index d0de33892c4..73e1fd862c1 100644
--- a/qa/qa/page/README.md
+++ b/doc/development/testing_guide/end_to_end/page_objects.md
@@ -82,15 +82,17 @@ module Page
end
# ...
+ end
end
end
```
-The `view` DSL method declares the filename of the view where an
-`element` is implemented.
+### Defining Elements
+
+The `view` DSL method will correspond to the rails View, partial, or vue component that renders the elements.
The `element` DSL method in turn declares an element for which a corresponding
-`qa-element-name-dasherized` CSS class need to be added to the view file.
+`qa-element-name-dasherized` CSS class will need to be added to the view file.
You can also define a value (String or Regexp) to match to the actual view
code but **this is deprecated** in favor of the above method for two reasons:
@@ -115,6 +117,37 @@ view 'app/views/my/view.html.haml' do
end
```
+### Adding Elements to a View
+
+Given the following elements...
+
+```ruby
+view 'app/views/my/view.html.haml' do
+ element :login_field
+ element :password_field
+ element :sign_in_button
+end
+```
+
+To add these elements to the view, you must change the rails View, partial, or vue component by adding a `qa-element-descriptor` class
+for each element defined.
+
+In our case, `qa-login-field`, `qa-password-field` and `qa-sign-in-button`
+
+**app/views/my/view.html.haml**
+
+```haml
+= f.text_field :login, class: "form-control top qa-login-field", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required."
+= f.password_field :password, class: "form-control bottom qa-password-field", required: true, title: "This field is required."
+= f.submit "Sign in", class: "btn btn-success qa-sign-in-button"
+```
+
+Things to note:
+
+- The CSS class must be `kebab-cased` (separated with hyphens "`-`")
+- If the element appears on the page unconditionally, add `required: true` to the element. See
+[Dynamic element validation](dynamic_element_validation.md)
+
## Running the test locally
During development, you can run the `qa:selectors` test by running
diff --git a/doc/development/testing_guide/end_to_end/quick_start_guide.md b/doc/development/testing_guide/end_to_end/quick_start_guide.md
new file mode 100644
index 00000000000..afe76acf9c9
--- /dev/null
+++ b/doc/development/testing_guide/end_to_end/quick_start_guide.md
@@ -0,0 +1,585 @@
+# Writing end-to-end tests step-by-step
+
+In this tutorial, you will find different examples, and the steps involved, in the creation of end-to-end (_e2e_) tests for GitLab CE and GitLab EE, using GitLab QA.
+
+> When referring to end-to-end tests in this document, this means testing a specific feature end-to-end, such as a user logging in, the creation of a project, the management of labels, breaking down epics into sub-epics and issues, etc.
+
+## Important information before we start writing tests
+
+It's important to understand that end-to-end tests of isolated features, such as the ones described in the above note, doesn't mean that everything needs to happen through the GUI.
+
+If you don't exactly understand what we mean by **not everything needs to happen through the GUI,** please make sure you've read the [best practices](best_practices.md) before moving on.
+
+## This document covers the following items:
+
+- [0.](#0-are-end-to-end-tests-needed) Identifying if end-to-end tests are really needed
+- [1.](#1-identifying-the-devops-stage) Identifying the [DevOps stage](https://about.gitlab.com/stages-devops-lifecycle/) of the feature that you are going to cover with end-to-end tests
+- [2.](#2-test-skeleton) Creating the skeleton of the test file (`*_spec.rb`)
+- [3.](#3-test-cases-mvc) The [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of the test cases' logic
+- [4.](#4-extracting-duplicated-code) Extracting duplicated code into methods
+- [5.](#5-tests-pre-conditions-using-resources-and-page-objects) Tests' pre-conditions (`before :context` and `before`) using resources and [Page Objects]
+- [6.](#6-optimization) Optimizing the test suite
+- [7.](#7-resources) Using and implementing resources
+- [8.](#8-page-objects) Moving element definitions and methods to [Page Objects]
+
+### 0. Are end-to-end tests needed?
+
+At GitLab we respect the [test pyramid](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/testing_guide/testing_levels.md), and so, we recommend you check the code coverage of a specific feature before writing end-to-end tests, for both [CE](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby/#_AllFiles) and [EE](https://gitlab-org.gitlab.io/gitlab-ee/coverage-ruby/#_AllFiles) projects.
+
+Sometimes you may notice that there is already good coverage in other test levels, and we can stay confident that if we break a feature, we will still have quick feedback about it, even without having end-to-end tests.
+
+If after this analysis you still think that end-to-end tests are needed, keep reading.
+
+### 1. Identifying the DevOps stage
+
+The GitLab QA end-to-end tests are organized by the different [stages in the DevOps lifecycle](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa/specs/features/browser_ui), and so, if you are creating tests for issue creation, for instance, you would locate the spec files under the `qa/qa/specs/features/browser_ui/2_plan/` directory since issue creation is part of the Plan stage.
+
+ In another case of a test for listing merged merge requests (MRs), the test should go under the `qa/qa/specs/features/browser_ui/3_create/` directory since merge requests are a feature from the Create stage.
+
+> There may be sub-directories inside the stages directories, for different features. For example: `.../browser_ui/2_plan/ee_epics/` and `.../browser_ui/2_plan/issues/`.
+
+Now, let's say we want to create tests for the [scoped labels](https://about.gitlab.com/2019/04/22/gitlab-11-10-released/#scoped-labels) feature, available on GitLab EE Premium (this feature is part of the Plan stage.)
+
+> Because these tests are for a feature available only on GitLab EE, we need to create them in the [EE repository](https://gitlab.com/gitlab-org/gitlab-ee).
+
+Since [there is no specific directory for this feature](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/qa/qa/specs/features/browser_ui/2_plan), we should create a sub-directory for it.
+
+Under `.../browser_ui/2_plan/`, let's create a sub-directory called `ee_scoped_labels/`.
+
+> Notice that since this feature is only available for GitLab EE we prefix the sub-directory with `ee_`.
+
+### 2. Test skeleton
+
+Inside the newly created sub-directory, let's create a file describing the test suite (e.g. `editing_scoped_labels_spec.rb`.)
+
+#### The `context` and `describe` blocks
+
+Specs have an outer `context` that indicates the DevOps stage. The next level is the `describe` block, that briefly states the subject of the test suite. See the following example:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels on issues' do
+ end
+ end
+end
+```
+
+#### The `it` blocks
+
+Every test suite is composed of at least one `it` block, and a good way to start writing end-to-end tests is by writing test cases descriptions as `it` blocks. These might help you to think of different test scenarios. Take a look at the following example:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels on issues' do
+ it 'replaces an existing label if it has the same key' do
+ end
+
+ it 'keeps both scoped labels when adding a label with a different key' do
+ end
+ end
+ end
+end
+```
+
+### 3. Test cases MVC
+
+For the [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of our test cases, let's say that we already have the application in the state needed for the tests, and then let's focus on the logic of the test cases only.
+
+To evolve the test cases drafted on step 2, let's imagine that the user is already logged into a GitLab EE instance, they already have at least a Premium license in use, there is already a project created, there is already an issue opened in the project, the issue already has a scoped label (e.g. `animal::fox`), there are other scoped labels (for the same scope and for a different scope (e.g. `animal::dolphin` and `plant::orchid`), and finally, the user is already on the issue's page. Let's also suppose that for every test case the application is in a clean state, meaning that one test case won't affect another.
+
+> Note: there are different approaches to creating an application state for end-to-end tests. Some of them are very time consuming and subject to failures, such as when using the GUI for all the pre-conditions of the tests. On the other hand, other approaches are more efficient, such as using the public APIs. The latter is more efficient since it doesn't depend on the GUI. We won't focus on this part yet, but it's good to keep it in mind.
+
+Let's now focus on the first test case.
+
+```ruby
+it 'replaces an existing label if it has the same key' do
+ # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects (which we cover on section 8).
+ page.find('.block.labels .edit-link').click
+ page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['animal::dolphin', :enter]
+ page.find('#content-body').click
+ page.refresh
+
+ labels_block = page.find('.qa-labels-block')
+
+ expect(labels_block).to have_content('animal::dolphin')
+ expect(labels_block).not_to have_content('animal::fox')
+ expect(page).to have_content('added animal::dolphin label and removed animal::fox')
+end
+```
+
+> Notice that the test itself is simple. The most challenging part is the creation of the application state, which will be covered later.
+
+> The exemplified test case's MVC is not enough for the change to be merged, but it helps to build up the test logic. The reason is that we do not want to use locators directly in the tests, and tests **must** use [Page Objects] before they can be merged. This way we better separate the responsibilities, where the Page Objects encapsulate elements and methods that allow us to interact with pages, while the spec files describe the test cases in more business-related language.
+
+Below are the steps that the test covers:
+
+1. The test finds the 'Edit' link for the labels and clicks on it.
+2. Then it fills in the 'Assign labels' input field with the value 'animal::dolphin' and press enters.
+3. Then it clicks in the content body to apply the label and refreshes the page.
+4. Finally, the expectations check that the previous scoped label was removed and that the new one was added.
+
+Let's now see how the second test case would look.
+
+```ruby
+it 'keeps both scoped labels when adding a label with a different key' do
+ # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects (which we cover on section 8).
+ page.find('.block.labels .edit-link').click
+ page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['plant::orchid', :enter]
+ page.find('#content-body').click
+ page.refresh
+
+ labels_block = page.find('.qa-labels-block')
+
+ expect(labels_block).to have_content('animal::fox')
+ expect(labels_block).to have_content('plant::orchid')
+ expect(page).to have_content('added animal::fox')
+ expect(page).to have_content('added plant::orchid')
+end
+```
+
+> Note that elements are always located using CSS selectors, and a good practice is to add test-specific selectors (this is called adding testability to the application and we will talk more about it later.) For example, the `labels_block` element uses the selector `.qa-labels-block`, which was added specifically for testing purposes.
+
+Below are the steps that the test covers:
+
+1. The test finds the 'Edit' link for the labels and clicks on it.
+2. Then it fills in the 'Assign labels' input field with the value 'plant::orchid' and press enters.
+3. Then it clicks in the content body to apply the label and refreshes the page.
+4. Finally, the expectations check that both scoped labels are present.
+
+> Similar to the previous test, this one is also very straightforward, but there is some code duplication. Let's address it.
+
+### 4. Extracting duplicated code
+
+If we refactor the tests created on step 3 we could come up with something like this:
+
+```ruby
+before do
+ ...
+
+ @initial_label = 'animal::fox'
+ @new_label_same_scope = 'animal::dolphin'
+ @new_label_different_scope = 'plant::orchid'
+
+ ...
+end
+
+it 'replaces an existing label if it has the same key' do
+ select_label_and_refresh @new_label_same_scope
+
+ labels_block = page.find('.qa-labels-block')
+
+ expect(labels_block).to have_content(@new_label_same_scope)
+ expect(labels_block).not_to have_content(@initial_label)
+ expect(page).to have_content("added #{@new_label_same_scope}")
+ expect(page).to have_content("and removed #{@initial_label}")
+end
+
+it 'keeps both scoped label when adding a label with a different key' do
+ select_label_and_refresh @new_label_different_scope
+
+ labels_block = page.find('.qa-labels-block')
+
+ expect(labels_blocks).to have_content(@new_label_different_scope)
+ expect(labels_blocks).to have_content(@initial_label)
+ expect(page).to have_content("added #{@new_label_different_scope}")
+ expect(page).to have_content("added #{@initial_label}")
+end
+
+def select_label_and_refresh(label)
+ page.find('.block.labels .edit-link').click
+ page.find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter]
+ page.find('#content-body').click
+ page.refresh
+end
+```
+
+First, we remove the duplication of strings by defining the global variables `@initial_label`, `@new_label_same_scope` and `@new_label_different_scope` in the `before` block, and by using them in the expectations.
+
+Then, by creating a reusable `select_label_and_refresh` method, we remove the code duplication of this action, and later we can move this method to a Page Object class that will be created for easier maintenance purposes.
+
+> Notice that the reusable method is created at the bottom of the file. The reason for that is that reading the code should be similar to reading a newspaper, where high-level information is at the top, like the title and summary of the news, while low level, or more specific information, is at the bottom (this helps readability).
+
+### 5. Tests' pre-conditions using resources and Page Objects
+
+In this section, we will address the previously mentioned subject of creating the application state for the tests, using the `before :context` and `before` blocks, together with resources and Page Objects.
+
+#### `before :context`
+
+A pre-condition for the entire test suite is defined in the `before :context` block.
+
+> For our test suite, due to the need of the tests being completely independent of each other, we won't use the `before :context` block. The `before :context` block would make the tests dependent on each other because the first test changes the label of the issue, and the second one depends on the `'animal::fox'` label being set.
+
+> **Tip:** In case of a test suite with only one `it` block it's ok to use only the `before` block (see below) with all the test's pre-conditions.
+
+#### `before`
+
+As the pre-conditions for our test suite, the things that needs to happen before each test starts are:
+
+- The user logging in;
+- A premium license already being set;
+- A project being created with an issue and labels already set;
+- The issue page being opened with only one scoped label applied to the it.
+
+> When running end-to-end tests as part of the GitLab's continuous integration process [a license is already set as an environment variable](https://gitlab.com/gitlab-org/gitlab-ee/blob/1a60d926740db10e3b5724713285780a4f470531/qa/qa/ee/strategy.rb#L20). For running tests locally you can set up such license by following the document [what tests can be run?](https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/what_tests_can_be_run.md#supported-remote-grid-environment-variables), based on the [supported GitLab environment variables](https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/what_tests_can_be_run.md#supported-gitlab-environment-variables).
+
+#### Implementation
+
+In the following code we will focus only on the test suite's pre-conditions:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels on issues' do
+ before do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+
+ @initial_label = 'animal::fox'
+ @new_label_same_scope = 'animal::dolphin'
+ @new_label_different_scope = 'plant::orchid'
+
+ issue = Resource::Issue.fabricate_via_api! do |issue|
+ issue.title = 'Issue to test the scoped labels'
+ issue.labels = @initial_label
+ end
+
+ [@new_label_same_scope, @new_label_different_scope].each do |label|
+ Resource::Label.fabricate_via_api! do |l|
+ l.project = issue.project.id
+ l.title = label
+ end
+ end
+
+ issue.visit!
+ end
+
+ it 'replaces an existing label if it has the same key' do
+ ...
+ end
+
+ it 'keeps both scoped labels when adding a label with a different key' do
+ ...
+ end
+
+ def select_label_and_refresh(label)
+ ...
+ end
+ end
+ end
+end
+```
+
+In the `before` block we create all the application state needed for the tests to run. We do that by using the `Runtime::Browser.visit` method to go to the login page, by performing a `sign_in_using_credentials` from the `Login` Page Object, by fabricating resources via APIs (`issue`, and `Resource::Label`), and by using the `issue.visit!` to visit the issue page.
+
+> A project is created in the background by creating the `issue` resource.
+
+> When creating the [Resources], notice that when calling the `fabricate_via_api` method, we pass some attribute:values, like `title`, and `labels` for the `issue` resource; and `project` and `title` for the `label` resource.
+
+> What's important to understand here is that by creating the application state mostly using the public APIs we save a lot of time in the test suite setup stage.
+
+> Soon we will cover the use of the already existing resources' methods and the creation of your own `fabricate_via_api` methods for resources where this is still not available, but first, let's optimize our implementation.
+
+### 6. Optimization
+
+As already mentioned in the [best practices](best_practices.md) document, end-to-end tests are very costly in terms of execution time, and it's our responsibility as software engineers to ensure that we optimize them as much as possible.
+
+> Note that end-to-end tests are slow to run and so they can have several actions and assertions in a single test, which helps us get feedback from the tests sooner. In comparison, unit tests are much faster to run and can exercise every little piece of the application in isolation, and so they usually have only one assertion per test.
+
+Some improvements that we could make in our test suite to optimize its time to run are:
+
+1. Having a single test case (an `it` block) that exercises both scenarios to avoid "wasting" time in the tests' pre-conditions, instead of having two different test cases.
+2. Making the selection of labels more performant by allowing for the selection of more than one label in the same reusable method.
+
+Let's look at a suggestion that addresses the above points, one by one:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels on issues' do
+ before do
+ ...
+ end
+
+ it 'correctly applies scoped labels depending on if they are from the same or a different scope' do
+ select_labels_and_refresh [@new_label_same_scope, @new_label_different_scope]
+
+ labels_block = page.all('.qa-labels-block')
+
+ expect(labels_block).to have_content(@new_label_same_scope)
+ expect(labels_block).to have_content(@new_label_different_scope)
+ expect(labels_block).not_to have_content(@initial_label)
+ expect(page).to have_content("added #{@initial_label}")
+ expect(page).to have_content("added #{@new_label_same_scope} #{@new_label_different_scope} labels and removed #{@initial_label}")
+ end
+
+ def select_labels_and_refresh(labels)
+ find('.block.labels .edit-link').click
+ labels.each do |label|
+ find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter]
+ end
+ find('#content-body').click
+ refresh
+ end
+ end
+ end
+ end
+```
+
+To address point 1, we changed the test implementation from two `it` blocks into a single one that exercises both scenarios. Now the new test description is: `'correctly applies the scoped labels depending if they are from the same or a different scope'`. It's a long description, but it describes well what the test does.
+
+> Notice that the implementation of the new and unique `it` block had to change a little bit. Below we describe in details what it does.
+
+1. It selects two scoped labels simultaneously, one from the same scope of the one already applied in the issue during the setup phase (in the `before` block), and another one from a different scope.
+2. It asserts that the correct labels are visible in the `labels_block`, and that the labels were correctly added and removed;
+3. Finally, the `select_label_and_refresh` method is changed to `select_labels_and_refresh`, which accepts an array of labels instead of a single label, and it iterates on them for faster label selection (this is what is used in step 1 explained above.)
+
+### 7. Resources
+
+**Note:** When writing this document, some code that is now merged to master was not implemented yet, but we left them here for the readers to understand the whole process of end-to-end test creation.
+
+You can think of [Resources] as anything that can be created on GitLab CE or EE, either through the GUI, the API, or the CLI.
+
+With that in mind, resources can be a project, an epic, an issue, a label, a commit, etc.
+
+As you saw in the tests' pre-conditions and the optimization sections, we're already creating some of these resources, and we are doing that by calling the `fabricate_via_api!` method.
+
+> We could be using the `fabricate!` method instead, which would use the `fabricate_via_api!` method if it exists, and fallback to GUI fabrication otherwise, but we recommend being explicit to make it clear what the test does. Also, we always recommend fabricating resources via API since this makes tests faster and more reliable.
+
+For our test suite example, the resources that we need to create don't have the necessary code for the `fabricate_via_api!` method to correctly work (e.g., the issue and label resources), so we will have to create them.
+
+#### Implementation
+
+In the following we describe the changes needed in each of the resource files mentioned above.
+
+**Issue resource**
+
+Now, let's make it possible to create an issue resource through the API.
+
+First, in the [issue resource](https://gitlab.com/gitlab-org/gitlab-ee/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/issue.rb), let's expose its labels attribute.
+
+Add the following `attribute :labels` right below the [`attribute :title`](https://gitlab.com/gitlab-org/gitlab-ee/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/issue.rb#L15).
+
+> This line is needed to allow for labels to be automatically added to an issue when fabricating it via API.
+
+Next, add the following code right below the [`fabricate!`](https://gitlab.com/gitlab-org/gitlab-ee/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/issue.rb#L27) method.
+
+```ruby
+def api_get_path
+ "/projects/#{project.id}/issues/#{id}"
+end
+
+def api_post_path
+ "/projects/#{project.id}/issues"
+end
+
+def api_post_body
+ {
+ title: title,
+ labels: [labels]
+ }
+end
+```
+
+By defining the `api_get_path` method, we allow the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to get a single issue.
+
+> This `GET` path can be found in the [public API documentation](https://docs.gitlab.com/ee/api/issues.html#single-issue).
+
+By defining the `api_post_path` method, we allow the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to create a new issue in a specific project.
+
+> This `POST` path can be found in the [public API documentation](https://docs.gitlab.com/ee/api/issues.html#new-issue).
+
+By defining the `api_post_body` method, we allow the [`ApiFabricator.api_post`](https://gitlab.com/gitlab-org/gitlab-ee/blob/a9177ca1812bac57e2b2fa4560e1d5dd8ffac38b/qa/qa/resource/api_fabricator.rb#L68) method to know which data to send when making the `POST` request.
+
+> Notice that we pass both `title` and `labels` attributes in the `api_post_body`, where `labels` receives an array of labels, and [`title` is required](https://docs.gitlab.com/ee/api/issues.html#new-issue).
+
+**Label resource**
+
+Finally, let's make it possible to create label resources through the API.
+
+Add the following code right below the [`fabricate!`](https://gitlab.com/gitlab-org/gitlab-ee/blob/a9177ca1812bac57e2b2fa4560e1d5dd8ffac38b/qa/qa/resource/label.rb#L36) method.
+
+```ruby
+def resource_web_url(resource)
+ super
+rescue ResourceURLMissingError
+ # this particular resource does not expose a web_url property
+end
+
+def api_get_path
+ raise NotImplementedError, "The Labels API doesn't expose a single-resource endpoint so this method cannot be properly implemented."
+end
+
+def api_post_path
+ "/projects/#{project}/labels"
+end
+
+def api_post_body
+ {
+ name: @title,
+ color: @color
+ }
+end
+```
+
+By defining the `resource_web_url(resource)` method, we override the one from the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb#L44) module. We do that to avoid failing the test due to this particular resource not exposing a `web_url` property.
+
+By defining the `api_get_path` method, we **would** allow for the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to get a single label, but since there's no path available for that in the publich API, we raise a `NotImplementedError` instead.
+
+By defining the `api_post_path` method, we allow for the [`ApiFabricator `](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to create a new label in a specific project.
+
+By defining the `api_post_body` method, we we allow for the [`ApiFabricator.api_post`](https://gitlab.com/gitlab-org/gitlab-ee/blob/a9177ca1812bac57e2b2fa4560e1d5dd8ffac38b/qa/qa/resource/api_fabricator.rb#L68) method to know which data to send when making the `POST` request.
+
+> Notice that we pass both `name` and `color` attributes in the `api_post_body` since [those are required](https://docs.gitlab.com/ee/api/labels.html#create-a-new-label).
+
+### 8. Page Objects
+
+Page Objects are used in end-to-end tests for maintenance reasons, where a page's elements and methods are defined to be reused in any test.
+
+> Page Objects are auto-loaded in the `qa/qa.rb` file and available in all the test files (`*_spec.rb`).
+
+Take a look at the [Page Objects] documentation.
+
+Now, let's go back to our example.
+
+As you may have noticed, we are defining elements with CSS selectors and the `select_labels_and_refresh` method directly in the test file, and this is an anti-pattern since we need to better separate the responsibilities.
+
+To address this issue, we will move the implementation to Page Objects, and the test suite will only focus on the business rules that we are testing.
+
+#### Updates in the test file
+
+As in a test-driven development approach, let's start changing the test file even before the Page Object implementation is in place.
+
+Replace the code of the `it` block in the test file by the following:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels on issues' do
+ before do
+ ...
+ end
+
+ it 'correctly applies scoped labels depending on if they are from the same or a different scope' do
+ Page::Project::Issue::Show.perform do |issue_page|
+ issue_page.select_labels_and_refresh [@new_label_same_scope, @new_label_different_scope]
+
+ expect(page).to have_content("added #{@initial_label}")
+ expect(page).to have_content("added #{@new_label_same_scope} #{@new_label_different_scope} labels and removed #{@initial_label}")
+ expect(issue_page.text_of_labels_block).to have_content(@new_label_same_scope)
+ expect(issue_page.text_of_labels_block).to have_content(@new_label_different_scope)
+ expect(issue_page.text_of_labels_block).not_to have_content(@initial_label)
+ end
+ end
+ end
+ end
+end
+```
+
+Notice that `select_labels_and_refresh` is now a method from the issue Page Object (which is not yet implemented), and that we verify the labels' text by using `text_of_labels_block`, instead of via the `labels_block` element. The `text_of_labels_block` method will also be implemented in the issue Page Object.
+
+Let's now update the Issue Page Object.
+
+#### Updates in the Issue Page Object
+
+> Page Objects are located in the `qa/qa/page/` directory, and its sub-directories.
+
+The file we will have to change is the [Issue Page Object](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/page/project/issue/show.rb).
+
+First, add the following code right below the definition of an already implemented view:
+
+```ruby
+view 'app/views/shared/issuable/_sidebar.html.haml' do
+ element :labels_block
+ element :edit_link_labels
+ element :dropdown_menu_labels
+end
+
+view 'app/helpers/dropdowns_helper.rb' do
+ element :dropdown_input_field
+end
+```
+
+Similarly to what we did before, let's first change the Page Object even without the elements being defined in the view (`_sidebar.html.haml`) and the `dropdowns_helper.rb` files, and later we will update them by adding the appropriate CSS selectors.
+
+Now, let's implement the methods `select_labels_and_refresh` and `text_of_labels_block`.
+
+Somewhere between the definition of the views and the private methods, add the following snippet of code:
+
+```ruby
+def select_labels_and_refresh(labels)
+ click_element(:edit_link_labels)
+ labels.each do |label|
+ within_element(:dropdown_menu_labels, text: label) do
+ send_keys_to_element(:dropdown_input_field, [label, :enter])
+ end
+ end
+ click_body
+ labels.each do |label|
+ has_element?(:labels_block, text: label)
+ end
+ refresh
+end
+
+def text_of_labels_block
+ find_element(:labels_block)
+end
+```
+
+##### Details of `select_labels_and_refresh`
+
+Notice that we have not only moved the `select_labels_and_refresh` method, but we have also changed its implementation to:
+1. Click the `:edit_link_labels` element previously defined, instead of using `find('.block.labels .edit-link').click`
+2. Use `within_element(:dropdown_menu_labels, text: label)`, and inside of it, we call `send_keys_to_element(:dropdown_input_field, [label, :enter])`, which is a method that we will implement in the `QA::Page::Base` class to replace `find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter]`
+3. Use `click_body` after iterating on each label, instead of using `find('#content-body').click`
+4. Iterate on every label again, and then we use `has_element?(:labels_block, text: label)` after clicking the page body (which applies the labels), and before refreshing the page, to avoid test flakiness due to refreshing too fast.
+
+##### Details of `text_of_labels_block`
+
+The `text_of_labels_block` method is a simple method that returns the `:labels_block` element (`find_element(:labels_block)`).
+
+#### Updates in the view (*.html.haml) and `dropdowns_helper.rb` files
+
+Now let's change the view and the `dropdowns_helper` files to add the selectors that relate to the Page Object.
+
+In the [app/views/shared/issuable/_sidebar.html.haml](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/app/views/shared/issuable/_sidebar.html.haml) file, on [line 105 ](https://gitlab.com/gitlab-org/gitlab-ee/blob/84043fa72ca7f83ae9cde48ad670e6d5d16501a3/app/views/shared/issuable/_sidebar.html.haml#L105), add an extra class `qa-edit-link-labels`.
+
+The code should look like this: `= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right'`.
+
+In the same file, on [line 121](https://gitlab.com/gitlab-org/gitlab-ee/blob/84043fa72ca7f83ae9cde48ad670e6d5d16501a3/app/views/shared/issuable/_sidebar.html.haml#L121), add an extra class `.qa-dropdown-menu-labels`.
+
+The code should look like this: `.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.qa-dropdown-menu-labels.dropdown-menu-selectable`.
+
+In the [`dropdowns_helper.rb`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/app/helpers/dropdowns_helper.rb) file, on [line 94](https://gitlab.com/gitlab-org/gitlab-ee/blob/99e51a374f2c20bee0989cac802e4b5621f72714/app/helpers/dropdowns_helper.rb#L94), add an extra class `qa-dropdown-input-field`.
+
+The code should look like this: `filter_output = search_field_tag search_id, nil, class: "dropdown-input-field qa-dropdown-input-field", placeholder: placeholder, autocomplete: 'off'`.
+
+> Classes starting with `qa-` are used for testing purposes only, and by defining such classes in the elements we add **testability** in the application.
+
+> When defining a class like `qa-labels-block`, it is transformed into `:labels_block` for usage in the Page Objects. So, `qa-edit-link-labels` is tranformed into `:edit_link_labels`, `qa-dropdown-menu-labels` is transformed into `:dropdown_menu_labels`, and `qa-dropdown-input-field` is transformed into `:dropdown_input_field`. Also, we use a [sanity test](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa/page#how-did-we-solve-fragile-tests-problem) to check that defined elements have their respective `qa-` selectors in the specified views.
+
+> We did not define the `qa-labels-block` class in the `app/views/shared/issuable/_sidebar.html.haml` file because it was already there to be used.
+
+#### Updates in the `QA::Page::Base` class
+
+The last thing that we have to do is to update `QA::Page::Base` class to add the `send_keys_to_element` method on it.
+
+Add the following snippet of code somewhere where class methods are defined:
+
+```ruby
+def send_keys_to_element(name, keys)
+ find_element(name).send_keys(keys)
+end
+```
+
+This method receives an element (`name`) and the `keys` that it will send to that element, and the keys are an array that can receive strings, or "special" keys, like `:enter`.
+
+As you might remember, in the Issue Page Object we call this method like this: `send_keys_to_element(:dropdown_input_field, [label, :enter])`.
+
+___
+
+With that, you should be able to start writing end-to-end tests yourself. *Congratulations!*
+
+[Page Objects]: page_objects.md
+[Resources]: resources.md
diff --git a/qa/qa/resource/README.md b/doc/development/testing_guide/end_to_end/resources.md
index 2c8859b6599..1e32db4f633 100644
--- a/qa/qa/resource/README.md
+++ b/doc/development/testing_guide/end_to_end/resources.md
@@ -5,11 +5,11 @@ be created via the API or the CLI.
## How to properly implement a resource class?
-All resource classes should inherit from [`Resource::Base`](./base.rb).
+All resource classes should inherit from `Resource::Base`.
There is only one mandatory method to implement to define a resource class.
This is the `#fabricate!` method, which is used to build the resource via the
-browser UI. Note that you should only use [Page objects](../page/README.md) to
+browser UI. Note that you should only use [Page objects](page_objects.md) to
interact with a Web page in this method.
Here is an imaginary example:
@@ -74,7 +74,7 @@ module QA
end
```
-The [`Project` resource](./project.rb) is a good real example of Browser
+The `Project` resource is a good real example of Browser
UI and API implementations.
#### Resource attributes
diff --git a/qa/docs/guidelines.md b/doc/development/testing_guide/end_to_end/style_guide.md
index cd4b939fd71..0272e1810f2 100644
--- a/qa/docs/guidelines.md
+++ b/doc/development/testing_guide/end_to_end/style_guide.md
@@ -1,6 +1,6 @@
-# Style guide for writing E2E tests
+# Style guide for writing end-to-end tests
-This document describes the conventions used at GitLab for writing E2E tests using the GitLab QA project.
+This document describes the conventions used at GitLab for writing End-to-end (E2E) tests using the GitLab QA project.
## `click_` versus `go_to_`
@@ -45,7 +45,7 @@ Notice that in the above example, before clicking the `:operations_environments_
> We can create these methods as helpers to abstract multi-step navigation.
-### Element Naming Convention
+### Element naming convention
When adding new elements to a page, it's important that we have a uniform element naming convention.
@@ -69,7 +69,8 @@ We follow a simple formula roughly based on hungarian notation.
#### Examples
-**Good**
+**Good**
+
```ruby
view '...' do
element :edit_button
@@ -80,7 +81,8 @@ view '...' do
end
```
-**Bad**
+**Bad**
+
```ruby
view '...' do
# `_confirmation` should be `_field`. what sort of confirmation? a checkbox confirmation? no real way to disambiguate.
diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md
index ecad9ba48a3..93ee2a6371a 100644
--- a/doc/development/testing_guide/index.md
+++ b/doc/development/testing_guide/index.md
@@ -71,7 +71,7 @@ Everything you should know about how to test Rake tasks.
---
-## [End-to-end tests](end_to_end_tests.md)
+## [End-to-end tests](end_to_end/index.md)
Everything you should know about how to run end-to-end tests using
[GitLab QA][gitlab-qa] testing framework.
diff --git a/doc/development/testing_guide/smoke.md b/doc/development/testing_guide/smoke.md
index 30d861d7d68..c9d3238fbe9 100644
--- a/doc/development/testing_guide/smoke.md
+++ b/doc/development/testing_guide/smoke.md
@@ -17,7 +17,7 @@ Currently, our suite consists of this basic functionality coverage:
Smoke tests have the `:smoke` RSpec metadata.
-See [End-to-end Testing](./end_to_end_tests.md) for more details about
+See [End-to-end Testing](end_to_end/index.md) for more details about
end-to-end tests.
---
diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md
index b5155b6b7fa..e1ce4d3b7d1 100644
--- a/doc/development/testing_guide/testing_levels.md
+++ b/doc/development/testing_guide/testing_levels.md
@@ -178,7 +178,7 @@ Every new feature should come with a [test plan].
| ---------- | -------------- | ----- |
| `qa/qa/specs/features/` | [Capybara] + [RSpec] + Custom QA framework | Tests should be placed under their corresponding [Product category] |
-> See [end-to-end tests](end_to_end_tests.md) for more information.
+> See [end-to-end tests](end_to_end/index.md) for more information.
Note that `qa/spec` contains unit tests of the QA framework itself, not to be
confused with the application's [unit tests](#unit-tests) or
diff --git a/doc/development/understanding_explain_plans.md b/doc/development/understanding_explain_plans.md
index 01a0044f096..bfbb7be70e3 100644
--- a/doc/development/understanding_explain_plans.md
+++ b/doc/development/understanding_explain_plans.md
@@ -80,8 +80,9 @@ Planning time: 2.861 ms
Execution time: 3428.596 ms
```
-For more information, refer to the official [EXPLAIN
-documentation](https://www.postgresql.org/docs/current/static/sql-explain.html).
+For more information, refer to the official
+[`EXPLAIN` documentation](https://www.postgresql.org/docs/current/sql-explain.html)
+and [using `EXPLAIN` guide](https://www.postgresql.org/docs/current/using-explain.html).
## Nodes
@@ -653,9 +654,39 @@ and related tools such as:
- <https://explain.depesz.com/>
- <http://tatiyants.com/postgres-query-plan-visualization/>
-GitLab employees can also use our chatops solution, available in Slack using the
-`/chatops` slash command. You can use chatops to get a query plan by running the
-following:
+
+## Producing query plans
+
+There are a few ways to get the output of a query plan. Of course you
+can directly run the `EXPLAIN` query in the `psql` console, or you can
+follow one of the other options below.
+
+### Rails console
+
+Using the [`activerecord-explain-analyze`](https://github.com/6/activerecord-explain-analyze)
+you can directly generate the query plan from the Rails console:
+
+```ruby
+pry(main)> require 'activerecord-explain-analyze'
+=> true
+pry(main)> Project.where('build_timeout > ?', 3600).explain(analyze: true)
+ Project Load (1.9ms) SELECT "projects".* FROM "projects" WHERE (build_timeout > 3600)
+ ↳ (pry):12
+=> EXPLAIN for: SELECT "projects".* FROM "projects" WHERE (build_timeout > 3600)
+Seq Scan on public.projects (cost=0.00..2.17 rows=1 width=742) (actual time=0.040..0.041 rows=0 loops=1)
+ Output: id, name, path, description, created_at, updated_at, creator_id, namespace_id, ...
+ Filter: (projects.build_timeout > 3600)
+ Rows Removed by Filter: 14
+ Buffers: shared hit=2
+Planning time: 0.411 ms
+Execution time: 0.113 ms
+```
+
+### Chatops
+
+[GitLab employees can also use our chatops solution, available in Slack using the
+`/chatops` slash command](chatops_on_gitlabcom.md).
+You can use chatops to get a query plan by running the following:
```
/chatops run explain SELECT COUNT(*) FROM projects WHERE visibility_level IN (0, 20)
@@ -674,3 +705,9 @@ For more information about the available options, run:
```
/chatops run explain --help
```
+
+## Further reading
+
+A more extensive guide on understanding query plans can be found in
+the [presentation](https://www.dalibo.org/_media/understanding_explain.pdf)
+from [Dalibo.org](https://www.dalibo.org/en/).
diff --git a/doc/git_hooks/git_hooks.md b/doc/git_hooks/git_hooks.md
index 9b8ad1578a0..b251e58410a 100644
--- a/doc/git_hooks/git_hooks.md
+++ b/doc/git_hooks/git_hooks.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'https://docs.gitlab.com/ee/push_rules/push_rules.html'
+redirect_to: '../push_rules/push_rules.md'
---
-This document was moved to [another location](https://docs.gitlab.com/ee/push_rules/push_rules.html)
+This document was moved to [another location](../push_rules/push_rules.md)
diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md
index 785e2ffb650..a9ae4fb23f9 100644
--- a/doc/gitlab-basics/create-project.md
+++ b/doc/gitlab-basics/create-project.md
@@ -16,7 +16,7 @@ To create a project in GitLab:
- [Import a project](../user/project/import/index.md) from a different repository,
if enabled on your GitLab instance. Contact your GitLab admin if this
is unavailable.
- - Run [CI/CD pipelines for external repositories](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/index.html). **[PREMIUM]**
+ - Run [CI/CD pipelines for external repositories](../ci/ci_cd_for_external_repos/index.md). **[PREMIUM]**
## Blank projects
diff --git a/doc/install/README.md b/doc/install/README.md
index 53778f7f0d3..9cc21412898 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -1,6 +1,7 @@
---
comments: false
description: Read through the GitLab installation methods.
+type: index
---
# Installation **[CORE ONLY]**
@@ -81,7 +82,7 @@ the above methods, provided the cloud provider supports it.
- [Install on AWS](aws/index.md): Install Omnibus GitLab on AWS using the community AMIs that GitLab provides.
- [Install GitLab on Google Cloud Platform](google_cloud_platform/index.md): Install Omnibus GitLab on a VM in GCP.
- [Install GitLab on Azure](azure/index.md): Install Omnibus GitLab from Azure Marketplace.
-- [Install GitLab on OpenShift](openshift_and_gitlab/index.md): Install GitLab using the Docker image on OpenShift.
+- [Install GitLab on OpenShift](https://docs.gitlab.com/charts/installation/cloud/openshift.html): Install GitLab on OpenShift by using GitLab's Helm charts.
- [Install GitLab on DC/OS](https://mesosphere.com/blog/gitlab-dcos/): Install GitLab on Mesosphere DC/OS via the [GitLab-Mesosphere integration](https://about.gitlab.com/2016/09/16/announcing-gitlab-and-mesosphere/).
- [Install GitLab on DigitalOcean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/): Install Omnibus GitLab on DigitalOcean.
- _Testing only!_ [DigitalOcean and Docker Machine](digitaloceandocker.md):
diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md
index 0000e03f1d7..73eaf758923 100644
--- a/doc/install/aws/index.md
+++ b/doc/install/aws/index.md
@@ -1,3 +1,7 @@
+---
+type: howto
+---
+
# Installing GitLab HA on Amazon Web Services (AWS)
This page offers a walkthrough of a common HA (Highly Available) configuration
@@ -383,7 +387,7 @@ after the instance is created.
CAUTION: **Caution:**
We **do not** recommend using the AWS Elastic File System (EFS), as it can result
-in [significantly degraded performance](../../administration/high_availability/nfs.html#avoid-using-awss-elastic-file-system-efs).
+in [significantly degraded performance](../../administration/high_availability/nfs.md#avoid-using-awss-elastic-file-system-efs).
### Configure security group
@@ -649,12 +653,24 @@ Have a read through these other resources and feel free to
[open an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/new)
to request additional material:
-- [GitLab High Availability](https://docs.gitlab.com/ee/administration/high_availability/):
+- [GitLab High Availability](../../administration/high_availability/README.md):
GitLab supports several different types of clustering and high-availability.
-- [Geo replication](https://docs.gitlab.com/ee/administration/geo/replication/):
+- [Geo replication](../../administration/geo/replication/index.md):
Geo is the solution for widely distributed development teams.
- [Omnibus GitLab](https://docs.gitlab.com/omnibus/) - Everything you need to know
about administering your GitLab instance.
-- [Upload a license](https://docs.gitlab.com/ee/user/admin_area/license.html):
+- [Upload a license](../../user/admin_area/license.md):
Activate all GitLab Enterprise Edition functionality with a license.
- [Pricing](https://about.gitlab.com/pricing): Pricing for the different tiers.
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/install/azure/index.md b/doc/install/azure/index.md
index fa5be1d30f9..b1f79893baf 100644
--- a/doc/install/azure/index.md
+++ b/doc/install/azure/index.md
@@ -1,14 +1,10 @@
---
-description: 'Learn how to spin up a
-pre-configured GitLab VM on Microsoft Azure and have your very own private GitLab instance up and running in around 30 minutes.'
+description: 'Learn how to spin up a pre-configured GitLab VM on Microsoft Azure.'
+type: howto
---
# Install GitLab on Microsoft Azure
-> _This article was originally written by Dave Wentzel and [published on the GitLab Blog][Original-Blog-Post]._
->
-> _Ported to the GitLab documentation and updated on 2017-08-24 by [Ian Scorer](https://gitlab.com/iscorer)._
-
Azure is Microsoft's business cloud and GitLab is a pre-configured offering on the Azure Marketplace.
Hopefully, you aren't surprised to hear that Microsoft and Azure have embraced open source software
like Ubuntu, Red Hat Enterprise Linux, and of course - GitLab! This means that you can spin up a
@@ -444,3 +440,15 @@ Check out our other [Technical Articles][GitLab-Technical-Articles] or browse th
[SSH]: https://en.wikipedia.org/wiki/Secure_Shell
[PuTTY]: http://www.putty.org/
[Using-SSH-In-Putty]: https://mediatemple.net/community/products/dv/204404604/using-ssh-in-putty-
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md
index e89846107b6..cbb3b766b4e 100644
--- a/doc/install/database_mysql.md
+++ b/doc/install/database_mysql.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Database MySQL
NOTE: **Note:**
@@ -301,3 +305,15 @@ Details can be found in the [PostgreSQL][postgres-text-type] and
[postgres-text-type]: http://www.postgresql.org/docs/9.2/static/datatype-character.html
[ce-38152]: https://gitlab.com/gitlab-org/gitlab-ce/issues/38152
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/install/digitaloceandocker.md b/doc/install/digitaloceandocker.md
index d67695d75b4..63bb941ad47 100644
--- a/doc/install/digitaloceandocker.md
+++ b/doc/install/digitaloceandocker.md
@@ -1,8 +1,11 @@
+---
+type: howto
+---
+
# Digital Ocean and Docker Machine test environment
-CAUTION: **Caution:**
-This guide is for quickly testing different versions of GitLab and not recommended for ease of
-future upgrades or keeping the data you create.
+This guide is for quickly testing different versions of GitLab and not
+recommended for ease of future upgrades or keeping the data you create.
## Initial setup
@@ -28,7 +31,7 @@ locally on either macOS or Linux.
NOTE: **Note:**
The rest of the steps are identical for macOS and Linux.
-### Create new docker host
+## Create new docker host
1. Login to Digital Ocean.
1. Generate a new API token at <https://cloud.digitalocean.com/settings/api/tokens>.
@@ -60,9 +63,9 @@ The rest of the steps are identical for macOS and Linux.
Resource: <https://docs.docker.com/machine/drivers/digital-ocean/>.
-### Creating GitLab test instance
+## Creating GitLab test instance
-#### Connect your shell to the new machine
+### Connect your shell to the new machine
In this example we'll create a GitLab EE 8.10.8 instance.
@@ -74,7 +77,7 @@ eval "$(docker-machine env gitlab-test-env-do)"
You can add this to your `~/.bash_profile` file to ensure the `docker` client uses the `gitlab-test-env-do` docker host
-#### Create new GitLab container
+### Create new GitLab container
- HTTP port: `8888`
- SSH port: `2222`
@@ -83,7 +86,7 @@ You can add this to your `~/.bash_profile` file to ensure the `docker` client us
- Container name: `gitlab-test-8.10`
- GitLab version: **EE** `8.10.8-ee.0`
-##### Set up container settings
+#### Set up container settings
```sh
export SSH_PORT=2222
@@ -92,7 +95,7 @@ export VERSION=8.10.8-ee.0
export NAME=gitlab-test-8.10
```
-##### Create container
+#### Create container
```sh
docker run --detach \
@@ -103,9 +106,9 @@ docker run --detach \
gitlab/gitlab-ee:$VERSION
```
-#### Connect to the GitLab container
+### Connect to the GitLab container
-##### Retrieve the docker host IP
+#### Retrieve the docker host IP
```sh
docker-machine ip gitlab-test-env-do
@@ -114,7 +117,7 @@ docker-machine ip gitlab-test-env-do
Browse to: <http://192.168.151.134:8888/>.
-##### Execute interactive shell/edit configuration
+#### Execute interactive shell/edit configuration
```sh
docker exec -it $NAME /bin/bash
@@ -126,8 +129,20 @@ root@192:/# vi /etc/gitlab/gitlab.rb
root@192:/# gitlab-ctl reconfigure
```
-#### Resources
+### Resources
- <https://docs.gitlab.com/omnibus/docker/>.
- <https://docs.docker.com/machine/get-started/>.
- <https://docs.docker.com/machine/reference/ip/>.
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/install/docker.md b/doc/install/docker.md
index 4568a949b0a..06da65189ba 100644
--- a/doc/install/docker.md
+++ b/doc/install/docker.md
@@ -1,3 +1,7 @@
+---
+type: index
+---
+
# Install GitLab with Docker
[Docker](https://www.docker.com) and container technology have been revolutionizing the software world for the past few years. They combine the performance and efficiency of native execution with the abstraction, security, and immutability of virtualization.
@@ -16,4 +20,4 @@ A [complete usage guide](https://docs.gitlab.com/omnibus/docker/) to these image
## Cloud native images
-GitLab is also working towards a [cloud native set of containers](https://gitlab.com/charts/helm.gitlab.io#docker-container-images), with a single image for each component service. We intend for these images to eventually replace the [Omnibus GitLab based images](#omnibus-gitlab-based-images).
+GitLab is also working towards a [cloud native set of containers](https://docs.gitlab.com/charts/), with a single image for each component service. We intend for these images to eventually replace the [Omnibus GitLab based images](#omnibus-gitlab-based-images).
diff --git a/doc/install/google-protobuf.md b/doc/install/google-protobuf.md
index a531b4519b3..434817c48cb 100644
--- a/doc/install/google-protobuf.md
+++ b/doc/install/google-protobuf.md
@@ -1,26 +1,5 @@
-# Installing a locally compiled google-protobuf gem
+---
+redirect_to: 'installation.md#google-protobuf-loaderror-libx86_64-linux-gnulibcso6-version-glibc_214-not-found'
+---
-First we must find the exact version of google-protobuf that your
-GitLab installation requires.
-
- cd /home/git/gitlab
-
- # Only one of the following two commands will print something. It
- # will look like: * google-protobuf (3.2.0)
- bundle list | grep google-protobuf
- bundle check | grep google-protobuf
-
-Below we use `3.2.0` as an example. Replace it with the version number
-you found above.
-
- cd /home/git/gitlab
- sudo -u git -H gem install google-protobuf --version 3.2.0 --platform ruby
-
-Finally, you can test whether google-protobuf loads correctly. The
-following should print 'OK'.
-
- sudo -u git -H bundle exec ruby -rgoogle/protobuf -e 'puts :OK'
-
-If the `gem install` command fails you may need to install developer
-tools. On Debian: `apt-get install build-essential libgmp-dev`, on
-Centos/RedHat `yum groupinstall 'Development Tools'`.
+This document was moved to [another location](installation.md#google-protobuf-loaderror-libx86_64-linux-gnulibcso6-version-glibc_214-not-found).
diff --git a/doc/install/google_cloud_platform/img/gcp_landing.png b/doc/install/google_cloud_platform/img/gcp_landing.png
deleted file mode 100644
index 92a9873728c..00000000000
--- a/doc/install/google_cloud_platform/img/gcp_landing.png
+++ /dev/null
Binary files differ
diff --git a/doc/install/google_cloud_platform/index.md b/doc/install/google_cloud_platform/index.md
index 242ad70ae82..77c61acbfd4 100644
--- a/doc/install/google_cloud_platform/index.md
+++ b/doc/install/google_cloud_platform/index.md
@@ -1,14 +1,13 @@
---
description: 'Learn how to install a GitLab instance on Google Cloud Platform.'
+type: howto
---
# Installing GitLab on Google Cloud Platform
-![GCP landing page](img/gcp_landing.png)
+This guide will help you install GitLab on a [Google Cloud Platform (GCP)][gcp] instance.
-Getting started with GitLab on a [Google Cloud Platform (GCP)][gcp] instance is quick and easy.
-
-NOTE: **Note:**
+NOTE: **Alternative installation method:**
Google provides a whitepaper for [deploying production-ready GitLab on
Google Kubernetes Engine](https://cloud.google.com/solutions/deploying-production-ready-gitlab-on-gke),
including all steps and external resource configuration. These are an alternative to using a GCP VM, and use
@@ -26,10 +25,9 @@ Once you have performed those two steps, you can [create a VM](#creating-the-vm)
## Creating the VM
-To deploy GitLab on GCP you need to follow five simple steps:
-
-1. Go to <https://console.cloud.google.com/compute/instances> and login with your Google credentials.
+To deploy GitLab on GCP you first need to create a virtual machine:
+1. Go to <https://console.cloud.google.com/compute/instances> and log in with your Google credentials.
1. Click on **Create**
![Search for GitLab](img/launch_vm.png)
@@ -142,3 +140,15 @@ Kerberos, etc. Here are some documents you might be interested in reading:
[ssh]: https://cloud.google.com/compute/docs/instances/connecting-to-instance "Connecting to Linux Instances"
[omni-smtp]: https://docs.gitlab.com/omnibus/settings/smtp.html#smtp-settings "Omnibus GitLab SMTP settings"
[omni-ssl]: https://docs.gitlab.com/omnibus/settings/nginx.html#enable-https "Omnibus GitLab enable HTTPS"
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 6b24dc8abdc..10436a15a9e 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -1,5 +1,28 @@
+---
+type: howto
+---
+
# Installation from source
+This is the official installation guide to set up a production GitLab server
+using the source files. To set up a **development installation** or for many
+other installation options, see the [main installation page](index.md).
+It was created for and tested on **Debian/Ubuntu** operating systems.
+Read [requirements.md](requirements.md) for hardware and operating system requirements.
+If you want to install on RHEL/CentOS, we recommend using the
+[Omnibus packages](https://about.gitlab.com/downloads/).
+
+This guide is long because it covers many cases and includes all commands you
+need, this is [one of the few installation scripts that actually works out of the box](https://twitter.com/robinvdvleuten/status/424163226532986880).
+The following steps have been known to work. **Use caution when you deviate**
+from this guide. Make sure you don't violate any assumptions GitLab makes about
+its environment. For example, many people run into permission problems because
+they changed the location of directories or run services as the wrong user.
+
+If you find a bug/error in this guide, **submit a merge request**
+following the
+[contributing guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md).
+
## Consider the Omnibus package installation
Since an installation from source is a lot of work and error prone we strongly recommend the fast and reliable [Omnibus package installation](https://about.gitlab.com/downloads/) (deb/rpm).
@@ -12,26 +35,42 @@ After this termination Runit will detect Sidekiq is not running and will start i
Since installations from source don't use Runit for process supervision, Sidekiq
can't be terminated and its memory usage will grow over time.
-## Select Version to Install
+## Select version to install
Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install (e.g., `11-7-stable`).
You can select the branch in the version dropdown in the top left corner of GitLab (below the menu bar).
If the highest number stable branch is unclear, check the [GitLab blog](https://about.gitlab.com/blog/) for installation guide links by version.
-## Important Notes
+## GitLab directory structure
-This guide is long because it covers many cases and includes all commands you need, this is [one of the few installation scripts that actually works out of the box](https://twitter.com/robinvdvleuten/status/424163226532986880).
+This is the main directory structure you will end up with following the instructions
+of this page:
-This installation guide was created for and tested on **Debian/Ubuntu** operating systems. Read [requirements.md](requirements.md) for hardware and operating system requirements. If you want to install on RHEL/CentOS, we recommend using the [Omnibus packages](https://about.gitlab.com/downloads/).
+```
+|-- home
+| |-- git
+| |-- .ssh
+| |-- gitlab
+| |-- gitlab-shell
+| |-- repositories
+```
-This is the official installation guide to set up a production server. To set up a **development installation** or for many other installation options, see [the installation section of the README](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#installation).
+- `/home/git/.ssh` - Contains OpenSSH settings. Specifically the `authorized_keys`
+ file managed by gitlab-shell.
+- `/home/git/gitlab` - GitLab core software.
+- `/home/git/gitlab-shell` - Core add-on component of GitLab. Maintains SSH
+ cloning and other functionality.
+- `/home/git/repositories` - Bare repositories for all projects organized by
+ namespace. This is where the git repositories which are pushed/pulled are
+ maintained for all projects. **This area contains critical data for projects.
+ [Keep a backup](../raketasks/backup_restore.md).**
-The following steps have been known to work. **Use caution when you deviate** from this guide. Make sure you don't violate any assumptions GitLab makes about its environment. For example, many people run into permission problems because they changed the location of directories or run services as the wrong user.
+NOTE: **Note:**
+The default locations for repositories can be configured in `config/gitlab.yml`
+of GitLab and `config.yml` of gitlab-shell.
-If you find a bug/error in this guide, **submit a merge request**
-following the
-[contributing guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md).
+For a more in-depth overview, see the [GitLab architecture doc](../development/architecture.md).
## Overview
@@ -324,9 +363,7 @@ Redis 2.8 with:
sudo apt-get install redis-server
```
-If you are using Debian 7 or Ubuntu 12.04, follow the special documentation
-on [an alternate Redis installation](redis.md). Once done, follow the rest of
-the guide here.
+Once done, you can configure Redis:
```sh
# Configure redis to use sockets
@@ -930,8 +967,50 @@ and correctly [configured Nginx](#site-configuration).
### google-protobuf "LoadError: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.14' not found"
This can happen on some platforms for some versions of the
-google-protobuf gem. The workaround is to [install a source-only
-version of this gem](google-protobuf.md).
+`google-protobuf` gem. The workaround is to install a source-only
+version of this gem.
+
+First, you must find the exact version of `google-protobuf` that your
+GitLab installation requires:
+
+```sh
+cd /home/git/gitlab
+
+# Only one of the following two commands will print something. It
+# will look like: * google-protobuf (3.2.0)
+bundle list | grep google-protobuf
+bundle check | grep google-protobuf
+```
+
+Below, `3.2.0` is used as an example. Replace it with the version number
+you found above:
+
+```sh
+cd /home/git/gitlab
+sudo -u git -H gem install google-protobuf --version 3.2.0 --platform ruby
+```
+
+Finally, you can test whether `google-protobuf` loads correctly. The
+following should print 'OK'.
+
+```sh
+sudo -u git -H bundle exec ruby -rgoogle/protobuf -e 'puts :OK'
+```
+
+If the `gem install` command fails, you may need to install the developer
+tools of your OS.
+
+On Debian/Ubuntu:
+
+```sh
+sudo apt-get install build-essential libgmp-dev
+```
+
+On RedHat/CentOS:
+
+```sh
+sudo yum groupinstall 'Development Tools'
+```
[RVM]: https://rvm.io/ "RVM Homepage"
[rbenv]: https://github.com/sstephenson/rbenv "rbenv on GitHub"
diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md
index 7312bf2d4f7..43655767002 100644
--- a/doc/install/kubernetes/index.md
+++ b/doc/install/kubernetes/index.md
@@ -1,41 +1,5 @@
---
-description: 'Read through the different methods to deploy GitLab on Kubernetes.'
+redirect_to: https://docs.gitlab.com/charts/
---
-# Installing GitLab on Kubernetes
-
-NOTE: **Kubernetes experience required:**
-Our Helm charts are recommended for those who are familiar with Kubernetes.
-If you're not sure if Kubernetes is for you, our
-[Omnibus GitLab packages](../README.md#installing-gitlab-using-the-omnibus-gitlab-package-recommended)
-are mature, scalable, support [high availability](../../administration/high_availability/README.md)
-and are used today on GitLab.com.
-It is not necessary to have GitLab installed on Kubernetes in order to use [GitLab Kubernetes integration](https://docs.gitlab.com/ee/user/project/clusters/index.html).
-
-The easiest method to deploy GitLab on [Kubernetes](https://kubernetes.io/) is
-to take advantage of GitLab's Helm charts. [Helm](https://github.com/kubernetes/helm/blob/master/README.md)
-is a package management tool for Kubernetes, allowing apps to be easily managed via their
-Charts. A [Chart](https://github.com/kubernetes/charts) is a detailed description
-of the application including how it should be deployed, upgraded, and configured.
-
-## GitLab Chart
-
-This chart contains all the required components to get started, and can scale to
-large deployments. It offers a number of benefits, among others:
-
-- Horizontal scaling of individual components.
-- No requirement for shared storage to scale.
-- Containers do not need `root` permissions.
-- Automatic SSL with Let's Encrypt.
-- An unprivileged GitLab Runner.
-
-Learn more about the [GitLab chart](https://docs.gitlab.com/charts/).
-
-## GitLab Runner Chart
-
-If you already have a GitLab instance running, inside or outside of Kubernetes,
-and you'd like to leverage the Runner's
-[Kubernetes capabilities](https://docs.gitlab.com/runner/executors/kubernetes.html),
-it can be deployed with the GitLab Runner chart.
-
-Learn more about the [GitLab Runner chart](https://docs.gitlab.com/runner/install/kubernetes.html).
+This document was moved to [another location](https://docs.gitlab.com/charts/).
diff --git a/doc/install/openshift_and_gitlab/index.md b/doc/install/openshift_and_gitlab/index.md
index 77bd9e9f7a9..18981c43464 100644
--- a/doc/install/openshift_and_gitlab/index.md
+++ b/doc/install/openshift_and_gitlab/index.md
@@ -1,8 +1,5 @@
---
-author: Achilleas Pipinellis
-author_gitlab: axil
-level: intermediary
-article_type: tutorial
+type: howto
date: 2016-06-28
---
diff --git a/doc/install/pivotal/index.md b/doc/install/pivotal/index.md
index 896e01ad975..f068572f1e9 100644
--- a/doc/install/pivotal/index.md
+++ b/doc/install/pivotal/index.md
@@ -1,45 +1,12 @@
# GitLab Pivotal Tile **[PREMIUM ONLY]**
-> Introduced in [GitLab Premium][eep] 8.2.
-
-Easily deploy GitLab as a pre-configured appliance using Ops Manager (BOSH) for
-[Pivotal Cloud Foundry][pcf].
-
-## Overview
-
-Enterprise admins want their development toolkit to be more customizable, more
-integrated, and more secure. With Pivotal Cloud Foundry, GitLab is installed and
-scales easily in a highly available environment.
-
-The upgrades are pain-free and well tested. All it takes is upload the new tile
-and click a button to begin the upgrade process.
-
-## Use cases
-
-- You want a highly available deployment with minimal effort. Scale horizontally
- as your user base grows.
-
-## Features
-
-The GitLab Pivotal Tile is based on [GitLab Premium][eep] and includes nearly all of its features. The features in Premium but _not_ supported on the Tile are:
-
-* PostgreSQL
-* Pages
-* Geo
-* Registry
-* Mattermost
-* Subgroups
-* Elasticsearch
-* Service Desk
-* OAuth & Kerberos Authentication
-
-## Installing GitLab with Pivotal
-
-The product information and installation documentation is hosted on Pivotal's
-website:
-
-- [Product page](https://network.pivotal.io/products/p-gitlab/)
-- [Documentation](https://docs.pivotal.io/partners/gitlab/index.html)
-
-[eep]: https://about.gitlab.com/pricing/
-[pcf]: https://pivotal.io/platform
+CAUTION: **Discontinued:**
+As of September 13, 2017, the GitLab Enterprise Plus for Pivotal Cloud Foundry
+tile on Pivotal Network has reached its End of Availability (“EoA”) and is no
+longer available for download or sale through Pivotal. Current customers with
+active subscriptions will continue to receive support from GitLab through their
+subscription term. Pivotal and GitLab are collaborating on creating a new
+Kubernetes-based tile for the Pivotal Container Service. Please contact GitLab
+support with any questions regarding GitLab Enterprise Plus for Pivotal Cloud Foundry.
+
+Original article: <https://docs.pivotal.io/partners/gitlab/index.html>.
diff --git a/doc/install/redis.md b/doc/install/redis.md
index 4075e6283d0..cff5c2f2611 100644
--- a/doc/install/redis.md
+++ b/doc/install/redis.md
@@ -1,60 +1,5 @@
-# Install Redis on old distributions
+---
+redirect_to: installation.md#7-redis
+---
-GitLab requires at least Redis 2.8. The following guide is for Debian 7 and
-Ubuntu 12.04. If you are using Debian 8 or Ubuntu 14.04 and up, follow the
-[installation guide](installation.md).
-
-## Install Redis 2.8 in Debian 7
-
-Redis 2.8 is included in the Debian Wheezy [backports] repository.
-
-1. Edit `/etc/apt/sources.list` and add the following line:
-
- ```
- deb http://http.debian.net/debian wheezy-backports main
- ```
-
-1. Update the repositories:
-
- ```
- sudo apt-get update
- ```
-
-1. Install `redis-server`:
-
- ```
- sudo apt-get -t wheezy-backports install redis-server
- ```
-
-1. Follow the rest of the [installation guide](installation.md).
-
-## Install Redis 2.8 in Ubuntu 12.04
-
-We will [use a PPA](https://launchpad.net/~chris-lea/+archive/ubuntu/redis-server)
-to install a recent version of Redis.
-
-1. Install the PPA repository:
-
- ```
- sudo add-apt-repository ppa:chris-lea/redis-server
- ```
-
- Your system will now fetch the PPA's key. This enables your Ubuntu system to
- verify that the packages in the PPA have not been interfered with since they
- were built.
-
-1. Update the repositories:
-
- ```
- sudo apt-get update
- ```
-
-1. Install `redis-server`:
-
- ```
- sudo apt-get install redis-server
- ```
-
-1. Follow the rest of the [installation guide](installation.md).
-
-[backports]: http://backports.debian.org/Instructions/ "Debian backports website"
+This document was moved to [another location](installation.md#7-redis).
diff --git a/doc/install/relative_url.md b/doc/install/relative_url.md
index 5f129fd3bd1..96b7d0f3648 100644
--- a/doc/install/relative_url.md
+++ b/doc/install/relative_url.md
@@ -1,18 +1,19 @@
+---
+type: reference
+---
+
# Install GitLab under a relative URL
-NOTE: **Note:**
+While it is recommended to install GitLab on its own (sub)domain, sometimes
+this is not possible due to a variety of reasons. In that case, GitLab can also
+be installed under a relative URL, for example `https://example.com/gitlab`.
+
This document describes how to run GitLab under a relative URL for installations
from source. If you are using an Omnibus package,
[the steps are different][omnibus-rel]. Use this guide along with the
[installation guide](installation.md) if you are installing GitLab for the
first time.
----
-
-While it is recommended to install GitLab on its own (sub)domain, sometimes
-this is not possible due to a variety of reasons. In that case, GitLab can also
-be installed under a relative URL, for example `https://example.com/gitlab`.
-
There is no limit to how deeply nested the relative URL can be. For example you
could serve GitLab under `/foo/bar/gitlab/git` without any issues.
@@ -20,8 +21,6 @@ Note that by changing the URL on an existing GitLab installation, all remote
URLs will change, so you'll have to manually edit them in any local repository
that points to your GitLab instance.
----
-
The TL;DR list of configuration files that you need to change in order to
serve GitLab under a relative URL is:
@@ -126,3 +125,15 @@ To disable the relative URL:
[omnibus-rel]: http://docs.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab "How to set up relative URL in Omnibus GitLab"
[restart gitlab]: ../administration/restart_gitlab.md#installations-from-source "How to restart GitLab"
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index f6a52205a0e..107d48fb90c 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -1,5 +1,12 @@
+---
+type: reference
+---
+
# Requirements
+This page includes useful information on the supported Operating Systems as well
+as the hardware requirements that are needed to install and use GitLab.
+
## Operating Systems
### Supported Unix distributions
@@ -12,7 +19,7 @@
- Scientific Linux (please use the CentOS packages and instructions)
- Oracle Linux (please use the CentOS packages and instructions)
-For the installations options please see [the installation page on the GitLab website](https://about.gitlab.com/installation/).
+For the installations options, see [the main installation page](README.md).
### Unsupported Unix distributions
@@ -105,9 +112,9 @@ features of GitLab work with MySQL/MariaDB:
1. MySQL support for subgroups was [dropped with GitLab 9.3][post].
See [issue #30472][30472] for more information.
-1. Geo does [not support MySQL](https://docs.gitlab.com/ee/administration/geo/replication/database.html). This means no supported Disaster Recovery solution if using MySQL. **[PREMIUM ONLY]**
+1. Geo does [not support MySQL](../administration/geo/replication/database.md). This means no supported Disaster Recovery solution if using MySQL. **[PREMIUM ONLY]**
1. [Zero downtime migrations](../update/README.md#upgrading-without-downtime) do not work with MySQL.
-1. [Database load balancing](https://docs.gitlab.com/ee/administration/database_load_balancing.html) is
+1. [Database load balancing](../administration/database_load_balancing.md) is
supported only for PostgreSQL. **[PREMIUM ONLY]**
1. GitLab [optimizes the loading of dashboard events](https://gitlab.com/gitlab-org/gitlab-ce/issues/31806) using [PostgreSQL LATERAL JOINs](https://blog.heapanalytics.com/postgresqls-powerful-new-join-type-lateral/).
1. In general, SQL optimized for PostgreSQL may run much slower in MySQL due to
@@ -143,14 +150,14 @@ On some systems you may need to install an additional package (e.g.
#### Additional requirements for GitLab Geo
-If you are using [GitLab Geo](https://docs.gitlab.com/ee/development/geo.html):
+If you are using [GitLab Geo](../development/geo.md):
- We strongly recommend running Omnibus-managed instances as they are actively
developed and tested. We aim to be compatible with most external (not managed
by Omnibus) databases (for example, AWS RDS) but we do not guarantee
compatibility.
- The
- [tracking database](https://docs.gitlab.com/ee/development/geo.html#geo-tracking-database)
+ [tracking database](../development/geo.md#using-the-tracking-database)
requires the
[postgres_fdw](https://www.postgresql.org/docs/9.6/static/postgres-fdw.html)
extension.
@@ -201,12 +208,23 @@ you decide to run GitLab Runner and the GitLab Rails application on the same
machine.
It is also not safe to install everything on a single machine, because of the
-[security reasons](https://docs.gitlab.com/runner/security/)
-- especially when you plan to use shell executor with GitLab
+[security reasons](https://docs.gitlab.com/runner/security/), especially when you plan to use shell executor with GitLab
Runner.
We recommend using a separate machine for each GitLab Runner, if you plan to
use the CI features.
+The GitLab Runner server requirements depend on:
+
+- The type of [executor](https://docs.gitlab.com/runner/executors/) you configured on GitLab Runner.
+- Resources required to run build jobs.
+- Job concurrency settings.
+
+Since the nature of the jobs varies for each use case, you will need to experiment by adjusting the job concurrency to get the optimum setting.
+
+For reference, GitLab.com's [auto-scaling shared runner](../user/gitlab_com/index.md#shared-runners) is configured so that a **single job** will run in a **single instance** with:
+
+- 1vCPU.
+- 3.75GB of RAM.
## Supported web browsers
@@ -225,3 +243,15 @@ Each time a new browser version is released, we begin supporting that version an
NOTE: **Note:** We do not support running GitLab with JavaScript disabled in the browser and have no plans of supporting that
in the future because we have features such as Issue Boards which require JavaScript extensively.
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/install/structure.md b/doc/install/structure.md
index 8fc6ab4ab2f..87ef11c60fe 100644
--- a/doc/install/structure.md
+++ b/doc/install/structure.md
@@ -1,19 +1,5 @@
-# GitLab directory structure
+---
+redirect_to: installation.md#gitlab-directory-structure
+---
-This is the directory structure you will end up with following the instructions in the Installation Guide.
-
- |-- home
- | |-- git
- | |-- .ssh
- | |-- gitlab
- | |-- gitlab-shell
- | |-- repositories
-
-- `/home/git/.ssh` - contains openssh settings. Specifically the `authorized_keys` file managed by gitlab-shell.
-- `/home/git/gitlab` - GitLab core software.
-- `/home/git/gitlab-shell` - Core add-on component of GitLab. Maintains SSH cloning and other functionality.
-- `/home/git/repositories` - bare repositories for all projects organized by namespace. This is where the git repositories which are pushed/pulled are maintained for all projects. **This area is critical data for projects. [Keep a backup](../raketasks/backup_restore.md).**
-
-*Note: the default locations for repositories can be configured in `config/gitlab.yml` of GitLab and `config.yml` of gitlab-shell.*
-
-To see a more in-depth overview see the [GitLab architecture doc](../development/architecture.md).
+This page was moved to [another location](#installation.md#gitlab-directory-structure).
diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md
index 15176ede733..57a5a42fbed 100644
--- a/doc/integration/elasticsearch.md
+++ b/doc/integration/elasticsearch.md
@@ -8,8 +8,8 @@ This document describes how to set up Elasticsearch with GitLab. Once enabled,
you'll have the benefit of fast search response times and the advantage of two
special searches:
-- [Advanced Global Search](https://docs.gitlab.com/ee/user/search/advanced_global_search.html)
-- [Advanced Syntax Search](https://docs.gitlab.com/ee/user/search/advanced_search_syntax.html)
+- [Advanced Global Search](../user/search/advanced_global_search.md)
+- [Advanced Syntax Search](../user/search/advanced_search_syntax.md)
## Version Requirements
<!-- Please remember to update ee/lib/system_check/app/elasticsearch_check.rb if this changes -->
@@ -192,9 +192,6 @@ Performing asynchronous indexing, as this will describe, will generate a lot of
Make sure to prepare for this task by either [Horizontally Scaling](../administration/high_availability/README.md#basic-scaling)
or creating [extra sidekiq processes](../administration/operations/extra_sidekiq_processes.md)
-NOTE: **Note**:
-After indexing the repositories asynchronously, you **MUST** index the database to be able to search.
-
Configure Elasticsearch's host and port in **Admin > Settings > Integrations**. Then create empty indexes using one of the following commands:
```sh
@@ -217,78 +214,49 @@ curl --request PUT localhost:9200/gitlab-production/_settings --data '{
} }'
```
-Then enable Elasticsearch indexing and run repository indexing tasks:
+Then enable Elasticsearch indexing and run project indexing tasks:
```sh
# Omnibus installations
-sudo gitlab-rake gitlab:elastic:index_repositories_async
+sudo gitlab-rake gitlab:elastic:index_projects
# Installations from source
-bundle exec rake gitlab:elastic:index_repositories_async RAILS_ENV=production
+bundle exec rake gitlab:elastic:index_projects RAILS_ENV=production
```
-This enqueues a number of Sidekiq jobs to index your existing repositories.
-You can view the jobs in the admin panel (they are placed in the `elastic_batch_project_indexer`)
+This enqueues a Sidekiq job for each project that needs to be indexed.
+You can view the jobs in the admin panel (they are placed in the `elastic_indexer`
queue), or you can query indexing status using a rake task:
```sh
# Omnibus installations
-sudo gitlab-rake gitlab:elastic:index_repositories_status
+sudo gitlab-rake gitlab:elastic:index_projects_status
# Installations from source
-bundle exec rake gitlab:elastic:index_repositories_status RAILS_ENV=production
+bundle exec rake gitlab:elastic:index_projects_status RAILS_ENV=production
Indexing is 65.55% complete (6555/10000 projects)
```
-By default, one job is created for every 300 projects. For large numbers of
-projects, you may wish to increase the batch size, by setting the `BATCH`
-environment variable.
-
-You can also run the initial indexing synchronously - this is most useful if
-you have a small number of projects or need finer-grained control over indexing
-than Sidekiq permits:
+If you want to limit the index to a range of projects you can provide the
+`ID_FROM` and `ID_TO` parameters:
```sh
# Omnibus installations
-sudo gitlab-rake gitlab:elastic:index_repositories
+sudo gitlab-rake gitlab:elastic:index_projects ID_FROM=1001 ID_TO=2000
# Installations from source
-bundle exec rake gitlab:elastic:index_repositories RAILS_ENV=production
-```
-
-It might take a while depending on how big your Git repositories are.
-
-If you want to run several tasks in parallel (probably in separate terminal
-windows) you can provide the `ID_FROM` and `ID_TO` parameters:
-
-```sh
-# Omnibus installations
-sudo gitlab-rake gitlab:elastic:index_repositories ID_FROM=1001 ID_TO=2000
-
-# Installations from source
-bundle exec rake gitlab:elastic:index_repositories ID_FROM=1001 ID_TO=2000 RAILS_ENV=production
+bundle exec rake gitlab:elastic:index_projects ID_FROM=1001 ID_TO=2000 RAILS_ENV=production
```
Where `ID_FROM` and `ID_TO` are project IDs. Both parameters are optional.
-As an example, if you have 3,000 repositories and you want to run three separate indexing tasks, you might run:
+The above examples will index all projects starting with ID `1001` up to (and including) ID `2000`.
-```sh
-# Omnibus installations
-sudo gitlab-rake gitlab:elastic:index_repositories ID_TO=1000
-sudo gitlab-rake gitlab:elastic:index_repositories ID_FROM=1001 ID_TO=2000
-sudo gitlab-rake gitlab:elastic:index_repositories ID_FROM=2001
-
-# Installations from source
-bundle exec rake gitlab:elastic:index_repositories RAILS_ENV=production ID_TO=1000
-bundle exec rake gitlab:elastic:index_repositories RAILS_ENV=production ID_FROM=1001 ID_TO=2000
-bundle exec rake gitlab:elastic:index_repositories RAILS_ENV=production ID_FROM=2001
-```
-
-Sometimes your repository index process `gitlab:elastic:index_repositories` or
-`gitlab:elastic:index_repositories_async` can get interrupted. This may happen
-for many reasons, but it's always safe to run the indexing job again - it will
-skip those repositories that have already been indexed.
+TIP: **Troubleshooting:**
+Sometimes the project indexing jobs queued by `gitlab:elastic:index_projects`
+can get interrupted. This may happen for many reasons, but it's always safe
+to run the indexing task again - it will skip those repositories that have
+already been indexed.
As the indexer stores the last commit SHA of every indexed repository in the
database, you can run the indexer with the special parameter `UPDATE_INDEX` and
@@ -297,10 +265,10 @@ that repository is indexed, it can be useful in case if your index is outdated:
```sh
# Omnibus installations
-sudo gitlab-rake gitlab:elastic:index_repositories UPDATE_INDEX=true ID_TO=1000
+sudo gitlab-rake gitlab:elastic:index_projects UPDATE_INDEX=true ID_TO=1000
# Installations from source
-bundle exec rake gitlab:elastic:index_repositories UPDATE_INDEX=true ID_TO=1000 RAILS_ENV=production
+bundle exec rake gitlab:elastic:index_projects UPDATE_INDEX=true ID_TO=1000 RAILS_ENV=production
```
You can also use the `gitlab:elastic:clear_index_status` Rake task to force the
@@ -320,16 +288,6 @@ bundle exec rake gitlab:elastic:index_wikis RAILS_ENV=production
The wiki indexer also supports the `ID_FROM` and `ID_TO` parameters if you want
to limit a project set.
-Index all database entities (Keep in mind it can take a while, so consider using `screen` or `tmux`):
-
-```sh
-# Omnibus installations
-sudo gitlab-rake gitlab:elastic:index_database
-
-# Installations from source
-bundle exec rake gitlab:elastic:index_database RAILS_ENV=production
-```
-
Enable replication and refreshing again after indexing (only if you previously disabled it):
```bash
@@ -376,25 +334,15 @@ There are several rake tasks available to you via the command line:
- This is a wrapper task. It does the following:
- `sudo gitlab-rake gitlab:elastic:create_empty_index`
- `sudo gitlab-rake gitlab:elastic:clear_index_status`
+ - `sudo gitlab-rake gitlab:elastic:index_projects`
- `sudo gitlab-rake gitlab:elastic:index_wikis`
- - `sudo gitlab-rake gitlab:elastic:index_database`
- - `sudo gitlab-rake gitlab:elastic:index_repositories`
-- [sudo gitlab-rake gitlab:elastic:index_repositories_async](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- - This iterates over all projects and places them in batches. It then sends these batches to the background via sidekiq jobs to be indexed.
-- [sudo gitlab-rake gitlab:elastic:index_repositories_status](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - `sudo gitlab-rake gitlab:elastic:index_snippets`
+- [sudo gitlab-rake gitlab:elastic:index_projects](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - This iterates over all projects and queues sidekiq jobs to index them in the background.
+- [sudo gitlab-rake gitlab:elastic:index_projects_status](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- This determines the overall status of the indexing. It is done by counting the total number of indexed projects, dividing by a count of the total number of projects, then multiplying by 100.
-- [sudo gitlab-rake gitlab:elastic:index_repositories](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- - This iterates over all projects and places them in batches. It then performs indexing on said batches synchronously.
- [sudo gitlab-rake gitlab:elastic:index_wikis](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- Iterates over every project, determines if said project contains wiki data, and then indexes the blobs (content) of said wiki data.
-- [sudo gitlab-rake gitlab:elastic:index_database](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- - This is a [rake multitask](https://www.rubydoc.info/github/ruby/rake/Rake/MultiTask). It does the following:
- - `sudo gitlab-rake gitlab:elastic:index_projects`
- - `sudo gitlab-rake gitlab:elastic:index_issues`
- - `sudo gitlab-rake gitlab:elastic:index_merge_requests`
- - `sudo gitlab-rake gitlab:elastic:index_snippets`
- - `sudo gitlab-rake gitlab:elastic:index_notes`
- - `sudo gitlab-rake gitlab:elastic:index_milestones`
- [sudo gitlab-rake gitlab:elastic:create_empty_index](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- This generates an empty index on the Elasticsearch side.
- [sudo gitlab-rake gitlab:elastic:clear_index_status](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
@@ -405,18 +353,8 @@ There are several rake tasks available to you via the command line:
- Does the same thing as `sudo gitlab-rake gitlab:elastic:create_empty_index`
- [sudo gitlab-rake gitlab:elastic:add_feature_visibility_levels_to_project](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- Adds visibility information to the indices for projects.
-- [sudo gitlab-rake gitlab:elastic:index_projects](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- - Performs an Elasticsearch import that indexes projects data.
-- [sudo gitlab-rake gitlab:elastic:index_issues](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- - Performs an Elasticsearch import that indexes issues data.
-- [sudo gitlab-rake gitlab:elastic:index_merge_requests](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- - Performs an Elasticsearch import that indexes merge requests data.
- [sudo gitlab-rake gitlab:elastic:index_snippets](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- Performs an Elasticsearch import that indexes the snippets data.
-- [sudo gitlab-rake gitlab:elastic:index_notes](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- - Performs an Elasticsearch import that indexes the notes data.
-- [sudo gitlab-rake gitlab:elastic:index_milestones](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- - Performs an Elasticsearch import that indexes the milestones data.
### Environment Variables
@@ -424,40 +362,16 @@ In addition to the rake tasks, there are some environment variables that can be
| Environment Variable | Data Type | What it does |
| -------------------- |:---------:| ---------------------------------------------------------------------------- |
-| `BATCH` | Integer | Modifies the size of the indexing batch (default 300 projects). |
| `UPDATE_INDEX` | Boolean | Tells the indexer to overwrite any existing index data (true/false). |
| `ID_TO` | Integer | Tells the indexer to only index projects less than or equal to the value. |
| `ID_FROM` | Integer | Tells the indexer to only index projects greater than or equal to the value. |
-### Batching
-
-The ability to apply batching makes the indexer run more efficiently. The default
-size of a batch is 300 projects, which may or may not be ideal for your setup.
-Depending on the resources available to your GitLab instance (sidekiq) and your
-Elasticsearch instance (RAM, CPU), you may be able to increase or decrease the
-batch size for more efficiency.
-
-- The larger the batch size is, the less sidekiq jobs and indexing requests get created.
-- The larger the batch size is, the more time and RAM it takes to process.
-- The smaller the batch size, the more sidekiq jobs, and indexing requests get created.
-- The smaller the batch size, the more CPU gets utilized.
-
-Finding the ideal size can be tricky, and will vary from GitLab instance to GitLab instance.
-Generally speaking, if the default is not ideal for you, try reducing it to somewhere in
-the 50-150 range (for bigger sized repos) or 450-600 range (for many small-sized repos).
-
-Example use:
-
-```sh
-sudo gitlab-rake gitlab:elastic:index_repositories_async BATCH=50
-```
-
### Indexing a specific project
Because the `ID_TO` and `ID_FROM` environment variables use the `or equal to` comparison, you can index only one project by using both these variables with the same project ID number:
```sh
-root@git:~# sudo gitlab-rake gitlab:elastic:index_repositories ID_TO=5 ID_FROM=5
+root@git:~# sudo gitlab-rake gitlab:elastic:index_projects ID_TO=5 ID_FROM=5
Indexing project repositories...I, [2019-03-04T21:27:03.083410 #3384] INFO -- : Indexing GitLab User / test (ID=33)...
I, [2019-03-04T21:27:05.215266 #3384] INFO -- : Indexing GitLab User / test (ID=33) is done!
```
@@ -554,7 +468,7 @@ Here are some common pitfalls and how to overcome them:
- **The indexing process is taking a very long time**
- The more data present in your GitLab instance, the longer the indexing process takes. You might want to try adjusting the BATCH sizes for asynchronous indexing to help speed up the process.
+ The more data present in your GitLab instance, the longer the indexing process takes.
- **No new data is added to the Elasticsearch index when I push code**
diff --git a/doc/integration/github.md b/doc/integration/github.md
index e145afbdd5e..5b01dd9feb7 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -21,10 +21,10 @@ To get the credentials (a pair of Client ID and Client Secret), you must registe
- Application name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
- Homepage URL: The URL of your GitLab installation. For example, `https://gitlab.example.com`.
- Application description: Fill this in if you wish.
- - Authorization callback URL: `http(s)://${YOUR_DOMAIN}/users/auth/github/callback`. Please make sure the port is included if your GitLab instance is not configured on default port.
+ - Authorization callback URL: `http(s)://${YOUR_DOMAIN}/users/auth`. Please make sure the port is included if your GitLab instance is not configured on default port.
![Register OAuth App](img/github_register_app.png)
- NOTE: Be sure to append `/users/auth/github/callback` to the end of the callback URL
+ NOTE: Be sure to append `/users/auth` to the end of the callback URL
to prevent a [OAuth2 convert
redirect](http://tetraph.com/covert_redirect/) vulnerability.
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 15d9d8c9c74..22e07594d6f 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -1,6 +1,6 @@
# SAML OmniAuth Provider
-> This topic is for SAML on self-managed GitLab instances. For SAML on GitLab.com, see [SAML SSO for GitLab.com Groups](https://docs.gitlab.com/ee/user/group/saml_sso/index.html).
+> This topic is for SAML on self-managed GitLab instances. For SAML on GitLab.com, see [SAML SSO for GitLab.com Groups](../user/group/saml_sso/index.md).
NOTE: **Note:**
You need to [enable OmniAuth](omniauth.md) in order to use this.
diff --git a/doc/integration/slash_commands.md b/doc/integration/slash_commands.md
index cd755089be8..71ea2e25533 100644
--- a/doc/integration/slash_commands.md
+++ b/doc/integration/slash_commands.md
@@ -20,8 +20,8 @@ Taking the trigger term as `project-name`, the commands are:
| `/project-name deploy <from> to <to>` | Deploy from the `<from>` environment to the `<to>` environment |
| `/project-name run <job name> <arguments>` | Execute [ChatOps](../ci/chatops/README.md) job `<job name>` on `master` |
-Note that if you are using the [GitLab Slack application](https://docs.gitlab.com/ee/user/project/integrations/gitlab_slack_application.html) for
-your GitLab.com projects, you need to [add the `gitlab` keyword at the beginning of the command](https://docs.gitlab.com/ee/user/project/integrations/gitlab_slack_application.html#usage).
+Note that if you are using the [GitLab Slack application](../user/project/integrations/gitlab_slack_application.md) for
+your GitLab.com projects, you need to [add the `gitlab` keyword at the beginning of the command](../user/project/integrations/gitlab_slack_application.md#usage).
## Issue commands
diff --git a/doc/license/README.md b/doc/license/README.md
index b9281fd5299..fd110a39b61 100644
--- a/doc/license/README.md
+++ b/doc/license/README.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'https://docs.gitlab.com/ee/user/admin_area/license.html'
+redirect_to: '../user/admin_area/license.md'
---
-This document was moved to [another location](https://docs.gitlab.com/ee/user/admin_area/license.html).
+This document was moved to [another location](../user/admin_area/license.md).
diff --git a/doc/push_rules/push_rules.md b/doc/push_rules/push_rules.md
index 7654023f266..e44eab2556e 100644
--- a/doc/push_rules/push_rules.md
+++ b/doc/push_rules/push_rules.md
@@ -61,7 +61,7 @@ The following options are available.
| --------- | :------------: | ----------- |
| Removal of tags with `git push` | **Starter** 7.10 | Forbid users to remove git tags with `git push`. Tags will still be able to be deleted through the web UI. |
| Check whether author is a GitLab user | **Starter** 7.10 | Restrict commits by author (email) to existing GitLab users. |
-| Check whether committer is the current authenticated user | **Premium** 10.2 | GitLab will reject any commit that was not committed by the current authenticated user |
+| Committer restriction | **Premium** 10.2 | GitLab will reject any commit that was not committed by the current authenticated user |
| Check whether commit is signed through GPG | **Premium** 10.1 | Reject commit when it is not signed through GPG. Read [signing commits with GPG][signing-commits]. |
| Prevent committing secrets to Git | **Starter** 8.12 | GitLab will reject any files that are likely to contain secrets. Read [what files are forbidden](#prevent-pushing-secrets-to-the-repository). |
| Restrict by commit message | **Starter** 7.10 | Only commit messages that match this regular expression are allowed to be pushed. Leave empty to allow any commit message. Uses multiline mode, which can be disabled using `(?-m)`. |
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 56db7b5eb3a..764916ca82d 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -917,9 +917,9 @@ backup beforehand.
1. Clear all the tokens for projects, groups, and the whole instance:
-CAUTION: **Caution:**
-The last UPDATE operation will stop the runners being able to pick up
-new jobs. You must register new runners.
+ CAUTION: **Caution:**
+ The last UPDATE operation will stop the runners being able to pick up
+ new jobs. You must register new runners.
```sql
-- Clear project tokens
diff --git a/doc/security/img/two_factor_authentication_group_settings.png b/doc/security/img/two_factor_authentication_group_settings.png
deleted file mode 100644
index 05d95554fd9..00000000000
--- a/doc/security/img/two_factor_authentication_group_settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/security/img/two_factor_authentication_settings.png b/doc/security/img/two_factor_authentication_settings.png
deleted file mode 100644
index 2a2208f98bd..00000000000
--- a/doc/security/img/two_factor_authentication_settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/security/two_factor_authentication.md b/doc/security/two_factor_authentication.md
index 4b65b901487..2ece4ed3fc9 100644
--- a/doc/security/two_factor_authentication.md
+++ b/doc/security/two_factor_authentication.md
@@ -16,39 +16,35 @@ enforce everyone to set up 2FA, you can choose from two different ways:
- Enforce on next login.
- Suggest on next login, but allow a grace period before enforcing.
-In the Admin area under **Settings** (`/admin/application_settings`), look for
-the "Sign-in Restrictions" area, where you can configure both.
+After the configured grace period has elapsed, users will be able to log in but
+won't be able to leave the 2FA configuration area at `/profile/two_factor_auth`.
+
+To enable 2FA for all users:
+
+1. Navigate to **Admin area > Settings > General** (`/admin/application_settings`).
+1. Expand the **Sign-in restrictions** section, where you can configure both.
If you want 2FA enforcement to take effect on next login, change the grace
period to `0`.
----
-
-![Two factor authentication admin settings](img/two_factor_authentication_settings.png)
+## Enforcing 2FA for all users in a group
----
+If you want to enforce 2FA only for certain groups, you can:
-## Enforcing 2FA for all users in a group
+1. Enable it in the group's **Settings > General** page.
+1. Optionally specify a grace period as above.
-If you want to enforce 2FA only for certain groups, you can enable it in the
-group settings and specify a grace period as above. To change this setting you
-need to be administrator or owner of the group.
+To change this setting, you need to be administrator or owner of the group.
If there are multiple 2FA requirements (i.e. group + all users, or multiple
groups) the shortest grace period will be used.
----
-
-![Two factor authentication group settings](img/two_factor_authentication_group_settings.png)
-
----
-
## Disabling 2FA for everyone
There may be some special situations where you want to disable 2FA for everyone
even when forced 2FA is disabled. There is a rake task for that:
-```
+```sh
# Omnibus installations
sudo gitlab-rake gitlab:two_factor:disable_for_all_users
@@ -56,5 +52,6 @@ sudo gitlab-rake gitlab:two_factor:disable_for_all_users
sudo -u git -H bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
```
-**IMPORTANT: this is a permanent and irreversible action. Users will have to
- reactivate 2FA from scratch if they want to use it again.**
+CAUTION: **Caution:**
+This is a permanent and irreversible action. Users will have to
+reactivate 2FA from scratch if they want to use it again.
diff --git a/doc/subscriptions/index.md b/doc/subscriptions/index.md
index b6d16536fee..dfd80f8882e 100644
--- a/doc/subscriptions/index.md
+++ b/doc/subscriptions/index.md
@@ -38,7 +38,7 @@ Future purchases will use the information in this section. The email listed in t
### Self-managed: Apply your license file
-After purchase, the license file is sent to the email address tied to the Customers portal account, which needs to be [uploaded to the GitLab instance](https://docs.gitlab.com/ee/user/admin_area/license.html#uploading-your-license).
+After purchase, the license file is sent to the email address tied to the Customers portal account, which needs to be [uploaded to the GitLab instance](../user/admin_area/license.md#uploading-your-license).
### Link your GitLab.com account with your Customers Portal account
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index 00b6c1dfdc2..228da2d1f57 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -17,11 +17,11 @@ This page gathers all the resources for the topic **Authentication** within GitL
## GitLab administrators
- [LDAP (Community Edition)](../../administration/auth/ldap.md)
-- [LDAP (Enterprise Edition)](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html) **[STARTER]**
+- [LDAP (Enterprise Edition)](../../administration/auth/ldap-ee.md) **[STARTER]**
- [Enforce Two-factor Authentication (2FA)](../../security/two_factor_authentication.md#enforce-two-factor-authentication-2fa)
- **Articles:**
- [How to Configure LDAP with GitLab CE](../../administration/auth/how_to_configure_ldap_gitlab_ce/index.md)
- - [How to Configure LDAP with GitLab EE](https://docs.gitlab.com/ee/administration/auth/how_to_configure_ldap_gitlab_ee/index.html) **[STARTER]**
+ - [How to Configure LDAP with GitLab EE](../../administration/auth/how_to_configure_ldap_gitlab_ee/index.md) **[STARTER]**
- [Feature Highlight: LDAP Integration](https://about.gitlab.com/2014/07/10/feature-highlight-ldap-sync/)
- [Debugging LDAP](https://about.gitlab.com/handbook/support/workflows/support-engineering/ldap/debugging_ldap.html)
- **Integrations:**
@@ -30,10 +30,10 @@ This page gathers all the resources for the topic **Authentication** within GitL
- [Atlassian Crowd OmniAuth Provider](../../administration/auth/crowd.md)
- [CAS OmniAuth Provider](../../integration/cas.md)
- [SAML OmniAuth Provider](../../integration/saml.md)
- - [SAML for GitLab.com Groups](https://docs.gitlab.com/ee/user/group/saml_sso/index.html) **[SILVER ONLY]**
- - [SCIM user provisioning for GitLab.com Groups](https://docs.gitlab.com/ee/user/group/saml_sso/scim_setup.html) **[SILVER ONLY]**
+ - [SAML for GitLab.com Groups](../../user/group/saml_sso/index.md) **[SILVER ONLY]**
+ - [SCIM user provisioning for GitLab.com Groups](../../user/group/saml_sso/scim_setup.md) **[SILVER ONLY]**
- [Okta SSO provider](../../administration/auth/okta.md)
- - [Kerberos integration (GitLab EE)](https://docs.gitlab.com/ee/integration/kerberos.html) **[STARTER]**
+ - [Kerberos integration (GitLab EE)](../../integration/kerberos.md) **[STARTER]**
## API
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 4ceccaabf86..b00a8afa386 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -13,7 +13,7 @@ Starting with GitLab 11.3, the Auto DevOps pipeline is enabled by default for al
projects. If it has not been explicitly enabled for the project, Auto DevOps will be automatically
disabled on the first pipeline failure. Your project will continue to use an alternative
[CI/CD configuration file](../../ci/yaml/README.md) if one is found. A GitLab
-administrator can [change this setting](../../user/admin_area/settings/continuous_integration.html#auto-devops-core-only)
+administrator can [change this setting](../../user/admin_area/settings/continuous_integration.md#auto-devops-core-only)
in the admin area.
With Auto DevOps, the software development process becomes easier to set up
@@ -181,7 +181,7 @@ Those environments are tied to jobs that use [Auto Deploy](#auto-deploy), so
except for the environment scope, they would also need to have a different
domain they would be deployed to. This is why you need to define a separate
`KUBE_INGRESS_BASE_DOMAIN` variable for all the above
-[based on the environment](https://docs.gitlab.com/ee/ci/variables/#limiting-environment-scopes-of-environment-variables-premium).
+[based on the environment](../../ci/variables/README.md#limiting-environment-scopes-of-environment-variables-premium).
The following table is an example of how the three different clusters would
be configured.
@@ -363,7 +363,7 @@ created, and is uploaded as an artifact which you can later download and check
out.
Any differences between the source and target branches are also
-[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html).
+[shown in the merge request widget](../../user/project/merge_requests/code_quality.md).
### Auto SAST **[ULTIMATE]**
@@ -376,7 +376,7 @@ report is created, it's uploaded as an artifact which you can later download and
check out.
Any security warnings are also shown in the merge request widget. Read more how
-[SAST works](https://docs.gitlab.com/ee/user/application_security/sast/index.html).
+[SAST works](../../user/application_security/sast/index.md).
NOTE: **Note:**
The Auto SAST stage will be skipped on licenses other than Ultimate.
@@ -395,7 +395,7 @@ report is created, it's uploaded as an artifact which you can later download and
check out.
Any security warnings are also shown in the merge request widget. Read more about
-[Dependency Scanning](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html).
+[Dependency Scanning](../../user/application_security/dependency_scanning/index.md).
NOTE: **Note:**
The Auto Dependency Scanning stage will be skipped on licenses other than Ultimate.
@@ -414,7 +414,7 @@ report is created, it's uploaded as an artifact which you can later download and
check out.
Any licenses are also shown in the merge request widget. Read more how
-[License Management works](https://docs.gitlab.com/ee/user/application_security/license_management/index.html).
+[License Management works](../../user/application_security/license_management/index.md).
NOTE: **Note:**
The Auto License Management stage will be skipped on licenses other than Ultimate.
@@ -430,7 +430,7 @@ created, it's uploaded as an artifact which you can later download and
check out.
Any security warnings are also shown in the merge request widget. Read more how
-[Container Scanning works](https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html).
+[Container Scanning works](../../user/application_security/container_scanning/index.md).
NOTE: **Note:**
The Auto Container Scanning stage will be skipped on licenses other than Ultimate.
@@ -486,7 +486,7 @@ issues. Once the report is created, it's uploaded as an artifact which you can
later download and check out.
Any security warnings are also shown in the merge request widget. Read how
-[DAST works](https://docs.gitlab.com/ee/user/application_security/dast/index.html).
+[DAST works](../../user/application_security/dast/index.md).
NOTE: **Note:**
The Auto DAST stage will be skipped on licenses other than Ultimate.
@@ -504,7 +504,7 @@ Auto Browser Performance Testing utilizes the [Sitespeed.io container](https://h
```
Any performance differences between the source and target branches are also
-[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/browser_performance_testing.html).
+[shown in the merge request widget](../../user/project/merge_requests/browser_performance_testing.md).
### Auto Deploy
@@ -582,16 +582,17 @@ Note that a post-install hook means that if any deploy succeeds,
If present, `DB_MIGRATE` will be run as a shell command within an application pod as
a helm pre-upgrade hook.
-For example, in a Rails application:
+For example, in a Rails application in an image built with
+[Herokuish](https://github.com/gliderlabs/herokuish):
-- `DB_INITIALIZE` can be set to `cd /app && RAILS_ENV=production
- bin/setup`
-- `DB_MIGRATE` can be set to `cd /app && RAILS_ENV=production bin/update`
+- `DB_INITIALIZE` can be set to `RAILS_ENV=production /bin/herokuish procfile exec bin/rails db:setup`
+- `DB_MIGRATE` can be set to `RAILS_ENV=production /bin/herokuish procfile exec bin/rails db:migrate`
NOTE: **Note:**
-The `/app` path is the directory of your project inside the docker image
-as [configured by
-Herokuish](https://github.com/gliderlabs/herokuish#paths)
+Unless you have a `Dockerfile` in your repo, your image is built with
+Herokuish. You must prefix commands run in these images with `/bin/herokuish
+procfile exec` in order to replicate the the environment your application is
+run in.
### Auto Monitoring
@@ -673,7 +674,7 @@ repo or by specifying a project variable:
### Custom Helm chart per environment **[PREMIUM]**
You can specify the use of a custom Helm chart per environment by scoping the environment variable
-to the desired environment. See [Limiting environment scopes of variables](https://docs.gitlab.com/ee/ci/variables/#limiting-environment-scopes-of-variables-premium).
+to the desired environment. See [Limiting environment scopes of variables](../../ci/variables/README.md#limiting-environment-scopes-of-environment-variables-premium).
### Customizing `.gitlab-ci.yml`
@@ -739,8 +740,8 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD` | From Gitlab 11.11, this variable can be used to set a password to connect to the helm repository. Defaults to no credentials. (Also set AUTO_DEVOPS_CHART_REPOSITORY_USERNAME) |
| `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 |
+| `CANARY_REPLICAS` | The number of canary replicas to deploy for [Canary Deployments](../../user/project/canary_deployments.md); defaults to 1 |
+| `CANARY_PRODUCTION_REPLICAS` | The number of canary replicas to deploy for [Canary Deployments](../../user/project/canary_deployments.md) in the production environment. This takes precedence over `CANARY_REPLICAS`; defaults to 1 |
| `ADDITIONAL_HOSTS` | Fully qualified domain names specified as a comma-separated list that are added to the ingress hosts. |
| `<ENVIRONMENT>_ADDITIONAL_HOSTS` | For a specific environment, the fully qualified domain names specified as a comma-separated list that are added to the ingress hosts. This takes precedence over `ADDITIONAL_HOSTS`. |
| `POSTGRES_ENABLED` | Whether PostgreSQL is enabled; defaults to `"true"`. Set to `false` to disable the automatic deployment of PostgreSQL. |
@@ -917,7 +918,7 @@ you when you're ready to manually deploy to production.
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ci-yml/merge_requests/171)
in GitLab 11.0.
-A [canary environment](https://docs.gitlab.com/ee/user/project/canary_deployments.html) can be used
+A [canary environment](../../user/project/canary_deployments.md) can be used
before any changes are deployed to production.
If `CANARY_ENABLED` is defined in your project (e.g., set `CANARY_ENABLED` to
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index 367e192b85a..cc83d20d65a 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -208,7 +208,7 @@ applications. In the rightmost column for the production environment, you can ma
application is running.
Right below, there is the
-[Deploy Board](https://docs.gitlab.com/ee/user/project/deploy_boards.html).
+[Deploy Board](../../user/project/deploy_boards.md).
The squares represent pods in your Kubernetes cluster that are associated with
the given environment. Hovering above each square you can see the state of a
deployment and clicking a square will take you to the pod's logs page.
diff --git a/doc/topics/git/how_to_install_git/index.md b/doc/topics/git/how_to_install_git/index.md
index d7e1979217e..c7bede2d269 100644
--- a/doc/topics/git/how_to_install_git/index.md
+++ b/doc/topics/git/how_to_install_git/index.md
@@ -5,14 +5,20 @@ level: beginner
article_type: user guide
date: 2017-05-15
description: 'This article describes how to install Git on macOS, Ubuntu Linux and Windows.'
+type: howto
+last_updated: 2019-05-31
---
# Installing Git
-To begin contributing to GitLab projects
+To begin contributing to GitLab projects,
you will need to install the Git client on your computer.
+
This article will show you how to install Git on macOS, Ubuntu Linux and Windows.
+Information on [installing Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
+is also available at the official Git website.
+
## Install Git on macOS using the Homebrew package manager
Although it is easy to use the version of Git shipped with macOS
@@ -21,7 +27,7 @@ we recommend installing it via Homebrew to get access to
an extensive selection of dependency managed libraries and applications.
If you are sure you don't need access to any additional development libraries
-or don't have approximately 15gb of available disk space for Xcode and Homebrew
+or don't have approximately 15gb of available disk space for Xcode and Homebrew,
use one of the aforementioned methods.
### Installing Xcode
@@ -40,11 +46,12 @@ for the official Homebrew installation instructions.
With Homebrew installed you are now ready to install Git.
Open a Terminal and enter in the following command:
-```bash
+```sh
brew install git
```
Congratulations you should now have Git installed via Homebrew.
+
Next read our article on [adding an SSH key to GitLab](../../../ssh/README.md).
## Install Git on Ubuntu Linux
@@ -55,16 +62,30 @@ it is recommended to use the built in package manager to install Git.
Open a Terminal and enter in the following commands
to install the latest Git from the official Git maintained package archives:
-```bash
+```sh
sudo apt-add-repository ppa:git-core/ppa
sudo apt-get update
sudo apt-get install git
```
Congratulations you should now have Git installed via the Ubuntu package manager.
+
Next read our article on [adding an SSH key to GitLab](../../../ssh/README.md).
## Installing Git on Windows from the Git website
Browse to the [Git website](https://git-scm.com/) and download and install Git for Windows.
+
Next read our article on [adding an SSH key to GitLab](../../../ssh/README.md).
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/topics/git/index.md b/doc/topics/git/index.md
index 7707d56764e..841746cc5de 100644
--- a/doc/topics/git/index.md
+++ b/doc/topics/git/index.md
@@ -1,8 +1,12 @@
-# Git documentation
+---
+type: index
+---
+
+# Git
Git is a [free and open source](https://git-scm.com/about/free-and-open-source)
distributed version control system designed to handle everything from small to
-very large projects with speed and efficiency.
+large projects with speed and efficiency.
[GitLab](https://about.gitlab.com) is a Git-based fully integrated platform for
software development. Besides Git's functionalities, GitLab has a lot of
@@ -11,64 +15,71 @@ powerful [features](https://about.gitlab.com/features/) to enhance your
We've gathered some resources to help you to get the best from Git with GitLab.
+More information is also available on the [Git website](https://git-scm.com).
+
## Getting started
-- [Git concepts](../../university/training/user_training.md#git-concepts)
+The following resources will help you get started with Git:
+
+- [Git Basics](https://git-scm.com/book/en/v2/Getting-Started-Git-Basics)
+- [Git on the Server - GitLab](https://git-scm.com/book/en/v2/Git-on-the-Server-GitLab)
- [How to install Git](how_to_install_git/index.md)
- [Start using Git on the command line](../../gitlab-basics/start-using-git.md)
- [Command Line basic commands](../../gitlab-basics/command-line-commands.md)
- [GitLab Git Cheat Sheet (download)](https://about.gitlab.com/images/press/git-cheat-sheet.pdf)
-- Commits
- - [Revert a commit](../../user/project/merge_requests/revert_changes.md#reverting-a-commit)
+- Commits:
+ - [Revert a commit](../../user/project/merge_requests/revert_changes.md#reverting-a-commit)
- [Cherry-picking a commit](../../user/project/merge_requests/cherry_pick_changes.md#cherry-picking-a-commit)
- [Squashing commits](../../workflow/gitlab_flow.md#squashing-commits-with-rebase)
-**Third-party references:**
+### Concepts
-- [Getting Started - Git website](https://git-scm.com)
-- [Getting Started - Version control](https://git-scm.com/book/en/v2/Getting-Started-About-Version-Control)
-- [Getting Started - Git Basics](https://git-scm.com/book/en/v2/Getting-Started-Git-Basics)
-- [Getting Started - Installing Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
-- [Git on the Server - GitLab](https://git-scm.com/book/en/v2/Git-on-the-Server-GitLab)
+The following are resources about version control concepts:
-### Concepts
+- [Git concepts](../../university/training/user_training.md#git-concepts)
+- [Why Git is Worth the Learning Curve](https://about.gitlab.com/2017/05/17/learning-curve-is-the-biggest-challenge-developers-face-with-git/)
+- [The future of SaaS hosted Git repository pricing](https://about.gitlab.com/2016/05/11/git-repository-pricing/)
+- [Git website topic about version control](https://git-scm.com/book/en/v2/Getting-Started-About-Version-Control)
+- [GitLab University presentation about Version Control](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit?usp=sharing)
-- Article (2017-05-17): [Why Git is Worth the Learning Curve](https://about.gitlab.com/2017/05/17/learning-curve-is-the-biggest-challenge-developers-face-with-git/)
-- Article (2016-05-11): [The future of SaaS hosted Git repository pricing](https://about.gitlab.com/2016/05/11/git-repository-pricing/)
-- GLU Course (Presentation): [About Version Control](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit?usp=sharing)
+## Git tips
-## Exploring Git
+The following resources may help you become more efficient at using Git:
- [Git Tips & Tricks](https://about.gitlab.com/2016/12/08/git-tips-and-tricks/)
- [Eight Tips to help you work better with Git](https://about.gitlab.com/2015/02/19/8-tips-to-help-you-work-better-with-git/)
## Troubleshooting Git
+If you have problems with Git, the following may help:
+
- [Numerous _undo_ possibilities in Git](numerous_undo_possibilities_in_git/index.md)
-- Learn a few [Git troubleshooting](troubleshooting_git.md) techniques to help you out.
+- Learn a few [Git troubleshooting](troubleshooting_git.md) techniques
## Branching strategies
-- [GitLab Flow](https://about.gitlab.com/2014/09/29/gitlab-flow/)
-
-**Third-party references:**
-
- [Git Branching - Branches in a Nutshell](https://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell)
- [Git Branching - Branching Workflows](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows)
+- [GitLab Flow](https://about.gitlab.com/2014/09/29/gitlab-flow/)
## Advanced use
+The following are advanced topics for those who want to get the most out of Git:
+
- [Custom Git Hooks](../../administration/custom_hooks.md)
- [Git Attributes](../../user/project/git_attributes.md)
- Git Submodules: [Using Git submodules with GitLab CI](../../ci/git_submodules.md#using-git-submodules-with-gitlab-ci)
## API
-- [Gitignore templates](../../api/templates/gitignores.md)
+[Gitignore templates](../../api/templates/gitignores.md) API allow for
+Git-related queries from GitLab.
## Git LFS
+The following relate to Git Large File Storage:
+
- [Getting Started with Git LFS](https://about.gitlab.com/2017/01/30/getting-started-with-git-lfs-tutorial/)
- [GitLab Git LFS documentation](../../workflow/lfs/manage_large_binaries_with_git_lfs.md)
-- [Git-Annex to Git-LFS migration guide](https://docs.gitlab.com/ee/workflow/lfs/migrate_from_git_annex_to_git_lfs.html)
-- Article (2015-08-13): [Towards a production quality open source Git LFS server](https://about.gitlab.com/2015/08/13/towards-a-production-quality-open-source-git-lfs-server/)
+- [Git-Annex to Git-LFS migration guide](../../workflow/lfs/migrate_from_git_annex_to_git_lfs.md)
+- [Towards a production quality open source Git LFS server](https://about.gitlab.com/2015/08/13/towards-a-production-quality-open-source-git-lfs-server/)
diff --git a/doc/topics/git/numerous_undo_possibilities_in_git/index.md b/doc/topics/git/numerous_undo_possibilities_in_git/index.md
index 8a8021dc36d..84201e11831 100644
--- a/doc/topics/git/numerous_undo_possibilities_in_git/index.md
+++ b/doc/topics/git/numerous_undo_possibilities_in_git/index.md
@@ -4,25 +4,30 @@ author_gitlab: Letme
level: intermediary
article_type: tutorial
date: 2017-05-15
+type: howto
+last_updated: 2019-05-31
---
# Numerous undo possibilities in Git
-## Introduction
-
In this tutorial, we will show you different ways of undoing your work in Git, for which
we will assume you have a basic working knowledge of. Check GitLab's
-[Git documentation](../index.md#git-documentation) for reference.
+[Git documentation](../index.md) for reference.
+
Also, we will only provide some general info of the commands, which is enough
-to get you started for the easy cases/examples, but for anything more advanced please refer to the [Git book](https://git-scm.com/book/en/v2).
+to get you started for the easy cases/examples, but for anything more advanced
+please refer to the [Git book](https://git-scm.com/book/en/v2).
We will explain a few different techniques to undo your changes based on the stage
of the change in your current development. Also, keep in mind that [nothing in
-Git is really deleted.][git-autoclean-ref]
+Git is really deleted][git-autoclean-ref].
+
This means that until Git automatically cleans detached commits (which cannot be
accessed by branch or tag) it will be possible to view them with `git reflog` command
and access them with direct commit-id. Read more about _[redoing the undo](#redoing-the-undo)_ on the section below.
+## Introduction
+
This guide is organized depending on the [stage of development][git-basics]
where you want to undo your changes from and if they were shared with other developers
or not. Because Git is tracking changes a created or edited file is in the unstaged state
@@ -31,35 +36,41 @@ a file into the **staged** state, which is then committed (`git commit`) to your
local repository. After that, file can be shared with other developers (`git push`).
Here's what we'll cover in this tutorial:
- - [Undo local changes](#undo-local-changes) which were not pushed to remote repository
+- [Undo local changes](#undo-local-changes) which were not pushed to remote repository:
- - Before you commit, in both unstaged and staged state
- - After you committed
+ - Before you commit, in both unstaged and staged state.
+ - After you committed.
- - Undo changes after they are pushed to remote repository
+- Undo changes after they are pushed to remote repository:
- - [Without history modification](#undo-remote-changes-without-changing-history) (preferred way)
- - [With history modification](#undo-remote-changes-with-modifying-history) (requires
- coordination with team and force pushes).
- - [Usecases when modifying history is generally acceptable](#where-modifying-history-is-generally-acceptable)
- - [How to modify history](#how-modifying-history-is-done)
- - [How to remove sensitive information from repository](#deleting-sensitive-information-from-commits)
+ - [Without history modification](#undo-remote-changes-without-changing-history) (preferred way).
+ - [With history modification](#undo-remote-changes-with-modifying-history) (requires
+ coordination with team and force pushes).
+ - [Use cases when modifying history is generally acceptable](#where-modifying-history-is-generally-acceptable).
+ - [How to modify history](#how-modifying-history-is-done).
+ - [How to remove sensitive information from repository](#deleting-sensitive-information-from-commits).
### Branching strategy
[Git][git-official] is a de-centralized version control system, which means that beside regular
versioning of the whole repository, it has possibilities to exchange changes
-with other repositories. To avoid chaos with
+with other repositories.
+
+To avoid chaos with
[multiple sources of truth][git-distributed], various
development workflows have to be followed, and it depends on your internal
workflow how certain changes or commits can be undone or changed.
+
[GitLab Flow][gitlab-flow] provides a good
balance between developers clashing with each other while
developing the same feature and cooperating seamlessly, but it does not enable
joined development of the same feature by multiple developers by default.
+
When multiple developers develop the same feature on the same branch, clashing
with every synchronization is unavoidable, but a proper or chosen Git Workflow will
-prevent that anything is lost or out of sync when feature is complete. You can also
+prevent that anything is lost or out of sync when feature is complete.
+
+You can also
read through this blog post on [Git Tips & Tricks][gitlab-git-tips-n-tricks]
to learn how to easily **do** things in Git.
@@ -97,19 +108,19 @@ no changes added to commit (use "git add" and/or "git commit -a")
At this point there are 3 options to undo the local changes you have:
-- Discard all local changes, but save them for possible re-use [later](#quickly-save-local-changes)
+- Discard all local changes, but save them for possible re-use [later](#quickly-save-local-changes):
```shell
git stash
```
-- Discarding local changes (permanently) to a file
+- Discarding local changes (permanently) to a file:
```shell
git checkout -- <file>
```
-- Discard all local changes to all files permanently
+- Discard all local changes to all files permanently:
```shell
git reset --hard
@@ -150,7 +161,7 @@ of the staging tree. You also have an option to discard all changes with
Lets start the example by editing a file, with your favorite editor, to change the
content and add it to staging
-```
+```sh
vim <file>
git add <file>
```
@@ -164,30 +175,30 @@ Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
- new file: <file>
+ new file: <file>
```
Now you have 4 options to undo your changes:
-- Unstage the file to current commit (HEAD)
+- Unstage the file to current commit (HEAD):
```shell
git reset HEAD <file>
```
-- Unstage everything - retain changes
+- Unstage everything - retain changes:
```shell
git reset
```
-- Discard all local changes, but save them for [later](#quickly-save-local-changes)
+- Discard all local changes, but save them for [later](#quickly-save-local-changes):
```shell
git stash
```
-- Discard everything permanently
+- Discard everything permanently:
```shell
git reset --hard
@@ -206,7 +217,9 @@ your code, you'll have less options to troubleshoot your work.
Through the development process some of the previously committed changes do not
fit anymore in the end solution, or are source of the bugs. Once you find the
commit which triggered bug, or once you have a faulty commit, you can simply
-revert it with `git revert commit-id`. This command inverts (swaps) the additions and
+revert it with `git revert commit-id`.
+
+This command inverts (swaps) the additions and
deletions in that commit, so that it does not modify history. Retaining history
can be helpful in future to notice that some changes have been tried
unsuccessfully in the past.
@@ -225,19 +238,19 @@ through simple bisection process. You can read more about it [in official Git To
In our example we will end up with commit `B`, that introduced bug/error. We have
4 options on how to remove it (or part of it) from our repository.
-- Undo (swap additions and deletions) changes introduced by commit `B`.
+- Undo (swap additions and deletions) changes introduced by commit `B`:
```shell
git revert commit-B-id
```
-- Undo changes on a single file or directory from commit `B`, but retain them in the staged state
+- Undo changes on a single file or directory from commit `B`, but retain them in the staged state:
```shell
git checkout commit-B-id <file>
```
-- Undo changes on a single file or directory from commit `B`, but retain them in the unstaged state
+- Undo changes on a single file or directory from commit `B`, but retain them in the unstaged state:
```shell
git reset commit-B-id <file>
@@ -246,7 +259,9 @@ In our example we will end up with commit `B`, that introduced bug/error. We hav
- There is one command we also must not forget: **creating a new branch**
from the point where changes are not applicable or where the development has hit a
dead end. For example you have done commits `A-B-C-D` on your feature-branch
- and then you figure `C` and `D` are wrong. At this point you either reset to `B`
+ and then you figure `C` and `D` are wrong.
+
+ At this point you either reset to `B`
and do commit `F` (which will cause problems with pushing and if forced pushed also with other developers)
since branch now looks `A-B-F`, which clashes with what other developers have locally (you will
[change history](#with-history-modification)), or you simply checkout commit `B` create
@@ -269,13 +284,13 @@ In our example we will end up with commit `B`, that introduced bug/error. We hav
There is one command for history modification and that is `git rebase`. Command
provides interactive mode (`-i` flag) which enables you to:
- - **reword** commit messages (there is also `git commit --amend` for editing
- last commit message)
- - **edit** the commit content (changes introduced by commit) and message
- - **squash** multiple commits into a single one, and have a custom or aggregated
- commit message
- - **drop** commits - simply delete them
- - and few more options
+- **reword** commit messages (there is also `git commit --amend` for editing
+ last commit message).
+- **edit** the commit content (changes introduced by commit) and message.
+- **squash** multiple commits into a single one, and have a custom or aggregated
+ commit message.
+- **drop** commits - simply delete them.
+- and few more options.
Let us check few examples. Again there are commits `A-B-C-D` where you want to
delete commit `B`.
@@ -301,7 +316,7 @@ In case you want to modify something introduced in commit `B`.
- Command opens your favorite text editor where you write `edit` in front of commit
`B`, but leave default `pick` with all other commits. Save and exit the editor to
- perform a rebase
+ perform a rebase.
- Now do your edits and commit changes:
@@ -348,7 +363,9 @@ and then on end description of that action.
This topic is roughly same as modifying committed local changes without modifying
history. **It should be the preferred way of undoing changes on any remote repository
or public branch.** Keep in mind that branching is the best solution when you want
-to retain the history of faulty development, yet start anew from certain point. Branching
+to retain the history of faulty development, yet start anew from certain point.
+
+Branching
enables you to include the existing changes in new development (by merging) and
it also provides a clear timeline and development structure.
@@ -386,12 +403,14 @@ the cleanup of detached commits (happens automatically).
Modified history breaks the development chain of other developers, as changed
history does not have matching commits'ids. For that reason it should not
be used on any public branch or on branch that *might* be used by other
-developers. When contributing to big open source repositories (e.g. [GitLab CE][gitlab-ce]),
+developers. When contributing to big open source repositories (for example, [GitLab CE][gitlab-ce]),
it is acceptable to *squash* commits into a single one, to present
a nicer history of your contribution.
+
Keep in mind that this also removes the comments attached to certain commits
in merge requests, so if you need to retain traceability in GitLab, then
modifying history is not acceptable.
+
A feature-branch of a merge request is a public branch and might be used by
other developers, but project process and rules might allow or require
you to use `git rebase` (command that changes history) to reduce number of
@@ -400,8 +419,8 @@ GitLab). There is a `git merge --squash` command which does exactly that
(squashes commits on feature-branch to a single commit on target branch
at merge).
->**Note:**
-Never modify the commit history of `master` or shared branch
+NOTE: **Note:**
+Never modify the commit history of `master` or shared branch.
### How modifying history is done
@@ -436,7 +455,7 @@ pick <commit3-id> <commit3-commit-message>
# Note that empty commits are commented out
```
->**Note:**
+NOTE: **Note:**
It is important to notice that comment from the output clearly states that, if
you decide to abort, then do not just close your editor (as that will in-fact
modify history), but remove all uncommented lines and save.
@@ -470,7 +489,7 @@ tools that can use some of Git specifics to enable faster execution of common
tasks (which is exactly what removing sensitive information file is about).
An alternative is [BFG Repo-cleaner][bfg-repo-cleaner]. Keep in mind that these
tools are faster because they do not provide a same fully feature set as `git filter-branch`
-does, but focus on specific usecases.
+does, but focus on specific use cases.
## Conclusion
@@ -480,6 +499,18 @@ depending on the stage of your process. Git also enables rewriting history, but
should be avoided as it might cause problems when multiple developers are
contributing to the same codebase.
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
+
<!-- Identifiers, in alphabetical order -->
[bfg-repo-cleaner]: https://rtyley.github.io/bfg-repo-cleaner/
diff --git a/doc/topics/git/troubleshooting_git.md b/doc/topics/git/troubleshooting_git.md
index 71651fcf421..417d91bf834 100644
--- a/doc/topics/git/troubleshooting_git.md
+++ b/doc/topics/git/troubleshooting_git.md
@@ -1,3 +1,7 @@
+---
+type: howto
+---
+
# Troubleshooting Git
Sometimes things don't work the way they should or as you might expect when
@@ -9,7 +13,7 @@ with Git.
'Broken pipe' errors can occur when attempting to push to a remote repository.
When pushing you will usually see:
-```
+```text
Write failed: Broken pipe
fatal: The remote end hung up unexpectedly
```
@@ -39,14 +43,15 @@ There's another option where you can prevent session timeouts by configuring
SSH 'keep alive' either on the client or on the server (if you are a GitLab
admin and have access to the server).
-NOTE: **Note:** configuring *both* the client and the server is unnecessary.
+NOTE: **Note:**
+Configuring *both* the client and the server is unnecessary.
**To configure SSH on the client side**:
-- On UNIX, edit `~/.ssh/config` (create the file if it doesn’t exist) and
- add or edit:
+- On UNIX, edit `~/.ssh/config` (create the file if it doesn’t exist) and
+ add or edit:
- ```
+ ```text
Host your-gitlab-instance-url.com
ServerAliveInterval 60
ServerAliveCountMax 5
@@ -58,7 +63,7 @@ NOTE: **Note:** configuring *both* the client and the server is unnecessary.
**To configure SSH on the server side**, edit `/etc/ssh/sshd_config` and add:
-```
+```text
ClientAliveInterval 60
ClientAliveCountMax 5
```
@@ -83,35 +88,35 @@ to >= 2.9 (see [Broken pipe when pushing to Git repository][Broken-Pipe]).
Users may experience the following error when attempting to push or pull
using Git over SSH:
-```
-Please make sure you have the correct access rights
-and the repository exists.
+```text
+Please make sure you have the correct access rights
+and the repository exists.
...
-ssh_exchange_identification: read: Connection reset by peer
-fatal: Could not read from remote repository.
+ssh_exchange_identification: read: Connection reset by peer
+fatal: Could not read from remote repository.
```
This error usually indicates that SSH daemon's `MaxStartups` value is throttling
-SSH connections. This setting specifies the maximum number of unauthenticated
+SSH connections. This setting specifies the maximum number of unauthenticated
connections to the SSH daemon. This affects users with proper authentication
credentials (SSH keys) because every connection is 'unauthenticated' in the
-beginning. The default value is `10`.
+beginning. The default value is `10`.
Increase `MaxStartups` by adding or modifying the value in `/etc/ssh/sshd_config`:
-```
+```text
MaxStartups 100
```
-Restart SSHD for the change to take effect.
+Restart SSHD for the change to take effect.
## Timeout during git push/pull
If pulling/pushing from/to your repository ends up taking more than 50 seconds,
-a timeout will be issued with a log of the number of operations performed
+a timeout will be issued with a log of the number of operations performed
and their respective timings, like the example below:
-```
+```text
remote: Running checks for branch: master
remote: Scanning for LFS objects... (153ms)
remote: Calculating new repository size... (cancelled after 729ms)
diff --git a/doc/university/training/topics/unstage.md b/doc/university/training/topics/unstage.md
index da36a3218e5..c926f0b4888 100644
--- a/doc/university/training/topics/unstage.md
+++ b/doc/university/training/topics/unstage.md
@@ -8,13 +8,13 @@ comments: false
## Unstage
-- To remove files from stage use reset HEAD. Where HEAD is the last commit of the current branch.
+- To remove files from stage use reset HEAD where HEAD is the last commit of the current branch. This will unstage the file but maintain the modifications.
```bash
git reset HEAD <file>
```
-- This will unstage the file but maintain the modifications. To revert the file back to the state it was in before the changes we can use:
+- To revert the file back to the state it was in before the changes we can use:
```bash
git checkout -- <file>
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index c2dff21b028..f2df4277ca8 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -108,7 +108,7 @@ sudo -u git -H make
### 8. Install/Update `gitlab-elasticsearch-indexer` (optional) **[STARTER ONLY]**
-If you're interested in using GitLab's new [elasticsearch repository indexer](https://docs.gitlab.com/ee/integration/elasticsearch.html#elasticsearch-repository-indexer-beta) (currently in beta)
+If you're interested in using GitLab's new [elasticsearch repository indexer](../integration/elasticsearch.md#elasticsearch-repository-indexer-beta) (currently in beta)
please follow the instructions on the document linked above and enable the
indexer usage in the GitLab admin settings.
diff --git a/doc/update/upgrading_from_ce_to_ee.md b/doc/update/upgrading_from_ce_to_ee.md
index e74a5c00f7e..428377adb19 100644
--- a/doc/update/upgrading_from_ce_to_ee.md
+++ b/doc/update/upgrading_from_ce_to_ee.md
@@ -75,7 +75,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
### 4. Install `gitlab-elasticsearch-indexer` (optional) **[STARTER ONLY]**
If you're interested in using GitLab's new [elasticsearch repository
-indexer][indexer-beta] (currently in beta) please follow the instructions on the
+indexer](../integration/elasticsearch.md) (currently in beta) please follow the instructions on the
document linked above and enable the indexer usage in the GitLab admin settings.
### 5. Start application
@@ -133,4 +133,3 @@ Additional instructions here.
[support@gitlab.com]: mailto:support@gitlab.com
[old-ee-upgrade-docs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/11-8-stable-ee/doc/update
-[indexer-beta]: https://docs.gitlab.com/ee/integration/elasticsearch.html
diff --git a/doc/update/upgrading_from_source.md b/doc/update/upgrading_from_source.md
index 82a86f3f343..f82d666c7be 100644
--- a/doc/update/upgrading_from_source.md
+++ b/doc/update/upgrading_from_source.md
@@ -114,7 +114,47 @@ sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
rm go1.10.5.linux-amd64.tar.gz
```
-### 6. Get latest code
+### 6. Update git
+
+NOTE: **Note:**
+GitLab 11.11 and higher only supports Git 2.21.x and newer, and
+[dropped support for older versions](https://gitlab.com/gitlab-org/gitlab-ce/issues/54255).
+Be sure to upgrade your installation if necessary.
+
+```bash
+# Make sure Git is version 2.21.0 or higher
+git --version
+
+# Remove packaged Git
+sudo apt-get remove git-core
+
+# Install dependencies
+sudo apt-get install -y libcurl4-openssl-dev libexpat1-dev gettext libz-dev libssl-dev build-essential
+
+# Download and compile pcre2 from source
+curl --silent --show-error --location https://ftp.pcre.org/pub/pcre/pcre2-10.33.tar.gz --output pcre2.tar.gz
+tar -xzf pcre2.tar.gz
+cd pcre2-10.33
+chmod +x configure
+./configure --prefix=/usr --enable-jit
+make
+make install
+
+# Download and compile from source
+cd /tmp
+curl --remote-name --location --progress https://www.kernel.org/pub/software/scm/git/git-2.21.0.tar.gz
+echo '85eca51c7404da75e353eba587f87fea9481ba41e162206a6f70ad8118147bee git-2.21.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.21.0.tar.gz
+cd git-2.21.0/
+./configure --with-libpcre
+make prefix=/usr/local all
+
+# Install into /usr/local/bin
+sudo make prefix=/usr/local install
+
+# You should edit config/gitlab.yml, change the git -> bin_path to /usr/local/bin/git
+```
+
+### 7. Get latest code
```bash
cd /home/git/gitlab
@@ -142,7 +182,7 @@ cd /home/git/gitlab
sudo -u git -H git checkout BRANCH-ee
```
-### 7. Update gitlab-shell
+### 8. Update gitlab-shell
```bash
cd /home/git/gitlab-shell
@@ -152,7 +192,7 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
sudo -u git -H bin/compile
```
-### 8. Update gitlab-workhorse
+### 9. Update gitlab-workhorse
Install and compile gitlab-workhorse. GitLab-Workhorse uses
[GNU Make](https://www.gnu.org/software/make/).
@@ -167,7 +207,7 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
sudo -u git -H make
```
-### 9. Update Gitaly
+### 10. Update Gitaly
#### Compile Gitaly
@@ -178,7 +218,7 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
sudo -u git -H make
```
-### 10. Update gitlab-pages
+### 11. Update gitlab-pages
#### Only needed if you use GitLab Pages
@@ -195,7 +235,7 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
sudo -u git -H make
```
-### 11. Update MySQL permissions
+### 12. Update MySQL permissions
If you are using MySQL you need to grant the GitLab user the necessary
permissions on the database:
@@ -217,7 +257,7 @@ You can make this setting permanent by adding it to your `my.cnf`:
log_bin_trust_function_creators=1
```
-### 12. Update configuration files
+### 13. Update configuration files
#### New configuration options for `gitlab.yml`
@@ -291,7 +331,7 @@ For Ubuntu 16.04.1 LTS:
sudo systemctl daemon-reload
```
-### 13. Install libs, migrations, etc.
+### 14. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
@@ -323,14 +363,14 @@ sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
**MySQL installations**: Run through the `MySQL strings limits` and `Tables and
data conversion to utf8mb4` [tasks](../install/database_mysql.md).
-### 14. Start application
+### 15. Start application
```bash
sudo service gitlab start
sudo service nginx restart
```
-### 15. Check application status
+### 16. Check application status
Check if GitLab and its environment are configured correctly:
diff --git a/doc/user/admin_area/diff_limits.md b/doc/user/admin_area/diff_limits.md
index 9205860ef1f..4063c40a751 100644
--- a/doc/user/admin_area/diff_limits.md
+++ b/doc/user/admin_area/diff_limits.md
@@ -1,21 +1,40 @@
+---
+type: reference
+---
+
# Diff limits administration
+You can set a maximum size for display of diff files (patches).
+
+## Maximum diff patch size
+
+Diff files which exceed this value will be presented as 'too large' and won't
+be expandable. Instead of an expandable view, a link to the blob view will be
+shown.
+
+Patches greater than 10% of this size will be automatically collapsed, and a
+link to expand the diff will be presented.
+
NOTE: **Note:**
Merge requests and branch comparison views will be affected.
CAUTION: **Caution:**
-These settings are currently under experimental state. They'll
-increase the resource consumption of your instance and should
-be edited mindfully.
+This setting is experimental. An increased maximum will increase resource
+consumption of your instance. Keep this in mind when adjusting the maximum.
-1. Access **Admin area > Settings > General**
-1. Expand **Diff limits**
+1. Go to **Admin area > Settings > General**.
+1. Expand **Diff limits**.
+1. Enter a value for **Maximum diff patch size**, measured in bytes.
+1. Click on **Save changes**.
-### Maximum diff patch size
+<!-- ## Troubleshooting
-This is the content size each diff file (patch) is allowed to reach before
-it's collapsed, without the possibility of being expanded. A link redirecting
-to the blob view will be presented for the patches that surpass this limit.
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
-Patches surpassing 10% of this content size will be automatically collapsed,
-but expandable (a link to expand the diff will be presented).
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/admin_area/geo_nodes.md b/doc/user/admin_area/geo_nodes.md
index fb0f9a3285d..d99b87cbc5c 100644
--- a/doc/user/admin_area/geo_nodes.md
+++ b/doc/user/admin_area/geo_nodes.md
@@ -5,7 +5,7 @@ type: howto
# Geo nodes admin area **[PREMIUM ONLY]**
You can configure various settings for GitLab Geo nodes. For more information, see
-[Geo documentation](https://docs.gitlab.com/ee/administration/geo/replication/index.md).
+[Geo documentation](../../administration/geo/replication/index.md).
On the primary node, go to **Admin area > Geo**. On secondary nodes, go to **Admin area > Geo > Nodes**.
@@ -29,7 +29,7 @@ changes on the **primary** node!
| Setting | Description |
|---------------------------|-------------|
-| Selective synchronization | Enable Geo [selective sync](https://docs.gitlab.com/ee/administration/geo/replication/configuration.html#selective-synchronization) for this **secondary** node. |
+| Selective synchronization | Enable Geo [selective sync](../../administration/geo/replication/configuration.md#selective-synchronization) for this **secondary** node. |
| Repository sync capacity | Number of concurrent requests this **secondary** node will make to the **primary** node when backfilling repositories. |
| File sync capacity | Number of concurrent requests this **secondary** node will make to the **primary** node when backfilling files. |
diff --git a/doc/user/admin_area/img/index_runners_search_or_filter.png b/doc/user/admin_area/img/index_runners_search_or_filter.png
new file mode 100644
index 00000000000..5176a1a39bf
--- /dev/null
+++ b/doc/user/admin_area/img/index_runners_search_or_filter.png
Binary files differ
diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md
index 4ed1287abbc..527110d53df 100644
--- a/doc/user/admin_area/index.md
+++ b/doc/user/admin_area/index.md
@@ -20,14 +20,14 @@ The Admin Area is made up of the following sections:
| Section | Description |
|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------|
-| Overview | View your GitLab [Dashboard](#admin-dashboard), and administer [projects](#administer-projects), [users](#administer-users), groups, jobs, runners, and Gitaly servers. |
+| Overview | View your GitLab [Dashboard](#admin-dashboard), and administer [projects](#administer-projects), [users](#administer-users), groups, [jobs](#administer-jobs), [Runners](#administer-runners), and [Gitaly servers](#administer-gitaly-servers). |
| Monitoring | View GitLab system information, and information on background jobs, logs, [health checks](monitoring/health_check.md), request profiles, and audit logs. |
| Messages | Send and manage [broadcast messages](broadcast_messages.md) for your users. |
| System Hooks | Configure [system hooks](../../system_hooks/system_hooks.md) for many events. |
| Applications | Create system [OAuth applications](../../integration/oauth_provider.md) for integrations with other services. |
| Abuse Reports | Manage [abuse reports](abuse_reports.md) submitted by your users. |
| License **[STARTER ONLY]** | Upload, display, and remove [licenses](license.md). |
-| Push Rules **[STARTER]** | Configure pre-defined git [push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html) for projects. |
+| Push Rules **[STARTER]** | Configure pre-defined git [push rules](../../push_rules/push_rules.md) for projects. |
| Geo **[PREMIUM ONLY]** | Configure and maintain [Geo nodes](geo_nodes.md). |
| Deploy Keys | Create instance-wide [SSH deploy keys](../../ssh/README.md#deploy-keys). |
| Service Templates | Create [service templates](../project/integrations/services_templates.md) for projects. |
@@ -109,8 +109,100 @@ created and the date of last activity. To edit a user, click the **Edit** button
row. To delete the user, or delete the user and their contributions, click the cog dropdown in
that user's row, and select the desired option.
-To change the sort order, click the sort dropdown and select the desired order. By default the sort dropdown shows **Name**.
+To change the sort order:
+
+1. Click the sort dropdown.
+1. Select the desired order.
+
+By default the sort dropdown shows **Name**.
To search for users, enter your criteria in the search field. The user search is case
insensitive, and applies partial matching to name and username. To search for an email address,
you must provide the complete email address.
+
+## Administer Jobs
+
+You can administer all jobs in the GitLab instance from the Admin Area's Jobs page.
+
+To access the Jobs page, go to **Admin Area > Overview > Jobs**.
+
+All jobs are listed, in reverse order of their job ID.
+
+Click the **All** tab to list all jobs. Click the **Pending**, **Running**, or **Finished** tab to list only jobs of that status.
+
+For each job, the following details are listed:
+
+| Field | Description |
+|--------- | ----------- |
+| Status | Job status, either **passed**, **skipped**, or **failed**. |
+| Job | Includes links to the job, branch, and the commit that started the job. |
+| Pipeline | Includes a link to the specific pipeline. |
+| Project | Name of the project, and organization, to which the job belongs. |
+| Runner | Name of the CI runner assigned to execute the job. |
+| Stage | Stage that the job is declared in a `.gitlab-ci.yml` file. |
+| Name | Name of the job specified in a `.gitlab-ci.yml` file. |
+| Timing | Duration of the job, and how long ago the job completed. |
+| Coverage | Percentage of tests coverage. |
+
+## Administer Runners
+
+You can adminster all Runners in the GitLab instance from the Admin Area's **Runners** page. See
+[GitLab Runner](https://docs.gitlab.com/runner/) for more information on Runner itself.
+
+To access the **Runners** page, go to **Admin Area > Overview > Runners**.
+
+The **Runners** page features:
+
+- A description of Runners, and their possible states.
+- Instructions on installing a Runner.
+- A list of all registered Runners.
+
+Runners are listed in descending order by the date they were created, by default. You can change
+the sort order to *Last Contacted* from the dropdown beside the search field.
+
+To search Runners' descriptions:
+
+1. In the **Search or filter results...** field, type the description of the Runner you want to
+find.
+1. Press Enter.
+
+You can also filter Runners by status, type, and tag. To filter:
+
+1. Click in the **Search or filter results...** field.
+1. Select **status:**, **type:**, or **tag:**
+1. Select or enter your search criteria.
+
+![Attributes of a Runner, with the **Search or filter results...** field active](img/index_runners_search_or_filter.png)
+
+For each Runner, the following attributes are listed:
+
+| Attribute | Description |
+| ------------ | ----------- |
+| Type | One or more of the following states: shared, group, specific, locked, or paused |
+| Runner token | Token used to identify the Runner, and which the Runner uses to communicate with the GitLab instance |
+| Description | Description given to the Runner when it was created |
+| Version | GitLab Runner version |
+| IP address | IP address of the host on which the Runner is registered |
+| Projects | Projects to which the Runner is assigned |
+| Jobs | Total of jobs run by the Runner |
+| Tags | Tags associated with the Runner |
+| Last contact | Timestamp indicating when the GitLab instance last contacted the Runner |
+
+You can also edit, pause, or remove each Runner.
+
+## Administer Gitaly servers
+
+You can list all Gitaly servers in the GitLab instance from the Admin Area's **Gitaly Servers**
+page. For more details, see [Gitaly](../../administration/gitaly/index.md).
+
+To access the **Gitaly Servers** page, go to **Admin Area > Overview > Gitaly Servers**.
+
+For each Gitaly server, the following details are listed:
+
+| Field | Description |
+| -------------- | ----------- |
+| Storage | Repository storage |
+| Address | Network address on which the Gitaly server is listening |
+| Server version | Gitaly version |
+| Git version | Version of Git installed on the Gitaly server |
+| Up to date | Indicates if the Gitaly server version is the latest version available. A green dot indicates the server is up to date. |
diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md
index 1d355824760..001e4b6bf48 100644
--- a/doc/user/admin_area/settings/account_and_limit_settings.md
+++ b/doc/user/admin_area/settings/account_and_limit_settings.md
@@ -40,11 +40,9 @@ These settings can be found within:
- **Admin Area > Settings > General**.
- The path `/admin/application_settings`.
-The very first push of a new project cannot be checked for size as of now, so
-the first push will allow you to upload more than the limit dictates, but every
-subsequent push will be denied. LFS objects, however, can be checked on first
-push and **will** be rejected if the sum of their sizes exceeds the maximum
-allowed repository size.
+The first push of a new project, including LFS objects, will be checked for size
+and **will** be rejected if the sum of their sizes exceeds the maximum allowed
+repository size.
For details on manually purging files, see [reducing the repository size using Git](../../project/repository/reducing_the_repo_size_using_git.md).
diff --git a/doc/user/admin_area/settings/email.md b/doc/user/admin_area/settings/email.md
index 912c2cff481..9555a695b13 100644
--- a/doc/user/admin_area/settings/email.md
+++ b/doc/user/admin_area/settings/email.md
@@ -40,7 +40,7 @@ In order to change this option:
1. Hit **Save** for the changes to take effect.
NOTE: **Note**: Once the hostname gets configured, every private commit email using the previous hostname, will not get
-recognized by GitLab. This can directly conflict with certain [Push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html) such as
+recognized by GitLab. This can directly conflict with certain [Push rules](../../../push_rules/push_rules.md) such as
`Check whether author is a GitLab user` and `Check whether committer is the current authenticated user`.
<!-- ## Troubleshooting
diff --git a/doc/user/admin_area/settings/index.md b/doc/user/admin_area/settings/index.md
index 0e697b9e445..eed087ae52b 100644
--- a/doc/user/admin_area/settings/index.md
+++ b/doc/user/admin_area/settings/index.md
@@ -1,9 +1,13 @@
-# Admin area settings **[CORE ONLY]**
+---
+type: index
+---
-In the admin area settings, you can find various options for your GitLab
+# Admin Area settings **[CORE ONLY]**
+
+In the Admin Area **Settings** page, you can find various options for your GitLab
instance like sign-up restrictions, account limits and quota, metrics, etc.
-Navigate to it by going to **Admin area > Settings**. Some of the settings
+Navigate to it by going to **Admin Area > Settings**. Some of the settings
include:
- [Account and limit settings](account_and_limit_settings.md) **[STARTER]**
@@ -22,7 +26,7 @@ in the **Localization** section of **Admin area > Settings > Preferences**.
## GitLab.com admin area settings
-Most of the settings under the admin area change the behavior of the whole
+Most of the settings under the Admin Area change the behavior of the whole
GitLab instance. For GitLab.com, the admin settings are available only for the
GitLab.com administrators, and the parameters can be found on the
[GitLab.com settings](../../gitlab_com/index.md) documentation.
diff --git a/doc/user/admin_area/settings/instance_template_repository.md b/doc/user/admin_area/settings/instance_template_repository.md
index 4010008f694..91286a67c31 100644
--- a/doc/user/admin_area/settings/instance_template_repository.md
+++ b/doc/user/admin_area/settings/instance_template_repository.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Instance template repository **[PREMIUM ONLY]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5986) in
@@ -61,3 +65,15 @@ top of the list.
If this feature is disabled or no templates are present, there will be
no "Custom" section in the selection dropdown.
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/admin_area/settings/sign_up_restrictions.md b/doc/user/admin_area/settings/sign_up_restrictions.md
index d3ecfd42903..cebf36c7ec1 100644
--- a/doc/user/admin_area/settings/sign_up_restrictions.md
+++ b/doc/user/admin_area/settings/sign_up_restrictions.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Sign-up restrictions
You can block email addresses of specific domains, or whitelist only some
@@ -37,5 +41,17 @@ semicolon, comma, or a new line.
![Domain Blacklist](img/domain_blacklist.png)
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
+
[ce-5259]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5259
[ce-598]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/598
diff --git a/doc/user/admin_area/settings/terms.md b/doc/user/admin_area/settings/terms.md
index e2290bf0598..a5f8d05f662 100644
--- a/doc/user/admin_area/settings/terms.md
+++ b/doc/user/admin_area/settings/terms.md
@@ -1,29 +1,35 @@
+---
+type: reference
+---
+
# Enforce accepting Terms of Service
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18570)
> in [GitLab Core](https://about.gitlab.com/pricing/) 10.8
+An admin can enforce acceptance of a terms of service and privacy policy. When this option is enabled, new and existing users must accept the terms.
+
## Configuration
-When it is required for all users of the GitLab instance to accept the
-Terms of Service, this can be configured by an admin on the settings
-page:
+To enforce acceptance of a Terms of Service and Privacy Policy:
-![Enable enforcing Terms of Service](img/enforce_terms.png).
+1. Log in to the GitLab instance as an admin user.
+1. Go to **Admin Area > Settings > General**.
+1. Expand the **Terms of Service and Privacy Policy** section.
+1. Check the **Require all users to accept Terms of Service and Privacy Policy when they access
+GitLab.** checkbox.
+1. Input the text of the **Terms of Service and Privacy Policy**. Markdown formatting can be used in this input box.
+1. Click **Save changes**.
+1. When you are presented with the **Terms of Service** statement, click **Accept terms**.
-The terms itself can be entered using Markdown. For each update to the
-terms, a new version is stored. When a user accepts or declines the
-terms, GitLab will keep track of which version they accepted or
-declined.
+![Enable enforcing Terms of Service](img/enforce_terms.png).
-When an admin enables this feature, they will automattically be
-directed to the page to accept the terms themselves. After they
-accept, they will be directed back to the settings page.
+For each update to the terms, a new version is stored. When a user accepts or declines the terms,
+GitLab will record which version they accepted or declined.
-## New registrations
+## New users
-When this feature is enabled, a checkbox will be available in the
-sign-up form.
+When this feature is enabled, a checkbox is added to the sign-up form.
![Sign up form](img/sign_up_terms.png)
@@ -49,3 +55,15 @@ If the user was already logged in when the feature was turned on,
they will be asked to accept the terms on their next interaction.
If a user declines the terms, they will be signed out.
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/admin_area/settings/third_party_offers.md b/doc/user/admin_area/settings/third_party_offers.md
index 23311801790..d3c9cf7d8ff 100644
--- a/doc/user/admin_area/settings/third_party_offers.md
+++ b/doc/user/admin_area/settings/third_party_offers.md
@@ -1,9 +1,26 @@
+---
+type: reference
+---
+
# Third party offers
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20379)
> in [GitLab Core](https://about.gitlab.com/pricing/) 11.1
-Within GitLab, we inform users of available third-party offers they might find valuable in order to enhance the development of their projects.
-An example is the Google Cloud Platform free credit for using [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/).
+Within GitLab, we inform users of available third-party offers they might find valuable in order
+to enhance the development of their projects. An example is the Google Cloud Platform free credit
+for using [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/).
+
+The display of third-party offers can be toggled in the **Admin Area > Settings** page.
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
-The display of third-party offers can be toggled in the Admin area on the Settings page.
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md
index 01d1eb1cd0e..652d6ad2cdd 100644
--- a/doc/user/admin_area/settings/usage_statistics.md
+++ b/doc/user/admin_area/settings/usage_statistics.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Usage statistics
GitLab Inc. will periodically collect information about your instance in order
@@ -83,6 +87,18 @@ of your instance to your users.
This can be restricted to admins by selecting "Only admins" in the Instance
Statistics visibility section under **Admin area > Settings > Usage statistics**.
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
+
[ee-557]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/557
[ee-735]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/735
[ce-23361]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23361
diff --git a/doc/user/admin_area/settings/visibility_and_access_controls.md b/doc/user/admin_area/settings/visibility_and_access_controls.md
index 4d1b7d0f252..a1229484388 100644
--- a/doc/user/admin_area/settings/visibility_and_access_controls.md
+++ b/doc/user/admin_area/settings/visibility_and_access_controls.md
@@ -1,8 +1,25 @@
+---
+type: reference
+---
+
# Visibility and access controls
+GitLab allows admins to:
+
+- Control access and visibility to GitLab resources including branches and projects.
+- Select from which hosting sites code can be imported into GitLab.
+- Select the protocols permitted to access GitLab.
+- Enable or disable repository mirroring.
+
+To access the visibility and access control options:
+
+1. Log in to GitLab as an admin.
+1. Go to **Admin Area > Settings > General**.
+1. Expand the **Visibility and access controls** section.
+
## Import sources
-Choose from which hosting sites the users can
+Choose from which hosting sites users can
[import their projects](../../project/import/index.md).
![import sources](img/import_sources.png)
@@ -11,12 +28,10 @@ Choose from which hosting sites the users can
> [Introduced][ce-4696] in GitLab 8.10.
-With GitLab's Access restrictions you can choose which Git access protocols you
-want your users to use to communicate with GitLab. This feature can be enabled
-via the `Application Settings` in the Admin interface.
+With GitLab's access restrictions, you can select with which protocols users can communicate with
+GitLab.
-The setting is called `Enabled Git access protocols`, and it gives you the option
-to choose between:
+From the **Enabled Git access protocols** dropdown, select one of the following:
- Both SSH and HTTP(S)
- Only SSH
@@ -24,10 +39,9 @@ to choose between:
![Settings Overview](img/access_restrictions.png)
-When both SSH and HTTP(S) are enabled, GitLab will behave as usual, it will give
-your users the option to choose which protocol they would like to use.
+When both SSH and HTTP(S) are enabled, your users can choose either protocol.
-When you choose to allow only one of the protocols, a couple of things will happen:
+When only one protocol is enabled:
- The project page will only show the allowed protocol's URL, with no option to
change it.
@@ -54,10 +68,22 @@ application level.
> [Introduced][ee-3586] in GitLab 10.3.
This option is enabled by default. By disabling it, both pull and push mirroring will no longer
-work in every repository and can only be re-enabled on a per-project basis by an admin.
+work in every repository and can only be re-enabled by an admin on a per-project basis.
![Mirror settings](img/mirror_settings.png)
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
+
[ce-4696]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4696
[ce-18021]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18021
[ee-3586]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3586
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index 5c635b09503..adb6868516e 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -119,7 +119,7 @@ container_scanning:
variables:
DOCKER_DRIVER: overlay2
## Define two new variables based on GitLab's CI/CD predefined variables
- ## https://docs.gitlab.com/ee/ci/variables/#predefined-environment-variables
+ ## https://docs.gitlab.com/ee/ci/variables/README.html#predefined-environment-variables
CI_APPLICATION_REPOSITORY: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG
CI_APPLICATION_TAG: $CI_COMMIT_SHA
CLAIR_LOCAL_SCAN_VERSION: v2.0.8_fe9b059d930314b54c78f75afe265955faf4fdc1
@@ -162,7 +162,7 @@ container_scanning:
variables:
DOCKER_DRIVER: overlay2
## Define two new variables based on GitLab's CI/CD predefined variables
- ## https://docs.gitlab.com/ee/ci/variables/#predefined-environment-variables
+ ## https://docs.gitlab.com/ee/ci/variables/README.html#predefined-environment-variables
CI_APPLICATION_REPOSITORY: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG
CI_APPLICATION_TAG: $CI_COMMIT_SHA
CLAIR_LOCAL_SCAN_VERSION: v2.0.8_fe9b059d930314b54c78f75afe265955faf4fdc1
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index abc6e771b0f..028ff72a160 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -39,6 +39,8 @@ However, DAST can be [configured](#full-scan)
to also perform a so-called "active scan". That is, attack your application and produce a more extensive security report.
It can be very useful combined with [Review Apps](../../../ci/review_apps/index.md).
+The [`dast`](https://gitlab.com/gitlab-org/security-products/dast/container_registry) Docker image in GitLab container registry is updated on a weekly basis to have all [`owasp2docker-weekly`](https://hub.docker.com/r/owasp/zap2docker-weekly/) updates in it.
+
## Use cases
It helps you automatically find security vulnerabilities in your running web
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index 5d2bb4e572b..d78cf778110 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -224,3 +224,20 @@ vulnerabilities in your groups and projects. Read more about the
Once a vulnerability is found, you can interact with it. Read more on how to
[interact with the vulnerabilities](../index.md#interacting-with-the-vulnerabilities).
+
+## Dependency List
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/10075)
+in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.0.
+
+An additional benefit of Dependency Scanning is the ability to get a list of your project's dependencies with their versions.
+
+This list can be generated only for [supported languages and package managers](#supported-languages-and-package-managers).
+
+To see the generated dependency list, navigate to the Dependency List page under your project's left sidebar menu **Project > Dependency List**.
+
+## Contributing to the vulnerability database
+
+You can search the [gemnasium-db](https://gitlab.com/gitlab-org/security-products/gemnasium-db) project
+to find a vulnerability in the Gemnasium database.
+You can also [submit new vulnerabilities](https://gitlab.com/gitlab-org/security-products/gemnasium-db/blob/master/CONTRIBUTING.md). \ No newline at end of file
diff --git a/doc/user/application_security/img/dismissed_info.png b/doc/user/application_security/img/dismissed_info.png
new file mode 100644
index 00000000000..64d5cf26ed2
--- /dev/null
+++ b/doc/user/application_security/img/dismissed_info.png
Binary files differ
diff --git a/doc/user/application_security/img/interactive_reports.png b/doc/user/application_security/img/interactive_reports.png
index 9f9812dc69d..373b39104db 100644
--- a/doc/user/application_security/img/interactive_reports.png
+++ b/doc/user/application_security/img/interactive_reports.png
Binary files differ
diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md
index 64e72fab198..679847b76d7 100644
--- a/doc/user/application_security/index.md
+++ b/doc/user/application_security/index.md
@@ -47,6 +47,16 @@ You can dismiss vulnerabilities by clicking the **Dismiss vulnerability** button
This will dismiss the vulnerability and re-render it to reflect its dismissed state.
If you wish to undo this dismissal, you can click the **Undo dismiss** button.
+#### Adding a dismissal reason
+
+> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing) 12.0.
+
+When dismissing a vulnerability, it's often helpful to provide a reason for doing so.
+If you press the comment button next to **Dismiss vulnerability** in the modal, a text box will appear, allowing you to add a comment with your dismissal.
+This comment can not currently be edited or removed, but [future versions](https://gitlab.com/gitlab-org/gitlab-ee/issues/11721) will add this functionality.
+
+![Dismissed vulnerability comment](img/dismissed_info.png)
+
### Creating an issue for a vulnerability
You can create an issue for a vulnerability by selecting the **Create issue**
diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard.png
index 3294e59e943..f0dad6c54d0 100644
--- a/doc/user/application_security/security_dashboard/img/project_security_dashboard.png
+++ b/doc/user/application_security/security_dashboard/img/project_security_dashboard.png
Binary files differ
diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md
index 97abe99fe62..b520c4fb579 100644
--- a/doc/user/clusters/applications.md
+++ b/doc/user/clusters/applications.md
@@ -140,8 +140,8 @@ that installs additional useful packages on top of the base Jupyter. You
will also see ready-to-use DevOps Runbooks built with Nurtch's [Rubix library](https://github.com/amit1rrr/rubix).
More information on
-creating executable runbooks can be found in [our Nurtch
-documentation](../project/clusters/runbooks/index.md#nurtch-executable-runbooks). Note that
+creating executable runbooks can be found in [our Runbooks
+documentation](../project/clusters/runbooks/index.md#executable-runbooks). Note that
Ingress must be installed and have an IP address assigned before
JupyterHub can be installed.
@@ -152,6 +152,33 @@ chart is used to install this application with a
[`values.yaml`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/vendor/jupyter/values.yaml)
file.
+#### Jupyter Git Integration
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/28783) in GitLab 12 for project-level clusters.
+
+When installing JupyterHub onto your Kubernetes cluster, [JupyterLab's Git extension](https://github.com/jupyterlab/jupyterlab-git)
+is automatically provisioned and configured using the authenticated user's:
+
+- Name
+- Email
+- Newly created access token
+
+JupyterLab's Git extension enables full version control of your notebooks as well as issuance of Git commands within Jupyter.
+Git commands can be issued via the **Git** tab on the left panel or via Jupyter's command line prompt.
+
+NOTE: **Note:**
+JupyterLab's Git extension stores the user token in the JupyterHub DB in encrypted format
+and in the single user Jupyter instance as plain text. This is because [Git requires storing
+credentials as plain text](https://git-scm.com/docs/git-credential-store). Potentially, if
+a nefarious user finds a way to read from the file system in the single user Jupyter instance
+they could retrieve the token.
+
+![Jupyter's Git Extension](img/jupyter-git-extension.gif)
+
+You can clone repositories from the files tab in Jupyter:
+
+![Jupyter clone repository](img/jupyter-gitclone.png)
+
### Knative
> Available for project-level clusters since GitLab 11.5.
diff --git a/doc/user/clusters/img/jupyter-git-extension.gif b/doc/user/clusters/img/jupyter-git-extension.gif
new file mode 100644
index 00000000000..13a88d97425
--- /dev/null
+++ b/doc/user/clusters/img/jupyter-git-extension.gif
Binary files differ
diff --git a/doc/user/clusters/img/jupyter-gitclone.png b/doc/user/clusters/img/jupyter-gitclone.png
new file mode 100644
index 00000000000..41d467f806a
--- /dev/null
+++ b/doc/user/clusters/img/jupyter-gitclone.png
Binary files differ
diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md
index 8458b4f5de3..3c5e820c1ca 100644
--- a/doc/user/group/clusters/index.md
+++ b/doc/user/group/clusters/index.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Group-level Kubernetes clusters
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/34758) in GitLab 11.6.
@@ -48,7 +52,7 @@ Add another cluster similar to the first one and make sure to
[set an environment scope](#environment-scopes-premium) that will
differentiate the new cluster from the rest.
-## Gitlab-managed clusters
+## GitLab-managed clusters
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22011) in GitLab 11.5.
> Became [optional](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/26565) in GitLab 11.11.
@@ -87,7 +91,7 @@ The domain should have a wildcard DNS configured to the Ingress IP address.
When adding more than one Kubernetes cluster to your project, you need to differentiate
them with an environment scope. The environment scope associates clusters with
[environments](../../../ci/environments.md) similar to how the
-[environment-specific variables](https://docs.gitlab.com/ee/ci/variables/#limiting-environment-scopes-of-environment-variables-premium)
+[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-environment-variables-premium)
work.
While evaluating which environment matches the environment scope of a
@@ -145,3 +149,15 @@ The following features are not currently available for group-level clusters:
1. Terminals (see [related issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/55487)).
1. Pod logs (see [related issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/55488)).
1. Deployment boards (see [related issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/55489)).
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/group/contribution_analytics/index.md b/doc/user/group/contribution_analytics/index.md
index bc88eff9ed2..7e6cb24a51e 100644
--- a/doc/user/group/contribution_analytics/index.md
+++ b/doc/user/group/contribution_analytics/index.md
@@ -1,46 +1,58 @@
-# Contribution Analytics **[STARTER]**
+---
+type: reference
+---
->**Note:**
-Introduced in [GitLab Starter][ee] 8.3.
+# Contribution Analytics **[STARTER]**
-Track your team members' activity across your organization.
+> Introduced in [GitLab Starter](https://about.gitlab.com/pricing/) 8.3.
## Overview
-With Contribution Analytics you can get an overview of the activity of
-issues, merge requests, and push events of your organization and its members.
+With Contribution Analytics you can get an overview of the following activity in your
+group:
-The analytics page is located at **Group > Contribution Analytics**
-under the URL `/groups/<groupname>/analytics`.
+- Issues
+- Merge requests
+- Push events
+
+To view the Contribution Analytics, go to your group's **Overview > Contribution Analytics**
+page.
## Use cases
-- Analyze your team's contributions over a period of time and offer a bonus for the top contributors
-- Identify opportunities for improvement with group members who may benefit from additional support
+- Analyze your team's contributions over a period of time, and offer a bonus for the top
+contributors.
+- Identify opportunities for improvement with group members who may benefit from additional
+support.
## Using Contribution Analytics
-There are three main bar graphs that are deducted from the number of
-contributions per group member. These contributions include push events, merge
-requests and closed issues. Hovering on each bar you can see the number of
-events for a specific member.
+There are three main bar graphs that illustrate the number of contributions per group
+member for the following:
+
+- Push events
+- Merge requests
+- Closed issues
+
+Hover over each bar to display the number of events for a specific group member.
![Contribution analytics bar graphs](img/group_stats_graph.png)
## Changing the period time
-There are three periods you can choose from, 'Last week', 'Last month' and
-'Last three months'. The default is 'Last week'.
+You can choose from the following three periods:
+
+- Last week (default)
+- Last month
+- Last three months
-You can choose which period to display by using the dropdown calendar menu in
-the upper right corner.
+Select the desired period from the calendar dropdown.
![Contribution analytics choose period](img/group_stats_cal.png)
## Sorting by different factors
-Apart from the bar graphs you can also see the contributions per group member
-which are depicted in a table that can be sorted by:
+Contributions per group member are also presented in tabular format. Click a column header to sort the table by that column:
* Member name
* Number of pushed events
@@ -52,4 +64,14 @@ which are depicted in a table that can be sorted by:
![Contribution analytics contributions table](img/group_stats_table.png)
-[ee]: https://about.gitlab.com/pricing/
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/group/custom_project_templates.md b/doc/user/group/custom_project_templates.md
index f67325272a6..aa088d2fcdb 100644
--- a/doc/user/group/custom_project_templates.md
+++ b/doc/user/group/custom_project_templates.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Custom group-level project templates **[PREMIUM]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6861) in [GitLab Premium](https://about.gitlab.com/pricing) 11.6.
@@ -24,3 +28,15 @@ Projects of nested subgroups of a selected template source cannot be used.
Repository and database information that are copied over to each new project are
identical to the data exported with [GitLab's Project Import/Export](../project/settings/import_export.md).
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/group/epics/index.md b/doc/user/group/epics/index.md
index 6035f0c7326..2e4106f55e5 100644
--- a/doc/user/group/epics/index.md
+++ b/doc/user/group/epics/index.md
@@ -1,3 +1,7 @@
+---
+type: reference, howto
+---
+
# Epics **[ULTIMATE]**
> Introduced in [GitLab Ultimate][ee] 10.2.
@@ -215,3 +219,15 @@ Once you wrote your comment, you can either:
## Notifications
- [Receive notifications](../../../workflow/notifications.md) for epic events.
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 7493e65e237..8335e532d0b 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -1,7 +1,15 @@
+---
+type: reference, howto
+---
+
# Groups
-With GitLab Groups you can assemble related projects together
-and grant members access to several projects at once.
+With GitLab Groups, you can:
+
+- Assemble related projects together.
+- Grant members access to several projects at once.
+
+For a video introduction to GitLab Groups, see [GitLab University: Repositories, Projects and Groups](https://www.youtube.com/watch?v=4TWfh1aKHHw).
Groups can also be nested in [subgroups](subgroups/index.md).
@@ -9,17 +17,21 @@ Find your groups by clicking **Groups > Your Groups** in the top navigation.
![GitLab Groups](img/groups.png)
-> The groups dropdown in the top navigation was [introduced][ce-36234] in [GitLab 11.1](https://about.gitlab.com/2018/07/22/gitlab-11-1-released/#groups-dropdown-in-navigation).
+> The **Groups** dropdown in the top navigation was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/36234) in [GitLab 11.1](https://about.gitlab.com/2018/07/22/gitlab-11-1-released/#groups-dropdown-in-navigation).
+
+The **Groups** page displays:
-The Groups page displays:
+- All groups you are a member of, when **Your groups** is selected.
+- A list of public groups, when **Explore public groups** is selected.
-- All groups you are a member of.
-- How many projects each group contains.
-- How many members a group has.
-- The group visibility.
-- A link to the group settings if you have sufficient permissions.
+Each group on the **Groups** page is listed with:
-By clicking the last button, you can leave that group.
+- How many subgroups it has.
+- How many projects it contains.
+- How many members the group has, not including members inherited from parent groups.
+- The group's visibility.
+- A link to the group's settings, if you have sufficient permissions.
+- A link to leave the group, if you are a member.
## Use cases
@@ -206,7 +218,7 @@ Get an overview of the vulnerabilities of all the projects in a group and its su
> Introduced in [GitLab Ultimate][ee] 11.9 behind the `insights` feature flag.
Configure the Insights that matter for your groups or projects, allowing users to explore data
-such as:
+such as:
- Triage hygiene
- Issues created/closed per a given period
@@ -296,7 +308,7 @@ To enable this feature, navigate to the group settings page. Select
Member lock lets a group owner prevent any new project membership to all of the
projects within a group, allowing tighter control over project membership.
-For example, if you want to lock the group for an [Audit Event](https://docs.gitlab.com/ee/administration/audit_events.html),
+For example, if you want to lock the group for an [Audit Event](../../administration/audit_events.md),
enable Member lock to guarantee that project membership cannot be modified during that audit.
To enable this feature:
@@ -315,7 +327,7 @@ request to add a new user to a project through API will not be possible.
Group file templates allow you to share a set of templates for common file
types with every project in a group. It is analogous to the
-[instance template repository](https://docs.gitlab.com/ee/user/admin_area/settings/instance_template_repository.html)
+[instance template repository](../admin_area/settings/instance_template_repository.md)
feature, and the selected project should follow the same naming conventions as
are documented on that page.
@@ -346,7 +358,7 @@ Define project templates at a group level by setting a group as the template sou
access each project's settings, and remove any project, all from the same screen.
- **Webhooks**: Configure [webhooks](../project/integrations/webhooks.md) for your group.
- **Kubernetes cluster integration**: Connect your GitLab group with [Kubernetes clusters](clusters/index.md).
-- **Audit Events**: View [Audit Events](https://docs.gitlab.com/ee/administration/audit_events.html)
+- **Audit Events**: View [Audit Events](../../administration/audit_events.md)
for the group. **[STARTER ONLY]**
- **Pipelines quota**: Keep track of the [pipeline quota](../admin_area/settings/continuous_integration.md) for the group.
@@ -364,5 +376,17 @@ With [GitLab Issues Analytics](issues_analytics/index.md), you can see a bar cha
Use GitLab as a [dependency proxy](dependency_proxy/index.md) for upstream Docker images.
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
+
[ee]: https://about.gitlab.com/pricing/
[ee-2534]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2534
diff --git a/doc/user/group/insights/index.md b/doc/user/group/insights/index.md
index 20f76c54ae7..a4ea71074ec 100644
--- a/doc/user/group/insights/index.md
+++ b/doc/user/group/insights/index.md
@@ -1,3 +1,7 @@
+---
+type: reference, howto
+---
+
# Insights **[ULTIMATE]**
> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.9 behind the `insights` feature flag.
@@ -28,10 +32,10 @@ the project that holds your `.gitlab/insights.yml` configuration file:
![group insights configuration](img/insights_group_configuration.png)
If no configuration was set, a [default configuration file](
-https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/fixtures/insights/ee/fixtures/insights/default.yml)
+https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/fixtures/insights/default.yml)
will be used.
-See the [Project's Insights documentation](https://docs.gitlab.com/ee/user/project/insights/index.html) for
+See the [Project's Insights documentation](../../project/insights/index.md) for
more details about the `.gitlab/insights.yml` configuration file.
## Permissions
@@ -44,3 +48,15 @@ access to the project they belong to, or because they are confidential) are
filtered out of the Insights charts.
You may also consult the [group permissions table](../../permissions.md#group-members-permissions).
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/group/issues_analytics/index.md b/doc/user/group/issues_analytics/index.md
index cf53d7423a6..46d5c1e2e09 100644
--- a/doc/user/group/issues_analytics/index.md
+++ b/doc/user/group/issues_analytics/index.md
@@ -1,16 +1,43 @@
+---
+type: reference
+---
+
# Issues Analytics **[PREMIUM]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7478) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.5.
-GitLab by default displays a bar chart of the number of issues created each month, for the
-current month, and 12 months prior, for a total of 13 months.
+Issues Analytics is a bar graph which illustrates the number of issues created each month.
+The default timespan is 13 months, which includes the current month, and the 12 months
+prior.
-You can change the total number of months displayed by setting a URL parameter.
-For example, `https://gitlab.com/groups/gitlab-org/-/issues_analytics?months_back=15`
-would show a total of 15 months for the chart in the GitLab.org group.
+To access the chart, navigate to a group's sidebar and select **Issues > Analytics**.
-The **Search or filter results...** field can be used for filtering the issues by any attribute. For example, labels, assignee, milestone, and author.
+Hover over each bar to see the total number of issues.
-To access the chart, navigate to a group's sidebar and select **Issues > Analytics**.
+To narrow the scope of issues included in the graph, enter your criteria in the
+**Search or filter results...** field. Criteria from the following list can be typed in or selected from a menu:
+
+- Author
+- Assignee
+- Milestone
+- Label
+- My reaction
+- Weight
+
+You can change the total number of months displayed by setting a URL parameter.
+For example, `https://gitlab.com/groups/gitlab-org/-/issues_analytics?months_back=15`
+shows a total of 15 months for the chart in the GitLab.org group.
![Issues created per month](img/issues_created_per_month.png)
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/group/roadmap/index.md b/doc/user/group/roadmap/index.md
index 310c4bb88d0..683c715c8d5 100644
--- a/doc/user/group/roadmap/index.md
+++ b/doc/user/group/roadmap/index.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Roadmap **[ULTIMATE]**
> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing) 10.5.
@@ -19,13 +23,20 @@ Epics in the view can be sorted by:
- **Start date**
- **Due date**
-Each option contains a button that can toggle the order between **ascending** and **descending**. The sort option and order will be persisted to be used wherever epics are browsed including the [epics list view](../epics/index.md).
+Each option contains a button that toggles the sort order between **ascending** and **descending**. The sort option and order will be persisted when browsing Epics,
+including the [epics list view](../epics/index.md).
Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap-in-epics).
## Timeline duration
-Starting with [GitLab Ultimate][ee] 11.0, Roadmap supports three different date ranges; Quarters, Months (Default) and Weeks.
+> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing) 11.0.
+
+Roadmap supports the following date ranges:
+
+- Quarters
+- Months (Default)
+- Weeks
### Quarters
@@ -62,3 +73,15 @@ and due date. If an epic doesn't have a due date, the timeline bar fades
away towards the future. Similarly, if an epic doesn't have a start date, the
timeline bar becomes more visible as it approaches the epic's due date on the
timeline.
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index 62a3ef52c34..fcfd638f185 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -1,3 +1,7 @@
+---
+type: reference, howto
+---
+
# SAML SSO for GitLab.com Groups **[SILVER ONLY]**
> Introduced in [GitLab.com Silver](https://about.gitlab.com/pricing/) 11.0.
@@ -15,7 +19,7 @@ SAML SSO for GitLab.com groups does not sync users between providers without usi
## Configuring your Identity Provider
1. Navigate to the group and click **Settings > SAML SSO**.
-1. Configure your SAML server using the **Assertion consumer service URL** and **Issuer**. Alternatively GitLab provides [metadata XML configuration](#metadata-configuration). See [your identity provider's documentation](#providers) for more details.
+1. Configure your SAML server using the **Assertion consumer service URL** and **Identifier**. Alternatively GitLab provides [metadata XML configuration](#metadata-configuration). See [your identity provider's documentation](#providers) for more details.
1. Configure the SAML response to include a NameID that uniquely identifies each user.
1. Configure required assertions using the [table below](#assertions).
1. Once the identity provider is set up, move on to [configuring GitLab](#configuring-gitlab).
@@ -43,12 +47,12 @@ GitLab.com uses the SAML NameID to identify users. The NameID element:
### Assertions
-| Field | Supported keys | Notes |
-|-|----------------|-------------|
-| Email | `email`, `mail` | (required) |
-| Full Name | `name` | |
-| First Name | `first_name`, `firstname`, `firstName` | |
-| Last Name | `last_name`, `lastname`, `lastName` | |
+| Field | Supported keys |
+|-------|----------------|
+| Email (required)| `email`, `mail` |
+| Full Name | `name` |
+| First Name | `first_name`, `firstname`, `firstName` |
+| Last Name | `last_name`, `lastname`, `lastName` |
## Metadata configuration
@@ -122,3 +126,15 @@ For example, to unlink the `MyOrg` account, the following **Disconnect** button
| Assertion consumer service URL | The callback on GitLab where users will be redirected after successfully authenticating with the identity provider. |
| Issuer | How GitLab identifies itself to the identity provider. Also known as a "Relying party trust identifier". |
| Certificate fingerprint | Used to confirm that communications over SAML are secure by checking that the server is signing communications with the correct certificate. Also known as a certificate thumbprint. |
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/group/saml_sso/scim_setup.md b/doc/user/group/saml_sso/scim_setup.md
index ec27ec3e336..c00628bf909 100644
--- a/doc/user/group/saml_sso/scim_setup.md
+++ b/doc/user/group/saml_sso/scim_setup.md
@@ -2,7 +2,7 @@
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9388) in [GitLab.com Silver](https://about.gitlab.com/pricing/) 11.10.
-GitLab's [SCIM API](https://docs.gitlab.com/ee/api/scim.html) implements part of [the RFC7644 protocol](https://tools.ietf.org/html/rfc7644).
+GitLab's [SCIM API](../../../api/scim.md) implements part of [the RFC7644 protocol](https://tools.ietf.org/html/rfc7644).
Currently, the following actions are available:
diff --git a/doc/user/group/security_dashboard/index.md b/doc/user/group/security_dashboard/index.md
index 43e910b29fe..c59198df081 100644
--- a/doc/user/group/security_dashboard/index.md
+++ b/doc/user/group/security_dashboard/index.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'https://docs.gitlab.com/ee/user/application_security/security_dashboard/index.html'
+redirect_to: '../../application_security/security_dashboard/index.md'
---
-This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/security_dashboard/index.html).
+This document was moved to [another location](../../application_security/security_dashboard/index.md).
diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md
index 4e81e28a45a..79ae94dd9ef 100644
--- a/doc/user/group/subgroups/index.md
+++ b/doc/user/group/subgroups/index.md
@@ -1,11 +1,17 @@
+---
+type: reference, howto, concepts
+---
+
# Subgroups
NOTE: **Note:**
[Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/2772) in GitLab 9.0. Not available when using MySQL as external
database (support removed in GitLab 9.3 [due to performance reasons](https://gitlab.com/gitlab-org/gitlab-ce/issues/30472#note_27747600)).
-With subgroups (aka nested groups or hierarchical groups) you can have
-up to 20 levels of nested groups, which among other things can help you to:
+Subgroups, also known as nested groups or hierarchical groups, allow you to have up to 20
+levels of groups.
+
+By using subgroups you can do the following:
- **Separate internal / external organizations.** Since every group
can have its own visibility level, you are able to host groups for different
@@ -17,8 +23,9 @@ up to 20 levels of nested groups, which among other things can help you to:
## Database Requirements
-Nested groups are only supported when you use PostgreSQL. Supporting nested
-groups on MySQL in an efficient way is not possible due to MySQL's limitations.
+Subgroups are only supported when you use PostgreSQL. Supporting subgroups on MySQL in an
+efficient way is not possible due to MySQL's limitations.
+
See the following links for more information:
- <https://gitlab.com/gitlab-org/gitlab-ce/issues/30472>
@@ -37,7 +44,7 @@ only 1 parent group. It resembles a directory behavior or a nested items list:
- Group 1.2.2.1
In a real world example, imagine maintaining a GNU/Linux distribution with the
-first group being the name of the distro and subsequent groups split like:
+first group being the name of the distribution, and subsequent groups split as follows:
- Organization Group - GNU/Linux distro
- Category Subgroup - Packages
@@ -69,22 +76,22 @@ Another example of GitLab as a company would be the following:
---
-The maximum nested groups a group can have, including the first one in the
+The maximum subgroups a group can have, including the first one in the
hierarchy, is 21.
-Things like transferring or importing a project inside nested groups, work like
+Actions such as transferring or importing a project inside subgroups, work like
when performing these actions the traditional way with the `group/project`
structure.
## Creating a subgroup
NOTE: **Note:**
-You need to be an Owner of a group in order to be able to create a subgroup. For
+You must be an Owner of a group to create a subgroup. For
more information check the [permissions table](../../permissions.md#group-members-permissions).
For a list of words that are not allowed to be used as group names see the
[reserved names](../../reserved_names.md).
Users can always create subgroups if they are explicitly added as an Owner to
-a parent group even if group creation is disabled by an administrator in their
+a parent group, even if group creation is disabled by an administrator in their
settings.
To create a subgroup:
@@ -111,7 +118,7 @@ When you add a member to a subgroup, they inherit the membership and permission
level from the parent group. This model allows access to nested groups if you
have membership in one of its parents.
-The group permissions for a member can be changed only by Owners and only on
+The group permissions for a member can be changed only by Owners, and only on
the **Members** page of the group the member was added.
You can tell if a member has inherited the permissions from a parent group by
@@ -119,24 +126,24 @@ looking at the group's **Members** page.
![Group members page](img/group_members.png)
-From the image above, we can deduct the following things:
+From the image above, we can deduce the following things:
-- There are 5 members that have access to the group `four`
+- There are 5 members that have access to the group `four`.
- User0 is a Reporter and has inherited their permissions from group `one`
- which is above the hierarchy of group `four`
+ which is above the hierarchy of group `four`.
- User1 is a Developer and has inherited their permissions from group
- `one/two` which is above the hierarchy of group `four`
+ `one/two` which is above the hierarchy of group `four`.
- User2 is a Developer and has inherited their permissions from group
- `one/two/three` which is above the hierarchy of group `four`
+ `one/two/three` which is above the hierarchy of group `four`.
- For User3 there is no indication of a parent group, therefore they belong to
- group `four`, the one we're inspecting
+ group `four`, the one we're inspecting.
- Administrator is the Owner and member of **all** subgroups and for that reason,
- same as User3, there is no indication of an ancestor group
+ as with User3, there is no indication of an ancestor group.
### Overriding the ancestor group membership
NOTE: **Note:**
-You need to be an Owner of a group in order to be able to add members to it.
+You must be an Owner of a group to be able to add members to it.
NOTE: **Note:**
A user's permissions in a subgroup cannot be lower than in any of its ancestor groups.
@@ -154,7 +161,7 @@ the permissions will fallback to those of the ancestor group.
## Mentioning subgroups
Mentioning groups (`@group`) in issues, commits and merge requests, would
-notify all members of that group. Now with subgroups, there is a more granular
+notify all members of that group. Now with subgroups, there is more granular
support if you want to split your group's structure. Mentioning works as before
and you can choose the group of people to be notified.
@@ -179,3 +186,15 @@ Here's a list of what you can't do with subgroups:
[permissions]: ../../permissions.md#group-members-permissions
[reserved]: ../../reserved_names.md
[issue]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30472#note_27747600
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/index.md b/doc/user/index.md
index 67d5cbf06ec..1fc4e4c43cf 100644
--- a/doc/user/index.md
+++ b/doc/user/index.md
@@ -49,22 +49,22 @@ GitLab is a Git-based platform that integrates a great number of essential tools
With GitLab Enterprise Edition, you can also:
-- Provide support with [Service Desk](https://docs.gitlab.com/ee/user/project/service_desk.html).
+- Provide support with [Service Desk](project/service_desk.md).
- Improve collaboration with
- [Merge Request Approvals](https://docs.gitlab.com/ee/user/project/merge_requests/index.html#merge-request-approvals-starter),
- [Multiple Assignees for Issues](https://docs.gitlab.com/ee/user/project/issues/multiple_assignees_for_issues.html),
+ [Merge Request Approvals](project/merge_requests/index.md#merge-request-approvals-starter),
+ [Multiple Assignees for Issues](project/issues/multiple_assignees_for_issues.md),
and [Multiple Issue Boards](project/issue_board.md#multiple-issue-boards-starter).
-- Create formal relationships between issues with [Related Issues](https://docs.gitlab.com/ee/user/project/issues/related_issues.html).
-- Use [Burndown Charts](https://docs.gitlab.com/ee/user/project/milestones/burndown_charts.html) to track progress during a sprint or while working on a new version of their software.
-- Leverage [Elasticsearch](https://docs.gitlab.com/ee/integration/elasticsearch.html) with [Advanced Global Search](search/advanced_global_search.md) and [Advanced Syntax Search](search/advanced_search_syntax.md) for faster, more advanced code search across your entire GitLab instance.
-- [Authenticate users with Kerberos](https://docs.gitlab.com/ee/integration/kerberos.html).
+- Create formal relationships between issues with [Related Issues](project/issues/related_issues.md).
+- Use [Burndown Charts](project/milestones/burndown_charts.md) to track progress during a sprint or while working on a new version of their software.
+- Leverage [Elasticsearch](../integration/elasticsearch.md) with [Advanced Global Search](search/advanced_global_search.md) and [Advanced Syntax Search](search/advanced_search_syntax.md) for faster, more advanced code search across your entire GitLab instance.
+- [Authenticate users with Kerberos](../integration/kerberos.md).
- [Mirror a repository](../workflow/repository_mirroring.md) from elsewhere on your local server.
-- [Export issues as CSV](https://docs.gitlab.com/ee/user/project/issues/csv_export.html).
-- View your entire CI/CD pipeline involving more than one project with [Multiple-Project Pipelines](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html).
-- [Lock files](https://docs.gitlab.com/ee/user/project/file_lock.html) to prevent conflicts.
-- View the current health and status of each CI environment running on Kubernetes with [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html).
-- Leverage continuous delivery method with [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html).
-- Scan your code for vulnerabilities and [display them in merge requests](https://docs.gitlab.com/ee/user/application_security/sast/index.html).
+- [Export issues as CSV](project/issues/csv_export.md).
+- View your entire CI/CD pipeline involving more than one project with [Multiple-Project Pipelines](../ci/multi_project_pipeline_graphs.md).
+- [Lock files](project/file_lock.md) to prevent conflicts.
+- View the current health and status of each CI environment running on Kubernetes with [Deploy Boards](project/deploy_boards.md).
+- Leverage continuous delivery method with [Canary Deployments](project/canary_deployments.md).
+- Scan your code for vulnerabilities and [display them in merge requests](application_security/sast/index.md).
You can also [integrate](project/integrations/project_services.md) GitLab with numerous third-party applications, such as Mattermost, Microsoft Teams, HipChat, Trello, Slack, Bamboo CI, JIRA, and a lot more.
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 5dad9621802..6b6e5ab7634 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -1027,7 +1027,7 @@ A link can be constructed relative to the current wiki page using `./<page>`,
it would link to `<your_wiki>/documentation/related`:
```markdown
- [Link to Related Page](./related)
+ [Link to Related Page](related)
```
- If this snippet was placed on a page at `<your_wiki>/documentation/related/content`,
@@ -1041,7 +1041,7 @@ A link can be constructed relative to the current wiki page using `./<page>`,
it would link to `<your_wiki>/documentation/related.md`:
```markdown
- [Link to Related Page](./related.md)
+ [Link to Related Page](related.md)
```
- If this snippet was placed on a page at `<your_wiki>/documentation/related/content`,
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 318053fdabb..a6e2f187090 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -36,91 +36,96 @@ In GitLab 11.0, the Master role was renamed to Maintainer.
The following table depicts the various user permission levels in a project.
-| Action | Guest | Reporter | Developer |Maintainer| Owner |
-|---------------------------------------|---------|------------|-------------|----------|--------|
-| Create new issue | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
-| Create confidential issue | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
-| View confidential issues | (✓) [^2] | ✓ | ✓ | ✓ | ✓ |
-| Leave comments | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
-| See related issues | ✓ | ✓ | ✓ | ✓ | ✓ |
-| See a list of jobs | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
-| See a job log | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
-| Download and browse job artifacts | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
-| View wiki pages | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
-| Create and edit wiki pages | | | ✓ | ✓ | ✓ |
-| Delete wiki pages | | | | ✓ | ✓ |
-| View license management reports **[ULTIMATE]** | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
-| View Security reports **[ULTIMATE]** | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
-| View project code | [^1] | ✓ | ✓ | ✓ | ✓ |
-| Pull project code | [^1] | ✓ | ✓ | ✓ | ✓ |
-| Download project | [^1] | ✓ | ✓ | ✓ | ✓ |
-| Assign issues | | ✓ | ✓ | ✓ | ✓ |
-| Assign merge requests | | | ✓ | ✓ | ✓ |
-| Label issues | | ✓ | ✓ | ✓ | ✓ |
-| Label merge requests | | | ✓ | ✓ | ✓ |
-| Create code snippets | | ✓ | ✓ | ✓ | ✓ |
-| Manage issue tracker | | ✓ | ✓ | ✓ | ✓ |
-| Manage labels | | ✓ | ✓ | ✓ | ✓ |
-| See a commit status | | ✓ | ✓ | ✓ | ✓ |
-| See a container registry | | ✓ | ✓ | ✓ | ✓ |
-| See environments | | ✓ | ✓ | ✓ | ✓ |
-| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
-| Manage related issues **[STARTER]** | | ✓ | ✓ | ✓ | ✓ |
-| Lock issue discussions | | ✓ | ✓ | ✓ | ✓ |
-| Create issue from vulnerability **[ULTIMATE]** | | ✓ | ✓ | ✓ | ✓ |
-| View Error Tracking list | | ✓ | ✓ | ✓ | ✓ |
-| Pull from [Maven repository](https://docs.gitlab.com/ee/user/project/packages/maven_repository.html) or [NPM registry](https://docs.gitlab.com/ee/user/project/packages/npm_registry.html) **[PREMIUM]** | | ✓ | ✓ | ✓ | ✓ |
-| Publish to [Maven repository](https://docs.gitlab.com/ee/user/project/packages/maven_repository.html) or [NPM registry](https://docs.gitlab.com/ee/user/project/packages/npm_registry.html) **[PREMIUM]** | | | ✓ | ✓ | ✓ |
-| Lock merge request discussions | | | ✓ | ✓ | ✓ |
-| Create new environments | | | ✓ | ✓ | ✓ |
-| Stop environments | | | ✓ | ✓ | ✓ |
-| Manage/Accept merge requests | | | ✓ | ✓ | ✓ |
-| Create new merge request | | | ✓ | ✓ | ✓ |
-| Create new branches | | | ✓ | ✓ | ✓ |
-| Push to non-protected branches | | | ✓ | ✓ | ✓ |
-| Force push to non-protected branches | | | ✓ | ✓ | ✓ |
-| Remove non-protected branches | | | ✓ | ✓ | ✓ |
-| Add tags | | | ✓ | ✓ | ✓ |
-| Cancel and retry jobs | | | ✓ | ✓ | ✓ |
-| Create or update commit status | | | ✓ | ✓ | ✓ |
-| Update a container registry | | | ✓ | ✓ | ✓ |
-| Remove a container registry image | | | ✓ | ✓ | ✓ |
-| Create/edit/delete project milestones | | | ✓ | ✓ | ✓ |
+| Action | Guest | Reporter | Developer |Maintainer| Owner |
+|---------------------------------------------------|---------|------------|-------------|----------|--------|
+| Download project | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
+| Leave comments | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
+| View Insights charts **[ULTIMATE]** | ✓ | ✓ | ✓ | ✓ | ✓ |
| View approved/blacklisted licenses **[ULTIMATE]** | ✓ | ✓ | ✓ | ✓ | ✓ |
-| Use security dashboard **[ULTIMATE]** | | | ✓ | ✓ | ✓ |
-| Dismiss vulnerability **[ULTIMATE]** | | | ✓ | ✓ | ✓ |
-| Apply code change suggestions | | | ✓ | ✓ | ✓ |
-| Use environment terminals | | | | ✓ | ✓ |
-| Run Web IDE's Interactive Web Terminals **[ULTIMATE ONLY]** | | | | ✓ | ✓ |
-| Add new team members | | | | ✓ | ✓ |
-| Push to protected branches | | | | ✓ | ✓ |
-| Enable/disable branch protection | | | | ✓ | ✓ |
-| Turn on/off protected branch push for devs| | | | ✓ | ✓ |
-| Enable/disable tag protections | | | | ✓ | ✓ |
-| Rewrite/remove Git tags | | | | ✓ | ✓ |
-| Edit project | | | | ✓ | ✓ |
-| Add deploy keys to project | | | | ✓ | ✓ |
-| Configure project hooks | | | | ✓ | ✓ |
-| Manage Runners | | | | ✓ | ✓ |
-| Manage job triggers | | | | ✓ | ✓ |
-| Manage variables | | | | ✓ | ✓ |
-| Manage GitLab Pages | | | | ✓ | ✓ |
-| Manage GitLab Pages domains and certificates | | | | ✓ | ✓ |
-| Remove GitLab Pages | | | | ✓ | ✓ |
+| View license management reports **[ULTIMATE]** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
+| View Security reports **[ULTIMATE]** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
+| View project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
+| Pull project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View GitLab Pages protected by [access control](project/pages/introduction.md#gitlab-pages-access-control-core-only) | ✓ | ✓ | ✓ | ✓ | ✓ |
-| Manage clusters | | | | ✓ | ✓ |
-| Manage license policy **[ULTIMATE]** | | | | ✓ | ✓ |
-| Edit comments (posted by any user) | | | | ✓ | ✓ |
-| Manage Error Tracking | | | | ✓ | ✓ |
-| Switch visibility level | | | | | ✓ |
-| Transfer project to another namespace | | | | | ✓ |
-| Remove project | | | | | ✓ |
-| Delete issues | | | | | ✓ |
-| Force push to protected branches [^4] | | | | | |
-| Remove protected branches [^4] | | | | | |
-| View project Audit Events | | | | ✓ | ✓ |
-| View project statistics | | ✓ | ✓ | ✓ | ✓ |
-| View Insights charts **[ULTIMATE]** | ✓ | ✓ | ✓ | ✓ | ✓ |
+| View wiki pages | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
+| See a list of jobs | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
+| See a job log | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
+| Download and browse job artifacts | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
+| Create new issue | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
+| See related issues | ✓ | ✓ | ✓ | ✓ | ✓ |
+| Create confidential issue | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
+| View confidential issues | (*2*) | ✓ | ✓ | ✓ | ✓ |
+| Assign issues | | ✓ | ✓ | ✓ | ✓ |
+| Label issues | | ✓ | ✓ | ✓ | ✓ |
+| Lock issue discussions | | ✓ | ✓ | ✓ | ✓ |
+| Manage issue tracker | | ✓ | ✓ | ✓ | ✓ |
+| Manage related issues **[STARTER]** | | ✓ | ✓ | ✓ | ✓ |
+| Create issue from vulnerability **[ULTIMATE]** | | ✓ | ✓ | ✓ | ✓ |
+| Manage labels | | ✓ | ✓ | ✓ | ✓ |
+| Create code snippets | | ✓ | ✓ | ✓ | ✓ |
+| See a commit status | | ✓ | ✓ | ✓ | ✓ |
+| See a container registry | | ✓ | ✓ | ✓ | ✓ |
+| See environments | | ✓ | ✓ | ✓ | ✓ |
+| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
+| View project statistics | | ✓ | ✓ | ✓ | ✓ |
+| View Error Tracking list | | ✓ | ✓ | ✓ | ✓ |
+| Pull from [Maven repository](project/packages/maven_repository.md) or [NPM registry](project/packages/npm_registry.md) **[PREMIUM]** | | ✓ | ✓ | ✓ | ✓ |
+| Publish to [Maven repository](project/packages/maven_repository.md) or [NPM registry](project/packages/npm_registry.md) **[PREMIUM]** | | | ✓ | ✓ | ✓ ||
+| Create new branches | | | ✓ | ✓ | ✓ |
+| Push to non-protected branches | | | ✓ | ✓ | ✓ |
+| Force push to non-protected branches | | | ✓ | ✓ | ✓ |
+| Remove non-protected branches | | | ✓ | ✓ | ✓ |
+| Create new merge request | | | ✓ | ✓ | ✓ |
+| Assign merge requests | | | ✓ | ✓ | ✓ |
+| Label merge requests | | | ✓ | ✓ | ✓ |
+| Lock merge request discussions | | | ✓ | ✓ | ✓ |
+| Manage/Accept merge requests | | | ✓ | ✓ | ✓ |
+| Create new environments | | | ✓ | ✓ | ✓ |
+| Stop environments | | | ✓ | ✓ | ✓ |
+| Add tags | | | ✓ | ✓ | ✓ |
+| Cancel and retry jobs | | | ✓ | ✓ | ✓ |
+| Create or update commit status | | | ✓ | ✓ | ✓ |
+| Update a container registry | | | ✓ | ✓ | ✓ |
+| Remove a container registry image | | | ✓ | ✓ | ✓ |
+| Create/edit/delete project milestones | | | ✓ | ✓ | ✓ |
+| Use security dashboard **[ULTIMATE]** | | | ✓ | ✓ | ✓ |
+| Dismiss vulnerability **[ULTIMATE]** | | | ✓ | ✓ | ✓ |
+| Apply code change suggestions | | | ✓ | ✓ | ✓ |
+| Create and edit wiki pages | | | ✓ | ✓ | ✓ |
+| Use environment terminals | | | | ✓ | ✓ |
+| Run Web IDE's Interactive Web Terminals **[ULTIMATE ONLY]** | | | | ✓ | ✓ |
+| Add new team members | | | | ✓ | ✓ |
+| Enable/disable branch protection | | | | ✓ | ✓ |
+| Push to protected branches | | | | ✓ | ✓ |
+| Turn on/off protected branch push for devs | | | | ✓ | ✓ |
+| Enable/disable tag protections | | | | ✓ | ✓ |
+| Rewrite/remove Git tags | | | | ✓ | ✓ |
+| Edit project | | | | ✓ | ✓ |
+| Add deploy keys to project | | | | ✓ | ✓ |
+| Configure project hooks | | | | ✓ | ✓ |
+| Manage Runners | | | | ✓ | ✓ |
+| Manage job triggers | | | | ✓ | ✓ |
+| Manage variables | | | | ✓ | ✓ |
+| Manage GitLab Pages | | | | ✓ | ✓ |
+| Manage GitLab Pages domains and certificates | | | | ✓ | ✓ |
+| Remove GitLab Pages | | | | ✓ | ✓ |
+| Manage clusters | | | | ✓ | ✓ |
+| Manage license policy **[ULTIMATE]** | | | | ✓ | ✓ |
+| Edit comments (posted by any user) | | | | ✓ | ✓ |
+| Manage Error Tracking | | | | ✓ | ✓ |
+| Delete wiki pages | | | | ✓ | ✓ |
+| View project Audit Events | | | | ✓ | ✓ |
+| Switch visibility level | | | | | ✓ |
+| Transfer project to another namespace | | | | | ✓ |
+| Remove project | | | | | ✓ |
+| Delete issues | | | | | ✓ |
+| Force push to protected branches [^4] | | | | | |
+| Remove protected branches [^4] | | | | | |
+
+- (*1*): All users are able to perform this action on public and internal projects, but not private projects.
+- (*2*): Guest users can only view the confidential issues they created themselves
+- (*3*): If **Public pipelines** is enabled in **Project Settings > CI/CD**
+- (*4*): Not allowed for Guest, Reporter, Developer, Maintainer, or Owner
## Project features permissions
@@ -163,7 +168,7 @@ to learn more.
The user that locks a file or directory is the only one that can edit and push their changes back to the repository where the locked objects are located.
-Read through the documentation on [permissions for File Locking](https://docs.gitlab.com/ee/user/project/file_lock.html#permissions-on-file-locking) to learn more.
+Read through the documentation on [permissions for File Locking](project/file_lock.md#permissions-on-file-locking) to learn more.
### Confidential Issues permissions
@@ -191,21 +196,21 @@ Any user can remove themselves from a group, unless they are the last Owner of
the group. The following table depicts the various user permission levels in a
group.
-| Action | Guest | Reporter | Developer | Maintainer | Owner |
-|-------------------------|-------|----------|-----------|--------|-------|
-| Browse group | ✓ | ✓ | ✓ | ✓ | ✓ |
-| Edit group | | | | | ✓ |
-| Create subgroup | | | | | ✓ |
-| Create project in group | | | ✓ | ✓ | ✓ |
-| Manage group members | | | | | ✓ |
-| Remove group | | | | | ✓ |
-| Manage group labels | | ✓ | ✓ | ✓ | ✓ |
-| Create/edit/delete group milestones | | | ✓ | ✓ | ✓ |
-| View group epic **[ULTIMATE]** | ✓ | ✓ | ✓ | ✓ | ✓ |
-| Create/edit group epic **[ULTIMATE]** | | ✓ | ✓ | ✓ | ✓ |
-| Delete group epic **[ULTIMATE]** | | | | | ✓ |
-| View group Audit Events | | | | | ✓ |
-| View Insights charts **[ULTIMATE]** | ✓ | ✓ | ✓ | ✓ | ✓ |
+| Action | Guest | Reporter | Developer | Maintainer | Owner |
+|---------------------------------------|-------|----------|-----------|------------|-------|
+| Browse group | ✓ | ✓ | ✓ | ✓ | ✓ |
+| View Insights charts **[ULTIMATE]** | ✓ | ✓ | ✓ | ✓ | ✓ |
+| View group epic **[ULTIMATE]** | ✓ | ✓ | ✓ | ✓ | ✓ |
+| Create/edit group epic **[ULTIMATE]** | | ✓ | ✓ | ✓ | ✓ |
+| Manage group labels | | ✓ | ✓ | ✓ | ✓ |
+| Create project in group | | | ✓ | ✓ | ✓ |
+| Create/edit/delete group milestones | | | ✓ | ✓ | ✓ |
+| Edit group | | | | | ✓ |
+| Create subgroup | | | | | ✓ |
+| Manage group members | | | | | ✓ |
+| Remove group | | | | | ✓ |
+| Delete group epic **[ULTIMATE]** | | | | | ✓ |
+| View group Audit Events | | | | | ✓ |
### Subgroup permissions
@@ -257,15 +262,15 @@ Please be aware that this regex could lead to a DOS attack, [see](https://en.wik
## Auditor users **[PREMIUM ONLY]**
->[Introduced][ee-998] in [GitLab Premium][eep] 8.17.
+>[Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/998) in [GitLab Premium](https://about.gitlab.com/pricing/) 8.17.
Auditor users are given read-only access to all projects, groups, and other
resources on the GitLab instance.
An Auditor user should be able to access all projects and groups of a GitLab instance
-with the permissions described on the documentation on [auditor users permissions](https://docs.gitlab.com/ee/administration/auditor_users.html#permissions-and-restrictions-of-an-auditor-user).
+with the permissions described on the documentation on [auditor users permissions](../administration/auditor_users.md#permissions-and-restrictions-of-an-auditor-user).
-[Read more about Auditor users.](https://docs.gitlab.com/ee/administration/auditor_users.html)
+[Read more about Auditor users.](../administration/auditor_users.md)
## Project features
@@ -298,7 +303,7 @@ instance and project. In addition, all admins can use the admin interface under
|---------------------------------------|-----------------|-------------|----------|--------|
| See commits and jobs | ✓ | ✓ | ✓ | ✓ |
| Retry or cancel job | | ✓ | ✓ | ✓ |
-| Erase job artifacts and trace | | ✓ [^5] | ✓ | ✓ |
+| Erase job artifacts and trace | | ✓ (*1*) | ✓ | ✓ |
| Remove project | | | ✓ | ✓ |
| Create project | | | ✓ | ✓ |
| Change project configuration | | | ✓ | ✓ |
@@ -307,6 +312,8 @@ instance and project. In addition, all admins can use the admin interface under
| See events in the system | | | | ✓ |
| Admin interface | | | | ✓ |
+- *1*: Only if the job was triggered by the user
+
### Job permissions
NOTE: **Note:**
@@ -314,25 +321,28 @@ In GitLab 11.0, the Master role was renamed to Maintainer.
>**Note:**
GitLab 8.12 has a completely redesigned job permissions system.
-Read all about the [new model and its implications][new-mod].
+Read all about the [new model and its implications](project/new_ci_build_permissions_model.md).
This table shows granted privileges for jobs triggered by specific types of
users:
-| Action | Guest, Reporter | Developer |Maintainer| Admin |
-|---------------------------------------------|-----------------|-------------|----------|--------|
-| Run CI job | | ✓ | ✓ | ✓ |
-| Clone source and LFS from current project | | ✓ | ✓ | ✓ |
-| Clone source and LFS from public projects | | ✓ | ✓ | ✓ |
-| Clone source and LFS from internal projects | | ✓ [^6] | ✓ [^6] | ✓ |
-| Clone source and LFS from private projects | | ✓ [^7] | ✓ [^7] | ✓ [^7] |
-| Push source and LFS | | | | |
-| Pull container images from current project | | ✓ | ✓ | ✓ |
-| Pull container images from public projects | | ✓ | ✓ | ✓ |
-| Pull container images from internal projects| | ✓ [^6] | ✓ [^6] | ✓ |
-| Pull container images from private projects | | ✓ [^7] | ✓ [^7] | ✓ [^7] |
-| Push container images to current project | | ✓ | ✓ | ✓ |
-| Push container images to other projects | | | | |
+| Action | Guest, Reporter | Developer |Maintainer| Admin |
+|---------------------------------------------|-----------------|-------------|----------|---------|
+| Run CI job | | ✓ | ✓ | ✓ |
+| Clone source and LFS from current project | | ✓ | ✓ | ✓ |
+| Clone source and LFS from public projects | | ✓ | ✓ | ✓ |
+| Clone source and LFS from internal projects | | ✓ (*1*) | ✓ (*1*) | ✓ |
+| Clone source and LFS from private projects | | ✓ (*2*) | ✓ (*2*) | ✓ (*2*) |
+| Pull container images from current project | | ✓ | ✓ | ✓ |
+| Pull container images from public projects | | ✓ | ✓ | ✓ |
+| Pull container images from internal projects| | ✓ (*1*) | ✓ (*1*) | ✓ |
+| Pull container images from private projects | | ✓ (*2*) | ✓ (*2*) | ✓ (*2*) |
+| Push container images to current project | | ✓ | ✓ | ✓ |
+| Push container images to other projects | | | | |
+| Push source and LFS | | | | |
+
+- *1*: Only if the user is not an external one
+- *2*: Only if the user is a member of the project
### New CI job permissions model
@@ -350,17 +360,4 @@ for details about the pipelines security model.
## LDAP users permissions
Since GitLab 8.15, LDAP user permissions can now be manually overridden by an admin user.
-Read through the documentation on [LDAP users permissions](https://docs.gitlab.com/ee/administration/auth/how_to_configure_ldap_gitlab_ee/index.html) to learn more.
-
-[^1]: On public and internal projects, all users are able to perform this action
-[^2]: Guest users can only view the confidential issues they created themselves
-[^3]: If **Public pipelines** is enabled in **Project Settings > CI/CD**
-[^4]: Not allowed for Guest, Reporter, Developer, Maintainer, or Owner
-[^5]: Only if the job was triggered by the user
-[^6]: Only if user is not external one
-[^7]: Only if user is a member of the project
-
-[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994
-[new-mod]: project/new_ci_build_permissions_model.md
-[ee-998]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/998
-[eep]: https://about.gitlab.com/pricing/
+Read through the documentation on [LDAP users permissions](../administration/auth/how_to_configure_ldap_gitlab_ee/index.html) to learn more.
diff --git a/doc/user/profile/account/delete_account.md b/doc/user/profile/account/delete_account.md
index 0d7bbb0af79..304a7984191 100644
--- a/doc/user/profile/account/delete_account.md
+++ b/doc/user/profile/account/delete_account.md
@@ -23,7 +23,7 @@ Here's a list of things that will **not** be deleted:
- Award emoji that the user created
Instead of being deleted, these records will be moved to a system-wide
-"Ghost User", whose sole purpose is to act as a container for such records.
+user with the username "Ghost User", whose sole purpose is to act as a container for such records. Any commits made by a deleted user will still display the username of the original user.
When a user is deleted from an [abuse report](../../admin_area/abuse_reports.md) or spam log, these associated
records are not ghosted and will be removed, along with any groups the user
diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md
index 5166524d13b..0b224fc7e01 100644
--- a/doc/user/profile/personal_access_tokens.md
+++ b/doc/user/profile/personal_access_tokens.md
@@ -37,20 +37,18 @@ Personal access tokens can be created with one or more scopes that allow various
actions that a given token can perform. The available scopes are depicted in
the following table.
-| Scope | Description |
-| ----- | ----------- |
-|`read_user` | Allows access to the read-only endpoints under `/users`. Essentially, any of the `GET` requests in the [Users API][users] are allowed ([introduced][ce-5951] in GitLab 8.15). |
-| `api` | Grants complete access to the API and Container Registry (read/write) ([introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951) in GitLab 8.15). |
-| `read_registry` | Allows to read (pull) [container registry] images if a project is private and authorization is required ([introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845) in GitLab 9.3). |
-| `sudo` | Allows performing API actions as any user in the system (if the authenticated user is an admin) ([introduced][ce-14838] in GitLab 10.2). |
-| `read_repository` | Allows read-only access (pull) to the repository through git clone. |
-| `write_repository` | Allows read-write access (pull, push) to the repository through git clone. Required for accessing Git repositories over HTTP when 2FA is enabled. |
+| Scope | Introduced in | Description |
+| ------------------ | ------------- | ----------- |
+| `read_user` | [GitLab 8.15](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951) | Allows access to the read-only endpoints under `/users`. Essentially, any of the `GET` requests in the [Users API][users] are allowed. |
+| `api` | [GitLab 8.15](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951) | Grants complete access to the API and Container Registry (read/write). |
+| `read_registry` | [GitLab 9.3](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845) | Allows to read (pull) [container registry] images if a project is private and authorization is required. |
+| `sudo` | [GitLab 10.2](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14838) | Allows performing API actions as any user in the system (if the authenticated user is an admin). |
+| `read_repository` | [GitLab 10.7](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17894) | Allows read-only access (pull) to the repository through git clone. |
+| `write_repository` | [GitLab 11.11](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/26021) | Allows read-write access (pull, push) to the repository through git clone. Required for accessing Git repositories over HTTP when 2FA is enabled. |
[2fa]: ../account/two_factor_authentication.md
[api]: ../../api/README.md
[ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749
-[ce-5951]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951
-[ce-14838]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14838
[container registry]: ../project/container_registry.md
[users]: ../../api/users.md
[usage]: ../../api/README.md#personal-access-tokens
diff --git a/doc/user/project/ci_cd_for_external_repo.md b/doc/user/project/ci_cd_for_external_repo.md
index 51b86a68c7b..a92d3a2c308 100644
--- a/doc/user/project/ci_cd_for_external_repo.md
+++ b/doc/user/project/ci_cd_for_external_repo.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/index.html'
+redirect_to: '../../ci/ci_cd_for_external_repos/index.md'
---
-This document was moved to [another location](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/index.html).
+This document was moved to [another location](../../ci/ci_cd_for_external_repos/index.md).
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index e38e4059117..dc21db603d6 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -450,7 +450,7 @@ differentiate the new cluster with the rest.
When adding more than one Kubernetes cluster to your project, you need to differentiate
them with an environment scope. The environment scope associates clusters with [environments](../../../ci/environments.md) similar to how the
-[environment-specific variables](https://docs.gitlab.com/ee/ci/variables/index.html#limiting-environment-scopes-of-environment-variables-premium) work.
+[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-environment-variables-premium) work.
The default environment scope is `*`, which means all jobs, regardless of their
environment, will use that cluster. Each scope can only be used by a single
@@ -588,7 +588,7 @@ displaying the status of the pods in the deployment. Developers and other
teammates can view the progress and status of a rollout, pod by pod, in the
workflow they already use without any need to access Kubernetes.
-[Read more about Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html)
+[Read more about Deploy Boards](../deploy_boards.md)
### Canary Deployments **[PREMIUM]**
@@ -596,7 +596,7 @@ Leverage [Kubernetes' Canary deployments](https://kubernetes.io/docs/concepts/cl
and visualize your canary deployments right inside the Deploy Board, without
the need to leave GitLab.
-[Read more about Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html)
+[Read more about Canary Deployments](../canary_deployments.md)
### Pod logs **[ULTIMATE]**
diff --git a/doc/user/project/clusters/kubernetes_pod_logs.md b/doc/user/project/clusters/kubernetes_pod_logs.md
index d5b60250860..368031070c1 100644
--- a/doc/user/project/clusters/kubernetes_pod_logs.md
+++ b/doc/user/project/clusters/kubernetes_pod_logs.md
@@ -7,10 +7,10 @@ By displaying the logs directly in GitLab, developers can avoid having to manage
## Overview
-[Kubernetes](https://kubernetes.io) pod logs can be viewed directly within GitLab. Logs can be displayed by clicking on a specific pod from [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html):
+[Kubernetes](https://kubernetes.io) pod logs can be viewed directly within GitLab. Logs can be displayed by clicking on a specific pod from [Deploy Boards](../deploy_boards.md):
1. Go to **Operations > Environments** and find the environment which contains the desired pod, like `production`.
-1. On the **Environments** page, you should see the status of the environment's pods with [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html).
+1. On the **Environments** page, you should see the status of the environment's pods with [Deploy Boards](../deploy_boards.md).
1. When mousing over the list of pods, a tooltip will appear with the exact pod name and status.
![Deploy Boards pod list](img/pod_logs_deploy_board.png)
1. Click on the desired pod to bring up the logs view, which will contain the last 500 lines for that pod. Support for pods with multiple containers is coming [in a future release](https://gitlab.com/gitlab-org/gitlab-ee/issues/6502).
@@ -18,4 +18,4 @@ By displaying the logs directly in GitLab, developers can avoid having to manage
## Requirements
-[Enabling Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html#enabling-deploy-boards) is required in order to be able to use Pod Logs.
+[Enabling Deploy Boards](../deploy_boards.md#enabling-deploy-boards) is required in order to be able to use Pod Logs.
diff --git a/doc/user/project/clusters/runbooks/index.md b/doc/user/project/clusters/runbooks/index.md
index 54c475a1762..c67b12fb91a 100644
--- a/doc/user/project/clusters/runbooks/index.md
+++ b/doc/user/project/clusters/runbooks/index.md
@@ -17,13 +17,15 @@ Modern implementations have introduced the concept of an "executable
runbooks", where, along with a well-defined process, operators can execute
pre-written code blocks or database queries against a given environment.
-## Nurtch Executable Runbooks
+## Executable Runbooks
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/45912) in GitLab 11.4.
The JupyterHub app offered via GitLab’s Kubernetes integration now ships
with Nurtch’s Rubix library, providing a simple way to create DevOps
-runbooks. A sample runbook is provided, showcasing common operations.
+runbooks. A sample runbook is provided, showcasing common operations. While Rubix makes it
+simple to create common Kubernetes and AWS workflows, you can also create them manually without
+Rubix.
**<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
Watch this [video](https://www.youtube.com/watch?v=Q_OqHIIUPjE)
@@ -34,7 +36,7 @@ for an overview of how this is accomplished in GitLab!**
To create an executable runbook, you will need:
1. **Kubernetes** - A Kubernetes cluster is required to deploy the rest of the applications.
- The simplest way to get started is to add a cluster using [GitLab's GKE integration](https://docs.gitlab.com/ee/user/project/clusters/#adding-and-creating-a-new-gke-cluster-via-gitlab).
+ The simplest way to get started is to add a cluster using [GitLab's GKE integration](../index.md#adding-and-creating-a-new-gke-cluster-via-gitlab).
1. **Helm Tiller** - Helm is a package manager for Kubernetes and is required to install
all the other applications. It is installed in its own pod inside the cluster which
can run the helm CLI in a safe environment.
@@ -59,7 +61,7 @@ the components outlined above and the preloaded demo runbook.
### 1. Add a Kubernetes cluster
-Follow the steps outlined in [Adding and creating a new GKE cluster via GitLab](https://docs.gitlab.com/ee/user/project/clusters/#adding-and-creating-a-new-gke-cluster-via-gitlab)
+Follow the steps outlined in [Adding and creating a new GKE cluster via GitLab](../index.md#adding-and-creating-a-new-gke-cluster-via-gitlab)
to add a Kubernetes cluster to your project.
### 2. Install Helm Tiller, Ingress, and JupyterHub
@@ -90,7 +92,7 @@ The server will take a couple of seconds to start.
### 4. Configure access
In order for the runbook to access your GitLab project, you will need to enter a
-[GitLab Access Token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html)
+[GitLab Access Token](../../../profile/personal_access_tokens.md)
as well as your Project ID in the **Setup** section of the demo runbook.
Double-click the **DevOps-Runbook-Demo** folder located on the left panel.
diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md
index dfbed0ec95f..2cda850f24e 100644
--- a/doc/user/project/clusters/serverless/index.md
+++ b/doc/user/project/clusters/serverless/index.md
@@ -216,7 +216,7 @@ The sample function can now be triggered from any HTTP client using a simple `PO
--header "Content-Type: application/json" \
--request POST \
--data '{"GitLab":"FaaS"}' \
- http://functions-echo.functions-1.functions.example.com
+ http://functions-echo.functions-1.functions.example.com/
```
2. Using a web-based tool (ie. postman, restlet, etc)
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index 83b268db967..58b7fe33906 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -53,7 +53,7 @@ If you visit the **Registry** link under your project's menu, you can see the
explicit instructions to login to the Container Registry using your GitLab
credentials.
-For example if the Registry's URL is `registry.example.com`, the you should be
+For example if the Registry's URL is `registry.example.com`, then you should be
able to login with:
```
@@ -204,7 +204,7 @@ at the communication between the client and the Registry.
The REST API between the Docker client and Registry is [described
here](https://docs.docker.com/registry/spec/api/). Normally, one would just
use Wireshark or tcpdump to capture the traffic and see where things went
-wrong. However, since all communication between Docker clients and servers
+wrong. However, since all communications between Docker clients and servers
are done over HTTPS, it's a bit difficult to decrypt the traffic quickly even
if you know the private key. What can we do instead?
diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md
index 7688508c6ac..92a29b68a22 100644
--- a/doc/user/project/deploy_tokens/index.md
+++ b/doc/user/project/deploy_tokens/index.md
@@ -5,7 +5,7 @@
Deploy tokens allow to download (through `git clone`), or read the container registry images of a project without the need of having a user and a password.
Please note, that the expiration of deploy tokens happens on the date you define,
-at midnight UTC and that they can be only managed by [maintainers](https://docs.gitlab.com/ee/user/permissions.html).
+at midnight UTC and that they can be only managed by [maintainers](../../permissions.md).
## Creating a Deploy Token
diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md
index e230444fa67..05ad15476ab 100644
--- a/doc/user/project/description_templates.md
+++ b/doc/user/project/description_templates.md
@@ -86,7 +86,7 @@ pre-filled with the text you entered in the template(s).
We make use of Description Templates for Issues and Merge Requests within the GitLab Community Edition project. Please refer to the [`.gitlab` folder][gitlab-ce-templates] for some examples.
> **Tip:**
-It is possible to use [quick actions](./quick_actions.md) within description templates to quickly add labels, assignees, and milestones. The quick actions will only be executed if the user submitting the Issue or Merge Request has the permissions perform the relevant actions.
+It is possible to use [quick actions](quick_actions.md) within description templates to quickly add labels, assignees, and milestones. The quick actions will only be executed if the user submitting the Issue or Merge Request has the permissions perform the relevant actions.
Here is an example for a Bug report template:
diff --git a/doc/user/project/file_lock.md b/doc/user/project/file_lock.md
index 541023692ea..3386eb9d0d4 100644
--- a/doc/user/project/file_lock.md
+++ b/doc/user/project/file_lock.md
@@ -1,10 +1,8 @@
# File Locking **[PREMIUM]**
-> **Notes:**
-> - [Introduced][ee-440] in [GitLab Premium][ee] 8.9.
-> - This feature needs to have a license with the "File Lock" option enabled. If
-> you are using Premium but you don't see the "Lock" button,
-> ask your GitLab administrator.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/440) in [GitLab Premium](https://about.gitlab.com/pricing/) 8.9.
+> - This feature needs to have a license with the "File Lock" option enabled.
+> - If you are using Premium but you don't see the "Lock" button, ask your GitLab administrator.
File Locking helps you avoid merge conflicts and better manage your binary files.
Lock any file or directory, make your changes, and then unlock it so another
@@ -37,7 +35,7 @@ lies under them is also locked.
The user that locks a file or directory **is the only one** that can edit and
push their changes back to the repository where the locked objects are located.
-Locks can be created by any person who has [push access] to the repository; i.e.,
+Locks can be created by any person who has [push access](../../user/permissions.md) to the repository; i.e.,
Developer and higher level, and can be removed solely by their author and any
user with Maintainer permissions and above.
@@ -101,7 +99,3 @@ To view or manage every existing lock, navigate to the
locks and [remove the ones you have permission for](#permissions-on-file-locking).
![Locked Files](img/file_lock_list.png)
-
-[ee-440]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/440 "File Lock"
-[ee]: https://about.gitlab.com/pricing/
-[push access]: ../../user/permissions.md
diff --git a/doc/user/project/import/gemnasium.md b/doc/user/project/import/gemnasium.md
index dc5b3fcd0bb..7f79ebf6353 100644
--- a/doc/user/project/import/gemnasium.md
+++ b/doc/user/project/import/gemnasium.md
@@ -9,9 +9,9 @@ Gemnasium has been [acquired by GitLab](https://about.gitlab.com/press/releases/
in January 2018. Since May 15, 2018, the services provided by Gemnasium are no longer available.
The team behind Gemnasium has joined GitLab as the new Security Products team
and is working on a wider range of tools than just Dependency Scanning:
-[SAST](https://docs.gitlab.com/ee/user/application_security/sast/index.html),
-[DAST](https://docs.gitlab.com/ee/user/application_security/dast/index.html),
-[Container Scanning](https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html) and more.
+[SAST](../../application_security/sast/index.md),
+[DAST](../../application_security/dast/index.md),
+[Container Scanning](../../application_security/container_scanning/index.md) and more.
If you want to continue monitoring your dependencies, see the
[Migrating to GitLab](#migrating-to-gitlab) section below.
@@ -45,7 +45,7 @@ Security features are free for public (open-source) projects hosted on GitLab.co
You're almost set! If you're already using
[Auto DevOps](../../../topics/autodevops/), you are already covered.
Otherwise, you must configure your `.gitlab-ci.yml` according to the
-[dependency scanning page](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html).
+[dependency scanning page](../../application_security/dependency_scanning/index.md).
### If your project is hosted on GitHub (https://github.com / GitHub Enterprise)
@@ -81,7 +81,7 @@ back to both GitLab and GitHub when completed.
1. To set up the dependency scanning job, corresponding to what Gemnasium was
doing, you must create a `.gitlab-ci.yml` file, or update it according to
- the [dependency scanning docs](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html).
+ the [dependency scanning docs](../../application_security/dependency_scanning/index.md).
The mirroring is pull-only by default, so you may create or update the file on
GitHub:
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
index 63b90dd76fd..8fba892594b 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -119,9 +119,9 @@ Depending your GitLab tier, [project mirroring](../../../workflow/repository_mir
your imported project in sync with its GitHub copy.
Additionally, you can configure GitLab to send pipeline status updates back GitHub with the
-[GitHub Project Integration](https://docs.gitlab.com/ee/user/project/integrations/github.html). **[PREMIUM]**
+[GitHub Project Integration](../integrations/github.md). **[PREMIUM]**
-If you import your project using [CI/CD for external repo](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/), then both
+If you import your project using [CI/CD for external repo](../../../ci/ci_cd_for_external_repos/index.md), then both
of the above are automatically configured. **[PREMIUM]**
## Improving the speed of imports on self-hosted instances
diff --git a/doc/user/project/import/gitlab_com.md b/doc/user/project/import/gitlab_com.md
index 3b37da67a5b..f48a158e2d3 100644
--- a/doc/user/project/import/gitlab_com.md
+++ b/doc/user/project/import/gitlab_com.md
@@ -1,8 +1,9 @@
# Project importing from GitLab.com to your private GitLab instance
You can import your existing GitLab.com projects to your GitLab instance. But keep in mind that it is possible only if
-GitLab support is enabled on your GitLab instance.
-You can read more about GitLab support [here](http://docs.gitlab.com/ce/integration/gitlab.html)
+GitLab.com integration is enabled on your GitLab instance.
+[Read more about GitLab.com integration for self-managed GitLab instances](../../../integration/gitlab.md).
+
To get to the importer page you need to go to "New project" page.
>**Note:**
diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md
index ebbc5ca133b..2b6927bd780 100644
--- a/doc/user/project/import/index.md
+++ b/doc/user/project/import/index.md
@@ -14,12 +14,13 @@
1. [From repo by URL](repo_by_url.md)
1. [By uploading a manifest file (AOSP)](manifest.md)
1. [From Gemnasium](gemnasium.md)
+1. [From Phabricator](phabricator.md)
In addition to the specific migration documentation above, you can import any
Git repository via HTTP from the New Project page. Be aware that if the
repository is too large the import can timeout.
-There is also the option of [connecting your external repository to get CI/CD benefits](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/index.html). **[PREMIUM]**
+There is also the option of [connecting your external repository to get CI/CD benefits](../../../ci/ci_cd_for_external_repos/index.md). **[PREMIUM]**
## Migrating from self-hosted GitLab to GitLab.com
diff --git a/doc/user/project/import/phabricator.md b/doc/user/project/import/phabricator.md
new file mode 100644
index 00000000000..5c624e3aff6
--- /dev/null
+++ b/doc/user/project/import/phabricator.md
@@ -0,0 +1,29 @@
+# Import Phabricator tasks into a GitLab project
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/60562) in
+GitLab 12.0.
+
+GitLab allows you to import all tasks from a Phabricator instance into
+GitLab issues. The import creates a single project with the
+repository disabled.
+
+Currently, only the following basic fields are imported:
+
+- Title
+- Description
+- State (open or closed)
+- Created at
+- Closed at
+
+## Enabling this feature
+
+While this feature is incomplete, a feature flag is required to enable it so that
+we can gain early feedback before releasing it for everyone. To enable it:
+
+1. Run the following command in a Rails console:
+
+ ```ruby
+ Feature.enable(:phabricator_import)
+ ```
+
+1. Enable Phabricator as an [import source](../../admin_area/settings/visibility_and_access_controls.md#import-sources) in the Admin area.
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index 6b3b40bf9f8..a24f525253d 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -37,7 +37,7 @@ When you create a project in GitLab, you'll have access to a large number of
- [Multiple Issue Boards](issue_board.md#multiple-issue-boards-starter): Allow your teams to create their own workflows (Issue Boards) for the same project **[STARTER]**
- [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): Ask for approval before
+ - [Merge Request Approvals](merge_requests/merge_request_approvals.md): 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
@@ -74,7 +74,7 @@ When you create a project in GitLab, you'll have access to a large number of
timeout (defines the maximum amount of time in minutes that a job is able run), custom path for `.gitlab-ci.yml`, test coverage parsing, pipeline's visibility, and much more
- [Kubernetes cluster integration](clusters/index.md): Connecting your GitLab project
with a Kubernetes cluster
- - [Feature Flags](https://docs.gitlab.com/ee/user/project/operations/feature_flags.html): Feature flags allow you to ship a project in
+ - [Feature Flags](operations/feature_flags.md): Feature flags allow you to ship a project in
different flavors by dynamically toggling certain functionality **[PREMIUM]**
- [GitLab Pages](pages/index.md): Build, test, and deploy your static
website with GitLab Pages
@@ -91,10 +91,10 @@ When you create a project in GitLab, you'll have access to a large number of
- [Releases](releases/index.md): a way to track deliverables in your project as snapshot in time of
the source, build output, and other metadata or artifacts
associated with a released version of your code.
-- [Maven packages](https://docs.gitlab.com/ee/user/project/packages/maven_repository.html): your private Maven repository in GitLab. **[PREMIUM]**
-- [NPM packages](https://docs.gitlab.com/ee/user/project/packages/npm_registry.html): your private NPM package registry in GitLab. **[PREMIUM]**
+- [Maven packages](packages/maven_repository.md): your private Maven repository in GitLab. **[PREMIUM]**
+- [NPM packages](packages/npm_registry.md): your private NPM package registry in GitLab. **[PREMIUM]**
- [Code owners](code_owners.md): specify code owners for certain files **[STARTER]**
-- [License Management](https://docs.gitlab.com/ee/user/application_security/license_management/index.html): approve and blacklist licenses for projects. **[ULTIMATE]**
+- [License Management](../application_security/license_management/index.md): approve and blacklist licenses for projects. **[ULTIMATE]**
### Project integrations
@@ -135,7 +135,7 @@ Read through the documentation on [project settings](settings/index.md).
Instead of importing a repository directly to GitLab, you can connect your repository
as a CI/CD project.
-Read through the documentation on [CI/CD for external repositories](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/index.html).
+Read through the documentation on [CI/CD for external repositories](../../ci/ci_cd_for_external_repos/index.md).
## Project members
diff --git a/doc/user/project/insights/index.md b/doc/user/project/insights/index.md
index b6cc1862cc2..3344e560870 100644
--- a/doc/user/project/insights/index.md
+++ b/doc/user/project/insights/index.md
@@ -14,7 +14,7 @@ requests to be merged and much more.
![Insights example bar chart](img/project_insights.png)
NOTE: **Note:**
-This feature is [also available at the group level](https://docs.gitlab.com/ee/user/group/insights/index.html).
+This feature is [also available at the group level](../../group/insights/index.md).
## View your project's Insights
@@ -33,7 +33,7 @@ for details about the content of this file.
NOTE: **Note:**
Once the configuration file is created, you can also
-[use it for your project's group](https://docs.gitlab.com/ee/user/group/insights/index.html#configure-your-insights).
+[use it for your project's group](../../group/insights/index.md#configure-your-insights).
NOTE: **Note:**
If the project doesn't have any configuration file, it'll try to use
@@ -179,7 +179,7 @@ Supported values are:
Filter by the state of the queried "issuable".
-If you omit it, no state filter will be applied.
+If you omit it, the `opened` state filter will be applied.
Supported values are:
@@ -187,6 +187,7 @@ Supported values are:
- `closed`: Closed Open issues / merge requests.
- `locked`: Issues / merge requests that have their discussion locked.
- `merged`: Merged merge requests.
+- `all`: Issues / merge requests in all states
#### `query.filter_labels`
diff --git a/doc/user/project/integrations/img/jira_add_user_to_group.png b/doc/user/project/integrations/img/jira_add_user_to_group.png
index 27dac49260c..d8cf541a81e 100644
--- a/doc/user/project/integrations/img/jira_add_user_to_group.png
+++ b/doc/user/project/integrations/img/jira_add_user_to_group.png
Binary files differ
diff --git a/doc/user/project/integrations/img/jira_added_user_to_group.png b/doc/user/project/integrations/img/jira_added_user_to_group.png
new file mode 100644
index 00000000000..b3e29a65d6e
--- /dev/null
+++ b/doc/user/project/integrations/img/jira_added_user_to_group.png
Binary files differ
diff --git a/doc/user/project/integrations/img/jira_create_new_group.png b/doc/user/project/integrations/img/jira_create_new_group.png
index 06c4e84fc61..84be3a94a45 100644
--- a/doc/user/project/integrations/img/jira_create_new_group.png
+++ b/doc/user/project/integrations/img/jira_create_new_group.png
Binary files differ
diff --git a/doc/user/project/integrations/img/jira_create_new_user.png b/doc/user/project/integrations/img/jira_create_new_user.png
index e9c03ed770d..8460dc98ef9 100644
--- a/doc/user/project/integrations/img/jira_create_new_user.png
+++ b/doc/user/project/integrations/img/jira_create_new_user.png
Binary files differ
diff --git a/doc/user/project/integrations/img/jira_group_access.png b/doc/user/project/integrations/img/jira_group_access.png
index 448cc55504d..58cf114bd55 100644
--- a/doc/user/project/integrations/img/jira_group_access.png
+++ b/doc/user/project/integrations/img/jira_group_access.png
Binary files differ
diff --git a/doc/user/project/integrations/img/jira_user_management_link.png b/doc/user/project/integrations/img/jira_user_management_link.png
index 5eb9d031c3e..43ef18da6c8 100644
--- a/doc/user/project/integrations/img/jira_user_management_link.png
+++ b/doc/user/project/integrations/img/jira_user_management_link.png
Binary files differ
diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md
index a90167b9767..c652149052e 100644
--- a/doc/user/project/integrations/jira.md
+++ b/doc/user/project/integrations/jira.md
@@ -47,11 +47,11 @@ project in Jira and then enter the correct values in GitLab.
When connecting to **JIRA Server**, which supports basic authentication, a **username and password** are required. Check the link below and proceed to the next step:
-- [Setting up an user in JIRA server](jira_server_configuration.md)
+- [Setting up a user in JIRA server](jira_server_configuration.md)
When connecting to **JIRA Cloud**, which supports authentication via API token, an **email and API token**, are required. Check the link below and proceed to the next step:
-- [Setting up an user in JIRA cloud](jira_cloud_configuration.md)
+- [Setting up a user in JIRA cloud](jira_cloud_configuration.md)
### Configuring GitLab
diff --git a/doc/user/project/integrations/jira_server_configuration.md b/doc/user/project/integrations/jira_server_configuration.md
index 20036183187..13d65c4d8e4 100644
--- a/doc/user/project/integrations/jira_server_configuration.md
+++ b/doc/user/project/integrations/jira_server_configuration.md
@@ -1,18 +1,16 @@
# Creating a username and password for JIRA server
We need to create a user in Jira which will have access to all projects that
-need to integrate with GitLab. Login to your Jira instance as admin and under
-*Administration*, go to *User Management* and create a new user.
+need to integrate with GitLab.
As an example, we'll create a user named `gitlab` and add it to the `Jira-developers`
group.
NOTE: **Note**
-It is important that the user `gitlab` has 'write' access to projects in Jira.
+It is important that the Jira user created for the integration is given 'write'
+access to your Jira projects. This is covered in the process below.
-We have split this stage in steps so it is easier to follow.
-
-1. Log in to your Jira instance as an administrator and under **Administration**
+1. Log in to your Jira instance as an administrator and under **Jira Administration**
go to **User Management** to create a new user.
![Jira user management link](img/jira_user_management_link.png)
@@ -27,27 +25,34 @@ We have split this stage in steps so it is easier to follow.
![Jira create new user](img/jira_create_new_user.png)
-1. Create a `gitlab-developers` group which will have write access
- to projects in Jira. Go to the **Groups** tab and select **Create group**.
+1. Create a `gitlab-developers` group. (We will give this group write access to Jira
+ projects in a later step). Go to the **Groups** tab on the left, and select **Add group**.
![Jira create new user](img/jira_create_new_group.png)
- Give it an optional description and click **Create group**.
+ Give it a name and click **Add group**.
- ![Jira create new group](img/jira_create_new_group_name.png)
+1. Add the `gitlab` user to the `gitlab-developers` group by clicking **Edit members**.
+ The `gitlab-developers` group should be listed in the leftmost box as a selected group.
+ Under **Add members to selected group(s)**, enter `gitlab`.
-1. To give the newly-created group 'write' access, go to
- **Application access > View configuration** and add the `gitlab-developers`
- group to Jira Core.
+ ![Jira add user to group](img/jira_add_user_to_group.png)
+
+ Click **Add selected users** and `gitlab` should appear in the **Group member(s)** box.
+ This membership is saved automatically.
+
+ ![Jira added user to group](img/jira_added_user_to_group.png)
+
+1. To give the newly-created group 'write' access, you need to create a **Permission Scheme**.
+ To do this, click the gear icon and select **Issues**. Then click **Permission Schemes**.
+ Click **Add Permission Scheme** and enter a **Name** and, optionally, a **Description**.
+
+1. Once your permission scheme is created, you'll be taken back to the permissions scheme list.
+ Locate your new permissions scheme and click **Permissions**. Next to **Administer Projects**,
+ click **Edit**. In the resulting dialog box, select **Group** and select `gitlab-developers`
+ from the dropdown.
![Jira group access](img/jira_group_access.png)
-1. Add the `gitlab` user to the `gitlab-developers` group by going to
- **Users > GitLab user > Add group** and selecting the `gitlab-developers`
- group from the dropdown menu. Notice that the group says _Access_, which is
- intended as part of this process.
-
- ![Jira add user to group](img/jira_add_user_to_group.png)
-
The Jira configuration is complete. Write down the new Jira username and its
password as they will be needed when [configuring GitLab in the next section](jira.md#configuring-gitlab).
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index f560de427c5..0bfee3bac99 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -39,7 +39,7 @@ Click on the service links to see further configuration instructions and details
| [HipChat](hipchat.md) | Private group chat and IM |
| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
| [JIRA](jira.md) | JIRA issue tracker |
-| [Jenkins](https://docs.gitlab.com/ee/integration/jenkins.html) **[STARTER]** | An extendable open source continuous integration server |
+| [Jenkins](../../../integration/jenkins.md) **[STARTER]** | An extendable open source continuous integration server |
| JetBrains TeamCity CI | A continuous integration and build server |
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md
index 436129f1dbc..8b1cf1a251a 100644
--- a/doc/user/project/integrations/prometheus_library/kubernetes.md
+++ b/doc/user/project/integrations/prometheus_library/kubernetes.md
@@ -27,7 +27,7 @@ integration services must be enabled.
Prometheus needs to be deployed into the cluster and configured properly in order to gather Kubernetes metrics. GitLab supports two methods for doing so:
-- GitLab [integrates with Kubernetes](../../clusters/index.md), and can [deploy Prometheus into a connected cluster](../prometheus.html#managed-prometheus-on-kubernetes). It is automatically configured to collect Kubernetes metrics.
+- GitLab [integrates with Kubernetes](../../clusters/index.md), and can [deploy Prometheus into a connected cluster](../prometheus.md#managed-prometheus-on-kubernetes). It is automatically configured to collect Kubernetes metrics.
- To configure your own Prometheus server, you can follow the [Prometheus documentation](https://prometheus.io/docs/introduction/overview/).
## Specifying the Environment
@@ -40,7 +40,7 @@ Instead, the [Deployment](https://kubernetes.io/docs/concepts/workloads/controll
> Introduced in [GitLab 10.2](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15201).
-GitLab also gathers Kubernetes metrics for [canary deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html), allowing easy comparison between the current deployed version and the canary.
+GitLab also gathers Kubernetes metrics for [canary deployments](../../canary_deployments.md), allowing easy comparison between the current deployed version and the canary.
These metrics expect the [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) or [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) name to begin with `$CI_ENVIRONMENT_SLUG-canary`, to isolate the canary metrics.
diff --git a/doc/user/project/integrations/youtrack.md b/doc/user/project/integrations/youtrack.md
index a2a468b6fe4..81c148e41fd 100644
--- a/doc/user/project/integrations/youtrack.md
+++ b/doc/user/project/integrations/youtrack.md
@@ -31,8 +31,8 @@ To disable the internal issue tracker in a project:
## Referencing YouTrack issues in GitLab
Issues in YouTrack can be referenced as `<PROJECT>-<ID>`. `<PROJECT>`
-must start with a capital letter and can then be followed by capital or lower case
-letters, numbers or underscores. `<ID>` is a number. An example reference is `YT-101` or `Api_32-143`.
+must start with a letter and is followed by letters, numbers, or underscores.
+`<ID>` is a number. An example reference is `YT-101`, `Api_32-143` or `gl-030`.
References to `<PROJECT>-<ID>` in merge requests, commits, or comments are automatically linked to the YouTrack issue URL.
For more information, see the [External Issue Tracker](../../../integration/external-issue-tracker.md) documentation.
diff --git a/doc/user/project/issues/create_new_issue.md b/doc/user/project/issues/create_new_issue.md
index 5e05846b77f..c2916c79876 100644
--- a/doc/user/project/issues/create_new_issue.md
+++ b/doc/user/project/issues/create_new_issue.md
@@ -67,7 +67,7 @@ or contacts to continue working._
## New issue via Service Desk **[PREMIUM]**
-Enable [Service Desk](https://docs.gitlab.com/ee/user/project/service_desk.html) to your project and offer email support.
+Enable [Service Desk](../service_desk.md) to your project and offer email support.
By doing so, when your customer sends a new email, a new issue can be created in
the appropriate project and followed up from there.
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index c82b7f100d2..94865ad46ee 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -102,13 +102,13 @@ For more information, see the [Issue Boards](../issue_board.md) page.
Epics let you manage your portfolio of projects more efficiently and with less effort by tracking groups of issues that share a theme, across projects and milestones.
-For more information, see the [Epics](https://docs.gitlab.com/ee/user/group/epics/) page.
+For more information, see the [Epics](../../group/epics/index.md) page.
### Related issues **[STARTER]**
You can mark two issues as related, so that when viewing each one, the other is always listed in its Related Issues section. This can help display important context, such as past work, dependencies, or duplicates.
-For more information, see [Related Issues](https://docs.gitlab.com/ee/user/project/issues/related_issues.html).
+For more information, see [Related Issues](related_issues.md).
### Crosslinking issues
@@ -129,7 +129,7 @@ For more information, see [Crosslinking issues](crosslinking_issues.md).
- [Bulk edit issues](../bulk_editing.md) - From the Issues List, select multiple issues in order to change their status, assignee, milestone, or labels in bulk.
- [Import issues](csv_import.md)
-- [Export issues](https://docs.gitlab.com/ee/user/project/issues/csv_export.html) **[STARTER]**
+- [Export issues](csv_export.md) **[STARTER]**
- [Issues API](../../../api/issues.md)
- Configure an [external issue tracker](../../../integration/external-issue-tracker.md) such as Jira, Redmine,
or Bugzilla.
diff --git a/doc/user/project/issues/issue_data_and_actions.md b/doc/user/project/issues/issue_data_and_actions.md
index ef9fcaec3e6..fc11c0251e0 100644
--- a/doc/user/project/issues/issue_data_and_actions.md
+++ b/doc/user/project/issues/issue_data_and_actions.md
@@ -90,7 +90,7 @@ If a label doesn't exist yet, you can click **Edit**, and it opens a dropdown me
- Assign a weight. Larger values are used to indicate more effort is required to complete the issue. Only positive values or zero are allowed.
-Learn more in the [Issue Weight documentation](https://docs.gitlab.com/ee/workflow/issue_weight.html).
+Learn more in the [Issue Weight documentation](../../../workflow/issue_weight.md).
#### 9. Participants
@@ -103,7 +103,7 @@ Learn more in the [Issue Weight documentation](https://docs.gitlab.com/ee/workfl
- Unsubscribe: if you are receiving notifications on that issue but no
longer want to receive them, unsubscribe from it.
-Read more in the [notifications documentation](https://docs.gitlab.com/ee/workflow/notifications.html#issue--epics--merge-request-events).
+Read more in the [notifications documentation](../../../workflow/notifications.md#issue--epics--merge-request-events).
#### 11. Reference
diff --git a/doc/user/project/issues/multiple_assignees_for_issues.md b/doc/user/project/issues/multiple_assignees_for_issues.md
index 8781ebdd5b0..d1db0790d69 100644
--- a/doc/user/project/issues/multiple_assignees_for_issues.md
+++ b/doc/user/project/issues/multiple_assignees_for_issues.md
@@ -22,7 +22,7 @@ Consider a team formed by frontend developers, backend developers,
UX designers, QA testers, and a product manager working together to bring an idea to
market.
-Multiple Assignees for Issues makes collaboration smother,
+Multiple Assignees for Issues makes collaboration smoother,
and allows shared responsibilities to be clearly displayed.
All assignees are shown across your team's workflows and receive notifications (as they
would as single assignees), simplifying communication and ownership.
diff --git a/doc/user/project/issues/related_issues.md b/doc/user/project/issues/related_issues.md
index db0ab65b442..e679ebf86e6 100644
--- a/doc/user/project/issues/related_issues.md
+++ b/doc/user/project/issues/related_issues.md
@@ -1,6 +1,6 @@
# Related issues **[STARTER]**
-> [Introduced][ee-1797] in [GitLab Starter][ee] 9.4.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1797) in [GitLab Starter](https://about.gitlab.com/pricing/) 9.4.
Related issues are a bi-directional relationship between any two issues
and appear in a block below the issue description. Issues can be across groups
@@ -35,11 +35,6 @@ will no longer appear in either issue.
![Removing a related issue](img/related_issues_remove.png)
-Please access our [permissions] page for more information.
+Please access our [permissions](../../permissions.md) page for more information.
-Additionally, you are also able to manage related issues through [our API].
-
-[ee]: https://about.gitlab.com/pricing/
-[ee-1797]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1797
-[permissions]: ../../permissions.md
-[Our API]: https://docs.gitlab.com/ee/api/issue_links.html
+Additionally, you are also able to manage related issues through [our API](../../../api/issue_links.md).
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index 8e9e9aa79cf..e5f62a3bb8d 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -32,6 +32,14 @@ An issue, epic, or merge request cannot have two scoped labels with the same key
For example, if an issue is already labeled `priority::3` and you apply the label `priority::2` to it,
`priority::3` is automatically removed.
+### Labels with multiple colon pairs
+
+If labels have multiple instances of `::`, the longest path from left to right, until the last `::`, is considered the "key" or the "scope".
+
+For example, `nested::key1::value1` and `nested::key1::value2` cannot both exist on the same issue. Adding the latter label will automatically remove the former due to the shared scope of `nested::key1`.
+
+`nested::key1::value1` and `nested::key2::value1` can both exist on the same issue, as these are considered to use two different label scopes, `nested::key1` and `nested::key2`.
+
### Workflows with scoped labels **[PREMIUM]**
Suppose you wanted a custom field in issues to track the platform operating system
diff --git a/doc/user/project/maven_packages.md b/doc/user/project/maven_packages.md
index d32d6084b38..48835a2dac7 100644
--- a/doc/user/project/maven_packages.md
+++ b/doc/user/project/maven_packages.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'https://docs.gitlab.com/ee/user/project/packages/maven_repository.html'
+redirect_to: 'packages/maven_repository.md'
---
-This document was moved to [another location](https://docs.gitlab.com/ee/user/project/packages/maven_repository.html).
+This document was moved to [another location](packages/maven_repository.md).
diff --git a/doc/user/project/merge_requests/allow_collaboration.md b/doc/user/project/merge_requests/allow_collaboration.md
index da6e6b5fd3a..e94125e658d 100644
--- a/doc/user/project/merge_requests/allow_collaboration.md
+++ b/doc/user/project/merge_requests/allow_collaboration.md
@@ -1,3 +1,7 @@
+---
+type: reference, howto
+---
+
# Allow collaboration on merge requests across forks
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17395)
@@ -70,3 +74,15 @@ Here's how the process would look like:
Note the colon (`:`) between the two branches. The above command will push the
local branch `thedude-awesome-project-update-docs` to the
`update-docs` branch of the `git@gitlab.com:thedude/awesome-project.git` repository.
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/project/merge_requests/authorization_for_merge_requests.md b/doc/user/project/merge_requests/authorization_for_merge_requests.md
index 79444ee5682..0579e3568da 100644
--- a/doc/user/project/merge_requests/authorization_for_merge_requests.md
+++ b/doc/user/project/merge_requests/authorization_for_merge_requests.md
@@ -1,8 +1,12 @@
+---
+type: concepts
+---
+
# Authorization for Merge requests
There are two main ways to have a merge request flow with GitLab:
-1. Working with [protected branches] in a single repository.
+1. Working with [protected branches](../protected_branches.md) in a single repository.
1. Working with forks of an authoritative project.
## Protected branch flow
@@ -53,4 +57,14 @@ forks.
- The project need to keep their forks up to date, which requires more advanced
Git skills (managing multiple remotes).
-[protected branches]: ../protected_branches.md
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/project/merge_requests/browser_performance_testing.md b/doc/user/project/merge_requests/browser_performance_testing.md
index 65ee2e128ae..f6c4f767e3c 100644
--- a/doc/user/project/merge_requests/browser_performance_testing.md
+++ b/doc/user/project/merge_requests/browser_performance_testing.md
@@ -1,14 +1,18 @@
+---
+type: reference, howto
+---
+
# Browser Performance Testing **[PREMIUM]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3507)
in [GitLab Premium](https://about.gitlab.com/pricing/) 10.3.
-## Overview
-
If your application offers a web interface and you are using
[GitLab CI/CD](../../../ci/README.md), you can quickly determine the performance
impact of pending code changes.
+## Overview
+
GitLab uses [Sitespeed.io](https://www.sitespeed.io), a free and open source
tool for measuring the performance of web sites, and has built a simple
[Sitespeed plugin](https://gitlab.com/gitlab-org/gl-performance)
@@ -52,3 +56,15 @@ Consecutive merge requests will have something to compare to and the Performance
report will be shown properly.
![Performance Widget](img/browser_performance_testing.png)
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/project/merge_requests/cherry_pick_changes.md b/doc/user/project/merge_requests/cherry_pick_changes.md
index 06b3779668b..a5c191c150f 100644
--- a/doc/user/project/merge_requests/cherry_pick_changes.md
+++ b/doc/user/project/merge_requests/cherry_pick_changes.md
@@ -1,8 +1,11 @@
-# Cherry-pick changes
+---
+type: reference, concepts
+---
-> [Introduced][ce-3514] in GitLab 8.7.
+# Cherry-pick changes
-GitLab implements Git's powerful feature to [cherry-pick any commit][git-cherry-pick]
+GitLab implements Git's powerful feature to
+[cherry-pick any commit](https://git-scm.com/docs/git-cherry-pick "Git cherry-pick documentation")
with introducing a **Cherry-pick** button in merge requests and commit details.
## Cherry-picking a merge request
@@ -18,9 +21,9 @@ where you can choose to either:
- Cherry-pick the changes directly into the selected branch.
- Create a new merge request with the cherry-picked changes.
-## Cherry-picking a Commit
+## Cherry-picking a commit
-You can cherry-pick a Commit from the Commit details page:
+You can cherry-pick a commit from the commit details page:
![Cherry-pick commit](img/cherry_pick_changes_commit.png)
@@ -39,5 +42,14 @@ mainline:
git cherry-pick -m 2 7a39eb0
```
-[ce-3514]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3514 "Cherry-pick button Merge Request"
-[git-cherry-pick]: https://git-scm.com/docs/git-cherry-pick "Git cherry-pick documentation"
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md
index e6811b5df5e..c3c2bdd94b3 100644
--- a/doc/user/project/merge_requests/code_quality.md
+++ b/doc/user/project/merge_requests/code_quality.md
@@ -1,14 +1,16 @@
+---
+type: reference, howto
+---
+
# Code Quality **[STARTER]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1984)
in [GitLab Starter](https://about.gitlab.com/pricing/) 9.3.
-## Overview
-
-If you are using [GitLab CI/CD](../../../ci/README.md), you can analyze your
+With the help of [GitLab CI/CD](../../../ci/README.md), you can analyze your
source code quality using GitLab Code Quality.
Code Quality uses [Code Climate Engines](https://codeclimate.com), which are
-free and open source. Code Quality doesn’t require a Code Climate subscription.
+free and open source. Code Quality doesn't require a Code Climate subscription.
Going a step further, GitLab can show the Code Quality report right
in the merge request widget area:
@@ -19,7 +21,7 @@ in the merge request widget area:
For instance, consider the following workflow:
-1. Your backend team member starts a new implementation for making certain feature in your app faster
+1. Your backend team member starts a new implementation for making a certain feature in your app faster
1. With Code Quality reports, they analyze how their implementation is impacting the code quality
1. The metrics show that their code degrade the quality in 10 points
1. You ask a co-worker to help them with this modification
@@ -63,20 +65,30 @@ Example:
NOTE: **Note:**
Although the Code Climate spec supports more properties, those are ignored by GitLab.
-For more information on how the Code Quality job should look like, check the
+For more information on what the Code Quality job should look like, check the
example on [analyzing a project's code quality](../../../ci/examples/code_quality.md).
GitLab then checks this report, compares the metrics between the source and target
branches, and shows the information right on the merge request.
-CAUTION: **Caution:**
If multiple jobs in a pipeline generate a code quality artifact, only the artifact from
the last created job (the job with the largest job ID) is used. To avoid confusion,
configure only one job to generate a code quality artifact.
-NOTE: **Note:**
If the Code Quality report doesn't have anything to compare to, no information
will be displayed in the merge request area. That is the case when you add the
Code Quality job in your `.gitlab-ci.yml` for the very first time.
Consecutive merge requests will have something to compare to and the Code Quality
report will be shown properly.
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/project/merge_requests/code_quality_diff.md b/doc/user/project/merge_requests/code_quality_diff.md
index 890058eec6f..ccc694672a6 100644
--- a/doc/user/project/merge_requests/code_quality_diff.md
+++ b/doc/user/project/merge_requests/code_quality_diff.md
@@ -1,5 +1,5 @@
---
-redirect_from: 'https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html'
+redirect_from: 'code_quality_diff.md'
redirect_to: 'code_quality.md'
---
diff --git a/doc/user/project/merge_requests/container_scanning.md b/doc/user/project/merge_requests/container_scanning.md
index 4d41e424f4a..a062731ea35 100644
--- a/doc/user/project/merge_requests/container_scanning.md
+++ b/doc/user/project/merge_requests/container_scanning.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html'
+redirect_to: '../../application_security/container_scanning/index.md'
---
-This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html).
+This document was moved to [another location](../../application_security/container_scanning/index.md).
diff --git a/doc/user/project/merge_requests/dast.md b/doc/user/project/merge_requests/dast.md
index b676c661267..98a2906e560 100644
--- a/doc/user/project/merge_requests/dast.md
+++ b/doc/user/project/merge_requests/dast.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'https://docs.gitlab.com/ee/user/application_security/dast/index.html'
+redirect_to: '../../application_security/dast/index.md'
---
-This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/dast/index.html).
+This document was moved to [another location](../../application_security/dast/index.md).
diff --git a/doc/user/project/merge_requests/dependency_scanning.md b/doc/user/project/merge_requests/dependency_scanning.md
index 3a8b53b425c..bdc1c355016 100644
--- a/doc/user/project/merge_requests/dependency_scanning.md
+++ b/doc/user/project/merge_requests/dependency_scanning.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html'
+redirect_to: '../../application_security/dependency_scanning/index.md'
---
-This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html).
+This document was moved to [another location](../../application_security/dependency_scanning/index.md).
diff --git a/doc/user/project/merge_requests/fast_forward_merge.md b/doc/user/project/merge_requests/fast_forward_merge.md
index 3cd91a185e3..4f3c68090e4 100644
--- a/doc/user/project/merge_requests/fast_forward_merge.md
+++ b/doc/user/project/merge_requests/fast_forward_merge.md
@@ -1,21 +1,24 @@
+---
+type: reference, concepts
+---
+
# Fast-forward merge requests
-Retain a linear Git history and a way to accept merge requests without
-creating merge commits.
+Sometimes, a workflow policy might mandate a clean commit history without
+merge commits. In such cases, the fast-forward merge is the perfect candidate.
+
+With fast-forward merge requests, you can retain a linear Git history and a way
+to accept merge requests without creating merge commits.
## Overview
-When the fast-forward merge ([`--ff-only`][ffonly]) setting is enabled, no merge
-commits will be created and all merges are fast-forwarded, which means that
-merging is only allowed if the branch could be fast-forwarded.
+When the fast-forward merge
+([`--ff-only`](https://git-scm.com/docs/git-merge#git-merge---ff-only)) setting
+is enabled, no merge commits will be created and all merges are fast-forwarded,
+which means that merging is only allowed if the branch could be fast-forwarded.
When a fast-forward merge is not possible, the user is given the option to rebase.
-## Use cases
-
-Sometimes, a workflow policy might mandate a clean commit history without
-merge commits. In such cases, the fast-forward merge is the perfect candidate.
-
## Enabling fast-forward merges
1. Navigate to your project's **Settings** and search for the 'Merge method'
@@ -32,4 +35,14 @@ source branch locally before you will be able to do a fast-forward merge.
![Fast forward merge rebase locally](img/ff_merge_rebase_locally.png)
-[ffonly]: https://git-scm.com/docs/git-merge#git-merge---ff-only
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 09736c7fc7e..447b338928c 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -1,7 +1,11 @@
+---
+type: index, reference, concepts
+---
+
# Merge requests
-Merge requests allow you to exchange changes you made to source code and
-collaborate with other people on the same project.
+Merge requests allow you to visualize and collaborate on the proposed changes
+to source code that exist as commits on a given Git branch.
![Merge request view](img/merge_request.png)
@@ -33,15 +37,15 @@ With GitLab merge requests, you can:
With **[GitLab Enterprise Edition][ee]**, you can also:
-- Prepare a full review and submit it once it's ready with [Merge Request Reviews](https://docs.gitlab.com/ee/user/discussions/index.md#merge-request-reviews-premium) **[PREMIUM]**
-- View the deployment process across projects with [Multi-Project Pipelines](https://docs.gitlab.com/ee/ci/multi_project_pipelines.md) **[PREMIUM]**
+- Prepare a full review and submit it once it's ready with [Merge Request Reviews](../../discussions/index.md#merge-request-reviews-premium) **[PREMIUM]**
+- View the deployment process across projects with [Multi-Project Pipelines](../../../ci/multi_project_pipelines.md) **[PREMIUM]**
- Request [approvals](merge_request_approvals.md) from your managers **[STARTER]**
- Analyze the impact of your changes with [Code Quality reports](code_quality.md) **[STARTER]**
-- Manage the licenses of your dependencies with [License Management](https://docs.gitlab.com/ee/user/application_security/license_management/index.md) **[ULTIMATE]**
-- Analyze your source code for vulnerabilities with [Static Application Security Testing](https://docs.gitlab.com/ee/user/application_security/sast/index.md) **[ULTIMATE]**
-- Analyze your running web applications for vulnerabilities with [Dynamic Application Security Testing](https://docs.gitlab.com/ee/user/application_security/dast/index.md) **[ULTIMATE]**
-- Analyze your dependencies for vulnerabilities with [Dependency Scanning](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.md) **[ULTIMATE]**
-- Analyze your Docker images for vulnerabilities with [Container Scanning](https://docs.gitlab.com/ee/user/application_security/container_scanning/index.md) **[ULTIMATE]**
+- Manage the licenses of your dependencies with [License Management](../../application_security/license_management/index.md) **[ULTIMATE]**
+- Analyze your source code for vulnerabilities with [Static Application Security Testing](../../application_security/sast/index.md) **[ULTIMATE]**
+- Analyze your running web applications for vulnerabilities with [Dynamic Application Security Testing](../../application_security/dast/index.md) **[ULTIMATE]**
+- Analyze your dependencies for vulnerabilities with [Dependency Scanning](../../application_security/dependency_scanning/index.md) **[ULTIMATE]**
+- Analyze your Docker images for vulnerabilities with [Container Scanning](../../application_security/container_scanning/index.md) **[ULTIMATE]**
- Determine the performance impact of changes with [Browser Performance Testing](#browser-performance-testing-premium) **[PREMIUM]**
## Use cases
@@ -174,7 +178,7 @@ Start a review in order to create multiple comments on a diff and publish them o
Starting a review allows you to get all your thoughts in order and ensure you haven't missed anything
before submitting all your comments.
-[Learn more about Merge Request Reviews](https://docs.gitlab.com/ee/user/discussions/index.html#merge-request-reviews-premium)
+[Learn more about Merge Request Reviews](../../discussions/index.md#merge-request-reviews-premium)
## Squash and merge
@@ -395,7 +399,7 @@ GitLab runs the [Sitespeed.io container][sitespeed-container] and displays the d
GitLab can scan and report any vulnerabilities found in your project.
-[Read more about security reports.](https://docs.gitlab.com/ee/user/application_security/index.html)
+[Read more about security reports.](../../application_security/index.md)
## JUnit test reports
@@ -511,7 +515,7 @@ seconds and the status will update automatically.
Merge Request pipeline statuses can't be retrieved when the following occurs:
-1. A Merge Requst is created
+1. A Merge Request is created
1. The Merge Request is closed
1. Changes are made in the project
1. The Merge Request is reopened
@@ -606,6 +610,9 @@ And to check out a particular merge request:
git checkout origin/merge-requests/1
```
+all the above can be done with [git-mr] script.
+
+[git-mr]: https://gitlab.com/glensc/git-mr
[products]: https://about.gitlab.com/products/ "GitLab products page"
[protected branches]: ../protected_branches.md
[ci]: ../../../ci/README.md
diff --git a/doc/user/project/merge_requests/license_management.md b/doc/user/project/merge_requests/license_management.md
index 08704425a75..93116ebd7c6 100644
--- a/doc/user/project/merge_requests/license_management.md
+++ b/doc/user/project/merge_requests/license_management.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'https://docs.gitlab.com/ee/user/application_security/license_management/index.html'
+redirect_to: '../../application_security/license_management/index.md'
---
-This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/license_management/index.html).
+This document was moved to [another location](../../application_security/license_management/index.md).
diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md
index d0291c4cef5..fd151a6df45 100644
--- a/doc/user/project/merge_requests/merge_request_approvals.md
+++ b/doc/user/project/merge_requests/merge_request_approvals.md
@@ -1,9 +1,25 @@
+---
+type: reference, concepts
+---
+
# Merge request approvals **[STARTER]**
> Introduced in [GitLab Enterprise Edition 7.12](https://about.gitlab.com/2015/06/22/gitlab-7-12-released/#merge-request-approvers-ee-only).
+Merge request approvals enable enforced code review by requiring specified people
+to approve a merge request before it can be unblocked for merging.
+
+## Use cases
+
+1. Enforcing review of all code that gets merged into a repository.
+2. Specifying code maintainers for an entire repository.
+3. Specifying reviewers for a given proposed code change.
+4. Specifying categories of reviewers, such as BE, FE, QA, DB, etc., for all proposed code changes.
+
+## Enabling the new approvals interface
+
NOTE: **Note:**
-If you are running a self-managed instance, the new interface shown on
+Prior to 12.0, if you are running a self-managed instance, the new interface shown on
this page will not be available unless the feature flag
`approval_rules` is enabled, which can be done from the Rails console by
instance administrators.
@@ -21,20 +37,6 @@ sudo -u git -H bin/rails console RAILS_ENV=production
Then run `Feature.enable(:approval_rules)` to enable the feature flag.
-The documentation for the older interface can be accessed
-[here](/11.7/ee/user/project/merge_requests/merge_request_approvals.html).
-
-## Overview
-
-Merge request approvals enable enforced code review by requiring specified people to approve a merge request before it can be unblocked for merging.
-
-## Use cases
-
-1. Enforcing review of all code that gets merged into a repository.
-2. Specifying code maintainers for an entire repository.
-3. Specifying reviewers for a given proposed code change.
-4. Specifying categories of reviewers, such as BE, FE, QA, DB, etc., for all proposed code changes.
-
## Editing approvals
To edit the merge request approvals:
@@ -105,7 +107,7 @@ any [eligible approver](#eligible-approvers) may approve.
The following can approve merge requests:
- Users being added as approvers at project or merge request level.
-- [Code owners](https://docs.gitlab.com/ee/user/project/code_owners.html) related to the merge request ([introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/7933) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.5).
+- [Code owners](../code_owners.md) related to the merge request ([introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/7933) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.5).
An individual user can be added as an approver for a project if they are a member of:
@@ -168,7 +170,7 @@ or a [failed CI/CD pipeline](merge_when_pipeline_succeeds.md).
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/4418) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.9.
It is possible to require at least one approval for each entry in the
-[`CODEOWNERS` file](https://docs.gitlab.com/ee/user/project/code_owners.html) that matches a file changed in
+[`CODEOWNERS` file](../code_owners.md) that matches a file changed in
the merge request. To enable this feature:
1. Navigate to your project's **Settings > General** and expand
@@ -328,3 +330,15 @@ To filter merge requests by an individual approver, you can type (or select from
the dropdown) `approver` and select the user.
![Filter MRs by an approver](img/filter_approver_merge_requests.png)
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
index 1477e35dca8..c93c7a5fe08 100644
--- a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
+++ b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
@@ -1,3 +1,7 @@
+---
+type: reference, concepts
+---
+
# Merge when pipeline succeeds
When reviewing a merge request that looks ready to merge but still has one or
@@ -7,6 +11,8 @@ finish and remember to merge the request manually.
![Enable](img/merge_when_pipeline_succeeds_enable.png)
+## How it works
+
When you hit the "Merge When Pipeline Succeeds" button, the status of the merge
request will be updated to represent the impending merge. If you cannot wait
for the pipeline to succeed and want to merge immediately, this option is
@@ -29,9 +35,6 @@ changes to be reviewed.
## Only allow merge requests to be merged if the pipeline succeeds
-> **Note:**
-You need to have jobs configured to enable this feature.
-
You can prevent merge requests from being merged if their pipeline did not succeed
or if there are discussions to be resolved.
@@ -39,9 +42,21 @@ Navigate to your project's settings page and expand the **Merge requests** secti
In the **Merge checks** subsection, select the **Pipelines must succeed** check
box and hit **Save** for the changes to take effect.
-![Pipelines must succeed settings](img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png)
+![Pipelines must succeed settings](img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png)
From now on, every time the pipeline fails you will not be able to merge the
merge request from the UI, until you make all relevant jobs pass.
![Only allow merge if pipeline succeeds message](img/merge_when_pipeline_succeeds_only_if_succeeds_msg.png)
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/project/merge_requests/resolve_conflicts.md b/doc/user/project/merge_requests/resolve_conflicts.md
index ccef2853e3f..6a0f2d18629 100644
--- a/doc/user/project/merge_requests/resolve_conflicts.md
+++ b/doc/user/project/merge_requests/resolve_conflicts.md
@@ -1,3 +1,7 @@
+---
+type: reference, concepts
+---
+
# Merge request conflict resolution
Merge conflicts occur when two branches have different changes that cannot be
@@ -66,3 +70,15 @@ Additionally, GitLab does not detect conflicts in renames away from a path. For
example, this will not create a conflict: on branch `a`, doing `git mv file1
file2`; on branch `b`, doing `git mv file1 file3`. Instead, both files will be
present in the branch after the merge request is merged.
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/project/merge_requests/revert_changes.md b/doc/user/project/merge_requests/revert_changes.md
index b9102798a49..1cbbcf45400 100644
--- a/doc/user/project/merge_requests/revert_changes.md
+++ b/doc/user/project/merge_requests/revert_changes.md
@@ -1,11 +1,13 @@
-# Reverting changes
+---
+type: reference, concepts
+---
-> [Introduced][ce-1990] in GitLab 8.5.
+# Reverting changes
-GitLab implements Git's powerful feature to [revert any commit][git-revert]
-with introducing a **Revert** button in merge requests and commit details.
+You can use Git's powerful feature to [revert any commit](https://git-scm.com/docs/git-revert "Git revert documentation")
+by clicking the **Revert** button in merge requests and commit details.
-## Reverting a Merge Request
+## Reverting a merge request
NOTE: **Note:**
The **Revert** button will only be available for merge requests
@@ -30,9 +32,9 @@ create a new merge request with the revert changes.
After the merge request has been reverted, the **Revert** button will not be
available anymore.
-## Reverting a Commit
+## Reverting a commit
-You can revert a Commit from the Commit details page:
+You can revert a commit from the commit details page:
![Revert commit](img/cherry_pick_changes_commit.png)
@@ -54,5 +56,14 @@ mainline:
git revert -m 2 7a39eb0
```
-[ce-1990]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1990 "Revert button Merge Request"
-[git-revert]: https://git-scm.com/docs/git-revert "Git revert documentation"
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/project/merge_requests/sast.md b/doc/user/project/merge_requests/sast.md
index 688cc79d0f6..165290eb114 100644
--- a/doc/user/project/merge_requests/sast.md
+++ b/doc/user/project/merge_requests/sast.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'https://docs.gitlab.com/ee/user/application_security/sast/index.html'
+redirect_to: '../../application_security/sast/index.md'
---
-This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/sast/index.html).
+This document was moved to [another location](../../application_security/sast/index.md).
diff --git a/doc/user/project/merge_requests/sast_docker.md b/doc/user/project/merge_requests/sast_docker.md
index 4d41e424f4a..a062731ea35 100644
--- a/doc/user/project/merge_requests/sast_docker.md
+++ b/doc/user/project/merge_requests/sast_docker.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html'
+redirect_to: '../../application_security/container_scanning/index.md'
---
-This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html).
+This document was moved to [another location](../../application_security/container_scanning/index.md).
diff --git a/doc/user/project/merge_requests/squash_and_merge.md b/doc/user/project/merge_requests/squash_and_merge.md
index 4ff8ec3a7e6..39fd2588811 100644
--- a/doc/user/project/merge_requests/squash_and_merge.md
+++ b/doc/user/project/merge_requests/squash_and_merge.md
@@ -1,8 +1,14 @@
+---
+type: reference, concepts
+---
+
# Squash and merge
-> [Introduced][ee-1024] in [GitLab Starter][ee] 8.17, and in [GitLab Core][ce] [11.0][ce-18956].
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1024) in [GitLab Starter](https://about.gitlab.com/pricing/) 8.17.
+> - [Ported](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18956) to GitLab Core 11.0.
-Combine all commits of your merge request into one and retain a clean history.
+With squash and merge you can combine all your merge request's commits into one
+and retain a clean history.
## Overview
@@ -12,11 +18,11 @@ and then merges that commit using the merge method set for the project.
In other words, squashing a merge request turns a long list of commits:
-![List of commits from a merge request][mr-commits]
+![List of commits from a merge request](img/squash_mr_commits.png)
Into a single commit on merge:
-![A squashed commit followed by a merge commit][squashed-commit]
+![A squashed commit followed by a merge commit](img/squash_squashed_commit.png)
The squashed commit's commit message will be either:
@@ -44,20 +50,18 @@ all you have to do is enable squashing before you press merge to join
the commits in the merge request into a single commit.
This way, the history of your base branch remains clean with
-meaningful commit messages and is simpler to [revert] if necessary.
+meaningful commit messages and is simpler to [revert](revert_changes.md) if necessary.
## Enabling squash for a merge request
Anyone who can create or edit a merge request can choose for it to be squashed
on the merge request form:
-![Squash commits checkbox on edit form][squash-edit-form]
-
----
+![Squash commits checkbox on edit form](img/squash_edit_form.png)
This can then be overridden at the time of accepting the merge request:
-![Squash commits checkbox on accept merge request form][squash-mr-widget]
+![Squash commits checkbox on accept merge request form](img/squash_mr_widget.png)
## Commit metadata for squashed commits
@@ -69,19 +73,20 @@ The squashed commit has the following metadata:
## Squash and fast-forward merge
-When a project has the [fast-forward merge setting enabled][ff-merge], the merge
+When a project has the [fast-forward merge setting enabled](fast_forward_merge.md#enabling-fast-forward-merges), the merge
request must be able to be fast-forwarded without squashing in order to squash
it. This is because squashing is only available when accepting a merge request,
so a merge request may need to be rebased before squashing, even though
squashing can itself be considered equivalent to rebasing.
-[ee-1024]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1024
-[ce-18956]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18956
-[mr-commits]: img/squash_mr_commits.png
-[squashed-commit]: img/squash_squashed_commit.png
-[squash-edit-form]: img/squash_edit_form.png
-[squash-mr-widget]: img/squash_mr_widget.png
-[ff-merge]: fast_forward_merge.md#enabling-fast-forward-merges
-[ce]: https://about.gitlab.com/pricing/
-[ee]: https://about.gitlab.com/pricing/
-[revert]: revert_changes.md
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/project/merge_requests/versions.md b/doc/user/project/merge_requests/versions.md
index 70bd1e60594..fbe216c3aed 100644
--- a/doc/user/project/merge_requests/versions.md
+++ b/doc/user/project/merge_requests/versions.md
@@ -1,13 +1,8 @@
-# Merge requests versions
+---
+type: reference, concepts
+---
-> **Notes:**
->
-> - [Introduced][ce-5467] in GitLab 8.12.
-> - Comments are disabled while viewing outdated merge versions or comparing to
-> versions other than base.
-> - Merge request versions are based on push not on commit. So, if you pushed 5
-> commits in a single push, it will be a single option in the dropdown. If you
-> pushed 5 times, that will count for 5 options.
+# Merge requests versions
Every time you push to a branch that is tied to a merge request, a new version
of merge request diff is created. When you visit a merge request that contains
@@ -16,25 +11,38 @@ request diffs.
![Merge request versions](img/versions.png)
----
+## Selecting a version
By default, the latest version of changes is shown. However, you
can select an older one from version dropdown.
![Merge request versions dropdown](img/versions_dropdown.png)
----
+Merge request versions are based on push not on commit. So, if you pushed 5
+commits in a single push, it will be a single option in the dropdown. If you
+pushed 5 times, that will count for 5 options.
You can also compare the merge request version with an older one to see what has
changed since then.
![Merge request versions compare](img/versions_compare.png)
----
+Comments are disabled while viewing outdated merge versions or comparing to
+versions other than base.
Every time you push new changes to the branch, a link to compare the last
changes appears as a system note.
![Merge request versions system note](img/versions_system_note.png)
-[ce-5467]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5467
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/project/merge_requests/work_in_progress_merge_requests.md b/doc/user/project/merge_requests/work_in_progress_merge_requests.md
index 6f33eb9a482..9cd868067bc 100644
--- a/doc/user/project/merge_requests/work_in_progress_merge_requests.md
+++ b/doc/user/project/merge_requests/work_in_progress_merge_requests.md
@@ -1,4 +1,8 @@
-# "Work In Progress" Merge Requests
+---
+type: reference, concepts
+---
+
+# "Work In Progress" merge requests
If a merge request is not yet ready to be merged, perhaps due to continued development
or open discussions, you can prevent it from being accepted before it's ready by flagging
@@ -7,7 +11,7 @@ being merged, and it will stay disabled until the "WIP" flag has been removed.
![Blocked Accept Button](img/wip_blocked_accept_button.png)
-## Adding the "Work In Progress" flag to a Merge Request
+## Adding the "Work In Progress" flag to a merge request
There are several ways to flag a merge request as a Work In Progress:
@@ -21,7 +25,7 @@ There are several ways to flag a merge request as a Work In Progress:
source branch. This is not a toggle, and doing it again in another commit will have
no effect.
-## Removing the "Work In Progress" flag from a Merge Request
+## Removing the "Work In Progress" flag from a merge request
Similar to above, when a Merge Request is ready to be merged, you can remove the
"Work in Progress" flag in several ways:
@@ -37,10 +41,22 @@ Similar to above, when a Merge Request is ready to be merged, you can remove the
Must have at least Developer level permissions on the project for the button to
be visible.
-## Including/Excluding WIP Merge Requests when searching
+## Including/excluding WIP merge requests when searching
When viewing/searching the merge requests list, you can choose to include or exclude
WIP merge requests by adding a "WIP" filter in the search box, and choosing "Yes"
(to include) or "No" (to exclude).
![Filter WIP MRs](img/filter_wip_merge_requests.png)
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/project/milestones/burndown_charts.md b/doc/user/project/milestones/burndown_charts.md
index 0ad08da8ff5..7ffeb032d7f 100644
--- a/doc/user/project/milestones/burndown_charts.md
+++ b/doc/user/project/milestones/burndown_charts.md
@@ -52,7 +52,7 @@ and select a milestone from your current ones, while for group's, access the **G
select a group, and go through **Issues > Milestones** on the sidebar.
NOTE: **Note:**
-You're able to [promote project](https://docs.gitlab.com/ee/user/project/milestones/#promoting-project-milestones-to-group-milestones) to group milestones and still see the **Burndown Chart** for them, respecting license limitations.
+You're able to [promote project](index.md#promoting-project-milestones-to-group-milestones) to group milestones and still see the **Burndown Chart** for them, respecting license limitations.
The chart indicates the project's progress throughout that milestone (for issues assigned to it).
diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
index 0d8ee0a6cd2..6cd866b5c0d 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -138,7 +138,7 @@ For group milestones in [GitLab Premium](https://about.gitlab.com/pricing), a [b
The milestone sidebar on the milestone view shows the following:
-- Percentage complete, which is calculated as number of closed issues plus number of closed/merged merge requests divided by total number issues and merge requests.
+- Percentage complete, which is calculated as number of closed issues divided by total number of issues.
- The start date and due date.
- The total time spent on all issues that have the milestone assigned.
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index d36312c9b8d..c07c4099f22 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -28,10 +28,10 @@ The reasons to do it like that are:
and maximizing security.
With the new behavior, any job that is triggered by the user, is also marked
-with their permissions. When a user does a `git push` or changes files through
+with their read permissions. When a user does a `git push` or changes files through
the web UI, a new pipeline will be usually created. This pipeline will be marked
as created be the pusher (local push or via the UI) and any job created in this
-pipeline will have the permissions of the pusher.
+pipeline will have the read permissions of the pusher but not write permissions.
This allows us to make it really easy to evaluate the access for all projects
that have [Git submodules][gitsub] or are using container images that the pusher
@@ -67,9 +67,10 @@ Let's consider the following scenario:
## Job token
-A unique job token is generated for each job and it allows the user to
+A unique job token is generated for each job and provides the user read
access all projects that would be normally accessible to the user creating that
-job.
+job. The unique job token does not have any write permissions, but there
+is a [proposal to add support](https://gitlab.com/gitlab-org/gitlab-ce/issues/18106).
We try to make sure that this token doesn't leak by:
diff --git a/doc/user/project/packages/maven_repository.md b/doc/user/project/packages/maven_repository.md
index 94785eb6aec..9b7af738696 100644
--- a/doc/user/project/packages/maven_repository.md
+++ b/doc/user/project/packages/maven_repository.md
@@ -12,7 +12,7 @@ project can have its own space to store its Maven artifacts.
NOTE: **Note:**
This option is available only if your GitLab administrator has
-[enabled support for the Maven repository](https://docs.gitlab.com/ee/administration/packages.html).**[PREMIUM ONLY]**
+[enabled support for the Maven repository](../../../administration/packages.md).**[PREMIUM ONLY]**
After the Packages feature is enabled, the Maven Repository will be available for
all new projects by default. To enable it for existing projects, or if you want
diff --git a/doc/user/project/packages/npm_registry.md b/doc/user/project/packages/npm_registry.md
index 9f4c01c9a0a..2e274573434 100644
--- a/doc/user/project/packages/npm_registry.md
+++ b/doc/user/project/packages/npm_registry.md
@@ -20,7 +20,7 @@ within a subgroup is not supported yet.
NOTE: **Note:**
This option is available only if your GitLab administrator has
-[enabled support for the NPM registry](https://docs.gitlab.com/ee/administration/packages.html).**[PREMIUM ONLY]**
+[enabled support for the NPM registry](../../../administration/packages.md).**[PREMIUM ONLY]**
After the NPM registry is enabled, it will be available for all new projects
by default. To enable it for existing projects, or if you want to disable it:
diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md
index 87cd4941ae6..8baf41dba78 100644
--- a/doc/user/project/pages/getting_started_part_four.md
+++ b/doc/user/project/pages/getting_started_part_four.md
@@ -1,10 +1,6 @@
---
-last_updated: 2018-02-16
-author: Marcia Ramos
-author_gitlab: marcia
-level: intermediate
-article_type: user guide
-date: 2017-02-22
+last_updated: 2019-06-04
+type: reference, howto
---
# Creating and Tweaking GitLab CI/CD for GitLab Pages
diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md
index 7dbf58b5715..6d538ca2455 100644
--- a/doc/user/project/pages/getting_started_part_one.md
+++ b/doc/user/project/pages/getting_started_part_one.md
@@ -1,5 +1,6 @@
---
-last_updated: 2018-02-16
+last_updated: 2018-06-04
+type: concepts, reference
---
# Static sites and GitLab Pages domains
diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md
index 9f2bc281f85..d585c19fc5c 100644
--- a/doc/user/project/pages/getting_started_part_three.md
+++ b/doc/user/project/pages/getting_started_part_three.md
@@ -1,10 +1,6 @@
---
-last_updated: 2018-11-19
-author: Marcia Ramos
-author_gitlab: marcia
-level: beginner
-article_type: user guide
-date: 2017-02-22
+last_updated: 2019-06-04
+type: concepts, reference, howto
---
# GitLab Pages custom domains and SSL/TLS Certificates
@@ -96,7 +92,7 @@ you need to log into your domain's admin control panel and add a DNS
`CNAME` record pointing your subdomain to your website URL
(`namespace.gitlab.io`) address.
-Notice that, despite it's a user or project website, the `CNAME`
+Note that, whether it's a user or a project website, the `CNAME`
should point to your Pages domain (`namespace.gitlab.io`),
without any `/project-name`.
diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md
index b74520e6556..3e50cd4887c 100644
--- a/doc/user/project/pages/getting_started_part_two.md
+++ b/doc/user/project/pages/getting_started_part_two.md
@@ -1,10 +1,6 @@
---
-last_updated: 2019-03-05
-author: Marcia Ramos
-author_gitlab: marcia
-level: beginner
-article_type: user guide
-date: 2017-02-22
+last_updated: 2019-06-04
+type: reference, howto
---
# Projects for GitLab Pages and URL structure
@@ -13,11 +9,11 @@ date: 2017-02-22
To get started with GitLab Pages, you need:
-1. A project
-1. A configuration file (`.gitlab-ci.yml`) to deploy your site
+1. A project, thus a repository to hold your website's codebase.
+1. A configuration file (`.gitlab-ci.yml`) to deploy your site.
1. A specific `job` called `pages` in the configuration file
- that will make GitLab aware that you are deploying a GitLab Pages website
-1. A `public` directory with the content of the website
+ that will make GitLab aware that you are deploying a GitLab Pages website.
+1. A `public` directory with the static content of the website.
Optional Features:
@@ -140,7 +136,7 @@ where you'll find its default URL.
repository to you local computer and moving your site files into it,
you can run `git init` in your local website directory, add the
remote URL: `git remote add origin git@gitlab.com:namespace/project-name.git`,
- then add, commit, and push.
+ then add, commit, and push to GitLab.
## URLs and Baseurls
@@ -173,4 +169,4 @@ baseurl: ""
## Custom Domains
GitLab Pages supports custom domains and subdomains, served under HTTP or HTTPS.
-Please check the [next part](getting_started_part_three.md) of this series for an overview.
+See [GitLab Pages custom domains and SSL/TLS Certificates](getting_started_part_three.md) for more information.
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
index 91098d51160..04bda212128 100644
--- a/doc/user/project/pages/index.md
+++ b/doc/user/project/pages/index.md
@@ -1,6 +1,7 @@
---
description: 'Learn how to use GitLab Pages to deploy a static website at no additional cost.'
-last_updated: 2019-03-05
+last_updated: 2019-06-04
+type: index, reference
---
# GitLab Pages
@@ -140,7 +141,7 @@ To learn more about configuration options for GitLab Pages, read the following:
| [Static websites and Pages domains](getting_started_part_one.md) | Understand what is a static website, and how GitLab Pages default domains work. |
| [Projects and URL structure](getting_started_part_two.md) | Forking projects and creating new ones from scratch, understanding URLs structure and baseurls. |
| [GitLab CI/CD for GitLab Pages](getting_started_part_four.md) | Understand how to create your own `.gitlab-ci.yml` for your site. |
-| [Exploring GitLab Pages](introduction.md) | Technical aspects, specific configuration options, custom 404 pages, limitations. |
+| [Exploring GitLab Pages](introduction.md) | Requirements, technical aspects, specific GitLab CI's configuration options, custom 404 pages, limitations, FAQ. |
|---+---|
| [Custom domains and SSL/TLS Certificates](getting_started_part_three.md) | How to add custom domains and subdomains to your website, configure DNS records and SSL/TLS certificates. |
| [CloudFlare certificates](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) | Secure your Pages site with CloudFlare certificates. |
diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md
index a14a446aead..4fab7f79e0c 100644
--- a/doc/user/project/pages/introduction.md
+++ b/doc/user/project/pages/introduction.md
@@ -1,3 +1,8 @@
+---
+type: reference
+last_updated: 2018-06-04
+---
+
# Exploring GitLab Pages
This document is a user guide to explore the options and settings
@@ -10,7 +15,7 @@ To familiarize yourself with GitLab Pages first:
- Learn how to enable GitLab Pages
across your GitLab instance on the [administrator documentation](../../../administration/pages/index.md).
-## Pages requirements
+## GitLab Pages requirements
In brief, this is what you need to upload your website in GitLab Pages:
@@ -34,6 +39,99 @@ If you are using [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlabcom) to hos
Visit the [GitLab Pages group](https://gitlab.com/groups/pages) for a complete list of example projects. Contributions are very welcome.
+## Custom error codes Pages
+
+You can provide your own 403 and 404 error pages by creating the `403.html` and
+`404.html` files respectively in the root directory of the `public/` directory
+that will be included in the artifacts. Usually this is the root directory of
+your project, but that may differ depending on your static generator
+configuration.
+
+If the case of `404.html`, there are different scenarios. For example:
+
+- If you use project Pages (served under `/projectname/`) and try to access
+ `/projectname/non/existing_file`, GitLab Pages will try to serve first
+ `/projectname/404.html`, and then `/404.html`.
+- If you use user/group Pages (served under `/`) and try to access
+ `/non/existing_file` GitLab Pages will try to serve `/404.html`.
+- If you use a custom domain and try to access `/non/existing_file`, GitLab
+ Pages will try to serve only `/404.html`.
+
+## Redirects in GitLab Pages
+
+Since you cannot use any custom server configuration files, like `.htaccess` or
+any `.conf` file, if you want to redirect a page to another
+location, you can use the [HTTP meta refresh tag][metarefresh].
+
+Some static site generators provide plugins for that functionality so that you
+don't have to create and edit HTML files manually. For example, Jekyll has the
+[redirect-from plugin](https://github.com/jekyll/jekyll-redirect-from).
+
+## GitLab Pages Access Control **[CORE ONLY]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/33422) in GitLab 11.5.
+
+NOTE: **Note:**
+GitLab Pages access control is not activated on GitLab.com. You can check its
+progress on the
+[infrastructure issue tracker](https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/5576).
+
+You can enable Pages access control on your project, so that only
+[members of your project](../../permissions.md#project-members-permissions)
+(at least Guest) can access your website:
+
+1. Navigate to your project's **Settings > General > Permissions**.
+1. Toggle the **Pages** button to enable the access control.
+
+ NOTE: **Note:**
+ If you don't see the toggle button, that means that it's not enabled.
+ Ask your administrator to [enable it](../../../administration/pages/index.md#access-control).
+
+1. The Pages access control dropdown allows you to set who can view pages hosted
+ with GitLab Pages, depending on your project's visibility:
+
+ - If your project is private:
+ - **Only project members**: Only project members will be able to browse the website.
+ - **Everyone**: Everyone, both logged into and logged out of GitLab, will be able to browse the website, no matter their project membership.
+ - If your project is internal:
+ - **Only project members**: Only project members will be able to browse the website.
+ - **Everyone with access**: Everyone logged into GitLab will be able to browse the website, no matter their project membership.
+ - **Everyone**: Everyone, both logged into and logged out of GitLab, will be able to browse the website, no matter their project membership.
+ - If your project is public:
+ - **Only project members**: Only project members will be able to browse the website.
+ - **Everyone with access**: Everyone, both logged into and logged out of GitLab, will be able to browse the website, no matter their project membership.
+
+1. Click **Save changes**.
+
+---
+
+The next time someone tries to access your website and the access control is
+enabled, they will be presented with a page to sign into GitLab and verify they
+can access the website.
+
+## Unpublishing your Pages
+
+If you ever feel the need to purge your Pages content, you can do so by going
+to your project's settings through the gear icon in the top right, and then
+navigating to **Pages**. Hit the **Remove pages** button and your Pages website
+will be deleted.
+
+![Remove pages](img/remove_pages.png)
+
+## Limitations
+
+When using Pages under the general domain of a GitLab instance (`*.example.io`),
+you _cannot_ use HTTPS with sub-subdomains. That means that if your
+username/groupname contains a dot, for example `foo.bar`, the domain
+`https://foo.bar.example.io` will _not_ work. This is a limitation of the
+[HTTP Over TLS protocol][rfc]. HTTP pages will continue to work provided you
+don't redirect HTTP to HTTPS.
+
+[rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC"
+
+GitLab Pages [does **not** support group websites for subgroups](../../group/subgroups/index.md#limitations).
+You can only create the highest-level group website.
+
## Specific configuration options for Pages
Learn how to set up GitLab CI/CD for specific use cases.
@@ -208,99 +306,6 @@ NOTE: **Note:**
When `public/data/index.html` exists, it takes priority over the `public/data.html`
file for both the `/data` and `/data/` URL paths.
-### Custom error codes pages
-
-You can provide your own 403 and 404 error pages by creating the `403.html` and
-`404.html` files respectively in the root directory of the `public/` directory
-that will be included in the artifacts. Usually this is the root directory of
-your project, but that may differ depending on your static generator
-configuration.
-
-If the case of `404.html`, there are different scenarios. For example:
-
-- If you use project Pages (served under `/projectname/`) and try to access
- `/projectname/non/existing_file`, GitLab Pages will try to serve first
- `/projectname/404.html`, and then `/404.html`.
-- If you use user/group Pages (served under `/`) and try to access
- `/non/existing_file` GitLab Pages will try to serve `/404.html`.
-- If you use a custom domain and try to access `/non/existing_file`, GitLab
- Pages will try to serve only `/404.html`.
-
-### Redirects in GitLab Pages
-
-Since you cannot use any custom server configuration files, like `.htaccess` or
-any `.conf` file, if you want to redirect a page to another
-location, you can use the [HTTP meta refresh tag][metarefresh].
-
-Some static site generators provide plugins for that functionality so that you
-don't have to create and edit HTML files manually. For example, Jekyll has the
-[redirect-from plugin](https://github.com/jekyll/jekyll-redirect-from).
-
-### GitLab Pages Access Control **[CORE ONLY]**
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/33422) in GitLab 11.5.
-
-NOTE: **Note:**
-GitLab Pages access control is not activated on GitLab.com. You can check its
-progress on the
-[infrastructure issue tracker](https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/5576).
-
-You can enable Pages access control on your project, so that only
-[members of your project](../../permissions.md#project-members-permissions)
-(at least Guest) can access your website:
-
-1. Navigate to your project's **Settings > General > Permissions**.
-1. Toggle the **Pages** button to enable the access control.
-
- NOTE: **Note:**
- If you don't see the toggle button, that means that it's not enabled.
- Ask your administrator to [enable it](../../../administration/pages/index.md#access-control).
-
-1. The Pages access control dropdown allows you to set who can view pages hosted
- with GitLab Pages, depending on your project's visibility:
-
- - If your project is private:
- - **Only project members**: Only project members will be able to browse the website.
- - **Everyone**: Everyone, both logged into and logged out of GitLab, will be able to browse the website, no matter their project membership.
- - If your project is internal:
- - **Only project members**: Only project members will be able to browse the website.
- - **Everyone with access**: Everyone logged into GitLab will be able to browse the website, no matter their project membership.
- - **Everyone**: Everyone, both logged into and logged out of GitLab, will be able to browse the website, no matter their project membership.
- - If your project is public:
- - **Only project members**: Only project members will be able to browse the website.
- - **Everyone with access**: Everyone, both logged into and logged out of GitLab, will be able to browse the website, no matter their project membership.
-
-1. Click **Save changes**.
-
----
-
-The next time someone tries to access your website and the access control is
-enabled, they will be presented with a page to sign into GitLab and verify they
-can access the website.
-
-## Unpublishing your Pages
-
-If you ever feel the need to purge your Pages content, you can do so by going
-to your project's settings through the gear icon in the top right, and then
-navigating to **Pages**. Hit the **Remove pages** button and your Pages website
-will be deleted.
-
-![Remove pages](img/remove_pages.png)
-
-## Limitations
-
-When using Pages under the general domain of a GitLab instance (`*.example.io`),
-you _cannot_ use HTTPS with sub-subdomains. That means that if your
-username/groupname contains a dot, for example `foo.bar`, the domain
-`https://foo.bar.example.io` will _not_ work. This is a limitation of the
-[HTTP Over TLS protocol][rfc]. HTTP pages will continue to work provided you
-don't redirect HTTP to HTTPS.
-
-[rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC"
-
-GitLab Pages [does **not** support group websites for subgroups](../../group/subgroups/index.md#limitations).
-You can only create the highest-level group website.
-
## Frequently Asked Questions
### Can I download my generated pages?
diff --git a/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md b/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md
index da1b7c59c8e..91a660c0f7a 100644
--- a/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md
+++ b/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md
@@ -1,5 +1,7 @@
---
description: "How to secure GitLab Pages websites with Let's Encrypt."
+type: howto
+last_updated: 2019-06-04
---
# Let's Encrypt for GitLab Pages
diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md
index 629b5e1fde4..002addfc043 100644
--- a/doc/user/project/pipelines/job_artifacts.md
+++ b/doc/user/project/pipelines/job_artifacts.md
@@ -16,7 +16,7 @@
> [administration/job_artifacts](../../../administration/job_artifacts.md).
Artifacts is a list of files and directories which are attached to a job
-after it completes successfully. This feature is enabled by default in all
+after it finishes. This feature is enabled by default in all
GitLab installations.
## Defining artifacts in `.gitlab-ci.yml`
@@ -36,12 +36,14 @@ pdf:
A job named `pdf` calls the `xelatex` command in order to build a pdf file from
the latex source file `mycv.tex`. We then define the `artifacts` paths which in
turn are defined with the `paths` keyword. All paths to files and directories
-are relative to the repository that was cloned during the build. These uploaded
-artifacts will be kept in GitLab for 1 week as defined by the `expire_in`
-definition. You have the option to keep the artifacts from expiring via the
-[web interface](#browsing-artifacts). If the expiry time is not defined,
-it defaults to the [instance wide
-setting](../../admin_area/settings/continuous_integration.md#default-artifacts-expiration-core-only).
+are relative to the repository that was cloned during the build.
+
+The artifacts will be uploaded when the job succeeds by default, but can be set to upload
+when the job fails, or always, if the [`artifacts:when`](../../../ci/yaml/README.md#artifactswhen)
+parameter is used. These uploaded artifacts will be kept in GitLab for 1 week as defined
+by the `expire_in` definition. You have the option to keep the artifacts from expiring
+via the [web interface](#browsing-artifacts). If the expiry time is not defined, it defaults
+to the [instance wide setting](../../admin_area/settings/continuous_integration.md#default-artifacts-expiration-core-only).
For more examples on artifacts, follow the [artifacts reference in
`.gitlab-ci.yml`](../../../ci/yaml/README.md#artifacts).
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index 8b762307ac4..16f48c462eb 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -20,6 +20,22 @@ There are two options. Using:
The default Git strategy can be overridden by the [GIT_STRATEGY variable](../../../ci/yaml/README.md#git-strategy)
in `.gitlab-ci.yml`.
+## Git shallow clone
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/28919) in GitLab 12.0.
+
+NOTE: **Note**: As of GitLab 12.0, newly created projects will automaticallyl have a default
+`git depth` value of `50`.
+
+It is possible to limit the number of changes that GitLab CI/CD will fetch when cloning
+a repository. Setting a limit to `git depth` can speed up Pipelines execution. Maximum
+allowed value is `1000`.
+
+To disable shallow clone and make GitLab CI/CD fetch all branches and tags each time,
+keep the value empty or set to `0`.
+
+This value can also be [overridden by `GIT_DEPTH`](../../../ci/large_repositories/index.md#shallow-cloning) variable in `.gitlab-ci.yml` file.
+
## Timeout
Timeout defines the maximum amount of time in minutes that a job is able run.
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index cb514b76a4e..6fccfd40987 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -123,7 +123,7 @@ You can live preview changes submitted to a new branch with
[Review Apps](../../../ci/review_apps/index.md).
With [GitLab Starter](https://about.gitlab.com/pricing/), you can also request
-[approval](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers.
+[approval](../merge_requests/merge_request_approvals.md) from your managers.
To create, delete, and view [branches](branches/index.md) via GitLab's UI:
@@ -154,7 +154,7 @@ Via command line, you can commit multiple times before pushing.
you will trigger a pipeline per push, not per commit.
- **Skip pipelines:**
You can add to you commit message the keyword
- [`[ci skip]`](../../../ci/yaml/README.html#skipping-jobs)
+ [`[ci skip]`](../../../ci/yaml/README.md#skipping-jobs)
and GitLab CI will skip that pipeline.
- **Cross-link issues and merge requests:**
[Cross-linking](../issues/crosslinking_issues.md#from-commit-messages)
@@ -226,7 +226,7 @@ Find it under your project's **Repository > Compare**.
## Locked files **[PREMIUM]**
-Use [File Locking](https://docs.gitlab.com/ee/user/project/file_lock.html) to
+Use [File Locking](../file_lock.md) to
lock your files to prevent any conflicting changes.
## Repository's API
diff --git a/doc/user/project/repository/reducing_the_repo_size_using_git.md b/doc/user/project/repository/reducing_the_repo_size_using_git.md
index 2339759ecc8..e3d771524ce 100644
--- a/doc/user/project/repository/reducing_the_repo_size_using_git.md
+++ b/doc/user/project/repository/reducing_the_repo_size_using_git.md
@@ -1,6 +1,6 @@
# Reducing the repository size using Git
-A GitLab Enterprise Edition administrator can set a [repository size limit][admin-repo-size]
+A GitLab Enterprise Edition administrator can set a [repository size limit](../../admin_area/settings/account_and_limit_settings.md)
which will prevent you from exceeding it.
When a project has reached its size limit, you will not be able to push to it,
@@ -14,7 +14,8 @@ move some blobs to LFS, or remove some old dependency updates from history.
Unfortunately, it's not so easy and that workflow won't work. Deleting files in
a commit doesn't actually reduce the size of the repo since the earlier commits
and blobs are still around. What you need to do is rewrite history with Git's
-[`filter-branch` option][gitscm], or a tool like the [BFG Repo-Cleaner][bfg].
+[`filter-branch` option](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History#The-Nuclear-Option:-filter-branch),
+or a tool like the [BFG Repo-Cleaner](https://rtyley.github.io/bfg-repo-cleaner/).
Note that even with that method, until `git gc` runs on the GitLab side, the
"removed" commits and blobs will still be around. You also need to be able to
@@ -137,7 +138,3 @@ remove some of them, but it should not be depended on for security purposes!
```
Your repository should now be below the size limit.
-
-[admin-repo-size]: https://docs.gitlab.com/ee/user/admin_area/settings/account_and_limit_settings.html#repository-size-limit
-[bfg]: https://rtyley.github.io/bfg-repo-cleaner/
-[gitscm]: https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History#The-Nuclear-Option:-filter-branch
diff --git a/doc/user/project/security_dashboard.md b/doc/user/project/security_dashboard.md
index 43e910b29fe..a3da1ec97d3 100644
--- a/doc/user/project/security_dashboard.md
+++ b/doc/user/project/security_dashboard.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'https://docs.gitlab.com/ee/user/application_security/security_dashboard/index.html'
+redirect_to: '../application_security/security_dashboard/index.md'
---
-This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/security_dashboard/index.html).
+This document was moved to [another location](../application_security/security_dashboard/index.md).
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index 737dea1de6c..e3502a632d9 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -36,7 +36,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). **[STARTER]**
+- Enable [merge request approvals](../merge_requests/merge_request_approvals.md). **[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).
@@ -44,7 +44,7 @@ Set up your project's merge request settings:
### Service Desk **[PREMIUM]**
-Enable [Service Desk](https://docs.gitlab.com/ee/user/project/service_desk.html) for your project to offer customer support.
+Enable [Service Desk](../service_desk.md) for your project to offer customer support.
### Export project
diff --git a/doc/user/search/advanced_global_search.md b/doc/user/search/advanced_global_search.md
index 38b26f31417..f80f4183802 100644
--- a/doc/user/search/advanced_global_search.md
+++ b/doc/user/search/advanced_global_search.md
@@ -2,7 +2,7 @@
> - [Introduced][ee-109] in GitLab [Starter][ee] 8.4.
> - This is the user documentation. To install and configure Elasticsearch,
-> visit the [administrator documentation](https://docs.gitlab.com/ee/integration/elasticsearch.html).
+> visit the [administrator documentation](../../integration/elasticsearch.md).
NOTE: **Note**
Advanced Global Search (powered by Elasticsearch) is not yet available on GitLab.com. We are working on adding it. [Follow this epic for the latest updates](https://gitlab.com/groups/gitlab-org/-/epics/153).
diff --git a/doc/user/search/advanced_search_syntax.md b/doc/user/search/advanced_search_syntax.md
index 8d4aac5f502..d302cb7a809 100644
--- a/doc/user/search/advanced_search_syntax.md
+++ b/doc/user/search/advanced_search_syntax.md
@@ -3,7 +3,7 @@
> **Notes:**
> - Introduced in [GitLab Enterprise Starter][ee] 9.2
> - This is the user documentation. To install and configure Elasticsearch,
-> visit the [administrator documentation](https://docs.gitlab.com/ee/integration/elasticsearch.html).
+> visit the [administrator documentation](../../integration/elasticsearch.md).
NOTE: **Note**
Advanced Global Search (powered by Elasticsearch) is not yet available on GitLab.com. We are working on adding it. [Follow this epic for the latest updates](https://gitlab.com/groups/gitlab-org/-/epics/153).
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 84ab7840140..40e2486ace5 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -13,15 +13,15 @@ comments: false
- [Groups](../user/group/index.md)
- Issues - The GitLab Issue Tracker is an advanced and complete tool for
tracking the evolution of a new idea or the process of solving a problem.
- - [Exporting Issues](https://docs.gitlab.com/ee/user/project/issues/csv_export.html) **[STARTER]** Export issues as a CSV, emailed as an attachment.
+ - [Exporting Issues](../user/project/issues/csv_export.md) **[STARTER]** Export issues as a CSV, emailed as an attachment.
- [Confidential issues](../user/project/issues/confidential_issues.md)
- [Due date for issues](../user/project/issues/due_dates.md)
- [Issue Board](../user/project/issue_board.md)
- [Keyboard shortcuts](shortcuts.md)
- [File finder](file_finder.md)
-- [File lock](https://docs.gitlab.com/ee/user/project/file_lock.html) **[PREMIUM]**
+- [File lock](../user/project/file_lock.md) **[PREMIUM]**
- [Labels](../user/project/labels.md)
-- [Issue weight](https://docs.gitlab.com/ee/workflow/issue_weight.html) **[STARTER]**
+- [Issue weight](issue_weight.md) **[STARTER]**
- [Notification emails](notifications.md)
- [Projects](../user/project/index.md)
- [Project forking workflow](forking_workflow.md)
@@ -44,9 +44,9 @@ comments: false
- [Merge requests versions](../user/project/merge_requests/versions.md)
- ["Work In Progress" merge requests](../user/project/merge_requests/work_in_progress_merge_requests.md)
- [Fast-forward merge requests](../user/project/merge_requests/fast_forward_merge.md)
- - [Merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) **[STARTER]**
+ - [Merge request approvals](../user/project/merge_requests/merge_request_approvals.md) **[STARTER]**
- [Repository mirroring](repository_mirroring.md) **[STARTER]**
-- [Service Desk](https://docs.gitlab.com/ee/user/project/service_desk.html) **[PREMIUM]**
+- [Service Desk](../user/project/service_desk.md) **[PREMIUM]**
- [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md)
- [Importing from SVN, GitHub, Bitbucket, etc](importing/README.md)
- [Todos](todos.md)
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 1b9fb504b15..7d0abb93262 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -166,7 +166,7 @@ This branch is the place for any work related to this change.
NOTE: **Note:**
The name of a branch might be dictated by organizational standards.
-For example, in GitLab, any branches in GitLab EE that are equivalent to branches in GitLab CE [must end in `-ee`](https://docs.gitlab.com/ee/development/automatic_ce_ee_merge.html#cherry-picking-from-ce-to-ee).
+For example, in GitLab, any branches in GitLab EE that are equivalent to branches in GitLab CE [must end in `-ee`](../development/automatic_ce_ee_merge.md#cherry-picking-from-ce-to-ee).
When you are done or want to discuss the code, open a merge request.
A merge request is an online place to discuss the change and review the code.
diff --git a/doc/workflow/img/notification_global_settings.png b/doc/workflow/img/notification_global_settings.png
index 8a5494d16a8..72f7418f1f8 100644
--- a/doc/workflow/img/notification_global_settings.png
+++ b/doc/workflow/img/notification_global_settings.png
Binary files differ
diff --git a/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md b/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md
index 1aeab5980a3..71c73e3dffe 100644
--- a/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md
+++ b/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md
@@ -42,7 +42,7 @@ Annex to Git LFS.
### TL; DR
If you know what you are doing and want to skip the reading, this is what you
-need to do (we assume you have [git-annex enabled][annex-gitlab-use] in your
+need to do (we assume you have [git-annex enabled](../git_annex.md#using-gitlab-git-annex) in your
repository and that you have made backups in case something goes wrong).
Fire up a terminal, navigate to your Git repository and:
@@ -82,7 +82,7 @@ Make sure the you read about the [`direct` mode][annex-direct] as it contains
useful information that may fit in your use case. Note that `annex direct` is
deprecated in Git Annex version 6, so you may need to upgrade your repository
if the server also has Git Annex 6 installed. Read more in the
-[Git Annex troubleshooting tips][annex-tips] section.
+[Git Annex troubleshooting tips](../git_annex.md#troubleshooting-tips) section.
1. Backup your repository
@@ -166,44 +166,45 @@ GitLab.com), therefore, you don't need to do anything server-side.
1. First, make sure you have `git-lfs` installed locally:
- ```bash
- git lfs help
- ```
+ ```bash
+ git lfs help
+ ```
- If the terminal doesn't prompt you with a full response on `git-lfs` commands,
- [install the Git LFS client][install-lfs] first.
+ If the terminal doesn't prompt you with a full response on `git-lfs` commands,
+ [install the Git LFS client][install-lfs] first.
1. Inside the repo, run the following command to initiate LFS:
- ```bash
- git lfs install
- ```
+ ```bash
+ git lfs install
+ ```
1. Enable `git-lfs` for the group of files you want to track. You
can track specific files, all files containing the same extension, or an
entire directory:
- ```bash
- git lfs track images/01.png # per file
- git lfs track **/*.png # per extension
- git lfs track images/ # per directory
- ```
+ ```bash
+ git lfs track images/01.png # per file
+ git lfs track **/*.png # per extension
+ git lfs track images/ # per directory
+ ```
- Once you do that, run `git status` and you'll see `.gitattributes` added
- to your repo. It collects all file patterns that you chose to track via
- `git-lfs`.
+ Once you do that, run `git status` and you'll see `.gitattributes` added
+ to your repo. It collects all file patterns that you chose to track via
+ `git-lfs`.
1. Add the files, commit and push them to GitLab:
- ```bash
- git add .
- git commit -m "commit message"
- git push
- ```
+ ```bash
+ git add .
+ git commit -m "commit message"
+ git push
+ ```
- If your remote is set up with HTTP, you will be asked to enter your login
- credentials. If you have [2FA enabled][2fa], make sure to use a
- [personal access token][token] instead of your password.
+ If your remote is set up with HTTP, you will be asked to enter your login
+ credentials. If you have [2FA enabled](../../user/profile/account/two_factor_authentication.md), make sure to use a
+ [personal access token](../../user/profile/account/two_factor_authentication.md#personal-access-tokens)
+ instead of your password.
## Removing the Git Annex branches
@@ -238,18 +239,11 @@ git annex uninit
- (Blog Post) [Getting Started with Git FLS][post-1]
- (Blog Post) [Announcing LFS Support in GitLab][post-2]
- (Blog Post) [GitLab Annex Solves the Problem of Versioning Large Binaries with Git][post-3]
-- (GitLab Docs) [Git Annex][doc-1]
-- (GitLab Docs) [Git LFS][doc-2]
+- (GitLab Docs) [Git Annex](../git_annex.md)
+- (GitLab Docs) [Git LFS](manage_large_binaries_with_git_lfs.md)
-[2fa]: ../../user/profile/account/two_factor_authentication.md
-[token]: ../../user/profile/account/two_factor_authentication.html#personal-access-tokens
-[annex-tips]: ../git_annex.html#troubleshooting-tips
[annex-direct]: https://git-annex.branchable.com/direct_mode/
-[annex-gitlab-use]: ../git_annex.md#using-gitlab-git-annex
-[annex-ee]: https://docs.gitlab.com/ee/workflow/git_annex.html
[bkp-ext-drive]: https://www.thomas-krenn.com/en/wiki/Git-annex_Repository_on_an_External_Hard_Drive
-[doc-1]: https://docs.gitlab.com/ee/workflow/git_annex.html
-[doc-2]: https://docs.gitlab.com/ee/workflow/lfs/manage_large_binaries_with_git_lfs.html
[Git Annex]: http://git-annex.branchable.com/
[Git LFS]: https://git-lfs.github.com/
[install-lfs]: https://git-lfs.github.com/
diff --git a/doc/workflow/merge_request_approvals.md b/doc/workflow/merge_request_approvals.md
index f8a99ec3d57..bfcd8faf236 100644
--- a/doc/workflow/merge_request_approvals.md
+++ b/doc/workflow/merge_request_approvals.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html'
+redirect_to: '../user/project/merge_requests/merge_request_approvals.md'
---
-This document was moved to [user/project/merge_requests/merge_request_approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html).
+This document was moved to [another location](../user/project/merge_requests/merge_request_approvals.md).
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index 0eb4f85e0ec..5d560f2e000 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -16,12 +16,16 @@ Notification settings are divided into three groups:
Each of these settings have levels of notification:
+- Global: For groups and projects, notifications as per global settings.
- Watch: Receive notifications for any activity.
-- On Mention: Receive notifications when `@mentioned` in comments.
- Participate: Receive notifications for threads you have participated in.
+- On Mention: Receive notifications when `@mentioned` in comments.
- Disabled: Turns off notifications.
- Custom: Receive notifications for custom selected events.
-- Global: For groups and projects, notifications as per global settings.
+
+> Introduced in GitLab 12.0
+
+You can also select an email address to receive notifications for each group you belong to.
### Global Settings
diff --git a/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md
index 5bc5cf427ce..9772bd385ba 100644
--- a/doc/workflow/repository_mirroring.md
+++ b/doc/workflow/repository_mirroring.md
@@ -283,10 +283,10 @@ project mirroring again by [Forcing an update](#forcing-an-update-core).
[GitLab Starter](https://about.gitlab.com/pricing/) 10.3.
Pull mirroring uses polling to detect new branches and commits added upstream, often minutes
-afterwards. If you notify GitLab by [API](https://docs.gitlab.com/ee/api/projects.html#start-the-pull-mirroring-process-for-a-project),
+afterwards. If you notify GitLab by [API](https://docs.gitlab.com/ee/api/projects.html#start-the-pull-mirroring-process-for-a-project-starter),
updates will be pulled immediately.
-For more information, see [Start the pull mirroring process for a Project](https://docs.gitlab.com/ee/api/projects.html#start-the-pull-mirroring-process-for-a-project).
+For more information, see [Start the pull mirroring process for a Project](https://docs.gitlab.com/ee/api/projects.html#start-the-pull-mirroring-process-for-a-project-starter).
## Forcing an update **[CORE]**
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 65eb9bfb87e..80913f4ca07 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -96,17 +96,27 @@ module API
end
end
optional :start_branch, type: String, desc: 'Name of the branch to start the new commit from'
+ optional :start_project, types: [Integer, String], desc: 'The ID or path of the project to start the commit from'
optional :author_email, type: String, desc: 'Author email for commit'
optional :author_name, type: String, desc: 'Author name for commit'
optional :stats, type: Boolean, default: true, desc: 'Include commit stats'
optional :force, type: Boolean, default: false, desc: 'When `true` overwrites the target branch with a new commit based on the `start_branch`'
end
post ':id/repository/commits' do
+ if params[:start_project]
+ start_project = find_project!(params[:start_project])
+
+ unless user_project.forked_from?(start_project)
+ forbidden!("Project is not included in the fork network for #{start_project.full_name}")
+ end
+ end
+
authorize_push_to_branch!(params[:branch])
attrs = declared_params
attrs[:branch_name] = attrs.delete(:branch)
attrs[:start_branch] ||= attrs[:branch_name]
+ attrs[:start_project] = start_project if start_project
result = ::Files::MultiService.new(user_project, current_user, attrs).execute
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
index 5928ee1657b..693172b7d08 100644
--- a/lib/api/discussions.rb
+++ b/lib/api/discussions.rb
@@ -206,7 +206,7 @@ module API
delete_note(noteable, params[:note_id])
end
- if Noteable::RESOLVABLE_TYPES.include?(noteable_type.to_s)
+ if Noteable.resolvable_types.include?(noteable_type.to_s)
desc "Resolve/unresolve an existing #{noteable_type.to_s.downcase} discussion" do
success Entities::Discussion
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 3d7cf2c0bb1..f8b950cb55d 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -303,6 +303,7 @@ module API
expose :commit_count
expose :storage_size
expose :repository_size
+ expose :wiki_size
expose :lfs_objects_size
expose :build_artifacts_size, as: :job_artifacts_size
end
@@ -355,6 +356,7 @@ module API
with_options format_with: -> (value) { value.to_i } do
expose :storage_size
expose :repository_size
+ expose :wiki_size
expose :lfs_objects_size
expose :build_artifacts_size, as: :job_artifacts_size
end
@@ -574,6 +576,8 @@ module API
expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |issue|
issue
end
+
+ expose :task_completion_status
end
class Issue < IssueBasic
@@ -697,7 +701,7 @@ module API
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/42344 for more
# information.
expose :merge_status do |merge_request|
- merge_request.check_if_can_be_merged
+ merge_request.check_mergeability
merge_request.merge_status
end
expose :diff_head_sha, as: :sha
@@ -722,6 +726,8 @@ module API
end
expose :squash
+
+ expose :task_completion_status
end
class MergeRequest < MergeRequestBasic
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 7e4539d0419..00bcf6b055b 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -445,7 +445,7 @@ module API
end
def present_carrierwave_file!(file, supports_direct_download: true)
- return not_found! unless file.exists?
+ return not_found! unless file&.exists?
if file.file_storage?
present_disk_file!(file.path, file.filename)
diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb
index 94b58a64d26..2a9b17ad22a 100644
--- a/lib/api/helpers/pagination.rb
+++ b/lib/api/helpers/pagination.rb
@@ -23,7 +23,7 @@ module API
def base_request_uri
@base_request_uri ||= URI.parse(request.url).tap do |uri|
uri.host = Gitlab.config.gitlab.host
- uri.port = nil
+ uri.port = Gitlab.config.gitlab.port
end
end
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index aaf32dafca4..813e46e9520 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -4,48 +4,45 @@ module API
module Helpers
module ProjectsHelpers
extend ActiveSupport::Concern
+ extend Grape::API::Helpers
- included do
- helpers do
- params :optional_project_params_ce do
- optional :description, type: String, desc: 'The description of the project'
- optional :ci_config_path, type: String, desc: 'The path to CI config file. Defaults to `.gitlab-ci.yml`'
- optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled'
- optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled'
- optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled'
- optional :jobs_enabled, type: Boolean, desc: 'Flag indication if jobs are enabled'
- optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled'
- optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
- optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push'
- optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
- optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project'
- optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the project.'
- optional :public_builds, type: Boolean, desc: 'Perform public builds'
- optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
- optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
- optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved'
- optional :tag_list, type: Array[String], desc: 'The list of tags for a project'
- optional :avatar, type: File, desc: 'Avatar image for project'
- optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line'
- optional :merge_method, type: String, values: %w(ff rebase_merge merge), desc: 'The merge method used when merging merge requests'
- optional :initialize_with_readme, type: Boolean, desc: "Initialize a project with a README.md"
- optional :external_authorization_classification_label, type: String, desc: 'The classification label for the project'
- end
+ params :optional_project_params_ce do
+ optional :description, type: String, desc: 'The description of the project'
+ optional :ci_config_path, type: String, desc: 'The path to CI config file. Defaults to `.gitlab-ci.yml`'
+ optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled'
+ optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled'
+ optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled'
+ optional :jobs_enabled, type: Boolean, desc: 'Flag indication if jobs are enabled'
+ optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled'
+ optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
+ optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push'
+ optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
+ optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project'
+ optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the project.'
+ optional :public_builds, type: Boolean, desc: 'Perform public builds'
+ optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
+ optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
+ optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved'
+ optional :tag_list, type: Array[String], desc: 'The list of tags for a project'
+ optional :avatar, type: File, desc: 'Avatar image for project'
+ optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line'
+ optional :merge_method, type: String, values: %w(ff rebase_merge merge), desc: 'The merge method used when merging merge requests'
+ optional :initialize_with_readme, type: Boolean, desc: "Initialize a project with a README.md"
+ optional :external_authorization_classification_label, type: String, desc: 'The classification label for the project'
+ end
+
+ params :optional_project_params_ee do
+ end
- if Gitlab.ee?
- params :optional_project_params_ee do
- optional :repository_storage, type: String, desc: 'Which storage shard the repository is on. Available only to admins'
- optional :approvals_before_merge, type: Integer, desc: 'How many approvers should approve merge request by default'
- optional :mirror, type: Boolean, desc: 'Enables pull mirroring in a project'
- optional :mirror_trigger_builds, type: Boolean, desc: 'Pull mirroring triggers builds'
- end
- end
+ params :optional_project_params do
+ use :optional_project_params_ce
+ use :optional_project_params_ee
+ end
+
+ params :optional_filter_params_ee do
+ end
- params :optional_project_params do
- use :optional_project_params_ce
- use :optional_project_params_ee if Gitlab.ee?
- end
- end
+ params :optional_update_params_ee do
end
def self.update_params_at_least_one_of
diff --git a/lib/api/helpers/settings_helpers.rb b/lib/api/helpers/settings_helpers.rb
new file mode 100644
index 00000000000..6441bb579ff
--- /dev/null
+++ b/lib/api/helpers/settings_helpers.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module SettingsHelpers
+ extend ActiveSupport::Concern
+ extend Grape::API::Helpers
+
+ params :optional_params_ee do
+ end
+
+ def self.optional_attributes
+ [*::ApplicationSettingsHelper.visible_attributes,
+ *::ApplicationSettingsHelper.external_authorization_service_attributes,
+ :performance_bar_allowed_group_id].freeze
+ end
+ end
+ end
+end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index ce85772e4ed..5bbf6df78b0 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -386,9 +386,8 @@ module API
)
if merge_when_pipeline_succeeds && merge_request.head_pipeline && merge_request.head_pipeline.active?
- ::MergeRequests::MergeWhenPipelineSucceedsService
- .new(merge_request.target_project, current_user, merge_params)
- .execute(merge_request)
+ AutoMergeService.new(merge_request.target_project, current_user, merge_params)
+ .execute(merge_request, AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
else
::MergeRequests::MergeService
.new(merge_request.target_project, current_user, merge_params)
@@ -398,28 +397,16 @@ module API
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
- desc 'Merge a merge request to its default temporary merge ref path'
- params do
- optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
- end
- put ':id/merge_requests/:merge_request_iid/merge_to_ref' do
+ desc 'Returns the up to date merge-ref HEAD commit'
+ get ':id/merge_requests/:merge_request_iid/merge_ref' do
merge_request = find_project_merge_request(params[:merge_request_iid])
- authorize! :admin_merge_request, user_project
-
- merge_params = {
- commit_message: params[:merge_commit_message]
- }
-
- result = ::MergeRequests::MergeToRefService
- .new(merge_request.target_project, current_user, merge_params)
- .execute(merge_request)
+ result = ::MergeRequests::MergeabilityCheckService.new(merge_request).execute
- if result[:status] == :success
- present result.slice(:commit_id), 200
+ if result.success?
+ present :commit_id, result.payload.dig(:merge_ref_head, :commit_id)
else
- http_status = result[:http_status] || 400
- render_api_error!(result[:message], http_status)
+ render_api_error!(result.message, 400)
end
end
@@ -429,11 +416,9 @@ module API
post ':id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds' do
merge_request = find_project_merge_request(params[:merge_request_iid])
- unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
+ unauthorized! unless merge_request.can_cancel_auto_merge?(current_user)
- ::MergeRequests::MergeWhenPipelineSucceedsService
- .new(merge_request.target_project, current_user)
- .cancel(merge_request)
+ AutoMergeService.new(merge_request.target_project, current_user).cancel(merge_request)
end
desc 'Rebase the merge request against its target branch' do
diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb
index c64ec2fcc95..71891e43dcc 100644
--- a/lib/api/project_import.rb
+++ b/lib/api/project_import.rb
@@ -3,7 +3,8 @@
module API
class ProjectImport < Grape::API
include PaginationParams
- include Helpers::ProjectsHelpers
+
+ helpers Helpers::ProjectsHelpers
helpers do
def import_params
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index cb0106592f5..1e14c77b5be 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -6,27 +6,12 @@ module API
class Projects < Grape::API
include PaginationParams
include Helpers::CustomAttributes
- include Helpers::ProjectsHelpers
+
+ helpers Helpers::ProjectsHelpers
before { authenticate_non_get! }
helpers do
- if Gitlab.ee?
- params :optional_filter_params_ee do
- optional :wiki_checksum_failed, type: Grape::API::Boolean, default: false, desc: 'Limit by projects where wiki checksum is failed'
- optional :repository_checksum_failed, type: Grape::API::Boolean, default: false, desc: 'Limit by projects where repository checksum is failed'
- end
-
- params :optional_update_params_ee do
- optional :mirror_user_id, type: Integer, desc: 'User responsible for all the activity surrounding a pull mirror event'
- optional :only_mirror_protected_branches, type: Grape::API::Boolean, desc: 'Only mirror protected branches'
- optional :mirror_overwrites_diverged_branches, type: Grape::API::Boolean, desc: 'Pull mirror overwrites diverged branches'
- optional :import_url, type: String, desc: 'URL from which the project is imported'
- optional :packages_enabled, type: Grape::API::Boolean, desc: 'Enable project packages feature'
- optional :fallback_approvals_required, type: Integer, desc: 'Overall approvals required when no rule is present'
- end
- end
-
# EE::API::Projects would override this method
def apply_filters(projects)
projects = projects.with_issues_available_for_user(current_user) if params[:with_issues_enabled]
@@ -77,7 +62,7 @@ module API
optional :with_programming_language, type: String, desc: 'Limit to repositories which use the given programming language'
optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Limit by minimum access level of authenticated user'
- use :optional_filter_params_ee if Gitlab.ee?
+ use :optional_filter_params_ee
end
params :create_params do
@@ -296,7 +281,7 @@ module API
optional :path, type: String, desc: 'The path of the repository'
use :optional_project_params
- use :optional_update_params_ee if Gitlab.ee?
+ use :optional_update_params_ee
at_least_one_of(*Helpers::ProjectsHelpers.update_params_at_least_one_of)
end
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 8046acfa397..6767ef882cb 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -4,6 +4,8 @@ module API
class Settings < Grape::API
before { authenticated_as_admin! }
+ helpers Helpers::SettingsHelpers
+
helpers do
def current_settings
@current_setting ||=
@@ -136,54 +138,10 @@ module API
desc: "Restrictions on the complexity of uploaded #{type.upcase} keys. A value of #{ApplicationSetting::FORBIDDEN_KEY_VALUE} disables all #{type.upcase} keys."
end
- if Gitlab.ee?
- optional :elasticsearch_aws, type: Boolean, desc: 'Enable support for AWS hosted elasticsearch'
-
- given elasticsearch_aws: ->(val) { val } do
- optional :elasticsearch_aws_access_key, type: String, desc: 'AWS IAM access key'
- requires :elasticsearch_aws_region, type: String, desc: 'The AWS region the elasticsearch domain is configured'
- optional :elasticsearch_aws_secret_access_key, type: String, desc: 'AWS IAM secret access key'
- end
-
- optional :elasticsearch_indexing, type: Boolean, desc: 'Enable Elasticsearch indexing'
-
- given elasticsearch_indexing: ->(val) { val } do
- optional :elasticsearch_search, type: Boolean, desc: 'Enable Elasticsearch search'
- requires :elasticsearch_url, type: String, desc: 'The url to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., "http://localhost:9200, http://localhost:9201")'
- optional :elasticsearch_limit_indexing, type: Boolean, desc: 'Limit Elasticsearch to index certain namespaces and projects'
- end
-
- given elasticsearch_limit_indexing: ->(val) { val } do
- optional :elasticsearch_namespace_ids, type: Array[Integer], coerce_with: Validations::Types::LabelsList.coerce, desc: 'The namespace ids to index with Elasticsearch.'
- optional :elasticsearch_project_ids, type: Array[Integer], coerce_with: Validations::Types::LabelsList.coerce, desc: 'The project ids to index with Elasticsearch.'
- end
-
- optional :email_additional_text, type: String, desc: 'Additional text added to the bottom of every email for legal/auditing/compliance reasons'
- optional :help_text, type: String, desc: 'GitLab server administrator information'
- optional :repository_size_limit, type: Integer, desc: 'Size limit per repository (MB)'
- optional :file_template_project_id, type: Integer, desc: 'ID of project where instance-level file templates are stored.'
- optional :repository_storages, type: Array[String], desc: 'A list of names of enabled storage paths, taken from `gitlab.yml`. New projects will be created in one of these stores, chosen at random.'
- optional :snowplow_enabled, type: Boolean, desc: 'Enable Snowplow'
-
- given snowplow_enabled: ->(val) { val } do
- requires :snowplow_collector_uri, type: String, desc: 'Snowplow Collector URI'
- optional :snowplow_cookie_domain, type: String, desc: 'Snowplow cookie domain'
- optional :snowplow_site_id, type: String, desc: 'Snowplow Site/Application ID'
- end
-
- optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.'
- end
-
- optional_attributes = [*::ApplicationSettingsHelper.visible_attributes,
- *::ApplicationSettingsHelper.external_authorization_service_attributes,
- :performance_bar_allowed_group_id]
-
- if Gitlab.ee?
- optional_attributes += EE::ApplicationSettingsHelper.possible_licensed_attributes
- end
+ use :optional_params_ee
- optional(*optional_attributes)
- at_least_one_of(*optional_attributes)
+ optional(*Helpers::SettingsHelpers.optional_attributes)
+ at_least_one_of(*Helpers::SettingsHelpers.optional_attributes)
end
put "application/settings" do
attrs = declared_params(include_missing: false)
diff --git a/lib/banzai/commit_renderer.rb b/lib/banzai/commit_renderer.rb
index f346151a3c1..2acc9d13f07 100644
--- a/lib/banzai/commit_renderer.rb
+++ b/lib/banzai/commit_renderer.rb
@@ -2,7 +2,7 @@
module Banzai
module CommitRenderer
- ATTRIBUTES = [:description, :title].freeze
+ ATTRIBUTES = [:description, :title, :full_title].freeze
def self.render(commits, project, user = nil)
obj_renderer = ObjectRenderer.new(user: user, default_project: project)
diff --git a/lib/banzai/filter/wiki_link_filter/rewriter.rb b/lib/banzai/filter/wiki_link_filter/rewriter.rb
index f4cc8beeb52..77b5053f38c 100644
--- a/lib/banzai/filter/wiki_link_filter/rewriter.rb
+++ b/lib/banzai/filter/wiki_link_filter/rewriter.rb
@@ -4,6 +4,8 @@ module Banzai
module Filter
class WikiLinkFilter < HTML::Pipeline::Filter
class Rewriter
+ UNSAFE_SLUG_REGEXES = [/\Ajavascript:/i].freeze
+
def initialize(link_string, wiki:, slug:)
@uri = Addressable::URI.parse(link_string)
@wiki_base_path = wiki && wiki.wiki_base_path
@@ -35,6 +37,8 @@ module Banzai
# Of the form `./link`, `../link`, or similar
def apply_hierarchical_link_rules!
+ return if slug_considered_unsafe?
+
@uri = Addressable::URI.join(@slug, @uri) if @uri.to_s[0] == '.'
end
@@ -54,6 +58,10 @@ module Banzai
def repository_upload?
@uri.relative? && @uri.path.starts_with?(Wikis::CreateAttachmentService::ATTACHMENT_PATH)
end
+
+ def slug_considered_unsafe?
+ UNSAFE_SLUG_REGEXES.any? { |r| r.match?(@slug) }
+ end
end
end
end
diff --git a/lib/banzai/redactor.rb b/lib/banzai/redactor.rb
index 7db5f5e1f7d..c2da7fec7cc 100644
--- a/lib/banzai/redactor.rb
+++ b/lib/banzai/redactor.rb
@@ -70,8 +70,11 @@ module Banzai
# Build the raw <a> tag just with a link as href and content if
# it's originally a link pattern. We shouldn't return a plain text href.
original_link =
- if link_reference == 'true' && href = original_content
- %(<a href="#{href}">#{href}</a>)
+ if link_reference == 'true'
+ href = node.attr('href')
+ content = original_content
+
+ %(<a href="#{href}">#{content}</a>)
end
# The reference should be replaced by the original link's content,
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index 3f107fbbf3b..ccaf06c5d6a 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -40,6 +40,7 @@ module Gitlab
SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}.freeze
VERSION = File.read(root.join("VERSION")).strip.freeze
INSTALLATION_TYPE = File.read(root.join("INSTALLATION_TYPE")).strip.freeze
+ HTTP_PROXY_ENV_VARS = %w(http_proxy https_proxy HTTP_PROXY HTTPS_PROXY).freeze
def self.com?
# Check `gl_subdomain?` as well to keep parity with gitlab.com
@@ -66,6 +67,10 @@ module Gitlab
end
end
+ def self.http_proxy_env?
+ HTTP_PROXY_ENV_VARS.any? { |name| ENV[name] }
+ end
+
def self.process_name
return 'sidekiq' if Sidekiq.server?
return 'console' if defined?(Rails::Console)
diff --git a/lib/gitlab/background_migration/calculate_wiki_sizes.rb b/lib/gitlab/background_migration/calculate_wiki_sizes.rb
new file mode 100644
index 00000000000..886c41a2b9d
--- /dev/null
+++ b/lib/gitlab/background_migration/calculate_wiki_sizes.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class CalculateWikiSizes
+ def perform(start_id, stop_id)
+ ::ProjectStatistics.where(wiki_size: nil)
+ .where(id: start_id..stop_id)
+ .includes(project: [:route, :group, namespace: [:owner]]).find_each do |statistics|
+ statistics.refresh!(only: [:wiki_size])
+ rescue => e
+ Rails.logger.error "Failed to update wiki statistics. id: #{statistics.id} message: #{e.message}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/fill_valid_time_for_pages_domain_certificate.rb b/lib/gitlab/background_migration/fill_valid_time_for_pages_domain_certificate.rb
new file mode 100644
index 00000000000..0e93b2cb2fa
--- /dev/null
+++ b/lib/gitlab/background_migration/fill_valid_time_for_pages_domain_certificate.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # save validity time pages domain
+ class FillValidTimeForPagesDomainCertificate
+ # define PagesDomain with only needed code
+ class PagesDomain < ActiveRecord::Base
+ self.table_name = 'pages_domains'
+
+ def x509
+ return unless certificate.present?
+
+ @x509 ||= OpenSSL::X509::Certificate.new(certificate)
+ rescue OpenSSL::X509::CertificateError
+ nil
+ end
+ end
+
+ def perform(start_id, stop_id)
+ PagesDomain.where(id: start_id..stop_id).find_each do |domain|
+ if Gitlab::Database.mysql?
+ domain.update_columns(
+ certificate_valid_not_before: domain.x509&.not_before,
+ certificate_valid_not_after: domain.x509&.not_after
+ )
+ else
+ # for some reason activerecord doesn't append timezone, iso8601 forces this
+ domain.update_columns(
+ certificate_valid_not_before: domain.x509&.not_before&.iso8601,
+ certificate_valid_not_after: domain.x509&.not_after&.iso8601
+ )
+ end
+ rescue => e
+ Rails.logger.error "Failed to update pages domain certificate valid time. id: #{domain.id}, message: #{e.message}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/reset_merge_status.rb b/lib/gitlab/background_migration/reset_merge_status.rb
new file mode 100644
index 00000000000..447fec8903c
--- /dev/null
+++ b/lib/gitlab/background_migration/reset_merge_status.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Updates the range of given MRs to merge_status "unchecked", if they're opened
+ # and mergeable.
+ class ResetMergeStatus
+ def perform(from_id, to_id)
+ relation = MergeRequest.where(id: from_id..to_id,
+ state: 'opened',
+ merge_status: 'can_be_merged')
+
+ relation.update_all(merge_status: 'unchecked')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb
index 0beeb44c272..21c42857895 100644
--- a/lib/gitlab/ci/config/entry/image.rb
+++ b/lib/gitlab/ci/config/entry/image.rb
@@ -24,7 +24,7 @@ module Gitlab
end
entry :ports, Entry::Ports,
- description: 'Ports used expose the image'
+ description: 'Ports used to expose the image'
attributes :ports
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 290c9591b98..762532f7007 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -34,7 +34,7 @@ module Gitlab
message: 'should be on_success, on_failure, ' \
'always, manual or delayed' }
validates :dependencies, array_of_strings: true
- validates :extends, type: String
+ validates :extends, array_of_strings_or_string: true
end
validates :start_in, duration: { limit: '1 day' }, if: :delayed?
diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb
index 084fa4047a4..8d16371e857 100644
--- a/lib/gitlab/ci/config/entry/service.rb
+++ b/lib/gitlab/ci/config/entry/service.rb
@@ -24,6 +24,9 @@ module Gitlab
validates :alias, type: String, presence: true, unless: ->(record) { record.ports.blank? }
end
+ entry :ports, Entry::Ports,
+ description: 'Ports used to expose the service'
+
def alias
value[:alias]
end
diff --git a/lib/gitlab/ci/config/extendable/entry.rb b/lib/gitlab/ci/config/extendable/entry.rb
index 7793db09d33..0001a259281 100644
--- a/lib/gitlab/ci/config/extendable/entry.rb
+++ b/lib/gitlab/ci/config/extendable/entry.rb
@@ -5,6 +5,8 @@ module Gitlab
class Config
class Extendable
class Entry
+ include Gitlab::Utils::StrongMemoize
+
InvalidExtensionError = Class.new(Extendable::ExtensionError)
CircularDependencyError = Class.new(Extendable::ExtensionError)
NestingTooDeepError = Class.new(Extendable::ExtensionError)
@@ -28,34 +30,46 @@ module Gitlab
end
def value
- @value ||= @context.fetch(@key)
+ strong_memoize(:value) do
+ @context.fetch(@key)
+ end
end
- def base_hash!
- @base ||= Extendable::Entry
- .new(extends_key, @context, self)
- .extend!
+ def base_hashes!
+ strong_memoize(:base_hashes) do
+ extends_keys.map do |key|
+ Extendable::Entry
+ .new(key, @context, self)
+ .extend!
+ end
+ end
end
- def extends_key
- value.fetch(:extends).to_s.to_sym if extensible?
+ def extends_keys
+ strong_memoize(:extends_keys) do
+ next unless extensible?
+
+ Array(value.fetch(:extends)).map(&:to_s).map(&:to_sym)
+ end
end
def ancestors
- @ancestors ||= Array(@parent&.ancestors) + Array(@parent&.key)
+ strong_memoize(:ancestors) do
+ Array(@parent&.ancestors) + Array(@parent&.key)
+ end
end
def extend!
return value unless extensible?
- if unknown_extension?
+ if unknown_extensions.any?
raise Entry::InvalidExtensionError,
- "#{key}: unknown key in `extends`"
+ "#{key}: unknown keys in `extends` (#{show_keys(unknown_extensions)})"
end
- if invalid_base?
+ if invalid_bases.any?
raise Entry::InvalidExtensionError,
- "#{key}: invalid base hash in `extends`"
+ "#{key}: invalid base hashes in `extends` (#{show_keys(invalid_bases)})"
end
if nesting_too_deep?
@@ -68,11 +82,18 @@ module Gitlab
"#{key}: circular dependency detected in `extends`"
end
- @context[key] = base_hash!.deep_merge(value)
+ merged = {}
+ base_hashes!.each { |h| merged.deep_merge!(h) }
+
+ @context[key] = merged.deep_merge!(value)
end
private
+ def show_keys(keys)
+ keys.join(', ')
+ end
+
def nesting_too_deep?
ancestors.count > MAX_NESTING_LEVELS
end
@@ -81,12 +102,16 @@ module Gitlab
ancestors.include?(key)
end
- def unknown_extension?
- !@context.key?(extends_key)
+ def unknown_extensions
+ strong_memoize(:unknown_extensions) do
+ extends_keys.reject { |key| @context.key?(key) }
+ end
end
- def invalid_base?
- !@context[extends_key].is_a?(Hash)
+ def invalid_bases
+ strong_memoize(:invalid_bases) do
+ extends_keys.reject { |key| @context[key].is_a?(Hash) }
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/and.rb b/lib/gitlab/ci/pipeline/expression/lexeme/and.rb
new file mode 100644
index 00000000000..54a0e2ad9dd
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/and.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class And < Lexeme::Operator
+ PATTERN = /&&/.freeze
+
+ def evaluate(variables = {})
+ @left.evaluate(variables) && @right.evaluate(variables)
+ end
+
+ def self.build(_value, behind, ahead)
+ new(behind, ahead)
+ end
+
+ def self.precedence
+ 11 # See: https://ruby-doc.org/core-2.5.0/doc/syntax/precedence_rdoc.html
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/base.rb b/lib/gitlab/ci/pipeline/expression/lexeme/base.rb
index 70c774416f6..7ebd2e25398 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/base.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/base.rb
@@ -15,10 +15,14 @@ module Gitlab
end
def self.scan(scanner)
- if scanner.scan(self::PATTERN)
+ if scanner.scan(pattern)
Expression::Token.new(scanner.matched, self)
end
end
+
+ def self.pattern
+ self::PATTERN
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb b/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb
index 668e85f5b9e..62f4c14f597 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb
@@ -8,11 +8,6 @@ module Gitlab
class Equals < Lexeme::Operator
PATTERN = /==/.freeze
- def initialize(left, right)
- @left = left
- @right = right
- end
-
def evaluate(variables = {})
@left.evaluate(variables) == @right.evaluate(variables)
end
@@ -20,6 +15,10 @@ module Gitlab
def self.build(_value, behind, ahead)
new(behind, ahead)
end
+
+ def self.precedence
+ 10 # See: https://ruby-doc.org/core-2.5.0/doc/syntax/precedence_rdoc.html
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
index cd17bc4d78b..ecfab627226 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
@@ -8,21 +8,36 @@ module Gitlab
class Matches < Lexeme::Operator
PATTERN = /=~/.freeze
- def initialize(left, right)
- @left = left
- @right = right
- end
-
def evaluate(variables = {})
text = @left.evaluate(variables)
regexp = @right.evaluate(variables)
regexp.scan(text.to_s).any?
+
+ if ci_variables_complex_expressions?
+ # return offset of first match, or nil if no matches
+ if match = regexp.scan(text.to_s).first
+ text.to_s.index(match)
+ end
+ else
+ # return true or false
+ regexp.scan(text.to_s).any?
+ end
end
def self.build(_value, behind, ahead)
new(behind, ahead)
end
+
+ def self.precedence
+ 10 # See: https://ruby-doc.org/core-2.5.0/doc/syntax/precedence_rdoc.html
+ end
+
+ private
+
+ def ci_variables_complex_expressions?
+ Feature.enabled?(:ci_variables_complex_expressions, default_enabled: true)
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/not_equals.rb b/lib/gitlab/ci/pipeline/expression/lexeme/not_equals.rb
index 5fcc9406cc8..8166bcd5730 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/not_equals.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/not_equals.rb
@@ -8,11 +8,6 @@ module Gitlab
class NotEquals < Lexeme::Operator
PATTERN = /!=/.freeze
- def initialize(left, right)
- @left = left
- @right = right
- end
-
def evaluate(variables = {})
@left.evaluate(variables) != @right.evaluate(variables)
end
@@ -20,6 +15,10 @@ module Gitlab
def self.build(_value, behind, ahead)
new(behind, ahead)
end
+
+ def self.precedence
+ 10 # See: https://ruby-doc.org/core-2.5.0/doc/syntax/precedence_rdoc.html
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
index 14544d33e25..831c27fa0ea 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
@@ -8,11 +8,6 @@ module Gitlab
class NotMatches < Lexeme::Operator
PATTERN = /\!~/.freeze
- def initialize(left, right)
- @left = left
- @right = right
- end
-
def evaluate(variables = {})
text = @left.evaluate(variables)
regexp = @right.evaluate(variables)
@@ -23,6 +18,10 @@ module Gitlab
def self.build(_value, behind, ahead)
new(behind, ahead)
end
+
+ def self.precedence
+ 10 # See: https://ruby-doc.org/core-2.5.0/doc/syntax/precedence_rdoc.html
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb b/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb
index 3ebceb92eb7..3ddab7800c8 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb
@@ -6,9 +6,29 @@ module Gitlab
module Expression
module Lexeme
class Operator < Lexeme::Base
+ # This operator class is design to handle single operators that take two
+ # arguments. Expression::Parser was originally designed to read infix operators,
+ # and so the two operands are called "left" and "right" here. If we wish to
+ # implement an Operator that takes a greater or lesser number of arguments, a
+ # structural change or additional Operator superclass will likely be needed.
+
+ OperatorError = Class.new(Expression::ExpressionError)
+
+ def initialize(left, right)
+ raise OperatorError, 'Invalid left operand' unless left.respond_to? :evaluate
+ raise OperatorError, 'Invalid right operand' unless right.respond_to? :evaluate
+
+ @left = left
+ @right = right
+ end
+
def self.type
:operator
end
+
+ def self.precedence
+ raise NotImplementedError
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/or.rb b/lib/gitlab/ci/pipeline/expression/lexeme/or.rb
new file mode 100644
index 00000000000..807876f905a
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/or.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class Or < Lexeme::Operator
+ PATTERN = /\|\|/.freeze
+
+ def evaluate(variables = {})
+ @left.evaluate(variables) || @right.evaluate(variables)
+ end
+
+ def self.build(_value, behind, ahead)
+ new(behind, ahead)
+ end
+
+ def self.precedence
+ 12 # See: https://ruby-doc.org/core-2.5.0/doc/syntax/precedence_rdoc.html
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
index 2b719c9c6fc..e4cf360a1c1 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
@@ -8,10 +8,11 @@ module Gitlab
require_dependency 're2'
class Pattern < Lexeme::Value
- PATTERN = %r{^/.+/[ismU]*$}.freeze
+ PATTERN = %r{^/.+/[ismU]*$}.freeze
+ NEW_PATTERN = %r{^\/([^\/]|\\/)+[^\\]\/[ismU]*}.freeze
def initialize(regexp)
- @value = regexp
+ @value = self.class.eager_matching_with_escape_characters? ? regexp.gsub(/\\\//, '/') : regexp
unless Gitlab::UntrustedRegexp::RubySyntax.valid?(@value)
raise Lexer::SyntaxError, 'Invalid regular expression!'
@@ -24,9 +25,17 @@ module Gitlab
raise Expression::RuntimeError, 'Invalid regular expression!'
end
+ def self.pattern
+ eager_matching_with_escape_characters? ? NEW_PATTERN : PATTERN
+ end
+
def self.build(string)
new(string)
end
+
+ def self.eager_matching_with_escape_characters?
+ Feature.enabled?(:ci_variables_complex_expressions, default_enabled: true)
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/expression/lexer.rb b/lib/gitlab/ci/pipeline/expression/lexer.rb
index e14edfae51d..22c210ae26b 100644
--- a/lib/gitlab/ci/pipeline/expression/lexer.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexer.rb
@@ -20,6 +20,19 @@ module Gitlab
Expression::Lexeme::NotMatches
].freeze
+ NEW_LEXEMES = [
+ Expression::Lexeme::Variable,
+ Expression::Lexeme::String,
+ Expression::Lexeme::Pattern,
+ Expression::Lexeme::Null,
+ Expression::Lexeme::Equals,
+ Expression::Lexeme::Matches,
+ Expression::Lexeme::NotEquals,
+ Expression::Lexeme::NotMatches,
+ Expression::Lexeme::And,
+ Expression::Lexeme::Or
+ ].freeze
+
MAX_TOKENS = 100
def initialize(statement, max_tokens: MAX_TOKENS)
@@ -45,7 +58,7 @@ module Gitlab
return tokens if @scanner.eos?
- lexeme = LEXEMES.find do |type|
+ lexeme = available_lexemes.find do |type|
type.scan(@scanner).tap do |token|
tokens.push(token) if token.present?
end
@@ -58,6 +71,10 @@ module Gitlab
raise Lexer::SyntaxError, 'Too many tokens!'
end
+
+ def available_lexemes
+ Feature.enabled?(:ci_variables_complex_expressions, default_enabled: true) ? NEW_LEXEMES : LEXEMES
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/expression/parser.rb b/lib/gitlab/ci/pipeline/expression/parser.rb
index ed184309ab4..589bf32a4d7 100644
--- a/lib/gitlab/ci/pipeline/expression/parser.rb
+++ b/lib/gitlab/ci/pipeline/expression/parser.rb
@@ -5,17 +5,30 @@ module Gitlab
module Pipeline
module Expression
class Parser
+ ParseError = Class.new(Expression::ExpressionError)
+
def initialize(tokens)
@tokens = tokens.to_enum
@nodes = []
end
- ##
- # This produces a reverse descent parse tree.
- #
- # It currently does not support precedence of operators.
- #
def tree
+ if Feature.enabled?(:ci_variables_complex_expressions, default_enabled: true)
+ rpn_parse_tree
+ else
+ reverse_descent_parse_tree
+ end
+ end
+
+ def self.seed(statement)
+ new(Expression::Lexer.new(statement).tokens)
+ end
+
+ private
+
+ # This produces a reverse descent parse tree.
+ # It does not support precedence of operators.
+ def reverse_descent_parse_tree
while token = @tokens.next
case token.type
when :operator
@@ -32,8 +45,51 @@ module Gitlab
@nodes.last || Lexeme::Null.new
end
- def self.seed(statement)
- new(Expression::Lexer.new(statement).tokens)
+ def rpn_parse_tree
+ results = []
+
+ tokens_rpn.each do |token|
+ case token.type
+ when :value
+ results.push(token.build)
+ when :operator
+ right_operand = results.pop
+ left_operand = results.pop
+
+ token.build(left_operand, right_operand).tap do |res|
+ results.push(res)
+ end
+ else
+ raise ParseError, 'Unprocessable token found in parse tree'
+ end
+ end
+
+ raise ParseError, 'Unreachable nodes in parse tree' if results.count > 1
+ raise ParseError, 'Empty parse tree' if results.count < 1
+
+ results.pop
+ end
+
+ # Parse the expression into Reverse Polish Notation
+ # (See: Shunting-yard algorithm)
+ def tokens_rpn
+ output = []
+ operators = []
+
+ @tokens.each do |token|
+ case token.type
+ when :value
+ output.push(token)
+ when :operator
+ if operators.any? && token.lexeme.precedence >= operators.last.lexeme.precedence
+ output.push(operators.pop)
+ end
+
+ operators.push(token)
+ end
+ end
+
+ output.concat(operators.reverse)
end
end
end
diff --git a/lib/gitlab/ci/pipeline/expression/statement.rb b/lib/gitlab/ci/pipeline/expression/statement.rb
index ab5ae9caeea..0e81e1bd34c 100644
--- a/lib/gitlab/ci/pipeline/expression/statement.rb
+++ b/lib/gitlab/ci/pipeline/expression/statement.rb
@@ -7,27 +7,6 @@ module Gitlab
class Statement
StatementError = Class.new(Expression::ExpressionError)
- GRAMMAR = [
- # presence matchers
- %w[variable],
-
- # positive matchers
- %w[variable equals string],
- %w[variable equals variable],
- %w[variable equals null],
- %w[string equals variable],
- %w[null equals variable],
- %w[variable matches pattern],
-
- # negative matchers
- %w[variable notequals string],
- %w[variable notequals variable],
- %w[variable notequals null],
- %w[string notequals variable],
- %w[null notequals variable],
- %w[variable notmatches pattern]
- ].freeze
-
def initialize(statement, variables = {})
@lexer = Expression::Lexer.new(statement)
@variables = variables.with_indifferent_access
@@ -36,10 +15,6 @@ module Gitlab
def parse_tree
raise StatementError if @lexer.lexemes.empty?
- unless GRAMMAR.find { |syntax| syntax == @lexer.lexemes }
- raise StatementError, 'Unknown pipeline expression!'
- end
-
Expression::Parser.new(@lexer.tokens).tree
end
@@ -54,6 +29,7 @@ module Gitlab
end
def valid?
+ evaluate
parse_tree.is_a?(Lexeme::Base)
rescue Expression::ExpressionError
false
diff --git a/lib/gitlab/ci/templates/.yamllint b/lib/gitlab/ci/templates/.yamllint
new file mode 100644
index 00000000000..669c8646ff2
--- /dev/null
+++ b/lib/gitlab/ci/templates/.yamllint
@@ -0,0 +1,5 @@
+extends: default
+
+rules:
+ line-length: disable
+ document-start: disable
diff --git a/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml b/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml
index 120272200c6..2ca6e73a803 100644
--- a/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml
@@ -54,7 +54,7 @@ ensureContainer:
- rm -f android-signing-keystore.jks || true
artifacts:
paths:
- - app/build/outputs
+ - app/build/outputs
buildDebug:
extends: .build_job
@@ -116,6 +116,6 @@ promoteProduction:
# We only allow production promotion on `master` because
# it has its own production scoped secret variables
only:
- - master
+ - master
script:
- bundle exec fastlane promote_beta_to_production
diff --git a/lib/gitlab/ci/templates/Android.gitlab-ci.yml b/lib/gitlab/ci/templates/Android.gitlab-ci.yml
index 2be7f933462..b7194110002 100644
--- a/lib/gitlab/ci/templates/Android.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Android.gitlab-ci.yml
@@ -6,7 +6,7 @@ image: openjdk:8-jdk
variables:
ANDROID_COMPILE_SDK: "28"
ANDROID_BUILD_TOOLS: "28.0.2"
- ANDROID_SDK_TOOLS: "4333796"
+ ANDROID_SDK_TOOLS: "4333796"
before_script:
- apt-get --quiet update --yes
@@ -35,7 +35,7 @@ assembleDebug:
- ./gradlew assembleDebug
artifacts:
paths:
- - app/build/outputs/
+ - app/build/outputs/
debugTests:
stage: test
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index 87cd62ef2d4..65a6630365d 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -51,7 +51,7 @@ variables:
POSTGRES_VERSION: 9.6.2
KUBERNETES_VERSION: 1.11.10
- HELM_VERSION: 2.13.1
+ HELM_VERSION: 2.14.0
DOCKER_DRIVER: overlay2
@@ -60,7 +60,7 @@ variables:
stages:
- build
- test
- - deploy # dummy stage to follow the template guidelines
+ - deploy # dummy stage to follow the template guidelines
- review
- dast
- staging
@@ -89,4 +89,4 @@ include:
dast:
except:
refs:
- - master \ No newline at end of file
+ - master
diff --git a/lib/gitlab/ci/templates/C++.gitlab-ci.yml b/lib/gitlab/ci/templates/C++.gitlab-ci.yml
index 9a8fa9d7091..33a2a534508 100644
--- a/lib/gitlab/ci/templates/C++.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/C++.gitlab-ci.yml
@@ -14,10 +14,10 @@ build:
artifacts:
paths:
- mybinary
- # depending on your build setup it's most likely a good idea to cache outputs to reduce the build time
- # cache:
- # paths:
- # - "*.o"
+ # depending on your build setup it's most likely a good idea to cache outputs to reduce the build time
+ # cache:
+ # paths:
+ # - "*.o"
# run tests using the binary built before
test:
diff --git a/lib/gitlab/ci/templates/Chef.gitlab-ci.yml b/lib/gitlab/ci/templates/Chef.gitlab-ci.yml
index 1e14aa8bea9..5f17c93b853 100644
--- a/lib/gitlab/ci/templates/Chef.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Chef.gitlab-ci.yml
@@ -23,31 +23,31 @@ stages:
foodcritic:
stage: lint
script:
- - chef exec foodcritic .
+ - chef exec foodcritic .
cookstyle:
stage: lint
script:
- - chef exec cookstyle .
+ - chef exec cookstyle .
chefspec:
stage: test
script:
- - chef exec rspec spec
+ - chef exec rspec spec
# Set up your test matrix here. Example:
-#verify-centos-6:
-# stage: functional
-# before_script:
-# - apt-get update
-# - apt-get -y install rsync
-# script:
-# - kitchen verify default-centos-6 --destroy=always
+# verify-centos-6:
+# stage: functional
+# before_script:
+# - apt-get update
+# - apt-get -y install rsync
+# script:
+# - kitchen verify default-centos-6 --destroy=always
#
-#verify-centos-7:
-# stage: functional
-# before_script:
-# - apt-get update
-# - apt-get -y install rsync
-# script:
-# - kitchen verify default-centos-7 --destroy=always
+# verify-centos-7:
+# stage: functional
+# before_script:
+# - apt-get update
+# - apt-get -y install rsync
+# script:
+# - kitchen verify default-centos-7 --destroy=always
diff --git a/lib/gitlab/ci/templates/Clojure.gitlab-ci.yml b/lib/gitlab/ci/templates/Clojure.gitlab-ci.yml
index 0610cb9ccc0..c3568c0d2c8 100644
--- a/lib/gitlab/ci/templates/Clojure.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Clojure.gitlab-ci.yml
@@ -8,7 +8,7 @@ before_script:
# If you need to install any external applications, like a
# postgres client, you may want to uncomment the line below:
#
- #- apt-get update -y
+ # - apt-get update -y
#
# Retrieve project dependencies
# Do this on before_script since it'll be shared between both test and
@@ -17,6 +17,6 @@ before_script:
test:
script:
- # If you need to run any migrations or configure the database, this
- # would be the point to do it.
- - lein test
+ # If you need to run any migrations or configure the database, this
+ # would be the point to do it.
+ - lein test
diff --git a/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml b/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml
index 36386a19fdc..e9301a2638d 100644
--- a/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml
@@ -22,15 +22,15 @@ cache:
# services such as redis or postgres
before_script:
- apt-get update -qq && apt-get install -y -qq libxml2-dev
- - crystal -v # Print out Crystal version for debugging
+ - crystal -v # Print out Crystal version for debugging
- shards
# If you are using built-in Crystal Spec.
spec:
script:
- - crystal spec
+ - crystal spec
# If you are using minitest.cr
minitest:
script:
- - crystal test/spec_test.cr # change to the file(s) you execute for tests
+ - crystal test/spec_test.cr # change to the file(s) you execute for tests
diff --git a/lib/gitlab/ci/templates/Django.gitlab-ci.yml b/lib/gitlab/ci/templates/Django.gitlab-ci.yml
index 1d8be6f017e..d35fcb0f807 100644
--- a/lib/gitlab/ci/templates/Django.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Django.gitlab-ci.yml
@@ -16,19 +16,19 @@ variables:
# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
cache:
paths:
- - ~/.cache/pip/
+ - ~/.cache/pip/
# This is a basic example for a gem or script which doesn't use
# services such as redis or postgres
before_script:
- - python -V # Print out python version for debugging
+ - python -V # Print out python version for debugging
# Uncomment next line if your Django app needs a JS runtime:
# - apt-get update -q && apt-get install nodejs -yqq
- pip install -r requirements.txt
# To get Django tests to work you may need to create a settings file using
# the following DATABASES:
-#
+#
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.postgresql_psycopg2',
@@ -46,4 +46,4 @@ test:
variables:
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
script:
- - python manage.py test
+ - python manage.py test
diff --git a/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml b/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml
index cf9c731637c..4d4c6a64cd5 100644
--- a/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml
@@ -15,4 +15,4 @@ before_script:
mix:
script:
- - mix test
+ - mix test
diff --git a/lib/gitlab/ci/templates/Go.gitlab-ci.yml b/lib/gitlab/ci/templates/Go.gitlab-ci.yml
index 55fda1a4799..1b686bc6cc0 100644
--- a/lib/gitlab/ci/templates/Go.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Go.gitlab-ci.yml
@@ -16,21 +16,21 @@ before_script:
- cd $GOPATH/src/$REPO_NAME
stages:
- - test
- - build
- - deploy
+ - test
+ - build
+ - deploy
format:
- stage: test
- script:
- - go fmt $(go list ./... | grep -v /vendor/)
- - go vet $(go list ./... | grep -v /vendor/)
- - go test -race $(go list ./... | grep -v /vendor/)
+ stage: test
+ script:
+ - go fmt $(go list ./... | grep -v /vendor/)
+ - go vet $(go list ./... | grep -v /vendor/)
+ - go test -race $(go list ./... | grep -v /vendor/)
compile:
- stage: build
- script:
- - go build -race -ldflags "-extldflags '-static'" -o $CI_PROJECT_DIR/mybinary
- artifacts:
- paths:
- - mybinary
+ stage: build
+ script:
+ - go build -race -ldflags "-extldflags '-static'" -o $CI_PROJECT_DIR/mybinary
+ artifacts:
+ paths:
+ - mybinary
diff --git a/lib/gitlab/ci/templates/Grails.gitlab-ci.yml b/lib/gitlab/ci/templates/Grails.gitlab-ci.yml
index dbc868238f8..efcd1d3ddc0 100644
--- a/lib/gitlab/ci/templates/Grails.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Grails.gitlab-ci.yml
@@ -16,25 +16,25 @@ variables:
# We use SDKMan as tool for managing versions
before_script:
- - apt-get update -qq && apt-get install -y -qq unzip
- - curl -sSL https://get.sdkman.io | bash
- - echo sdkman_auto_answer=true > /root/.sdkman/etc/config
- - source /root/.sdkman/bin/sdkman-init.sh
- - sdk install gradle $GRADLE_VERSION < /dev/null
- - sdk use gradle $GRADLE_VERSION
-# As it's not a good idea to version gradle.properties feel free to add your
-# environments variable here
- - echo grailsVersion=$GRAILS_VERSION > gradle.properties
- - echo gradleWrapperVersion=2.14 >> gradle.properties
-# refresh dependencies from your project
- - ./gradlew --refresh-dependencies
-# Be aware that if you are using Angular profile,
-# Bower cannot be run as root if you don't allow it before.
-# Feel free to remove next line if you are not using Bower
- - echo {\"allow_root\":true} > /root/.bowerrc
+ - apt-get update -qq && apt-get install -y -qq unzip
+ - curl -sSL https://get.sdkman.io | bash
+ - echo sdkman_auto_answer=true > /root/.sdkman/etc/config
+ - source /root/.sdkman/bin/sdkman-init.sh
+ - sdk install gradle $GRADLE_VERSION < /dev/null
+ - sdk use gradle $GRADLE_VERSION
+ # As it's not a good idea to version gradle.properties feel free to add your
+ # environments variable here
+ - echo grailsVersion=$GRAILS_VERSION > gradle.properties
+ - echo gradleWrapperVersion=2.14 >> gradle.properties
+ # refresh dependencies from your project
+ - ./gradlew --refresh-dependencies
+ # Be aware that if you are using Angular profile,
+ # Bower cannot be run as root if you don't allow it before.
+ # Feel free to remove next line if you are not using Bower
+ - echo {\"allow_root\":true} > /root/.bowerrc
# This build job does the full grails pipeline
# (compile, test, integrationTest, war, assemble).
build:
- script:
- - ./gradlew build
+ script:
+ - ./gradlew build
diff --git a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
index 546c4affb4e..a09217e8cf0 100644
--- a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
@@ -26,8 +26,8 @@ performance:
- mv sitespeed-results/data/performance.json performance.json
artifacts:
paths:
- - performance.json
- - sitespeed-results/
+ - performance.json
+ - sitespeed-results/
only:
refs:
- branches
diff --git a/lib/gitlab/ci/templates/Julia.gitlab-ci.yml b/lib/gitlab/ci/templates/Julia.gitlab-ci.yml
index 2c4683fbfbb..32d4e07d398 100644
--- a/lib/gitlab/ci/templates/Julia.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Julia.gitlab-ci.yml
@@ -20,7 +20,7 @@
# want coverage results.
- julia -e 'using Pkg; Pkg.clone(pwd()); Pkg.build("MyPackage"); Pkg.test("MyPackage"; coverage = true)'
# Comment out below if you do not want coverage results.
- - julia -e 'using Pkg; Pkg.add("Coverage");
+ - julia -e 'using Pkg; Pkg.add("Coverage");
import MyPackage; cd(joinpath(dirname(pathof(MyPackage)), ".."));
using Coverage; cl, tl = get_summary(process_folder());
println("(", cl/tl*100, "%) covered")'
@@ -55,17 +55,16 @@ pages:
image: julia:0.7
stage: deploy
script:
- - apt-get update -qq && apt-get install -y git # needed by Documenter
- - julia -e 'using Pkg; Pkg.clone(pwd()); Pkg.build("MyPackage");' # rebuild Julia (can be put somewhere else I'm sure
- - julia -e 'using Pkg; import MyPackage; Pkg.add("Documenter")' # install Documenter
- - julia --color=yes docs/make.jl # make documentation
- - mv docs/build public # move to the directory picked up by Gitlab pages
+ - apt-get update -qq && apt-get install -y git # needed by Documenter
+ - julia -e 'using Pkg; Pkg.clone(pwd()); Pkg.build("MyPackage");' # rebuild Julia (can be put somewhere else I'm sure
+ - julia -e 'using Pkg; import MyPackage; Pkg.add("Documenter")' # install Documenter
+ - julia --color=yes docs/make.jl # make documentation
+ - mv docs/build public # move to the directory picked up by Gitlab pages
artifacts:
paths:
- public
only:
- - master
-
+ - master
# WARNING: This template is using the `julia` images from [Docker
# Hub][3]. One can use custom Julia images and/or the official ones found
diff --git a/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml b/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml
index e1cd29ecc94..9bde04dff19 100644
--- a/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml
@@ -16,8 +16,8 @@ variables:
# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
cache:
paths:
- - vendor/
- - node_modules/
+ - vendor/
+ - node_modules/
# This is a basic example for a gem or script which doesn't use
# services such as redis or postgres
@@ -37,7 +37,7 @@ before_script:
- docker-php-ext-enable xdebug
# Install Composer and project dependencies.
- curl -sS https://getcomposer.org/installer | php
- - php composer.phar install
+ - php composer.phar install
# Install Node dependencies.
# comment this out if you don't have a node dependency
- npm install
@@ -63,10 +63,10 @@ before_script:
test:
script:
- # run laravel tests
- - php vendor/bin/phpunit --coverage-text --colors=never
- # run frontend tests
- # if you have any task for testing frontend
- # set it in your package.json script
- # comment this out if you don't have a frontend test
- - npm test
+ # run laravel tests
+ - php vendor/bin/phpunit --coverage-text --colors=never
+ # run frontend tests
+ # if you have any task for testing frontend
+ # set it in your package.json script
+ # comment this out if you don't have a frontend test
+ - npm test
diff --git a/lib/gitlab/ci/templates/Maven.gitlab-ci.yml b/lib/gitlab/ci/templates/Maven.gitlab-ci.yml
index c9838c7a7ff..13ab98d3a16 100644
--- a/lib/gitlab/ci/templates/Maven.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Maven.gitlab-ci.yml
@@ -1,18 +1,14 @@
----
+# This file is a template, and might need editing before it works on your project.
+
# Build JAVA applications using Apache Maven (http://maven.apache.org)
# For docker image tags see https://hub.docker.com/_/maven/
#
# For general lifecycle information see https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
-#
-# This template will build and test your projects as well as create the documentation.
-#
+
+# This template will build and test your projects
# * Caches downloaded dependencies and plugins between invocation.
# * Verify but don't deploy merge requests.
# * Deploy built artifacts from master branch only.
-# * Shows how to use multiple jobs in test stage for verifying functionality
-# with multiple JDKs.
-# * Uses site:stage to collect the documentation for multi-module projects.
-# * Publishes the documentation for `master` branch.
variables:
# This will suppress any download for dependencies and plugins or upload messages which would clutter the console log.
@@ -23,78 +19,38 @@ variables:
# `installAtEnd` and `deployAtEnd` are only effective with recent version of the corresponding plugins.
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true"
+# This template uses jdk8 for verifying and deploying images
+image: maven:3.3.9-jdk-8
+
# Cache downloaded dependencies and plugins between builds.
# To keep cache across branches add 'key: "$CI_JOB_NAME"'
cache:
paths:
- .m2/repository
-# This will only validate and compile stuff and run e.g. maven-enforcer-plugin.
-# Because some enforcer rules might check dependency convergence and class duplications
-# we use `test-compile` here instead of `validate`, so the correct classpath is picked up.
-.validate: &validate
- stage: build
- script:
- - 'mvn $MAVEN_CLI_OPTS test-compile'
-
# For merge requests do not `deploy` but only run `verify`.
# See https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
.verify: &verify
stage: test
script:
- - 'mvn $MAVEN_CLI_OPTS verify site site:stage'
+ - 'mvn $MAVEN_CLI_OPTS verify'
except:
- master
-# Validate merge requests using JDK7
-validate:jdk7:
- <<: *validate
- image: maven:3.3.9-jdk-7
-
-# Validate merge requests using JDK8
-validate:jdk8:
- <<: *validate
- image: maven:3.3.9-jdk-8
-
-# Verify merge requests using JDK7
-verify:jdk7:
- <<: *verify
- image: maven:3.3.9-jdk-7
-
# Verify merge requests using JDK8
verify:jdk8:
<<: *verify
- image: maven:3.3.9-jdk-8
+# To deploy packages from CI, create a ci_settings.xml file
+# For deploying packages to GitLab's Maven Repository: See https://docs.gitlab.com/ee/user/project/packages/maven_repository.html#creating-maven-packages-with-gitlab-cicd for more details.
+# Please note: The GitLab Maven Repository is currently only available in GitLab Premium / Ultimate.
# For `master` branch run `mvn deploy` automatically.
-# Here you need to decide whether you want to use JDK7 or 8.
-# To get this working you need to define a volume while configuring your gitlab-ci-multi-runner.
-# Mount your `settings.xml` as `/root/.m2/settings.xml` which holds your secrets.
-# See https://maven.apache.org/settings.html
deploy:jdk8:
- # Use stage test here, so the pages job may later pickup the created site.
- stage: test
- script:
- - 'mvn $MAVEN_CLI_OPTS deploy site site:stage'
- only:
- - master
- # Archive up the built documentation site.
- artifacts:
- paths:
- - target/staging
- image: maven:3.3.9-jdk-8
-
-pages:
- image: busybox:latest
stage: deploy
script:
- # Because Maven appends the artifactId automatically to the staging path if you did define a parent pom,
- # you might need to use `mv target/staging/YOUR_ARTIFACT_ID public` instead.
- - mv target/staging public
- dependencies:
- - deploy:jdk8
- artifacts:
- paths:
- - public
+ - if [ ! -f ci_settings.xml ];
+ then echo "CI settings missing\! If deploying to GitLab Maven Repository, please see https://docs.gitlab.com/ee/user/project/packages/maven_repository.html#creating-maven-packages-with-gitlab-cicd for instructions.";
+ fi
+ - 'mvn $MAVEN_CLI_OPTS deploy -s ci_settings.xml'
only:
- master
diff --git a/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml b/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml
index 41de1458582..b87178141a1 100644
--- a/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml
@@ -14,14 +14,14 @@ services:
# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
cache:
paths:
- - node_modules/
+ - node_modules/
test_async:
script:
- - npm install
- - node ./specs/start.js ./specs/async.spec.js
+ - npm install
+ - node ./specs/start.js ./specs/async.spec.js
test_db:
script:
- - npm install
- - node ./specs/start.js ./specs/db-postgres.spec.js
+ - npm install
+ - node ./specs/start.js ./specs/db-postgres.spec.js
diff --git a/lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml b/lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml
index 61a925e0d2d..65abee1f5eb 100644
--- a/lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml
@@ -1,9 +1,9 @@
image: ayufan/openshift-cli
stages:
- - build # dummy stage to follow the template guidelines
+ - build # dummy stage to follow the template guidelines
- test
- - deploy # dummy stage to follow the template guidelines
+ - deploy # dummy stage to follow the template guidelines
- review
- staging
- production
diff --git a/lib/gitlab/ci/templates/PHP.gitlab-ci.yml b/lib/gitlab/ci/templates/PHP.gitlab-ci.yml
index 33f44ee9222..b9fee2d5731 100644
--- a/lib/gitlab/ci/templates/PHP.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/PHP.gitlab-ci.yml
@@ -4,19 +4,19 @@ image: php:7.1.1
# Select what we should cache between builds
cache:
paths:
- - vendor/
+ - vendor/
before_script:
-- apt-get update -yqq
-- apt-get install -yqq git libmcrypt-dev libpq-dev libcurl4-gnutls-dev libicu-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 libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev
-# Install PHP extensions
-- docker-php-ext-install mbstring mcrypt pdo_pgsql curl json intl gd xml zip bz2 opcache
-# Install & enable Xdebug for code coverage reports
-- pecl install xdebug
-- docker-php-ext-enable xdebug
-# Install and run Composer
-- curl -sS https://getcomposer.org/installer | php
-- php composer.phar install
+ - apt-get update -yqq
+ - apt-get install -yqq git libmcrypt-dev libpq-dev libcurl4-gnutls-dev libicu-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 libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev
+ # Install PHP extensions
+ - docker-php-ext-install mbstring mcrypt pdo_pgsql curl json intl gd xml zip bz2 opcache
+ # Install & enable Xdebug for code coverage reports
+ - pecl install xdebug
+ - docker-php-ext-enable xdebug
+ # Install and run Composer
+ - curl -sS https://getcomposer.org/installer | php
+ - php composer.phar install
# Bring in any services we need http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
# See http://docs.gitlab.com/ce/ci/services/README.html for examples.
@@ -33,4 +33,4 @@ variables:
# If Xdebug was installed you can generate a coverage report and see code coverage metrics.
test:
script:
- - vendor/bin/phpunit --configuration phpunit.xml --coverage-text --colors=never \ No newline at end of file
+ - vendor/bin/phpunit --configuration phpunit.xml --coverage-text --colors=never
diff --git a/lib/gitlab/ci/templates/Pages/Brunch.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Brunch.gitlab-ci.yml
index d6de8cab5d1..d2dd3fbfb75 100644
--- a/lib/gitlab/ci/templates/Pages/Brunch.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Brunch.gitlab-ci.yml
@@ -4,12 +4,12 @@ image: node:4.2.2
pages:
cache:
paths:
- - node_modules/
+ - node_modules/
script:
- - npm install -g brunch
- - brunch build --production
+ - npm install -g brunch
+ - brunch build --production
artifacts:
paths:
- - public
+ - public
only:
- - master
+ - master
diff --git a/lib/gitlab/ci/templates/Pages/Doxygen.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Doxygen.gitlab-ci.yml
index 791afdd23f1..ba422c08614 100644
--- a/lib/gitlab/ci/templates/Pages/Doxygen.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Doxygen.gitlab-ci.yml
@@ -3,11 +3,11 @@ image: alpine
pages:
script:
- - apk update && apk add doxygen
- - doxygen doxygen/Doxyfile
- - mv doxygen/documentation/html/ public/
+ - apk update && apk add doxygen
+ - doxygen doxygen/Doxyfile
+ - mv doxygen/documentation/html/ public/
artifacts:
paths:
- - public
+ - public
only:
- - master
+ - master
diff --git a/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml
index 9df2a4797b2..a683561a455 100644
--- a/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml
@@ -4,14 +4,14 @@ image: node:latest
# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
cache:
paths:
- - node_modules/
+ - node_modules/
pages:
script:
- - yarn install
- - ./node_modules/.bin/gatsby build --prefix-paths
+ - yarn install
+ - ./node_modules/.bin/gatsby build --prefix-paths
artifacts:
paths:
- - public
+ - public
only:
- - master
+ - master
diff --git a/lib/gitlab/ci/templates/Pages/HTML.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/HTML.gitlab-ci.yml
index 249a168aa33..92f25280c6e 100644
--- a/lib/gitlab/ci/templates/Pages/HTML.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/HTML.gitlab-ci.yml
@@ -2,11 +2,11 @@
pages:
stage: deploy
script:
- - mkdir .public
- - cp -r * .public
- - mv .public public
+ - mkdir .public
+ - cp -r * .public
+ - mv .public public
artifacts:
paths:
- - public
+ - public
only:
- - master
+ - master
diff --git a/lib/gitlab/ci/templates/Pages/Harp.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Harp.gitlab-ci.yml
index 4b58003ee10..0e206423fa5 100644
--- a/lib/gitlab/ci/templates/Pages/Harp.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Harp.gitlab-ci.yml
@@ -4,12 +4,12 @@ image: node:4.2.2
pages:
cache:
paths:
- - node_modules
+ - node_modules
script:
- - npm install -g harp
- - harp compile ./ public
+ - npm install -g harp
+ - harp compile ./ public
artifacts:
paths:
- - public
+ - public
only:
- - master
+ - master
diff --git a/lib/gitlab/ci/templates/Pages/Hexo.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Hexo.gitlab-ci.yml
index a9e195370f7..d91a8d7421f 100644
--- a/lib/gitlab/ci/templates/Pages/Hexo.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Hexo.gitlab-ci.yml
@@ -3,15 +3,15 @@ image: node:10.15.3
pages:
script:
- - npm install hexo-cli -g
- - test -e package.json && npm install
- - hexo generate
+ - npm install hexo-cli -g
+ - test -e package.json && npm install
+ - hexo generate
artifacts:
paths:
- - public
+ - public
cache:
paths:
- node_modules
key: project
only:
- - master
+ - master
diff --git a/lib/gitlab/ci/templates/Pages/Hugo.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Hugo.gitlab-ci.yml
index f9ddcc6fb0a..9a3ecd1c34f 100644
--- a/lib/gitlab/ci/templates/Pages/Hugo.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Hugo.gitlab-ci.yml
@@ -3,15 +3,15 @@ image: dettmering/hugo-build
pages:
script:
- - hugo
+ - hugo
artifacts:
paths:
- - public
+ - public
only:
- - master
+ - master
test:
script:
- - hugo
+ - hugo
except:
- - master
+ - master
diff --git a/lib/gitlab/ci/templates/Pages/Hyde.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Hyde.gitlab-ci.yml
index f5b40f2b9f1..7a441a2f70f 100644
--- a/lib/gitlab/ci/templates/Pages/Hyde.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Hyde.gitlab-ci.yml
@@ -3,7 +3,7 @@ image: python:2.7
cache:
paths:
- - vendor/
+ - vendor/
test:
stage: test
@@ -20,6 +20,6 @@ pages:
- hyde gen -d public
artifacts:
paths:
- - public
+ - public
only:
- master
diff --git a/lib/gitlab/ci/templates/Pages/JBake.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/JBake.gitlab-ci.yml
index 7a485f8d135..886b6c36249 100644
--- a/lib/gitlab/ci/templates/Pages/JBake.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/JBake.gitlab-ci.yml
@@ -4,28 +4,28 @@
# JBake https://jbake.org/ is a Java based, open source, static site/blog generator for developers & designers
#
# This yml works with jBake 2.5.1
-# Feel free to change JBAKE_VERSION version
+# Feel free to change JBAKE_VERSION version
#
# HowTo at: https://jorge.aguilera.gitlab.io/howtojbake/
image: java:8
variables:
- JBAKE_VERSION: 2.5.1
+ JBAKE_VERSION: 2.5.1
# We use SDKMan as tool for managing versions
before_script:
- - apt-get update -qq && apt-get install -y -qq unzip zip
- - curl -sSL https://get.sdkman.io | bash
- - echo sdkman_auto_answer=true > /root/.sdkman/etc/config
- - source /root/.sdkman/bin/sdkman-init.sh
- - sdk install jbake $JBAKE_VERSION < /dev/null
- - sdk use jbake $JBAKE_VERSION
+ - apt-get update -qq && apt-get install -y -qq unzip zip
+ - curl -sSL https://get.sdkman.io | bash
+ - echo sdkman_auto_answer=true > /root/.sdkman/etc/config
+ - source /root/.sdkman/bin/sdkman-init.sh
+ - sdk install jbake $JBAKE_VERSION < /dev/null
+ - sdk use jbake $JBAKE_VERSION
# This build job produced the output directory of your site
pages:
- script:
- - jbake . public
- artifacts:
- paths:
- - public
+ script:
+ - jbake . public
+ artifacts:
+ paths:
+ - public
diff --git a/lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml
index 37f50554036..0d742aa282d 100644
--- a/lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml
@@ -6,25 +6,24 @@ variables:
JEKYLL_ENV: production
before_script:
-- bundle install
+ - bundle install
test:
stage: test
script:
- - bundle exec jekyll build -d test
+ - bundle exec jekyll build -d test
artifacts:
paths:
- - test
+ - test
except:
- - master
+ - master
pages:
stage: deploy
script:
- - bundle exec jekyll build -d public
+ - bundle exec jekyll build -d public
artifacts:
paths:
- - public
+ - public
only:
- - master
-
+ - master
diff --git a/lib/gitlab/ci/templates/Pages/Jigsaw.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Jigsaw.gitlab-ci.yml
index 5ca4619e200..2d26b86a328 100644
--- a/lib/gitlab/ci/templates/Pages/Jigsaw.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Jigsaw.gitlab-ci.yml
@@ -7,8 +7,8 @@ image: php:7.2
# These folders are cached between builds
cache:
paths:
- - vendor/
- - node_modules/
+ - vendor/
+ - node_modules/
before_script:
# Update packages
@@ -28,10 +28,10 @@ before_script:
pages:
script:
- - npm run production
- - mv build_production public
+ - npm run production
+ - mv build_production public
artifacts:
paths:
- - public
+ - public
only:
- - master
+ - master
diff --git a/lib/gitlab/ci/templates/Pages/Lektor.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Lektor.gitlab-ci.yml
index c5c44a5d86c..93ab8e0be0d 100644
--- a/lib/gitlab/ci/templates/Pages/Lektor.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Lektor.gitlab-ci.yml
@@ -3,10 +3,10 @@ image: python:2.7
pages:
script:
- - pip install lektor
- - lektor build --output-path public
+ - pip install lektor
+ - lektor build --output-path public
artifacts:
paths:
- - public
+ - public
only:
- - master
+ - master
diff --git a/lib/gitlab/ci/templates/Pages/Metalsmith.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Metalsmith.gitlab-ci.yml
index c6ded272150..6524405133a 100644
--- a/lib/gitlab/ci/templates/Pages/Metalsmith.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Metalsmith.gitlab-ci.yml
@@ -4,13 +4,13 @@ image: node:4.2.2
pages:
cache:
paths:
- - node_modules/
+ - node_modules/
script:
- - npm install -g metalsmith
- - npm install
- - make build
+ - npm install -g metalsmith
+ - npm install
+ - make build
artifacts:
paths:
- - public
+ - public
only:
- - master
+ - master
diff --git a/lib/gitlab/ci/templates/Pages/Middleman.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Middleman.gitlab-ci.yml
index 9f4cc0574d6..57ac323dfdf 100644
--- a/lib/gitlab/ci/templates/Pages/Middleman.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Middleman.gitlab-ci.yml
@@ -3,25 +3,25 @@ image: ruby:2.3
cache:
paths:
- - vendor
+ - vendor
test:
script:
- - apt-get update -yqqq
- - apt-get install -y nodejs
- - bundle install --path vendor
- - bundle exec middleman build
+ - apt-get update -yqqq
+ - apt-get install -y nodejs
+ - bundle install --path vendor
+ - bundle exec middleman build
except:
- master
pages:
script:
- - apt-get update -yqqq
- - apt-get install -y nodejs
- - bundle install --path vendor
- - bundle exec middleman build
+ - apt-get update -yqqq
+ - apt-get install -y nodejs
+ - bundle install --path vendor
+ - bundle exec middleman build
artifacts:
paths:
- - public
+ - public
only:
- - master
+ - master
diff --git a/lib/gitlab/ci/templates/Pages/Nanoc.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Nanoc.gitlab-ci.yml
index b469b316ba5..7f037b5f5cf 100644
--- a/lib/gitlab/ci/templates/Pages/Nanoc.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Nanoc.gitlab-ci.yml
@@ -3,10 +3,10 @@ image: ruby:2.3
pages:
script:
- - bundle install -j4
- - nanoc
+ - bundle install -j4
+ - nanoc
artifacts:
paths:
- - public
+ - public
only:
- - master
+ - master
diff --git a/lib/gitlab/ci/templates/Pages/Octopress.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Octopress.gitlab-ci.yml
index 4762ec9acfd..6d912a89bc1 100644
--- a/lib/gitlab/ci/templates/Pages/Octopress.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Octopress.gitlab-ci.yml
@@ -3,13 +3,13 @@ image: ruby:2.3
pages:
script:
- - apt-get update -qq && apt-get install -qq nodejs
- - bundle install -j4
- - bundle exec rake generate
- - mv public .public
- - mv .public/octopress public
+ - apt-get update -qq && apt-get install -qq nodejs
+ - bundle install -j4
+ - bundle exec rake generate
+ - mv public .public
+ - mv .public/octopress public
artifacts:
paths:
- - public
+ - public
only:
- - master
+ - master
diff --git a/lib/gitlab/ci/templates/Pages/Pelican.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Pelican.gitlab-ci.yml
index c5f3154f587..09c6649fc13 100644
--- a/lib/gitlab/ci/templates/Pages/Pelican.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Pelican.gitlab-ci.yml
@@ -3,8 +3,8 @@ image: python:2.7-alpine
pages:
script:
- - pip install -r requirements.txt
- - pelican -s publishconf.py
+ - pip install -r requirements.txt
+ - pelican -s publishconf.py
artifacts:
paths:
- - public/
+ - public/
diff --git a/lib/gitlab/ci/templates/Python.gitlab-ci.yml b/lib/gitlab/ci/templates/Python.gitlab-ci.yml
index 3eaed4e91cd..00b8b94b574 100644
--- a/lib/gitlab/ci/templates/Python.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Python.gitlab-ci.yml
@@ -18,34 +18,34 @@ cache:
- 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:
- - python setup.py test
- - pip install tox flake8 # you can also use tox
- - tox -e py36,flake8
+ - python setup.py test
+ - pip install tox flake8 # you can also use tox
+ - tox -e py36,flake8
run:
script:
- - python setup.py bdist_wheel
- # an alternative approach is to install and run:
- - pip install dist/*
- # run the command here
+ - python setup.py bdist_wheel
+ # an alternative approach is to install and run:
+ - pip install dist/*
+ # run the command here
artifacts:
paths:
- - dist/*.whl
+ - dist/*.whl
pages:
script:
- - pip install sphinx sphinx-rtd-theme
- - cd doc ; make html
- - mv build/html/ ../public/
+ - pip install sphinx sphinx-rtd-theme
+ - cd doc ; make html
+ - mv build/html/ ../public/
artifacts:
paths:
- - public
+ - public
only:
- - master
+ - master
diff --git a/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml b/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml
index 93196dbd475..b3cad8b858a 100644
--- a/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml
@@ -21,7 +21,7 @@ cache:
# This is a basic example for a gem or script which doesn't use
# services such as redis or postgres
before_script:
- - ruby -v # Print out ruby version for debugging
+ - ruby -v # Print out ruby version for debugging
# Uncomment next line if your rails app needs a JS runtime:
# - apt-get update -q && apt-get install nodejs -yqq
- bundle install -j $(nproc) --path vendor # Install dependencies into ./vendor/ruby
@@ -29,19 +29,19 @@ before_script:
# Optional - Delete if not using `rubocop`
rubocop:
script:
- - rubocop
+ - rubocop
rspec:
script:
- - rspec spec
+ - rspec spec
rails:
variables:
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
script:
- - rails db:migrate
- - rails db:seed
- - rails test
+ - rails db:migrate
+ - rails db:seed
+ - rails test
# This deploy job uses a simple deploy flow to Heroku, other providers, e.g. AWS Elastic Beanstalk
# are supported too: https://github.com/travis-ci/dpl
@@ -49,5 +49,5 @@ deploy:
type: deploy
environment: production
script:
- - gem install dpl
- - dpl --provider=heroku --app=$HEROKU_APP_NAME --api-key=$HEROKU_PRODUCTION_KEY
+ - gem install dpl
+ - dpl --provider=heroku --app=$HEROKU_APP_NAME --api-key=$HEROKU_PRODUCTION_KEY
diff --git a/lib/gitlab/ci/templates/Rust.gitlab-ci.yml b/lib/gitlab/ci/templates/Rust.gitlab-ci.yml
index cab087c48c7..a25dc38e4e7 100644
--- a/lib/gitlab/ci/templates/Rust.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Rust.gitlab-ci.yml
@@ -5,19 +5,19 @@ image: "rust:latest"
# Optional: Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service
-#services:
-# - mysql:latest
-# - redis:latest
-# - postgres:latest
+# services:
+# - mysql:latest
+# - redis:latest
+# - postgres:latest
# Optional: Install a C compiler, cmake and git into the container.
# You will often need this when you (or any of your dependencies) depends on C code.
-#before_script:
-#- apt-get update -yqq
-#- apt-get install -yqq --no-install-recommends build-essential
+# before_script:
+# - apt-get update -yqq
+# - apt-get install -yqq --no-install-recommends build-essential
# Use cargo to test the project
test:cargo:
script:
- - rustc --version && cargo --version # Print version info for debugging
- - cargo test --all --verbose
+ - rustc --version && cargo --version # Print version info for debugging
+ - cargo test --all --verbose
diff --git a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
index 27a498b2daf..4b55ffd3771 100644
--- a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
@@ -15,22 +15,22 @@ dast:
image:
name: "registry.gitlab.com/gitlab-org/security-products/dast:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable"
variables:
- # URL to scan:
- # DAST_WEBSITE: https://example.com/
- #
- # Time limit for target availability (scan is attempted even when timeout):
- # DAST_TARGET_AVAILABILITY_TIMEOUT: 60
- #
- # Set these variables to scan with an authenticated user:
- # DAST_AUTH_URL: https://example.com/sign-in
- # DAST_USERNAME: john.doe@example.com
- # DAST_PASSWORD: john-doe-password
- # DAST_USERNAME_FIELD: session[user] # the name of username field at the sign-in HTML form
- # DAST_PASSWORD_FIELD: session[password] # the name of password field at the sign-in HTML form
- # DAST_AUTH_EXCLUDE_URLS: http://example.com/sign-out,http://example.com/sign-out-2 # optional: URLs to skip during the authenticated scan; comma-separated, no spaces in between
- #
- # Perform ZAP Full Scan, which includes both passive and active scanning:
- # DAST_FULL_SCAN_ENABLED: "true"
+ # URL to scan:
+ # DAST_WEBSITE: https://example.com/
+ #
+ # Time limit for target availability (scan is attempted even when timeout):
+ # DAST_TARGET_AVAILABILITY_TIMEOUT: 60
+ #
+ # Set these variables to scan with an authenticated user:
+ # DAST_AUTH_URL: https://example.com/sign-in
+ # DAST_USERNAME: john.doe@example.com
+ # DAST_PASSWORD: john-doe-password
+ # DAST_USERNAME_FIELD: session[user] # the name of username field at the sign-in HTML form
+ # DAST_PASSWORD_FIELD: session[password] # the name of password field at the sign-in HTML form
+ # DAST_AUTH_EXCLUDE_URLS: http://example.com/sign-out,http://example.com/sign-out-2 # optional: URLs to skip during the authenticated scan; comma-separated, no spaces in between
+ #
+ # Perform ZAP Full Scan, which includes both passive and active scanning:
+ # DAST_FULL_SCAN_ENABLED: "true"
allow_failure: true
script:
- export DAST_WEBSITE=${DAST_WEBSITE:-$(cat environment_url.txt)}
diff --git a/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml
index 0208beb35b8..b6555e627a1 100644
--- a/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml
@@ -1,7 +1,7 @@
# Read more about this feature here: https://docs.gitlab.com/ee/user/project/merge_requests/license_management.html
variables:
- LICENSE_MANAGEMENT_SETUP_CMD: '' # If needed, specify a command to setup your environment with a custom package manager.
+ LICENSE_MANAGEMENT_SETUP_CMD: '' # If needed, specify a command to setup your environment with a custom package manager.
license_management:
stage: test
diff --git a/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml b/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml
index 0fb7c57ab72..3f46bb89e94 100644
--- a/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml
@@ -8,25 +8,13 @@ stages:
- deploy
.serverless:build:image:
- variables:
- DOCKERFILE: "Dockerfile"
stage: build
- image:
- name: gcr.io/kaniko-project/executor:debug
- entrypoint: [""]
- only:
- refs:
- - master
- script:
- - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
- - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/$DOCKERFILE --destination $CI_REGISTRY_IMAGE
+ image: registry.gitlab.com/gitlab-org/gitlabktl:latest
+ script: /usr/bin/gitlabktl app build
.serverless:deploy:image:
stage: deploy
- image: gcr.io/triggermesh/tm@sha256:e3ee74db94d215bd297738d93577481f3e4db38013326c90d57f873df7ab41d5
- only:
- refs:
- - master
+ image: gcr.io/triggermesh/tm@sha256:3cfdd470a66b741004fb02354319d79f1598c70117ce79978d2e07e192bfb336 # v0.0.11
environment: development
script:
- echo "$CI_REGISTRY_IMAGE"
diff --git a/lib/gitlab/ci/templates/Swift.gitlab-ci.yml b/lib/gitlab/ci/templates/Swift.gitlab-ci.yml
index 9aa4abd4791..ffed7a0fec2 100644
--- a/lib/gitlab/ci/templates/Swift.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Swift.gitlab-ci.yml
@@ -25,7 +25,7 @@ archive_project:
- master
artifacts:
paths:
- - build/ProjectName.ipa
+ - build/ProjectName.ipa
tags:
- ios_11-3
- xcode_9-3
diff --git a/lib/gitlab/ci/templates/dotNET-Core.yml b/lib/gitlab/ci/templates/dotNET-Core.yml
index 708b75f83e8..40ca296d7bd 100644
--- a/lib/gitlab/ci/templates/dotNET-Core.yml
+++ b/lib/gitlab/ci/templates/dotNET-Core.yml
@@ -1,4 +1,5 @@
-# This is a simple example illustrating how to build and test .NET Core project
+---
+# This is a simple example illustrating how to build and test .NET Core project
# with GitLab Continuous Integration / Continuous Delivery.
# ### Specify the Docker image
@@ -17,21 +18,21 @@ image: microsoft/dotnet:latest
# ### Define variables
#
variables:
- # 1) Name of directory where restore and build objects are stored.
- OBJECTS_DIRECTORY: 'obj'
- # 2) Name of directory used for keeping restored dependencies.
- NUGET_PACKAGES_DIRECTORY: '.nuget'
- # 3) A relative path to the source code from project repository root.
- # NOTE: Please edit this path so it matches the structure of your project!
- SOURCE_CODE_PATH: '*/*/'
+ # 1) Name of directory where restore and build objects are stored.
+ OBJECTS_DIRECTORY: 'obj'
+ # 2) Name of directory used for keeping restored dependencies.
+ NUGET_PACKAGES_DIRECTORY: '.nuget'
+ # 3) A relative path to the source code from project repository root.
+ # NOTE: Please edit this path so it matches the structure of your project!
+ SOURCE_CODE_PATH: '*/*/'
# ### Define stage list
#
# In this example there are only two stages.
# Initially, the project will be built and then tested.
stages:
- - build
- - test
+ - build
+ - test
# ### Define global cache rule
#
@@ -50,31 +51,31 @@ stages:
# With global cache rule, cached dependencies will be downloaded before every job
# and then unpacked to the paths as specified below.
cache:
- # Per-stage and per-branch caching.
- key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG"
- paths:
- # Specify three paths that should be cached:
+ # Per-stage and per-branch caching.
+ key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG"
+ paths:
+ # Specify three paths that should be cached:
+ #
+ # 1) Main JSON file holding information about package dependency tree, packages versions,
+ # frameworks etc. It also holds information where to the dependencies were restored.
+ - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/project.assets.json'
+ # 2) Other NuGet and MSBuild related files. Also needed.
+ - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/*.csproj.nuget.*'
+ # 3) Path to the directory where restored dependencies are kept.
+ - '$NUGET_PACKAGES_DIRECTORY'
#
- # 1) Main JSON file holding information about package dependency tree, packages versions,
- # frameworks etc. It also holds information where to the dependencies were restored.
- - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/project.assets.json'
- # 2) Other NuGet and MSBuild related files. Also needed.
- - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/*.csproj.nuget.*'
- # 3) Path to the directory where restored dependencies are kept.
- - '$NUGET_PACKAGES_DIRECTORY'
- #
- # 'pull-push' policy means that latest cache will be downloaded (if it exists)
- # before executing the job, and a newer version will be uploaded afterwards.
- # Such a setting saves time when there are no changes in referenced third-party
- # packages.
- #
- # For example, if you run a pipeline with changes in your code,
- # but with no changes within third-party packages which your project is using,
- # then project restore will happen quickly as all required dependencies
- # will already be there — unzipped from cache.
+ # 'pull-push' policy means that latest cache will be downloaded (if it exists)
+ # before executing the job, and a newer version will be uploaded afterwards.
+ # Such a setting saves time when there are no changes in referenced third-party
+ # packages.
+ #
+ # For example, if you run a pipeline with changes in your code,
+ # but with no changes within third-party packages which your project is using,
+ # then project restore will happen quickly as all required dependencies
+ # will already be there — unzipped from cache.
- # 'pull-push' policy is the default cache policy, you do not have to specify it explicitly.
- policy: pull-push
+ # 'pull-push' policy is the default cache policy, you do not have to specify it explicitly.
+ policy: pull-push
# ### Restore project dependencies
#
@@ -87,30 +88,30 @@ cache:
#
# Learn more about GitLab cache: https://docs.gitlab.com/ee/ci/caching/index.html
before_script:
- - 'dotnet restore --packages $NUGET_PACKAGES_DIRECTORY'
+ - 'dotnet restore --packages $NUGET_PACKAGES_DIRECTORY'
build:
- stage: build
- # ### Build all projects discovered from solution file.
- #
- # Note: this will fail if you have any projects in your solution that are not
- # .NET Core-based projects (e.g. WCF service), which is based on .NET Framework,
- # not .NET Core. In this scenario, you will need to build every .NET Core-based
- # project by explicitly specifying a relative path to the directory
- # where it is located (e.g. 'dotnet build ./src/ConsoleApp').
- # Only one project path can be passed as a parameter to 'dotnet build' command.
- script:
- - 'dotnet build --no-restore'
+ stage: build
+ # ### Build all projects discovered from solution file.
+ #
+ # Note: this will fail if you have any projects in your solution that are not
+ # .NET Core-based projects (e.g. WCF service), which is based on .NET Framework,
+ # not .NET Core. In this scenario, you will need to build every .NET Core-based
+ # project by explicitly specifying a relative path to the directory
+ # where it is located (e.g. 'dotnet build ./src/ConsoleApp').
+ # Only one project path can be passed as a parameter to 'dotnet build' command.
+ script:
+ - 'dotnet build --no-restore'
tests:
- stage: test
- # ### Run the tests
- #
- # You can either run tests for all test projects that are defined in your solution
- # with 'dotnet test' or run tests only for specific project by specifying
- # a relative path to the directory where it is located (e.g. 'dotnet test ./test/UnitTests').
- #
- # You may want to define separate testing jobs for different types of testing
- # (e.g. integration tests, unit tests etc).
- script:
- - 'dotnet test --no-restore'
+ stage: test
+ # ### Run the tests
+ #
+ # You can either run tests for all test projects that are defined in your solution
+ # with 'dotnet test' or run tests only for specific project by specifying
+ # a relative path to the directory where it is located (e.g. 'dotnet test ./test/UnitTests').
+ #
+ # You may want to define separate testing jobs for different types of testing
+ # (e.g. integration tests, unit tests etc).
+ script:
+ - 'dotnet test --no-restore'
diff --git a/lib/gitlab/ci/templates/dotNET.gitlab-ci.yml b/lib/gitlab/ci/templates/dotNET.gitlab-ci.yml
index 5e128b793d0..b29f45323f5 100644
--- a/lib/gitlab/ci/templates/dotNET.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/dotNET.gitlab-ci.yml
@@ -3,12 +3,12 @@
# (located in gitlab-runner.exe directory):
# shell = "powershell"
#
-# The script is composed of 3 stages: build, test and deploy.
+# The script is composed of 3 stages: build, test and deploy.
#
# The build stage restores NuGet packages and uses msbuild to build the exe and msi
# One major issue you'll find is that you can't build msi projects from command line
# if you use vdproj. There are workarounds building msi via devenv, but they rarely work
-# The best solution is migrating your vdproj projects to WiX, as it can be build directly
+# The best solution is migrating your vdproj projects to WiX, as it can be build directly
# by msbuild.
#
# The test stage runs nunit from command line against Test project inside your solution
@@ -38,28 +38,28 @@ stages:
build_job:
stage: build
only:
- - tags # the build process will only be started by git tag commits
+ - tags # the build process will only be started by git tag commits
script:
- - '& "$env:NUGET_PATH" restore' # restore Nuget dependencies
- - '& "$env:MSBUILD_PATH" /p:Configuration=Release' # build the project
+ - '& "$env:NUGET_PATH" restore' # restore Nuget dependencies
+ - '& "$env:MSBUILD_PATH" /p:Configuration=Release' # build the project
artifacts:
- expire_in: 1 week # save gitlab server space, we copy the files we need to deploy folder later on
+ expire_in: 1 week # save gitlab server space, we copy the files we need to deploy folder later on
paths:
- - '$env:EXE_RELEASE_FOLDER\YourApp.exe' # saving exe to copy to deploy folder
- - '$env:MSI_RELEASE_FOLDER\YourApp Setup.msi' # saving msi to copy to deploy folder
- - '$env:TEST_FOLDER\' # saving entire Test project so NUnit can run tests
+ - '$env:EXE_RELEASE_FOLDER\YourApp.exe' # saving exe to copy to deploy folder
+ - '$env:MSI_RELEASE_FOLDER\YourApp Setup.msi' # saving msi to copy to deploy folder
+ - '$env:TEST_FOLDER\' # saving entire Test project so NUnit can run tests
test_job:
stage: test
only:
- tags
script:
- - '& "$env:NUNIT_PATH" ".\$env:TEST_FOLDER\Tests.dll"' # running NUnit tests
+ - '& "$env:NUNIT_PATH" ".\$env:TEST_FOLDER\Tests.dll"' # running NUnit tests
artifacts:
- when: always # save test results even when the task fails
- expire_in: 1 week # save gitlab server space, we copy the files we need to deploy folder later on
+ when: always # save test results even when the task fails
+ expire_in: 1 week # save gitlab server space, we copy the files we need to deploy folder later on
paths:
- - '.\TestResult.xml' # saving NUnit results to copy to deploy folder
+ - '.\TestResult.xml' # saving NUnit results to copy to deploy folder
dependencies:
- build_job
@@ -79,7 +79,6 @@ deploy_job:
- 'xcopy /y ".\$env:EXE_RELEASE_FOLDER\YourApp.exe" "$deployFolder"'
- 'xcopy /y ".\$env:MSI_RELEASE_FOLDER\YourApp Setup.msi" "$deployFolder"'
- 'xcopy /y ".\TestResult.xml" "$deployFolder"'
-
dependencies:
- build_job
- test_job
diff --git a/lib/gitlab/ci/templates/iOS-Fastlane.gitlab-ci.yml b/lib/gitlab/ci/templates/iOS-Fastlane.gitlab-ci.yml
index a4a9e96c1d2..87aea8527d1 100644
--- a/lib/gitlab/ci/templates/iOS-Fastlane.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/iOS-Fastlane.gitlab-ci.yml
@@ -1,13 +1,13 @@
# This is a very simple template that mainly relies on FastLane to build and distribute your app.
# Read more about how to use this template on the blog post https://about.gitlab.com/2019/03/06/ios-publishing-with-gitlab-and-fastlane/
-# You will also need fastlane and signing configuration for this to work, along with a MacOS runner.
+# You will also need fastlane and signing configuration for this to work, along with a MacOS runner.
# These details are provided in the blog post.
# Note that when you're using the shell executor for MacOS builds, the
# build and tests run as the identity of the runner logged in user, directly on
# the build host. This is less secure than using container executors, so please
-# take a look at our security implications documentation at
-# https://docs.gitlab.com/runner/security/#usage-of-shell-executor for additional
+# take a look at our security implications documentation at
+# https://docs.gitlab.com/runner/security/#usage-of-shell-executor for additional
# detail on what to keep in mind in this scenario.
stages:
@@ -27,4 +27,4 @@ build:
- bundle exec fastlane build
artifacts:
paths:
- - ./*.ipa
+ - ./*.ipa
diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb
index f0ca397609d..7a0fb419f8e 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -1,6 +1,4 @@
# frozen_string_literal: true
-require 'net/http'
-require 'json'
require_relative 'teammate'
@@ -8,7 +6,6 @@ module Gitlab
module Danger
module Helper
RELEASE_TOOLS_BOT = 'gitlab-release-tools-bot'
- ROULETTE_DATA_URL = URI.parse('https://about.gitlab.com/roulette.json').freeze
# Returns a list of all files that have been added, modified or renamed.
# `git.modified_files` might contain paths that already have been renamed,
@@ -49,32 +46,6 @@ module Gitlab
ee? ? 'gitlab-ee' : 'gitlab-ce'
end
- # Looks up the current list of GitLab team members and parses it into a
- # useful form
- #
- # @return [Array<Teammate>]
- def team
- @team ||=
- begin
- rsp = Net::HTTP.get_response(ROULETTE_DATA_URL)
- raise "Failed to read #{ROULETTE_DATA_URL}: #{rsp.code} #{rsp.message}" unless
- rsp.is_a?(Net::HTTPSuccess)
-
- data = JSON.parse(rsp.body)
- data.map { |hash| ::Gitlab::Danger::Teammate.new(hash) }
- rescue JSON::ParserError
- raise "Failed to parse JSON response from #{ROULETTE_DATA_URL}"
- end
- end
-
- # Like +team+, but only returns teammates in the current project, based on
- # project_name.
- #
- # @return [Array<Teammate>]
- def project_team
- team.select { |member| member.in_project?(project_name) }
- end
-
# @return [Hash<String,Array<String>>]
def changes_by_category
all_changed_files.each_with_object(Hash.new { |h, k| h[k] = [] }) do |file, hash|
@@ -101,7 +72,8 @@ module Gitlab
CATEGORY_LABELS = {
docs: "~Documentation", # Docs are reviewed along DevOps stages, so don't need roulette for now.
none: "",
- qa: "~QA"
+ qa: "~QA",
+ test: "~test for `spec/features/*`"
}.freeze
CATEGORIES = {
%r{\Adoc/} => :none, # To reinstate roulette for documentation, set to `:docs`.
@@ -133,6 +105,7 @@ module Gitlab
%r{\A(ee/)?app/(?!assets|views)[^/]+} => :backend,
%r{\A(ee/)?(bin|config|danger|generator_templates|lib|rubocop|scripts)/} => :backend,
+ %r{\A(ee/)?spec/features/} => :test,
%r{\A(ee/)?spec/(?!javascripts|frontend)[^/]+} => :backend,
%r{\A(ee/)?vendor/(?!assets)[^/]+} => :backend,
%r{\A(ee/)?vendor/(languages\.yml|licenses\.csv)\z} => :backend,
diff --git a/lib/gitlab/danger/roulette.rb b/lib/gitlab/danger/roulette.rb
new file mode 100644
index 00000000000..062eda38ee4
--- /dev/null
+++ b/lib/gitlab/danger/roulette.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'net/http'
+require 'json'
+require 'cgi'
+
+require_relative 'teammate'
+
+module Gitlab
+ module Danger
+ module Roulette
+ ROULETTE_DATA_URL = 'https://about.gitlab.com/roulette.json'
+ HTTPError = Class.new(RuntimeError)
+
+ # Looks up the current list of GitLab team members and parses it into a
+ # useful form
+ #
+ # @return [Array<Teammate>]
+ def team
+ @team ||=
+ begin
+ data = http_get_json(ROULETTE_DATA_URL)
+ data.map { |hash| ::Gitlab::Danger::Teammate.new(hash) }
+ rescue JSON::ParserError
+ raise "Failed to parse JSON response from #{ROULETTE_DATA_URL}"
+ end
+ end
+
+ # Like +team+, but only returns teammates in the current project, based on
+ # project_name.
+ #
+ # @return [Array<Teammate>]
+ def project_team(project_name)
+ team.select { |member| member.in_project?(project_name) }
+ end
+
+ def canonical_branch_name(branch_name)
+ branch_name.gsub(/^[ce]e-|-[ce]e$/, '')
+ end
+
+ def new_random(seed)
+ Random.new(Digest::MD5.hexdigest(seed).to_i(16))
+ end
+
+ # Known issue: If someone is rejected due to OOO, and then becomes not OOO, the
+ # selection will change on next spin
+ def spin_for_person(people, random:)
+ person = nil
+ people = people.dup
+
+ people.size.times do
+ person = people.sample(random: random)
+
+ break person unless out_of_office?(person)
+
+ people -= [person]
+ end
+
+ person
+ end
+
+ private
+
+ def out_of_office?(person)
+ username = CGI.escape(person.username)
+ api_endpoint = "https://gitlab.com/api/v4/users/#{username}/status"
+ response = http_get_json(api_endpoint)
+ response["message"]&.match?(/OOO/i)
+ rescue HTTPError, JSON::ParserError
+ false # this is no worse than not checking for OOO
+ end
+
+ def http_get_json(url)
+ rsp = Net::HTTP.get_response(URI.parse(url))
+
+ unless rsp.is_a?(Net::HTTPSuccess)
+ raise HTTPError, "Failed to read #{url}: #{rsp.code} #{rsp.message}"
+ end
+
+ JSON.parse(rsp.body)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/danger/teammate.rb b/lib/gitlab/danger/teammate.rb
index bfada512727..b44f134f2c1 100644
--- a/lib/gitlab/danger/teammate.rb
+++ b/lib/gitlab/danger/teammate.rb
@@ -3,11 +3,12 @@
module Gitlab
module Danger
class Teammate
- attr_reader :name, :username, :projects
+ attr_reader :name, :username, :role, :projects
def initialize(options = {})
- @name = options['name']
@username = options['username']
+ @name = options['name'] || @username
+ @role = options['role']
@projects = options['projects']
end
@@ -20,20 +21,32 @@ module Gitlab
end
# Traintainers also count as reviewers
- def reviewer?(project, category)
- capabilities(project).include?("reviewer #{category}") || traintainer?(project, category)
+ def reviewer?(project, category, labels)
+ has_capability?(project, category, :reviewer, labels) ||
+ traintainer?(project, category, labels)
end
- def traintainer?(project, category)
- capabilities(project).include?("trainee_maintainer #{category}")
+ def traintainer?(project, category, labels)
+ has_capability?(project, category, :trainee_maintainer, labels)
end
- def maintainer?(project, category)
- capabilities(project).include?("maintainer #{category}")
+ def maintainer?(project, category, labels)
+ has_capability?(project, category, :maintainer, labels)
end
private
+ def has_capability?(project, category, kind, labels)
+ case category
+ when :test
+ area = role[/Test Automation Engineer, (\w+)/, 1]
+
+ area && labels.any?(area) if kind == :reviewer
+ else
+ capabilities(project).include?("#{kind} #{category}")
+ end
+ end
+
def capabilities(project)
Array(projects.fetch(project, []))
end
diff --git a/lib/gitlab/diff/suggestions_parser.rb b/lib/gitlab/diff/suggestions_parser.rb
index c8c03d5d001..6e17ffaf6ff 100644
--- a/lib/gitlab/diff/suggestions_parser.rb
+++ b/lib/gitlab/diff/suggestions_parser.rb
@@ -10,10 +10,12 @@ module Gitlab
# Returns an array of Gitlab::Diff::Suggestion which represents each
# suggestion in the given text.
#
- def parse(text, position:, project:)
+ def parse(text, position:, project:, supports_suggestion: true)
return [] unless position.complete?
- html = Banzai.render(text, project: nil, no_original_data: true)
+ html = Banzai.render(text, project: nil,
+ no_original_data: true,
+ suggestions_filter_enabled: supports_suggestion)
doc = Nokogiri::HTML(html)
suggestion_nodes = doc.search('pre.suggestion')
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 455588f3c66..8a2e711ec4e 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -34,34 +34,6 @@ module Gitlab
TagExistsError = Class.new(StandardError)
ChecksumError = Class.new(StandardError)
- class << self
- def create_hooks(repo_path, global_hooks_path)
- local_hooks_path = File.join(repo_path, 'hooks')
- real_local_hooks_path = :not_found
-
- begin
- real_local_hooks_path = File.realpath(local_hooks_path)
- rescue Errno::ENOENT
- # real_local_hooks_path == :not_found
- end
-
- # Do nothing if hooks already exist
- unless real_local_hooks_path == File.realpath(global_hooks_path)
- if File.exist?(local_hooks_path)
- # Move the existing hooks somewhere safe
- FileUtils.mv(
- local_hooks_path,
- "#{local_hooks_path}.old.#{Time.now.to_i}")
- end
-
- # Create the hooks symlink
- FileUtils.ln_sf(global_hooks_path, local_hooks_path)
- end
-
- true
- end
- end
-
# Directory name of repo
attr_reader :name
@@ -310,6 +282,11 @@ module Gitlab
(size.to_f / 1024).round(2)
end
+ # Return git object directory size in bytes
+ def object_directory_size
+ gitaly_repository_client.get_object_directory_size.to_f * 1024
+ end
+
# Build an array of commits.
#
# Usage.
diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb
index 3f13ebeb9d0..dfff6823689 100644
--- a/lib/gitlab/git_ref_validator.rb
+++ b/lib/gitlab/git_ref_validator.rb
@@ -5,12 +5,15 @@
module Gitlab
module GitRefValidator
extend self
+
+ EXPANDED_PREFIXES = %w[refs/heads/ refs/remotes/].freeze
+ DISALLOWED_PREFIXES = %w[-].freeze
+
# Validates a given name against the git reference specification
#
# Returns true for a valid reference name, false otherwise
def validate(ref_name)
- not_allowed_prefixes = %w(refs/heads/ refs/remotes/ -)
- return false if ref_name.start_with?(*not_allowed_prefixes)
+ return false if ref_name.start_with?(*(EXPANDED_PREFIXES + DISALLOWED_PREFIXES))
return false if ref_name == 'HEAD'
begin
@@ -19,5 +22,21 @@ module Gitlab
return false
end
end
+
+ def validate_merge_request_branch(ref_name)
+ return false if ref_name.start_with?(*DISALLOWED_PREFIXES)
+
+ expanded_name = if ref_name.start_with?(*EXPANDED_PREFIXES)
+ ref_name
+ else
+ "refs/heads/#{ref_name}"
+ end
+
+ begin
+ Rugged::Reference.valid_name?(expanded_name)
+ rescue ArgumentError
+ return false
+ end
+ end
end
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 05e06eec012..e683d4e5bbe 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -33,7 +33,7 @@ module Gitlab
SERVER_FEATURE_CATFILE_CACHE = 'catfile-cache'.freeze
# Server feature flags should use '_' to separate words.
- SERVER_FEATURE_FLAGS = [SERVER_FEATURE_CATFILE_CACHE, 'delta_islands'].freeze
+ SERVER_FEATURE_FLAGS = [SERVER_FEATURE_CATFILE_CACHE].freeze
MUTEX = Mutex.new
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 68b17e86608..d8e9dccb644 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -47,6 +47,13 @@ module Gitlab
response.size
end
+ def get_object_directory_size
+ request = Gitaly::GetObjectDirectorySizeRequest.new(repository: @gitaly_repo)
+ response = GitalyClient.call(@storage, :repository_service, :get_object_directory_size, request, timeout: GitalyClient.medium_timeout)
+
+ response.size
+ end
+
def apply_gitattributes(revision)
request = Gitaly::ApplyGitattributesRequest.new(repository: @gitaly_repo, revision: encode_binary(revision))
GitalyClient.call(@storage, :repository_service, :apply_gitattributes, request, timeout: GitalyClient.fast_timeout)
diff --git a/lib/gitlab/github_import/parallel_importer.rb b/lib/gitlab/github_import/parallel_importer.rb
index 9d81441d96e..1d3541b80c7 100644
--- a/lib/gitlab/github_import/parallel_importer.rb
+++ b/lib/gitlab/github_import/parallel_importer.rb
@@ -29,29 +29,13 @@ module Gitlab
end
def execute
- jid = generate_jid
-
- # The original import JID is the JID of the RepositoryImportWorker job,
- # which will be removed once that job completes. Reusing that JID could
- # result in StuckImportJobsWorker marking the job as stuck before we get
- # to running Stage::ImportRepositoryWorker.
- #
- # We work around this by setting the JID to a custom generated one, then
- # refreshing it in the various stages whenever necessary.
- Gitlab::SidekiqStatus
- .set(jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
-
- project.import_state.update_column(:jid, jid)
+ Gitlab::Import::SetAsyncJid.set_jid(project)
Stage::ImportRepositoryWorker
.perform_async(project.id)
true
end
-
- def generate_jid
- "github-importer/#{project.id}"
- end
end
end
end
diff --git a/lib/gitlab/graphql/loaders/batch_model_loader.rb b/lib/gitlab/graphql/loaders/batch_model_loader.rb
index 5a0099dc6b1..50d3293fcbb 100644
--- a/lib/gitlab/graphql/loaders/batch_model_loader.rb
+++ b/lib/gitlab/graphql/loaders/batch_model_loader.rb
@@ -12,7 +12,7 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def find
- BatchLoader.for({ model: model_class, id: model_id }).batch do |loader_info, loader|
+ BatchLoader.for({ model: model_class, id: model_id.to_i }).batch do |loader_info, loader|
per_model = loader_info.group_by { |info| info[:model] }
per_model.each do |model, info|
ids = info.map { |i| i[:id] }
diff --git a/lib/gitlab/graphql/loaders/batch_project_statistics_loader.rb b/lib/gitlab/graphql/loaders/batch_project_statistics_loader.rb
new file mode 100644
index 00000000000..5e151f4dbd7
--- /dev/null
+++ b/lib/gitlab/graphql/loaders/batch_project_statistics_loader.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Loaders
+ class BatchProjectStatisticsLoader
+ attr_reader :project_id
+
+ def initialize(project_id)
+ @project_id = project_id
+ end
+
+ def find
+ BatchLoader.for(project_id).batch do |project_ids, loader|
+ ProjectStatistics.for_project_ids(project_ids).each do |statistics|
+ loader.call(statistics.project_id, statistics)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/query_analyzers/log_query_complexity.rb b/lib/gitlab/graphql/query_analyzers/log_query_complexity.rb
deleted file mode 100644
index 95db130dbfc..00000000000
--- a/lib/gitlab/graphql/query_analyzers/log_query_complexity.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Graphql
- module QueryAnalyzers
- class LogQueryComplexity
- class << self
- def analyzer
- GraphQL::Analysis::QueryComplexity.new do |query, complexity|
- # temporary until https://gitlab.com/gitlab-org/gitlab-ce/issues/59587
- Rails.logger.info("[GraphQL Query Complexity] #{complexity} | admin? #{query.context[:current_user]&.admin?}")
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb b/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb
new file mode 100644
index 00000000000..01b55a1667f
--- /dev/null
+++ b/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module QueryAnalyzers
+ class LoggerAnalyzer
+ COMPLEXITY_ANALYZER = GraphQL::Analysis::QueryComplexity.new { |query, complexity_value| complexity_value }
+ DEPTH_ANALYZER = GraphQL::Analysis::QueryDepth.new { |query, depth_value| depth_value }
+
+ def analyze?(query)
+ Feature.enabled?(:graphql_logging, default_enabled: true)
+ end
+
+ def initial_value(query)
+ variables = process_variables(query.provided_variables)
+ default_initial_values(query).merge({
+ query_string: query.query_string,
+ variables: variables
+ })
+ rescue => e
+ Gitlab::Sentry.track_exception(e)
+ default_initial_values(query)
+ end
+
+ def call(memo, visit_type, irep_node)
+ memo
+ end
+
+ def final_value(memo)
+ return if memo.nil?
+
+ analyzers = [COMPLEXITY_ANALYZER, DEPTH_ANALYZER]
+ complexity, depth = GraphQL::Analysis.analyze_query(memo[:query], analyzers)
+
+ memo[:depth] = depth
+ memo[:complexity] = complexity
+ memo[:duration] = duration(memo[:time_started]).round(1)
+
+ GraphqlLogger.info(memo.except!(:time_started, :query))
+ rescue => e
+ Gitlab::Sentry.track_exception(e)
+ end
+
+ private
+
+ def process_variables(variables)
+ if variables.respond_to?(:to_s)
+ variables.to_s
+ else
+ variables
+ end
+ end
+
+ def duration(time_started)
+ nanoseconds = Gitlab::Metrics::System.monotonic_time - time_started
+ nanoseconds * 1000000
+ end
+
+ def default_initial_values(query)
+ {
+ time_started: Gitlab::Metrics::System.monotonic_time,
+ query_string: nil,
+ query: query,
+ variables: nil,
+ duration: nil
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/representation/tree_entry.rb b/lib/gitlab/graphql/representation/tree_entry.rb
new file mode 100644
index 00000000000..7ea83db5876
--- /dev/null
+++ b/lib/gitlab/graphql/representation/tree_entry.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Representation
+ class TreeEntry < SimpleDelegator
+ class << self
+ def decorate(entries, repository)
+ return if entries.nil?
+
+ entries.map do |entry|
+ if entry.is_a?(TreeEntry)
+ entry
+ else
+ self.new(entry, repository)
+ end
+ end
+ end
+ end
+
+ attr_accessor :repository
+
+ def initialize(raw_entry, repository)
+ @repository = repository
+
+ super(raw_entry)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql_logger.rb b/lib/gitlab/graphql_logger.rb
new file mode 100644
index 00000000000..43d917908b6
--- /dev/null
+++ b/lib/gitlab/graphql_logger.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class GraphqlLogger < Gitlab::JsonLogger
+ def self.file_name_noext
+ 'graphql_json'
+ end
+ end
+end
diff --git a/lib/gitlab/http.rb b/lib/gitlab/http.rb
index bcd9e2be35f..db2b4dde244 100644
--- a/lib/gitlab/http.rb
+++ b/lib/gitlab/http.rb
@@ -9,9 +9,16 @@ module Gitlab
BlockedUrlError = Class.new(StandardError)
RedirectionTooDeep = Class.new(StandardError)
+ HTTP_ERRORS = [
+ SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET,
+ Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout,
+ Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError,
+ Gitlab::HTTP::RedirectionTooDeep
+ ].freeze
+
include HTTParty # rubocop:disable Gitlab/HTTParty
- connection_adapter ProxyHTTPConnectionAdapter
+ connection_adapter HTTPConnectionAdapter
def self.perform_request(http_method, path, options, &block)
super
diff --git a/lib/gitlab/proxy_http_connection_adapter.rb b/lib/gitlab/http_connection_adapter.rb
index a64cb47e77e..41eab3658bc 100644
--- a/lib/gitlab/proxy_http_connection_adapter.rb
+++ b/lib/gitlab/http_connection_adapter.rb
@@ -10,17 +10,19 @@
#
# This option will take precedence over the global setting.
module Gitlab
- class ProxyHTTPConnectionAdapter < HTTParty::ConnectionAdapter
+ class HTTPConnectionAdapter < HTTParty::ConnectionAdapter
def connection
- unless allow_local_requests?
- begin
- Gitlab::UrlBlocker.validate!(uri, allow_local_network: false)
- rescue Gitlab::UrlBlocker::BlockedUrlError => e
- raise Gitlab::HTTP::BlockedUrlError, "URL '#{uri}' is blocked: #{e.message}"
- end
+ begin
+ @uri, hostname = Gitlab::UrlBlocker.validate!(uri, allow_local_network: allow_local_requests?,
+ allow_localhost: allow_local_requests?,
+ dns_rebind_protection: dns_rebind_protection?)
+ rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ raise Gitlab::HTTP::BlockedUrlError, "URL '#{uri}' is blocked: #{e.message}"
end
- super
+ super.tap do |http|
+ http.hostname_override = hostname if hostname
+ end
end
private
@@ -29,6 +31,12 @@ module Gitlab
options.fetch(:allow_local_requests, allow_settings_local_requests?)
end
+ def dns_rebind_protection?
+ return false if Gitlab.http_proxy_env?
+
+ Gitlab::CurrentSettings.dns_rebinding_protection_enabled?
+ end
+
def allow_settings_local_requests?
Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services?
end
diff --git a/lib/gitlab/import/set_async_jid.rb b/lib/gitlab/import/set_async_jid.rb
new file mode 100644
index 00000000000..584d24045ee
--- /dev/null
+++ b/lib/gitlab/import/set_async_jid.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+# The original import JID is the JID of the RepositoryImportWorker job,
+# which will be removed once that job completes. Reusing that JID could
+# result in StuckImportJobsWorker marking the job as stuck before we get
+# to running Stage::ImportRepositoryWorker.
+#
+# We work around this by setting the JID to a custom generated one, then
+# refreshing it in the various stages whenever necessary.
+module Gitlab
+ module Import
+ module SetAsyncJid
+ def self.set_jid(project)
+ jid = generate_jid(project)
+
+ Gitlab::SidekiqStatus
+ .set(jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
+
+ project.import_state.update_column(:jid, jid)
+ end
+
+ def self.generate_jid(project)
+ "async-import/#{project.id}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/attribute_cleaner.rb b/lib/gitlab/import_export/attribute_cleaner.rb
index 93b37b7bc5f..c28a1674018 100644
--- a/lib/gitlab/import_export/attribute_cleaner.rb
+++ b/lib/gitlab/import_export/attribute_cleaner.rb
@@ -4,6 +4,7 @@ module Gitlab
module ImportExport
class AttributeCleaner
ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id']
+ PROHIBITED_REFERENCES = Regexp.union(/\Acached_markdown_version\Z/, /_id\Z/, /_html\Z/).freeze
def self.clean(*args)
new(*args).clean
@@ -24,7 +25,11 @@ module Gitlab
private
def prohibited_key?(key)
- key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key)
+ key =~ PROHIBITED_REFERENCES && !permitted_key?(key)
+ end
+
+ def permitted_key?(key)
+ ALLOWED_REFERENCES.include?(key)
end
def excluded_key?(key)
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 67952ca0f2d..e4d625b5738 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -20,7 +20,8 @@ module Gitlab
ImportSource.new('git', 'Repo by URL', nil),
ImportSource.new('gitlab_project', 'GitLab export', Gitlab::ImportExport::Importer),
ImportSource.new('gitea', 'Gitea', Gitlab::LegacyGithubImport::Importer),
- ImportSource.new('manifest', 'Manifest file', nil)
+ ImportSource.new('manifest', 'Manifest file', nil),
+ ImportSource.new('phabricator', 'Phabricator', Gitlab::PhabricatorImport::Importer)
].freeze
class << self
diff --git a/lib/gitlab/kubernetes/helm/api.rb b/lib/gitlab/kubernetes/helm/api.rb
index ff1dadf9247..978cafae9ac 100644
--- a/lib/gitlab/kubernetes/helm/api.rb
+++ b/lib/gitlab/kubernetes/helm/api.rb
@@ -24,6 +24,7 @@ module Gitlab
def uninstall(command)
namespace.ensure_exists!
+ create_config_map(command)
delete_pod!(command.pod_name)
kubeclient.create_pod(command.pod_resource)
diff --git a/lib/gitlab/lets_encrypt/challenge.rb b/lib/gitlab/lets_encrypt/challenge.rb
index 6a7f5e965c5..136268c974b 100644
--- a/lib/gitlab/lets_encrypt/challenge.rb
+++ b/lib/gitlab/lets_encrypt/challenge.rb
@@ -7,7 +7,7 @@ module Gitlab
@acme_challenge = acme_challenge
end
- delegate :url, :token, :file_content, :status, :request_validation, to: :acme_challenge
+ delegate :token, :file_content, :status, :request_validation, to: :acme_challenge
private
diff --git a/lib/gitlab/lets_encrypt/client.rb b/lib/gitlab/lets_encrypt/client.rb
index 5501f7981ec..66aea137012 100644
--- a/lib/gitlab/lets_encrypt/client.rb
+++ b/lib/gitlab/lets_encrypt/client.rb
@@ -3,6 +3,8 @@
module Gitlab
module LetsEncrypt
class Client
+ include Gitlab::Utils::StrongMemoize
+
PRODUCTION_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory'
STAGING_DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory'
@@ -35,6 +37,8 @@ module Gitlab
def enabled?
return false unless Feature.enabled?(:pages_auto_ssl)
+ return false unless private_key
+
Gitlab::CurrentSettings.lets_encrypt_terms_of_service_accepted
end
@@ -45,7 +49,11 @@ module Gitlab
end
def private_key
- @private_key ||= OpenSSL::PKey.read(Gitlab::CurrentSettings.lets_encrypt_private_key)
+ strong_memoize(:private_key) do
+ private_key_string = Gitlab::CurrentSettings.lets_encrypt_private_key
+ private_key_string ||= generate_private_key
+ OpenSSL::PKey.read(private_key_string) if private_key_string
+ end
end
def admin_email
@@ -69,6 +77,19 @@ module Gitlab
STAGING_DIRECTORY_URL
end
end
+
+ def generate_private_key
+ return if Gitlab::Database.read_only?
+
+ application_settings = Gitlab::CurrentSettings.current_application_settings
+ application_settings.with_lock do
+ unless application_settings.lets_encrypt_private_key
+ application_settings.update(lets_encrypt_private_key: OpenSSL::PKey::RSA.new(4096).to_pem)
+ end
+
+ application_settings.lets_encrypt_private_key
+ end
+ end
end
end
end
diff --git a/lib/gitlab/lets_encrypt/order.rb b/lib/gitlab/lets_encrypt/order.rb
index 5109b5e9843..9c2365f98a8 100644
--- a/lib/gitlab/lets_encrypt/order.rb
+++ b/lib/gitlab/lets_encrypt/order.rb
@@ -13,7 +13,16 @@ module Gitlab
::Gitlab::LetsEncrypt::Challenge.new(challenge)
end
- delegate :url, :status, to: :acme_order
+ def request_certificate(domain:, private_key:)
+ csr = ::Acme::Client::CertificateRequest.new(
+ private_key: OpenSSL::PKey.read(private_key),
+ subject: { common_name: domain }
+ )
+
+ acme_order.finalize(csr: csr)
+ end
+
+ delegate :url, :status, :expires, :certificate, to: :acme_order
private
diff --git a/lib/gitlab/markdown_cache.rb b/lib/gitlab/markdown_cache.rb
new file mode 100644
index 00000000000..0354c710a3f
--- /dev/null
+++ b/lib/gitlab/markdown_cache.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module MarkdownCache
+ # Increment this number every time the renderer changes its output
+ CACHE_COMMONMARK_VERSION_START = 10
+ CACHE_COMMONMARK_VERSION = 16
+
+ BaseError = Class.new(StandardError)
+ UnsupportedClassError = Class.new(BaseError)
+ end
+end
diff --git a/lib/gitlab/markdown_cache/active_record/extension.rb b/lib/gitlab/markdown_cache/active_record/extension.rb
new file mode 100644
index 00000000000..f3abe631779
--- /dev/null
+++ b/lib/gitlab/markdown_cache/active_record/extension.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module MarkdownCache
+ module ActiveRecord
+ module Extension
+ extend ActiveSupport::Concern
+
+ included do
+ # Using before_update here conflicts with elasticsearch-model somehow
+ before_create :refresh_markdown_cache, if: :invalidated_markdown_cache?
+ before_update :refresh_markdown_cache, if: :invalidated_markdown_cache?
+ end
+
+ # Always exclude _html fields from attributes (including serialization).
+ # They contain unredacted HTML, which would be a security issue
+ def attributes
+ attrs = super
+ html_fields = cached_markdown_fields.html_fields
+ whitelisted = cached_markdown_fields.html_fields_whitelisted
+ exclude_fields = html_fields - whitelisted
+
+ attrs.except!(*exclude_fields)
+ attrs.delete('cached_markdown_version') if whitelisted.empty?
+
+ attrs
+ end
+
+ def changed_markdown_fields
+ changed_attributes.keys.map(&:to_s) & cached_markdown_fields.markdown_fields.map(&:to_s)
+ end
+
+ def write_markdown_field(field_name, value)
+ write_attribute(field_name, value)
+ end
+
+ def markdown_field_changed?(field_name)
+ attribute_changed?(field_name)
+ end
+
+ def save_markdown(updates)
+ return unless persisted? && Gitlab::Database.read_write?
+
+ update_columns(updates)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown_cache/field_data.rb b/lib/gitlab/markdown_cache/field_data.rb
new file mode 100644
index 00000000000..14622c0f186
--- /dev/null
+++ b/lib/gitlab/markdown_cache/field_data.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module MarkdownCache
+ # Knows about the relationship between markdown and html field names, and
+ # stores the rendering contexts for the latter
+ class FieldData
+ def initialize
+ @data = {}
+ end
+
+ delegate :[], :[]=, to: :@data
+
+ def markdown_fields
+ @data.keys
+ end
+
+ def html_field(markdown_field)
+ "#{markdown_field}_html"
+ end
+
+ def html_fields
+ @html_fields ||= markdown_fields.map { |field| html_field(field) }
+ end
+
+ def html_fields_whitelisted
+ markdown_fields.each_with_object([]) do |field, fields|
+ if @data[field].fetch(:whitelisted, false)
+ fields << html_field(field)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown_cache/redis/extension.rb b/lib/gitlab/markdown_cache/redis/extension.rb
new file mode 100644
index 00000000000..97fc23343b4
--- /dev/null
+++ b/lib/gitlab/markdown_cache/redis/extension.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module MarkdownCache
+ module Redis
+ module Extension
+ extend ActiveSupport::Concern
+
+ attr_reader :cached_markdown_version
+
+ class_methods do
+ def cache_markdown_field(markdown_field, context = {})
+ super
+
+ # define the `[field]_html` accessor
+ html_field = cached_markdown_fields.html_field(markdown_field)
+ define_method(html_field) do
+ load_cached_markdown unless markdown_data_loaded?
+
+ instance_variable_get("@#{html_field}")
+ end
+ end
+ end
+
+ private
+
+ def save_markdown(updates)
+ markdown_store.save(updates)
+ end
+
+ def write_markdown_field(field_name, value)
+ instance_variable_set("@#{field_name}", value)
+ end
+
+ def markdown_field_changed?(field_name)
+ false
+ end
+
+ def changed_markdown_fields
+ []
+ end
+
+ def cached_markdown
+ @cached_data ||= markdown_store.read
+ end
+
+ def load_cached_markdown
+ cached_markdown.each do |field_name, value|
+ write_markdown_field(field_name, value)
+ end
+ end
+
+ def markdown_data_loaded?
+ cached_markdown_version.present? || markdown_store.loaded?
+ end
+
+ def markdown_store
+ @store ||= Gitlab::MarkdownCache::Redis::Store.new(self)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown_cache/redis/store.rb b/lib/gitlab/markdown_cache/redis/store.rb
new file mode 100644
index 00000000000..0f954404808
--- /dev/null
+++ b/lib/gitlab/markdown_cache/redis/store.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module MarkdownCache
+ module Redis
+ class Store
+ EXPIRES_IN = 1.day
+
+ def initialize(subject)
+ @subject = subject
+ @loaded = false
+ end
+
+ def save(updates)
+ @loaded = false
+
+ Gitlab::Redis::Cache.with do |r|
+ r.mapped_hmset(markdown_cache_key, updates)
+ r.expire(markdown_cache_key, EXPIRES_IN)
+ end
+ end
+
+ def read
+ @loaded = true
+
+ results = Gitlab::Redis::Cache.with do |r|
+ r.mapped_hmget(markdown_cache_key, *fields)
+ end
+ # The value read from redis is a string, so we're converting it back
+ # to an int.
+ results[:cached_markdown_version] = results[:cached_markdown_version].to_i
+ results
+ end
+
+ def loaded?
+ @loaded
+ end
+
+ private
+
+ def fields
+ @fields ||= @subject.cached_markdown_fields.html_fields + [:cached_markdown_version]
+ end
+
+ def markdown_cache_key
+ unless @subject.respond_to?(:cache_key)
+ raise Gitlab::MarkdownCache::UnsupportedClassError,
+ "This class has no cache_key to use for caching"
+ end
+
+ "markdown_cache:#{@subject.cache_key}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/samplers/puma_sampler.rb b/lib/gitlab/metrics/samplers/puma_sampler.rb
index 87669b253bc..25e40c70230 100644
--- a/lib/gitlab/metrics/samplers/puma_sampler.rb
+++ b/lib/gitlab/metrics/samplers/puma_sampler.rb
@@ -51,10 +51,11 @@ module Gitlab
set_master_metrics(stats)
stats['worker_status'].each do |worker|
+ last_status = worker['last_status']
labels = { worker: "worker_#{worker['index']}" }
metrics[:puma_phase].set(labels, worker['phase'])
- set_worker_metrics(worker['last_status'], labels)
+ set_worker_metrics(last_status, labels) if last_status.present?
end
end
diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb
index 18a69321905..17eacbd21d8 100644
--- a/lib/gitlab/metrics/samplers/ruby_sampler.rb
+++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb
@@ -23,25 +23,32 @@ module Gitlab
end
def init_metrics
- metrics = {}
- metrics[:sampler_duration] = ::Gitlab::Metrics.counter(with_prefix(:sampler, :duration_seconds_total), 'Sampler time', labels)
- metrics[:total_time] = ::Gitlab::Metrics.counter(with_prefix(:gc, :duration_seconds_total), 'Total GC time', labels)
+ metrics = {
+ file_descriptors: ::Gitlab::Metrics.gauge(with_prefix(:file, :descriptors), 'File descriptors used', labels, :livesum),
+ memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:memory, :bytes), 'Memory used', labels, :livesum),
+ process_cpu_seconds_total: ::Gitlab::Metrics.gauge(with_prefix(:process, :cpu_seconds_total), 'Process CPU seconds total'),
+ process_max_fds: ::Gitlab::Metrics.gauge(with_prefix(:process, :max_fds), 'Process max fds'),
+ process_resident_memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:process, :resident_memory_bytes), 'Memory used', labels, :livesum),
+ process_start_time_seconds: ::Gitlab::Metrics.gauge(with_prefix(:process, :start_time_seconds), 'Process start time seconds'),
+ sampler_duration: ::Gitlab::Metrics.counter(with_prefix(:sampler, :duration_seconds_total), 'Sampler time', labels),
+ total_time: ::Gitlab::Metrics.counter(with_prefix(:gc, :duration_seconds_total), 'Total GC time', labels)
+ }
+
GC.stat.keys.each do |key|
metrics[key] = ::Gitlab::Metrics.gauge(with_prefix(:gc_stat, key), to_doc_string(key), labels, :livesum)
end
- metrics[:memory_usage] = ::Gitlab::Metrics.gauge(with_prefix(:memory, :bytes), 'Memory used', labels, :livesum)
- metrics[:file_descriptors] = ::Gitlab::Metrics.gauge(with_prefix(:file, :descriptors), 'File descriptors used', labels, :livesum)
-
metrics
end
def sample
start_time = System.monotonic_time
- metrics[:memory_usage].set(labels.merge(worker_label), System.memory_usage)
metrics[:file_descriptors].set(labels.merge(worker_label), System.file_descriptor_count)
-
+ metrics[:process_cpu_seconds_total].set(labels.merge(worker_label), ::Gitlab::Metrics::System.cpu_time)
+ metrics[:process_max_fds].set(labels.merge(worker_label), ::Gitlab::Metrics::System.max_open_file_descriptors)
+ metrics[:process_start_time_seconds].set(labels.merge(worker_label), ::Gitlab::Metrics::System.process_start_time)
+ set_memory_usage_metrics
sample_gc
metrics[:sampler_duration].increment(labels, System.monotonic_time - start_time)
@@ -61,11 +68,19 @@ module Gitlab
metrics[:total_time].increment(labels, GC::Profiler.total_time)
end
+ def set_memory_usage_metrics
+ memory_usage = System.memory_usage
+ memory_labels = labels.merge(worker_label)
+
+ metrics[:memory_bytes].set(memory_labels, memory_usage)
+ metrics[:process_resident_memory_bytes].set(memory_labels, memory_usage)
+ end
+
def worker_label
+ return { worker: 'sidekiq' } if Sidekiq.server?
return {} unless defined?(Unicorn::Worker)
worker_no = ::Prometheus::Client::Support::Unicorn.worker_id
-
if worker_no
{ worker: worker_no }
else
diff --git a/lib/gitlab/metrics/samplers/unicorn_sampler.rb b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
index bec64e864b3..9af7e0afed4 100644
--- a/lib/gitlab/metrics/samplers/unicorn_sampler.rb
+++ b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
@@ -4,16 +4,16 @@ module Gitlab
module Metrics
module Samplers
class UnicornSampler < BaseSampler
- def initialize(interval)
- super(interval)
+ def metrics
+ @metrics ||= init_metrics
end
- def unicorn_active_connections
- @unicorn_active_connections ||= ::Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max)
- end
-
- def unicorn_queued_connections
- @unicorn_queued_connections ||= ::Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max)
+ def init_metrics
+ {
+ unicorn_active_connections: ::Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max),
+ unicorn_queued_connections: ::Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max),
+ unicorn_workers: ::Gitlab::Metrics.gauge(:unicorn_workers, 'Unicorn workers')
+ }
end
def enabled?
@@ -23,14 +23,13 @@ module Gitlab
def sample
Raindrops::Linux.tcp_listener_stats(tcp_listeners).each do |addr, stats|
- unicorn_active_connections.set({ socket_type: 'tcp', socket_address: addr }, stats.active)
- unicorn_queued_connections.set({ socket_type: 'tcp', socket_address: addr }, stats.queued)
+ set_unicorn_connection_metrics('tcp', addr, stats)
end
-
Raindrops::Linux.unix_listener_stats(unix_listeners).each do |addr, stats|
- unicorn_active_connections.set({ socket_type: 'unix', socket_address: addr }, stats.active)
- unicorn_queued_connections.set({ socket_type: 'unix', socket_address: addr }, stats.queued)
+ set_unicorn_connection_metrics('unix', addr, stats)
end
+
+ metrics[:unicorn_workers].set({}, unicorn_workers_count)
end
private
@@ -39,6 +38,13 @@ module Gitlab
@tcp_listeners ||= Unicorn.listener_names.grep(%r{\A[^/]+:\d+\z})
end
+ def set_unicorn_connection_metrics(type, addr, stats)
+ labels = { socket_type: type, socket_address: addr }
+
+ metrics[:unicorn_active_connections].set(labels, stats.active)
+ metrics[:unicorn_queued_connections].set(labels, stats.queued)
+ end
+
def unix_listeners
@unix_listeners ||= Unicorn.listener_names - tcp_listeners
end
@@ -46,6 +52,10 @@ module Gitlab
def unicorn_with_listeners?
defined?(Unicorn) && Unicorn.listener_names.any?
end
+
+ def unicorn_workers_count
+ `pgrep -f '[u]nicorn_rails worker.+ #{Rails.root.to_s}'`.split.count
+ end
end
end
end
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index 426496855e3..33c0de91c11 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -23,6 +23,22 @@ module Gitlab
def self.file_descriptor_count
Dir.glob('/proc/self/fd/*').length
end
+
+ def self.max_open_file_descriptors
+ match = File.read('/proc/self/limits').match(/Max open files\s*(\d+)/)
+
+ return unless match && match[1]
+
+ match[1].to_i
+ end
+
+ def self.process_start_time
+ fields = File.read('/proc/self/stat').split
+
+ # fields[21] is linux proc stat field "(22) starttime".
+ # The value is expressed in clock ticks, divide by clock ticks for seconds.
+ ( fields[21].to_i || 0 ) / clk_tck
+ end
else
def self.memory_usage
0.0
@@ -31,6 +47,14 @@ module Gitlab
def self.file_descriptor_count
0
end
+
+ def self.max_open_file_descriptors
+ 0
+ end
+
+ def self.process_start_time
+ 0
+ end
end
# THREAD_CPUTIME is not supported on OS X
@@ -59,6 +83,10 @@ module Gitlab
def self.monotonic_time
Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second)
end
+
+ def self.clk_tck
+ @clk_tck ||= `getconf CLK_TCK`.to_i
+ end
end
end
end
diff --git a/lib/gitlab/omniauth_initializer.rb b/lib/gitlab/omniauth_initializer.rb
index 2a2083ebae0..83204fa5d18 100644
--- a/lib/gitlab/omniauth_initializer.rb
+++ b/lib/gitlab/omniauth_initializer.rb
@@ -63,6 +63,12 @@ module Gitlab
{ remote_sign_out_handler: authentiq_signout_handler }
when 'shibboleth'
{ fail_with_empty_uid: true }
+ when 'openid_connect'
+ # If a name argument is omitted, OmniAuth will expect that the
+ # matching route is /auth/users/openidconnect instead of
+ # /auth/users/openid_connect because of
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/62208#note_178780341.
+ { name: 'openid_connect' }
else
{}
end
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index aa2c1ac9cef..a07b1246bee 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -58,7 +58,6 @@ module Gitlab
uploads
users
v2
- visual-review-toolbar.js
].freeze
# This list should contain all words following `/*namespace_id/:project_id` in
diff --git a/lib/gitlab/performance_bar/peek_query_tracker.rb b/lib/gitlab/performance_bar/peek_query_tracker.rb
index 16c16aa0265..3a27e26eaba 100644
--- a/lib/gitlab/performance_bar/peek_query_tracker.rb
+++ b/lib/gitlab/performance_bar/peek_query_tracker.rb
@@ -27,15 +27,16 @@ module Gitlab
subscribe('sql.active_record') do |_, start, finish, _, data|
if Gitlab::SafeRequestStore.store[:peek_enabled]
unless data[:cached]
- track_query(data[:sql].strip, data[:binds], start, finish)
+ backtrace = Gitlab::Profiler.clean_backtrace(caller)
+ track_query(data[:sql].strip, data[:binds], backtrace, start, finish)
end
end
end
end
- def track_query(raw_query, bindings, start, finish)
+ def track_query(raw_query, bindings, backtrace, start, finish)
duration = (finish - start) * 1000.0
- query_info = { duration: duration.round(3), sql: raw_query }
+ query_info = { duration: duration.round(3), sql: raw_query, backtrace: backtrace }
PEEK_DB_CLIENT.query_details << query_info
end
diff --git a/lib/gitlab/phabricator_import.rb b/lib/gitlab/phabricator_import.rb
new file mode 100644
index 00000000000..3885a9934d5
--- /dev/null
+++ b/lib/gitlab/phabricator_import.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module PhabricatorImport
+ BaseError = Class.new(StandardError)
+
+ def self.available?
+ Feature.enabled?(:phabricator_import) &&
+ Gitlab::CurrentSettings.import_sources.include?('phabricator')
+ end
+ end
+end
diff --git a/lib/gitlab/phabricator_import/base_worker.rb b/lib/gitlab/phabricator_import/base_worker.rb
new file mode 100644
index 00000000000..b69c65e78f8
--- /dev/null
+++ b/lib/gitlab/phabricator_import/base_worker.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+# All workers within a Phabricator import should inherit from this worker and
+# implement the `#import` method. The jobs should then be scheduled using the
+# `.schedule` class method instead of `.perform_async`
+#
+# Doing this makes sure that only one job of that type is running at the same time
+# for a certain project. This will avoid deadlocks. When a job is already running
+# we'll wait for it for 10 times 5 seconds to restart. If the running job hasn't
+# finished, by then, we'll retry in 30 seconds.
+#
+# It also makes sure that we keep the import state of the project up to date:
+# - It keeps track of the jobs so we know how many jobs are running for the
+# project
+# - It refreshes the import jid, so it doesn't get cleaned up by the
+# `StuckImportJobsWorker`
+# - It marks the import as failed if a job failed to many times
+# - It marks the import as finished when all remaining jobs are done
+module Gitlab
+ module PhabricatorImport
+ class BaseWorker
+ include ApplicationWorker
+ include ProjectImportOptions # This marks the project as failed after too many tries
+ include Gitlab::ExclusiveLeaseHelpers
+
+ class << self
+ def schedule(project_id, *args)
+ perform_async(project_id, *args)
+ add_job(project_id)
+ end
+
+ def add_job(project_id)
+ worker_state(project_id).add_job
+ end
+
+ def remove_job(project_id)
+ worker_state(project_id).remove_job
+ end
+
+ def worker_state(project_id)
+ Gitlab::PhabricatorImport::WorkerState.new(project_id)
+ end
+ end
+
+ def perform(project_id, *args)
+ in_lock("#{self.class.name.underscore}/#{project_id}/#{args}", ttl: 2.hours, sleep_sec: 5.seconds) do
+ project = Project.find_by_id(project_id)
+ next unless project
+
+ # Bail if the import job already failed
+ next unless project.import_state&.in_progress?
+
+ project.import_state.refresh_jid_expiration
+
+ import(project, *args)
+
+ # If this is the last running job, finish the import
+ project.after_import if self.class.worker_state(project_id).running_count < 2
+
+ self.class.remove_job(project_id)
+ end
+ rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
+ # Reschedule a job if there was already a running one
+ # Running them at the same time could cause a deadlock updating the same
+ # resource
+ self.class.perform_in(30.seconds, project_id, *args)
+ end
+
+ private
+
+ def import(project, *args)
+ importer_class.new(project, *args).execute
+ end
+
+ def importer_class
+ raise NotImplementedError, "Implement `#{__method__}` on #{self.class}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/phabricator_import/cache/map.rb b/lib/gitlab/phabricator_import/cache/map.rb
new file mode 100644
index 00000000000..fa8b37b20ca
--- /dev/null
+++ b/lib/gitlab/phabricator_import/cache/map.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+module Gitlab
+ module PhabricatorImport
+ module Cache
+ class Map
+ def initialize(project)
+ @project = project
+ end
+
+ def get_gitlab_model(phabricator_id)
+ cached_info = get(phabricator_id)
+ return unless cached_info[:classname] && cached_info[:database_id]
+
+ cached_info[:classname].constantize.find_by_id(cached_info[:database_id])
+ end
+
+ def set_gitlab_model(object, phabricator_id)
+ set(object.class, object.id, phabricator_id)
+ end
+
+ private
+
+ attr_reader :project
+
+ def set(klass_name, object_id, phabricator_id)
+ key = cache_key_for_phabricator_id(phabricator_id)
+
+ redis.with do |r|
+ r.multi do |multi|
+ multi.mapped_hmset(key,
+ { classname: klass_name, database_id: object_id })
+ multi.expire(key, timeout)
+ end
+ end
+ end
+
+ def get(phabricator_id)
+ key = cache_key_for_phabricator_id(phabricator_id)
+
+ redis.with do |r|
+ r.pipelined do |pipe|
+ # Extend the TTL when a key was
+ pipe.expire(key, timeout)
+ pipe.mapped_hmget(key, :classname, :database_id)
+ end.last
+ end
+ end
+
+ def cache_key_for_phabricator_id(phabricator_id)
+ "#{Redis::Cache::CACHE_NAMESPACE}/phabricator-import/#{project.id}/#{phabricator_id}"
+ end
+
+ def redis
+ Gitlab::Redis::Cache
+ end
+
+ def timeout
+ # Setting the timeout to the same one as we do for clearing stuck jobs
+ # this makes sure all cache is available while the import is running.
+ StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/phabricator_import/conduit.rb b/lib/gitlab/phabricator_import/conduit.rb
new file mode 100644
index 00000000000..4c64d737389
--- /dev/null
+++ b/lib/gitlab/phabricator_import/conduit.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+module Gitlab
+ module PhabricatorImport
+ module Conduit
+ ApiError = Class.new(Gitlab::PhabricatorImport::BaseError)
+ ResponseError = Class.new(ApiError)
+ end
+ end
+end
diff --git a/lib/gitlab/phabricator_import/conduit/client.rb b/lib/gitlab/phabricator_import/conduit/client.rb
new file mode 100644
index 00000000000..4469a3f5849
--- /dev/null
+++ b/lib/gitlab/phabricator_import/conduit/client.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+module Gitlab
+ module PhabricatorImport
+ module Conduit
+ class Client
+ def initialize(phabricator_url, api_token)
+ @phabricator_url = phabricator_url
+ @api_token = api_token
+ end
+
+ def get(path, params: {})
+ response = Gitlab::HTTP.get(build_url(path), body: build_params(params), headers: headers)
+ Response.parse!(response)
+ rescue *Gitlab::HTTP::HTTP_ERRORS => e
+ # Wrap all errors from the API into an API-error.
+ raise ApiError.new(e)
+ end
+
+ private
+
+ attr_reader :phabricator_url, :api_token
+
+ def headers
+ { "Accept" => 'application/json' }
+ end
+
+ def build_url(path)
+ URI.join(phabricator_url, '/api/', path).to_s
+ end
+
+ def build_params(params)
+ params = params.dup
+ params.compact!
+ params.reverse_merge!("api.token" => api_token)
+
+ CGI.unescape(params.to_query)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/phabricator_import/conduit/maniphest.rb b/lib/gitlab/phabricator_import/conduit/maniphest.rb
new file mode 100644
index 00000000000..848b71e49e7
--- /dev/null
+++ b/lib/gitlab/phabricator_import/conduit/maniphest.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+module Gitlab
+ module PhabricatorImport
+ module Conduit
+ class Maniphest
+ def initialize(phabricator_url:, api_token:)
+ @client = Client.new(phabricator_url, api_token)
+ end
+
+ def tasks(after: nil)
+ TasksResponse.new(get_tasks(after))
+ end
+
+ private
+
+ def get_tasks(after)
+ client.get('maniphest.search',
+ params: {
+ after: after,
+ attachments: { projects: 1, subscribers: 1, columns: 1 }
+ })
+ end
+
+ attr_reader :client
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/phabricator_import/conduit/pagination.rb b/lib/gitlab/phabricator_import/conduit/pagination.rb
new file mode 100644
index 00000000000..5f54cccdbc8
--- /dev/null
+++ b/lib/gitlab/phabricator_import/conduit/pagination.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+module Gitlab
+ module PhabricatorImport
+ module Conduit
+ class Pagination
+ def initialize(cursor_json)
+ @cursor_json = cursor_json
+ end
+
+ def has_next_page?
+ next_page.present?
+ end
+
+ def next_page
+ cursor_json["after"]
+ end
+
+ private
+
+ attr_reader :cursor_json
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/phabricator_import/conduit/response.rb b/lib/gitlab/phabricator_import/conduit/response.rb
new file mode 100644
index 00000000000..6053ecfbd5e
--- /dev/null
+++ b/lib/gitlab/phabricator_import/conduit/response.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+module Gitlab
+ module PhabricatorImport
+ module Conduit
+ class Response
+ def self.parse!(http_response)
+ unless http_response.success?
+ raise Gitlab::PhabricatorImport::Conduit::ResponseError,
+ "Phabricator responded with #{http_response.status}"
+ end
+
+ response = new(JSON.parse(http_response.body))
+
+ unless response.success?
+ raise ResponseError,
+ "Phabricator Error: #{response.error_code}: #{response.error_info}"
+ end
+
+ response
+ rescue JSON::JSONError => e
+ raise ResponseError.new(e)
+ end
+
+ def initialize(json)
+ @json = json
+ end
+
+ def success?
+ error_code.nil?
+ end
+
+ def error_code
+ json['error_code']
+ end
+
+ def error_info
+ json['error_info']
+ end
+
+ def data
+ json_result&.fetch('data')
+ end
+
+ def pagination
+ return unless cursor_info = json_result&.fetch('cursor')
+
+ @pagination ||= Pagination.new(cursor_info)
+ end
+
+ private
+
+ attr_reader :json
+
+ def json_result
+ json['result']
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/phabricator_import/conduit/tasks_response.rb b/lib/gitlab/phabricator_import/conduit/tasks_response.rb
new file mode 100644
index 00000000000..cbcf7259fb2
--- /dev/null
+++ b/lib/gitlab/phabricator_import/conduit/tasks_response.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+module Gitlab
+ module PhabricatorImport
+ module Conduit
+ class TasksResponse
+ def initialize(conduit_response)
+ @conduit_response = conduit_response
+ end
+
+ delegate :pagination, to: :conduit_response
+
+ def tasks
+ @tasks ||= conduit_response.data.map do |task_json|
+ Gitlab::PhabricatorImport::Representation::Task.new(task_json)
+ end
+ end
+
+ private
+
+ attr_reader :conduit_response
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/phabricator_import/import_tasks_worker.rb b/lib/gitlab/phabricator_import/import_tasks_worker.rb
new file mode 100644
index 00000000000..c36954a8d41
--- /dev/null
+++ b/lib/gitlab/phabricator_import/import_tasks_worker.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+module Gitlab
+ module PhabricatorImport
+ class ImportTasksWorker < BaseWorker
+ def importer_class
+ Gitlab::PhabricatorImport::Issues::Importer
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/phabricator_import/importer.rb b/lib/gitlab/phabricator_import/importer.rb
new file mode 100644
index 00000000000..c1797f4027e
--- /dev/null
+++ b/lib/gitlab/phabricator_import/importer.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module PhabricatorImport
+ class Importer
+ def self.async?
+ true
+ end
+
+ def self.imports_repository?
+ # This does not really import a repository, but we want to skip all
+ # repository related tasks in the `Projects::ImportService`
+ true
+ end
+
+ def initialize(project)
+ @project = project
+ end
+
+ def execute
+ Gitlab::Import::SetAsyncJid.set_jid(project)
+ schedule_first_tasks_page
+
+ true
+ rescue => e
+ fail_import(e.message)
+
+ false
+ end
+
+ private
+
+ attr_reader :project
+
+ def schedule_first_tasks_page
+ ImportTasksWorker.schedule(project.id)
+ end
+
+ def fail_import(message)
+ project.import_state.mark_as_failed(message)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/phabricator_import/issues/importer.rb b/lib/gitlab/phabricator_import/issues/importer.rb
new file mode 100644
index 00000000000..a58438452ff
--- /dev/null
+++ b/lib/gitlab/phabricator_import/issues/importer.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+module Gitlab
+ module PhabricatorImport
+ module Issues
+ class Importer
+ def initialize(project, after = nil)
+ @project, @after = project, after
+ end
+
+ def execute
+ schedule_next_batch
+
+ tasks_response.tasks.each do |task|
+ TaskImporter.new(project, task).execute
+ end
+ end
+
+ private
+
+ attr_reader :project, :after
+
+ def schedule_next_batch
+ return unless tasks_response.pagination.has_next_page?
+
+ Gitlab::PhabricatorImport::ImportTasksWorker
+ .schedule(project.id, tasks_response.pagination.next_page)
+ end
+
+ def tasks_response
+ @tasks_response ||= client.tasks(after: after)
+ end
+
+ def client
+ @client ||=
+ Gitlab::PhabricatorImport::Conduit::Maniphest
+ .new(phabricator_url: project.import_data.data['phabricator_url'],
+ api_token: project.import_data.credentials[:api_token])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/phabricator_import/issues/task_importer.rb b/lib/gitlab/phabricator_import/issues/task_importer.rb
new file mode 100644
index 00000000000..40d4392cbc1
--- /dev/null
+++ b/lib/gitlab/phabricator_import/issues/task_importer.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+module Gitlab
+ module PhabricatorImport
+ module Issues
+ class TaskImporter
+ def initialize(project, task)
+ @project, @task = project, task
+ end
+
+ def execute
+ # TODO: get the user from the project namespace from the username loaded by Phab-id
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/60565
+ issue.author = User.ghost
+
+ # TODO: Reformat the description with attachments, escaping accidental
+ # links and add attachments
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/60603
+ issue.assign_attributes(task.issue_attributes)
+
+ save!
+
+ issue
+ end
+
+ private
+
+ attr_reader :project, :task
+
+ def save!
+ # Just avoiding an extra redis call, we've already updated the expiry
+ # when reading the id from the map
+ was_persisted = issue.persisted?
+
+ issue.save! if issue.changed?
+
+ object_map.set_gitlab_model(issue, task.phabricator_id) unless was_persisted
+ end
+
+ def issue
+ @issue ||= find_issue_by_phabricator_id(task.phabricator_id) ||
+ project.issues.new
+ end
+
+ def find_issue_by_phabricator_id(phabricator_id)
+ object_map.get_gitlab_model(phabricator_id)
+ end
+
+ def object_map
+ Gitlab::PhabricatorImport::Cache::Map.new(project)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/phabricator_import/project_creator.rb b/lib/gitlab/phabricator_import/project_creator.rb
new file mode 100644
index 00000000000..b37a5b44980
--- /dev/null
+++ b/lib/gitlab/phabricator_import/project_creator.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module PhabricatorImport
+ class ProjectCreator
+ def initialize(current_user, params)
+ @current_user = current_user
+ @params = params.dup
+ end
+
+ def execute
+ return unless import_url.present? && api_token.present?
+
+ project = Projects::CreateService.new(current_user, create_params).execute
+ return project unless project.persisted?
+
+ project.project_feature.update!(project_feature_attributes)
+
+ project
+ end
+
+ private
+
+ attr_reader :current_user, :params
+
+ def create_params
+ {
+ name: project_name,
+ path: project_path,
+ namespace_id: namespace_id,
+ import_type: 'phabricator',
+ import_url: Project::UNKNOWN_IMPORT_URL,
+ import_data: import_data
+ }
+ end
+
+ def project_name
+ params[:name]
+ end
+
+ def project_path
+ params[:path]
+ end
+
+ def namespace_id
+ params[:namespace_id] || current_user.namespace_id
+ end
+
+ def import_url
+ params[:phabricator_server_url]
+ end
+
+ def api_token
+ params[:api_token]
+ end
+
+ def project_feature_attributes
+ @project_features_attributes ||= begin
+ # everything disabled except for issues
+ ProjectFeature::FEATURES.map do |feature|
+ [ProjectFeature.access_level_attribute(feature), ProjectFeature::DISABLED]
+ end.to_h.merge(ProjectFeature.access_level_attribute(:issues) => ProjectFeature::ENABLED)
+ end
+ end
+
+ def import_data
+ {
+ data: {
+ phabricator_url: import_url
+ },
+ credentials: {
+ api_token: params.fetch(:api_token)
+ }
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/phabricator_import/representation/task.rb b/lib/gitlab/phabricator_import/representation/task.rb
new file mode 100644
index 00000000000..6aedc71b626
--- /dev/null
+++ b/lib/gitlab/phabricator_import/representation/task.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+module Gitlab
+ module PhabricatorImport
+ module Representation
+ class Task
+ def initialize(json)
+ @json = json
+ end
+
+ def phabricator_id
+ json['phid']
+ end
+
+ def issue_attributes
+ @issue_attributes ||= {
+ title: issue_title,
+ description: issue_description,
+ state: issue_state,
+ created_at: issue_created_at,
+ closed_at: issue_closed_at
+ }
+ end
+
+ private
+
+ attr_reader :json
+
+ def issue_title
+ # The 255 limit is the validation we impose on the Issue title in
+ # Issuable
+ @issue_title ||= json['fields']['name'].truncate(255)
+ end
+
+ def issue_description
+ json['fields']['description']['raw']
+ end
+
+ def issue_state
+ issue_closed_at.present? ? :closed : :opened
+ end
+
+ def issue_created_at
+ return unless json['fields']['dateCreated']
+
+ @issue_created_at ||= cast_datetime(json['fields']['dateCreated'])
+ end
+
+ def issue_closed_at
+ return unless json['fields']['dateClosed']
+
+ @issue_closed_at ||= cast_datetime(json['fields']['dateClosed'])
+ end
+
+ def cast_datetime(value)
+ Time.at(value.to_i)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/phabricator_import/worker_state.rb b/lib/gitlab/phabricator_import/worker_state.rb
new file mode 100644
index 00000000000..38829e34509
--- /dev/null
+++ b/lib/gitlab/phabricator_import/worker_state.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+module Gitlab
+ module PhabricatorImport
+ class WorkerState
+ def initialize(project_id)
+ @project_id = project_id
+ end
+
+ def add_job
+ redis.with do |r|
+ r.pipelined do |pipe|
+ pipe.incr(all_jobs_key)
+ pipe.expire(all_jobs_key, timeout)
+ end
+ end
+ end
+
+ def remove_job
+ redis.with do |r|
+ r.decr(all_jobs_key)
+ end
+ end
+
+ def running_count
+ redis.with { |r| r.get(all_jobs_key) }.to_i
+ end
+
+ private
+
+ attr_reader :project_id
+
+ def redis
+ Gitlab::Redis::SharedState
+ end
+
+ def all_jobs_key
+ @all_jobs_key ||= "phabricator-import/jobs/project-#{project_id}/job-count"
+ end
+
+ def timeout
+ # Make sure we get rid of all the information after a job is marked
+ # as failed/succeeded
+ StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 78337518988..0f3b97e2317 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -138,6 +138,12 @@ module Gitlab
project
end
+ def filter_milestones_by_project(milestones)
+ return Milestone.none unless Ability.allowed?(@current_user, :read_milestone, @project)
+
+ milestones.where(project_id: project.id) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
def repository_project_ref
@repository_project_ref ||= repository_ref || project.default_branch
end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 4a097a00101..3daa03d01d6 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -85,6 +85,10 @@ module Gitlab
UsersFinder.new(current_user, search: query).execute
end
+ def display_options(_scope)
+ {}
+ end
+
private
def projects
@@ -103,9 +107,11 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def milestones
- milestones = Milestone.where(project_id: project_ids_relation)
- milestones = milestones.search(query)
- milestones.reorder('milestones.updated_at DESC')
+ milestones = Milestone.search(query)
+
+ milestones = filter_milestones_by_project(milestones)
+
+ milestones.reorder('updated_at DESC')
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -123,6 +129,26 @@ module Gitlab
'projects'
end
+ # Filter milestones by authorized projects.
+ # For performance reasons project_id is being plucked
+ # to be used on a smaller query.
+ #
+ # rubocop: disable CodeReuse/ActiveRecord
+ def filter_milestones_by_project(milestones)
+ project_ids =
+ milestones.where(project_id: project_ids_relation)
+ .select(:project_id).distinct
+ .pluck(:project_id)
+
+ return Milestone.none if project_ids.nil?
+
+ authorized_project_ids_relation =
+ Project.where(id: project_ids).ids_with_milestone_available_for(current_user)
+
+ milestones.where(project_id: authorized_project_ids_relation)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
# rubocop: disable CodeReuse/ActiveRecord
def project_ids_relation
limit_projects.select(:id).reorder(nil)
diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb
index 2b7e12639be..0d3e78c0a66 100644
--- a/lib/gitlab/setup_helper.rb
+++ b/lib/gitlab/setup_helper.rb
@@ -44,6 +44,13 @@ module Gitlab
config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
config[:bin_dir] = Gitlab.config.gitaly.client_path
+ if Rails.env.test?
+ # Compared to production, tests run in constrained environments. This
+ # number is meant to grow with the number of concurrent rails requests /
+ # sidekiq jobs, and concurrency will be low anyway in test.
+ config[:git] = { catfile_cache_size: 5 }
+ end
+
TomlRB.dump(config)
end
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
index 641ba70ef83..9a8df719827 100644
--- a/lib/gitlab/url_blocker.rb
+++ b/lib/gitlab/url_blocker.rb
@@ -8,38 +8,68 @@ module Gitlab
BlockedUrlError = Class.new(StandardError)
class << self
- def validate!(url, ports: [], schemes: [], allow_localhost: false, allow_local_network: true, ascii_only: false, enforce_user: false, enforce_sanitization: false)
- return true if url.nil?
+ # Validates the given url according to the constraints specified by arguments.
+ #
+ # ports - Raises error if the given URL port does is not between given ports.
+ # allow_localhost - Raises error if URL resolves to a localhost IP address and argument is true.
+ # allow_local_network - Raises error if URL resolves to a link-local address and argument is true.
+ # ascii_only - Raises error if URL has unicode characters and argument is true.
+ # enforce_user - Raises error if URL user doesn't start with alphanumeric characters and argument is true.
+ # enforce_sanitization - Raises error if URL includes any HTML/CSS/JS tags and argument is true.
+ #
+ # Returns an array with [<uri>, <original-hostname>].
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/ParameterLists
+ def validate!(
+ url,
+ ports: [],
+ schemes: [],
+ allow_localhost: false,
+ allow_local_network: true,
+ ascii_only: false,
+ enforce_user: false,
+ enforce_sanitization: false,
+ dns_rebind_protection: true)
+ # rubocop:enable Metrics/CyclomaticComplexity
+ # rubocop:enable Metrics/ParameterLists
+
+ return [nil, nil] if url.nil?
# Param url can be a string, URI or Addressable::URI
uri = parse_url(url)
validate_html_tags!(uri) if enforce_sanitization
- # Allow imports from the GitLab instance itself but only from the configured ports
- return true if internal?(uri)
-
+ hostname = uri.hostname
port = get_port(uri)
- validate_scheme!(uri.scheme, schemes)
- validate_port!(port, ports) if ports.any?
- validate_user!(uri.user) if enforce_user
- validate_hostname!(uri.hostname)
- validate_unicode_restriction!(uri) if ascii_only
+
+ unless internal?(uri)
+ validate_scheme!(uri.scheme, schemes)
+ validate_port!(port, ports) if ports.any?
+ validate_user!(uri.user) if enforce_user
+ validate_hostname!(hostname)
+ validate_unicode_restriction!(uri) if ascii_only
+ end
begin
- addrs_info = Addrinfo.getaddrinfo(uri.hostname, port, nil, :STREAM).map do |addr|
+ addrs_info = Addrinfo.getaddrinfo(hostname, port, nil, :STREAM).map do |addr|
addr.ipv6_v4mapped? ? addr.ipv6_to_ipv4 : addr
end
rescue SocketError
- return true
+ return [uri, nil]
end
+ protected_uri_with_hostname = enforce_uri_hostname(addrs_info, uri, hostname, dns_rebind_protection)
+
+ # Allow url from the GitLab instance itself but only for the configured hostname and ports
+ return protected_uri_with_hostname if internal?(uri)
+
validate_localhost!(addrs_info) unless allow_localhost
validate_loopback!(addrs_info) unless allow_localhost
validate_local_network!(addrs_info) unless allow_local_network
validate_link_local!(addrs_info) unless allow_local_network
- true
+ protected_uri_with_hostname
end
def blocked_url?(*args)
@@ -52,6 +82,25 @@ module Gitlab
private
+ # Returns the given URI with IP address as hostname and the original hostname respectively
+ # in an Array.
+ #
+ # It checks whether the resolved IP address matches with the hostname. If not, it changes
+ # the hostname to the resolved IP address.
+ #
+ # The original hostname is used to validate the SSL, given in that scenario
+ # we'll be making the request to the IP address, instead of using the hostname.
+ def enforce_uri_hostname(addrs_info, uri, hostname, dns_rebind_protection)
+ address = addrs_info.first
+ ip_address = address&.ip_address
+
+ return [uri, nil] unless dns_rebind_protection && ip_address && ip_address != hostname
+
+ uri = uri.dup
+ uri.hostname = ip_address
+ [uri, hostname]
+ end
+
def get_port(uri)
uri.port || uri.default_port
end
diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb
index 880712de5fe..215454fe63c 100644
--- a/lib/gitlab/url_sanitizer.rb
+++ b/lib/gitlab/url_sanitizer.rb
@@ -47,6 +47,10 @@ module Gitlab
@credentials ||= { user: @url.user.presence, password: @url.password.presence }
end
+ def user
+ credentials[:user]
+ end
+
def full_url
@full_url ||= generate_full_url.to_s
end
diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake
index 2235a6ba194..91a52144dd2 100644
--- a/lib/tasks/gettext.rake
+++ b/lib/tasks/gettext.rake
@@ -19,6 +19,7 @@ namespace :gettext do
Rake::Task['gettext:po_to_json'].invoke
end
+ desc 'Regenerate gitlab.pot file'
task :regenerate do
pot_file = 'locale/gitlab.pot'
# Remove all translated files, this speeds up finding
diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake
index 7a42e4e92a0..a07ae3a418a 100644
--- a/lib/tasks/gitlab/assets.rake
+++ b/lib/tasks/gitlab/assets.rake
@@ -10,9 +10,15 @@ namespace :gitlab do
rake:assets:precompile
webpack:compile
gitlab:assets:fix_urls
+ gitlab:assets:compile_vrt
].each(&Gitlab::TaskHelpers.method(:invoke_and_time_task))
end
+ desc 'GitLab | Assets | Compile visual review toolbar'
+ task :compile_vrt do
+ system 'yarn', 'webpack-vrt'
+ end
+
desc 'GitLab | Assets | Clean up old compiled frontend assets'
task clean: ['rake:assets:clean']
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index ee3ef9dad6e..487808a7baa 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -51,9 +51,6 @@ namespace :gitlab do
end
end
- # (Re)create hooks
- Rake::Task['gitlab:shell:create_hooks'].invoke
-
Gitlab::Shell.ensure_secret_token!
end
@@ -78,15 +75,6 @@ namespace :gitlab do
end
end
end
-
- desc 'Create or repair repository hooks symlink'
- task create_hooks: :gitlab_environment do
- warn_user_is_not_gitlab
-
- puts 'Creating/Repairing hooks symlinks for all repositories'
- system(*%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args)
- puts 'done'.color(:green)
- end
end
def setup
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index af53da84bea..20c8a834d04 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -107,7 +107,7 @@ msgstr ""
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr ""
-msgid "%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS"
+msgid "%{counter_repositories} repositories, %{counter_wikis} wikis, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS"
msgstr ""
msgid "%{count} more"
@@ -340,6 +340,9 @@ msgstr ""
msgid "2FA enabled"
msgstr ""
+msgid "2FADevice|Registered On"
+msgstr ""
+
msgid "3 days"
msgstr ""
@@ -382,6 +385,9 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<no name set>"
+msgstr ""
+
msgid "<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes"
msgstr ""
@@ -421,7 +427,7 @@ msgstr ""
msgid "A fork is a copy of a project.<br />Forking a repository allows you to make changes without affecting the original project."
msgstr ""
-msgid "A member of GitLab's abuse team will review your report as soon as possible."
+msgid "A member of the abuse team will review your report as soon as possible."
msgstr ""
msgid "A new branch will be created in your fork and a new merge request will be started."
@@ -451,6 +457,9 @@ msgstr ""
msgid "API Help"
msgstr ""
+msgid "API Token"
+msgstr ""
+
msgid "About GitLab"
msgstr ""
@@ -496,6 +505,9 @@ msgstr ""
msgid "Account and limit"
msgstr ""
+msgid "Account: %{account}"
+msgstr ""
+
msgid "Active"
msgstr ""
@@ -517,6 +529,9 @@ msgstr ""
msgid "Add README"
msgstr ""
+msgid "Add a GPG key"
+msgstr ""
+
msgid "Add a bullet list"
msgstr ""
@@ -541,12 +556,18 @@ msgstr ""
msgid "Add a todo"
msgstr ""
+msgid "Add an SSH key"
+msgstr ""
+
msgid "Add bold text"
msgstr ""
msgid "Add comment now"
msgstr ""
+msgid "Add email address"
+msgstr ""
+
msgid "Add header and footer to emails. Please note that color settings will only be applied within the application interface"
msgstr ""
@@ -748,12 +769,18 @@ msgstr ""
msgid "After a successful password update you will be redirected to login screen."
msgstr ""
+msgid "After a successful password update, you will be redirected to the login page where you can log in with your new password."
+msgstr ""
+
msgid "All"
msgstr ""
msgid "All changes are committed"
msgstr ""
+msgid "All email addresses will be used to identify your commits."
+msgstr ""
+
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
@@ -832,9 +859,6 @@ msgstr ""
msgid "An error has occurred"
msgstr ""
-msgid "An error occurding while fetching folder content."
-msgstr ""
-
msgid "An error occurred creating the new branch."
msgstr ""
@@ -862,6 +886,9 @@ msgstr ""
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
msgstr ""
+msgid "An error occurred while fetching folder content."
+msgstr ""
+
msgid "An error occurred while fetching label colors."
msgstr ""
@@ -1093,6 +1120,9 @@ msgstr ""
msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
+msgid "Are you sure you want to delete this device? This action cannot be undone."
+msgstr ""
+
msgid "Are you sure you want to delete this list?"
msgstr ""
@@ -1138,9 +1168,21 @@ msgstr ""
msgid "Are you sure?"
msgstr ""
+msgid "Are you sure? All commits that were signed with this GPG key will be unverified."
+msgstr ""
+
+msgid "Are you sure? Removing this GPG key does not affect already signed commits."
+msgstr ""
+
+msgid "Are you sure? This will invalidate your registered applications and U2F devices."
+msgstr ""
+
msgid "Artifacts"
msgstr ""
+msgid "As U2F devices are only supported by a few browsers, we require that you set up a two-factor authentication app before a U2F device. That way you'll always be able to log in - even when you're using an unsupported browser."
+msgstr ""
+
msgid "AsanaService|%{user} pushed to branch %{branch} of %{project_name} ( %{commit_url} ):"
msgstr ""
@@ -1293,6 +1335,12 @@ msgstr ""
msgid "AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found. %{more_information_link}"
msgstr ""
+msgid "Automatic certificate management using %{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end}"
+msgstr ""
+
+msgid "Automatic certificate management using Let's Encrypt"
+msgstr ""
+
msgid "Automatically marked as default internal user"
msgstr ""
@@ -1443,9 +1491,6 @@ msgstr ""
msgid "Begin with the selected commit"
msgstr ""
-msgid "Behavior"
-msgstr ""
-
msgid "Below are examples of regex for existing tools:"
msgstr ""
@@ -1722,6 +1767,9 @@ msgstr ""
msgid "Can't find variable: ZiteReader"
msgstr ""
+msgid "Can't scan the code?"
+msgstr ""
+
msgid "Cancel"
msgstr ""
@@ -1773,6 +1821,12 @@ msgstr ""
msgid "Change title"
msgstr ""
+msgid "Change your password"
+msgstr ""
+
+msgid "Change your password or recover your current one"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr ""
@@ -1860,9 +1914,6 @@ msgstr ""
msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
msgstr ""
-msgid "Choose between fixed (max. 1280px) and fluid (100%%) application layout."
-msgstr ""
-
msgid "Choose file…"
msgstr ""
@@ -1872,9 +1923,6 @@ msgstr ""
msgid "Choose visibility level, enable/disable project features (issues, repository, wiki, snippets) and set permissions."
msgstr ""
-msgid "Choose what content you want to see on a project’s overview page."
-msgstr ""
-
msgid "Choose your merge method, options, checks, and set up a default merge request description template."
msgstr ""
@@ -2085,7 +2133,7 @@ msgstr ""
msgid "ClusterIntegration|%{title} uninstalled successfully."
msgstr ""
-msgid "ClusterIntegration|%{title} upgraded successfully."
+msgid "ClusterIntegration|%{title} updated successfully."
msgstr ""
msgid "ClusterIntegration|A service token scoped to %{code}kube-system%{end_code} with %{code}cluster-admin%{end_code} privileges."
@@ -2280,21 +2328,9 @@ msgstr ""
msgid "ClusterIntegration|Ingress gives you a way to route requests to services based on the request host or path, centralizing a number of services into a single entrypoint."
msgstr ""
-msgid "ClusterIntegration|Install"
-msgstr ""
-
-msgid "ClusterIntegration|Installed"
-msgstr ""
-
-msgid "ClusterIntegration|Installing"
-msgstr ""
-
msgid "ClusterIntegration|Installing Ingress may incur additional costs. Learn more about %{pricingLink}."
msgstr ""
-msgid "ClusterIntegration|Installing Knative may incur additional costs. Learn more about %{pricingLink}."
-msgstr ""
-
msgid "ClusterIntegration|Instance cluster"
msgstr ""
@@ -2328,6 +2364,9 @@ msgstr ""
msgid "ClusterIntegration|Knative Endpoint:"
msgstr ""
+msgid "ClusterIntegration|Knative domain name was updated successfully."
+msgstr ""
+
msgid "ClusterIntegration|Knative extends Kubernetes to provide a set of middleware components that are essential to build modern, source-centric, and container-based applications that can run anywhere: on premises, in the cloud, or even in a third-party data center."
msgstr ""
@@ -2376,9 +2415,6 @@ msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters"
msgstr ""
-msgid "ClusterIntegration|Manage"
-msgstr ""
-
msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}"
msgstr ""
@@ -2445,9 +2481,6 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
-msgid "ClusterIntegration|Retry update"
-msgstr ""
-
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -2502,6 +2535,9 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong while uninstalling %{title}"
msgstr ""
+msgid "ClusterIntegration|Something went wrong while updating Knative domain name."
+msgstr ""
+
msgid "ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain."
msgstr ""
@@ -2544,21 +2580,6 @@ msgstr ""
msgid "ClusterIntegration|Update failed. Please check the logs and try again."
msgstr ""
-msgid "ClusterIntegration|Updating"
-msgstr ""
-
-msgid "ClusterIntegration|Upgrade"
-msgstr ""
-
-msgid "ClusterIntegration|Upgrade failed"
-msgstr ""
-
-msgid "ClusterIntegration|Upgraded"
-msgstr ""
-
-msgid "ClusterIntegration|Upgrading"
-msgstr ""
-
msgid "ClusterIntegration|Validating project billing status"
msgstr ""
@@ -2783,9 +2804,15 @@ msgstr ""
msgid "Confirm"
msgstr ""
+msgid "Confirmation email sent to %{email}"
+msgstr ""
+
msgid "Confirmation required"
msgstr ""
+msgid "Congratulations! You have enabled Two-factor Authentication!"
+msgstr ""
+
msgid "Connect"
msgstr ""
@@ -2888,6 +2915,9 @@ msgstr ""
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
msgstr ""
+msgid "Control emails linked to your account"
+msgstr ""
+
msgid "Control the display of third party offers."
msgstr ""
@@ -3107,6 +3137,9 @@ msgstr ""
msgid "Current Branch"
msgstr ""
+msgid "Current password"
+msgstr ""
+
msgid "CurrentUser|Profile"
msgstr ""
@@ -3137,9 +3170,6 @@ msgstr ""
msgid "Customize language and region related settings."
msgstr ""
-msgid "Customize the appearance of the application header and navigation sidebar."
-msgstr ""
-
msgid "Customize your pipeline configuration, view your pipeline status and coverage report."
msgstr ""
@@ -3227,9 +3257,6 @@ msgstr ""
msgid "Default classification label"
msgstr ""
-msgid "Default dashboard"
-msgstr ""
-
msgid "Default first day of the week"
msgstr ""
@@ -3496,6 +3523,9 @@ msgstr ""
msgid "Disable shared Runners"
msgstr ""
+msgid "Disable two-factor authentication"
+msgstr ""
+
msgid "Disabled"
msgstr ""
@@ -3559,6 +3589,9 @@ msgstr ""
msgid "Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled"
msgstr ""
+msgid "Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'."
+msgstr ""
+
msgid "Don't show again"
msgstr ""
@@ -3574,6 +3607,9 @@ msgstr ""
msgid "Download asset"
msgstr ""
+msgid "Download codes"
+msgstr ""
+
msgid "Download export"
msgstr ""
@@ -3613,6 +3649,9 @@ msgstr ""
msgid "Edit Milestone"
msgstr ""
+msgid "Edit Password"
+msgstr ""
+
msgid "Edit Pipeline Schedule %{id}"
msgstr ""
@@ -3730,6 +3769,9 @@ msgstr ""
msgid "Enable Sentry for error reporting and logging."
msgstr ""
+msgid "Enable access to the Performance Bar for a given group."
+msgstr ""
+
msgid "Enable and configure InfluxDB metrics."
msgstr ""
@@ -3763,9 +3805,6 @@ msgstr ""
msgid "Enable shared Runners"
msgstr ""
-msgid "Enable the Performance Bar for a given group."
-msgstr ""
-
msgid "Enable two-factor authentication"
msgstr ""
@@ -3787,12 +3826,18 @@ msgstr ""
msgid "Ends at (UTC)"
msgstr ""
+msgid "Enforce DNS rebinding attack protection"
+msgstr ""
+
msgid "Enter at least three characters to search"
msgstr ""
msgid "Enter in your Bitbucket Server URL and personal access token below"
msgstr ""
+msgid "Enter in your Phabricator Server URL and personal access token below"
+msgstr ""
+
msgid "Enter the issue description"
msgstr ""
@@ -3805,7 +3850,7 @@ msgstr ""
msgid "Enter the merge request title"
msgstr ""
-msgid "Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. Additionally, they will be masked by default so they are hidden in job logs, though they must match certain regexp requirements to do so. You can use environment variables for passwords, secret keys, or whatever you want."
+msgid "Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. Additionally, they can be masked so they are hidden in job logs, though they must match certain regexp requirements to do so. You can use environment variables for passwords, secret keys, or whatever you want."
msgstr ""
msgid "Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default"
@@ -4440,6 +4485,9 @@ msgstr ""
msgid "Fingerprint"
msgstr ""
+msgid "Fingerprint:"
+msgstr ""
+
msgid "Fingerprints"
msgstr ""
@@ -4572,6 +4620,9 @@ msgstr ""
msgid "GPG Keys"
msgstr ""
+msgid "GPG keys allow you to verify signed commits."
+msgstr ""
+
msgid "GPG signature (loading...)"
msgstr ""
@@ -4614,6 +4665,9 @@ msgstr ""
msgid "Git revision"
msgstr ""
+msgid "Git shallow clone"
+msgstr ""
+
msgid "Git strategy for pipelines"
msgstr ""
@@ -4997,6 +5051,9 @@ msgstr ""
msgid "I accept the|Terms of Service and Privacy Policy"
msgstr ""
+msgid "I forgot my password"
+msgstr ""
+
msgid "I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end}"
msgstr ""
@@ -5069,7 +5126,10 @@ msgstr ""
msgid "If this was a mistake you can leave the %{source_type}."
msgstr ""
-msgid "If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>."
+msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
+msgstr ""
+
+msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
msgid "ImageDiffViewer|2-up"
@@ -5150,6 +5210,12 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import tasks"
+msgstr ""
+
+msgid "Import tasks from Phabricator into issues"
+msgstr ""
+
msgid "Import timed out. Import took longer than %{import_jobs_expiration} seconds"
msgstr ""
@@ -5243,12 +5309,24 @@ msgstr ""
msgid "Insert suggestion"
msgstr ""
+msgid "Install"
+msgstr ""
+
msgid "Install GitLab Runner"
msgstr ""
msgid "Install Runner on Kubernetes"
msgstr ""
+msgid "Install a soft token authenticator like %{free_otp_link} or Google Authenticator from your application repository and scan this QR code. More information is available in the %{help_link_start}documentation%{help_link_end}."
+msgstr ""
+
+msgid "Installed"
+msgstr ""
+
+msgid "Installing"
+msgstr ""
+
msgid "Instance Statistics"
msgstr ""
@@ -5291,6 +5369,9 @@ msgstr ""
msgid "Invalid Login or password"
msgstr ""
+msgid "Invalid date"
+msgstr ""
+
msgid "Invalid feature"
msgstr ""
@@ -5300,6 +5381,9 @@ msgstr ""
msgid "Invalid file."
msgstr ""
+msgid "Invalid import params"
+msgstr ""
+
msgid "Invalid input, please avoid emojis"
msgstr ""
@@ -5348,6 +5432,9 @@ msgstr ""
msgid "Issue update failed"
msgstr ""
+msgid "Issue was closed by %{name} %{reason}"
+msgstr ""
+
msgid "IssueBoards|Board"
msgstr ""
@@ -5528,6 +5615,9 @@ msgstr ""
msgid "Key (PEM)"
msgstr ""
+msgid "Key: %{key}"
+msgstr ""
+
msgid "Kubernetes"
msgstr ""
@@ -5635,6 +5725,9 @@ msgstr[1] ""
msgid "Last Pipeline"
msgstr ""
+msgid "Last accessed on"
+msgstr ""
+
msgid "Last activity"
msgstr ""
@@ -5665,6 +5758,9 @@ msgstr ""
msgid "Last used"
msgstr ""
+msgid "Last used on:"
+msgstr ""
+
msgid "LastPushEvent|You pushed to"
msgstr ""
@@ -5677,9 +5773,6 @@ msgstr ""
msgid "Latest pipeline for this branch"
msgstr ""
-msgid "Layout width"
-msgstr ""
-
msgid "Lead"
msgstr ""
@@ -5698,6 +5791,9 @@ msgstr ""
msgid "Learn more about Kubernetes"
msgstr ""
+msgid "Learn more about adding certificates to your project by following the %{docs_link_start}documentation on GitLab Pages%{docs_link_end}."
+msgstr ""
+
msgid "Learn more about signing commits"
msgstr ""
@@ -5725,11 +5821,17 @@ msgstr ""
msgid "Let's Encrypt does not accept emails on example.com"
msgstr ""
+msgid "Let's Encrypt is a free, automated, and open certificate authority (CA) that gives digital certificates in order to enable HTTPS (SSL/TLS) for websites. Learn more about Let's Encrypt configuration by following the %{docs_link_start}documentation on GitLab Pages%{docs_link_end}."
+msgstr ""
+
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] ""
msgstr[1] ""
+msgid "Linked emails (%{email_count})"
+msgstr ""
+
msgid "LinkedIn"
msgstr ""
@@ -5754,6 +5856,9 @@ msgstr ""
msgid "Live preview"
msgstr ""
+msgid "Loading functions timed out. Please reload the page to try again."
+msgstr ""
+
msgid "Loading the GitLab IDE..."
msgstr ""
@@ -5817,6 +5922,9 @@ msgstr ""
msgid "Makes this issue confidential"
msgstr ""
+msgid "Manage"
+msgstr ""
+
msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
msgstr ""
@@ -6371,6 +6479,9 @@ msgstr ""
msgid "New milestone"
msgstr ""
+msgid "New password"
+msgstr ""
+
msgid "New pipelines will cancel older, pending pipelines on the same branch"
msgstr ""
@@ -6536,9 +6647,6 @@ msgstr ""
msgid "Not found."
msgstr ""
-msgid "Not implemented!"
-msgstr ""
-
msgid "Not now"
msgstr ""
@@ -6826,16 +6934,28 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "Password (optional)"
+msgstr ""
+
msgid "Password authentication is unavailable."
msgstr ""
+msgid "Password confirmation"
+msgstr ""
+
+msgid "Password successfully changed"
+msgstr ""
+
+msgid "Password was successfully updated. Please login with it"
+msgstr ""
+
msgid "Past due"
msgstr ""
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
-msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_ed25519.pub' or '~/.ssh/id_rsa.pub' and begins with 'ssh-ed25519' or 'ssh-rsa'. Don't use your private SSH key."
msgstr ""
msgid "Path"
@@ -6859,6 +6979,9 @@ msgstr ""
msgid "People without permission will never get a notification and won't be able to comment."
msgstr ""
+msgid "People without permission will never get a notification."
+msgstr ""
+
msgid "Perform advanced options such as changing path, transferring, or removing the group."
msgstr ""
@@ -6880,12 +7003,27 @@ msgstr ""
msgid "Personal project creation is not allowed. Please contact your administrator with questions"
msgstr ""
+msgid "Phabricator Server Import"
+msgstr ""
+
+msgid "Phabricator Server URL"
+msgstr ""
+
+msgid "Phabricator Tasks"
+msgstr ""
+
msgid "Pick a name"
msgstr ""
+msgid "Pin code"
+msgstr ""
+
msgid "Pipeline"
msgstr ""
+msgid "Pipeline ID (IID)"
+msgstr ""
+
msgid "Pipeline Schedule"
msgstr ""
@@ -7165,7 +7303,7 @@ msgstr ""
msgid "Please try again"
msgstr ""
-msgid "Please use this form to report users to GitLab who create spam issues, comments or behave inappropriately."
+msgid "Please use this form to report to the admin users who create spam issues, comments or behave inappropriately."
msgstr ""
msgid "Please wait a moment, this page will automatically refresh when ready."
@@ -7180,21 +7318,51 @@ msgstr ""
msgid "Preferences saved."
msgstr ""
+msgid "Preferences|Behavior"
+msgstr ""
+
+msgid "Preferences|Choose between fixed (max. 1280px) and fluid (100%%) application layout."
+msgstr ""
+
+msgid "Preferences|Choose what content you want to see on a project’s overview page."
+msgstr ""
+
+msgid "Preferences|Customize the appearance of the application header and navigation sidebar."
+msgstr ""
+
+msgid "Preferences|Default dashboard"
+msgstr ""
+
msgid "Preferences|Display time in 24-hour format"
msgstr ""
msgid "Preferences|For example: 30 mins ago."
msgstr ""
+msgid "Preferences|Layout width"
+msgstr ""
+
msgid "Preferences|Navigation theme"
msgstr ""
+msgid "Preferences|Project overview content"
+msgstr ""
+
+msgid "Preferences|Syntax highlighting theme"
+msgstr ""
+
msgid "Preferences|These settings will update how dates and times are displayed for you."
msgstr ""
msgid "Preferences|This feature is experimental and translations are not complete yet"
msgstr ""
+msgid "Preferences|This setting allows you to customize the appearance of the syntax."
+msgstr ""
+
+msgid "Preferences|This setting allows you to customize the behavior of the system layout and default views."
+msgstr ""
+
msgid "Preferences|Time display"
msgstr ""
@@ -7255,12 +7423,18 @@ msgstr ""
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
+msgid "Proceed"
+msgstr ""
+
msgid "Profile"
msgstr ""
msgid "Profile Settings"
msgstr ""
+msgid "ProfileSession|on"
+msgstr ""
+
msgid "Profiles| You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. Once you confirm %{deleteAccount}, it cannot be undone or recovered."
msgstr ""
@@ -7315,6 +7489,9 @@ msgstr ""
msgid "Profiles|Click on icon to activate signin with one of the following services"
msgstr ""
+msgid "Profiles|Commit email"
+msgstr ""
+
msgid "Profiles|Connect"
msgstr ""
@@ -7327,6 +7504,9 @@ msgstr ""
msgid "Profiles|Current status"
msgstr ""
+msgid "Profiles|Default notification email"
+msgstr ""
+
msgid "Profiles|Delete Account"
msgstr ""
@@ -7375,6 +7555,9 @@ msgstr ""
msgid "Profiles|Invalid username"
msgstr ""
+msgid "Profiles|Key"
+msgstr ""
+
msgid "Profiles|Learn more"
msgstr ""
@@ -7390,6 +7573,9 @@ msgstr ""
msgid "Profiles|No file chosen"
msgstr ""
+msgid "Profiles|Notification email"
+msgstr ""
+
msgid "Profiles|Organization"
msgstr ""
@@ -7399,6 +7585,9 @@ msgstr ""
msgid "Profiles|Position and size your new avatar"
msgstr ""
+msgid "Profiles|Primary email"
+msgstr ""
+
msgid "Profiles|Private contributions"
msgstr ""
@@ -7408,6 +7597,9 @@ msgstr ""
msgid "Profiles|Public Avatar"
msgstr ""
+msgid "Profiles|Public email"
+msgstr ""
+
msgid "Profiles|Remove avatar"
msgstr ""
@@ -7450,7 +7642,7 @@ msgstr ""
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr ""
-msgid "Profiles|Typically starts with \"ssh-rsa …\""
+msgid "Profiles|Typically starts with \"ssh-ed25519 …\" or \"ssh-rsa …\""
msgstr ""
msgid "Profiles|Update profile settings"
@@ -7621,9 +7813,6 @@ msgstr ""
msgid "Project name"
msgstr ""
-msgid "Project overview content"
-msgstr ""
-
msgid "Project slug"
msgstr ""
@@ -8053,6 +8242,9 @@ msgstr ""
msgid "Recent searches"
msgstr ""
+msgid "Recovery Codes"
+msgstr ""
+
msgid "Reference:"
msgstr ""
@@ -8064,6 +8256,9 @@ msgstr[1] ""
msgid "Regenerate key"
msgstr ""
+msgid "Regenerate recovery codes"
+msgstr ""
+
msgid "Regex pattern"
msgstr ""
@@ -8073,15 +8268,24 @@ msgstr ""
msgid "Register / Sign In"
msgstr ""
+msgid "Register Two-Factor Authenticator"
+msgstr ""
+
msgid "Register U2F device"
msgstr ""
+msgid "Register Universal Two-Factor (U2F) Device"
+msgstr ""
+
msgid "Register and see your runners for this group."
msgstr ""
msgid "Register and see your runners for this project."
msgstr ""
+msgid "Register with two-factor app"
+msgstr ""
+
msgid "Registry"
msgstr ""
@@ -8211,7 +8415,7 @@ msgstr ""
msgid "Reply to this email directly or %{view_it_on_gitlab}."
msgstr ""
-msgid "Report abuse to GitLab"
+msgid "Report abuse to admin"
msgstr ""
msgid "Reporting"
@@ -8292,6 +8496,9 @@ msgstr ""
msgid "Require users to prove ownership of custom domains"
msgstr ""
+msgid "Resend confirmation email"
+msgstr ""
+
msgid "Resend invite"
msgstr ""
@@ -8325,6 +8532,9 @@ msgstr ""
msgid "Resolved by %{resolvedByName}"
msgstr ""
+msgid "Resolves IP addresses once and uses them to submit requests"
+msgstr ""
+
msgid "Response metrics (AWS ELB)"
msgstr ""
@@ -8355,6 +8565,9 @@ msgstr ""
msgid "Retry this job in order to create the necessary resources."
msgstr ""
+msgid "Retry update"
+msgstr ""
+
msgid "Retry verification"
msgstr ""
@@ -8456,6 +8669,9 @@ msgstr ""
msgid "Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects."
msgstr ""
+msgid "SSH Key"
+msgstr ""
+
msgid "SSH Keys"
msgstr ""
@@ -8465,6 +8681,9 @@ msgstr ""
msgid "SSH host keys"
msgstr ""
+msgid "SSH keys allow you to establish a secure connection between your computer and GitLab."
+msgstr ""
+
msgid "SSH public key"
msgstr ""
@@ -8492,6 +8711,9 @@ msgstr ""
msgid "Save comment"
msgstr ""
+msgid "Save password"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr ""
@@ -8681,6 +8903,9 @@ msgstr ""
msgid "Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. \"By <a href=\"#\">@johnsmith</a>\"). It will also associate and/or assign these issues and comments with the selected user."
msgstr ""
+msgid "Send confirmation email"
+msgstr ""
+
msgid "Send email"
msgstr ""
@@ -8882,6 +9107,9 @@ msgstr ""
msgid "Sherlock Transactions"
msgstr ""
+msgid "Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account. Please save them in a safe place, or you %{b_start}will%{b_end} lose access to your account."
+msgstr ""
+
msgid "Show all activity"
msgstr ""
@@ -8941,6 +9169,15 @@ msgstr ""
msgid "Sign-up restrictions"
msgstr ""
+msgid "SignUp|Name is too long (maximum is %{max_length} characters)."
+msgstr ""
+
+msgid "SignUp|Username is too long (maximum is %{max_length} characters)."
+msgstr ""
+
+msgid "Signed in"
+msgstr ""
+
msgid "Signed in with %{authentication} authentication"
msgstr ""
@@ -9112,6 +9349,9 @@ msgstr ""
msgid "SortOptions|Least popular"
msgstr ""
+msgid "SortOptions|Manual"
+msgstr ""
+
msgid "SortOptions|Milestone due date"
msgstr ""
@@ -9343,6 +9583,9 @@ msgstr ""
msgid "Subgroups and projects"
msgstr ""
+msgid "Subkeys"
+msgstr ""
+
msgid "Submit as spam"
msgstr ""
@@ -9400,6 +9643,69 @@ msgstr ""
msgid "Suggested change"
msgstr ""
+msgid "SuggestedColors|Bright green"
+msgstr ""
+
+msgid "SuggestedColors|Dark grayish cyan"
+msgstr ""
+
+msgid "SuggestedColors|Dark moderate blue"
+msgstr ""
+
+msgid "SuggestedColors|Dark moderate orange"
+msgstr ""
+
+msgid "SuggestedColors|Dark moderate pink"
+msgstr ""
+
+msgid "SuggestedColors|Dark moderate violet"
+msgstr ""
+
+msgid "SuggestedColors|Feijoa"
+msgstr ""
+
+msgid "SuggestedColors|Lime green"
+msgstr ""
+
+msgid "SuggestedColors|Moderate blue"
+msgstr ""
+
+msgid "SuggestedColors|Pure red"
+msgstr ""
+
+msgid "SuggestedColors|Slightly desaturated blue"
+msgstr ""
+
+msgid "SuggestedColors|Slightly desaturated green"
+msgstr ""
+
+msgid "SuggestedColors|Soft orange"
+msgstr ""
+
+msgid "SuggestedColors|Soft red"
+msgstr ""
+
+msgid "SuggestedColors|Strong pink"
+msgstr ""
+
+msgid "SuggestedColors|Strong red"
+msgstr ""
+
+msgid "SuggestedColors|Strong yellow"
+msgstr ""
+
+msgid "SuggestedColors|UA blue"
+msgstr ""
+
+msgid "SuggestedColors|Very dark desaturated blue"
+msgstr ""
+
+msgid "SuggestedColors|Very dark lime green"
+msgstr ""
+
+msgid "SuggestedColors|Very pale orange"
+msgstr ""
+
msgid "Sunday"
msgstr ""
@@ -9418,9 +9724,6 @@ msgstr ""
msgid "Switch to the source to copy it to the clipboard"
msgstr ""
-msgid "Syntax highlighting theme"
-msgstr ""
-
msgid "System Hooks"
msgstr ""
@@ -9621,6 +9924,9 @@ msgstr ""
msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
msgstr ""
+msgid "The certificate will be shown here once it has been obtained from Let's Encrypt. This process may take up to an hour to complete."
+msgstr ""
+
msgid "The character highlighter helps you keep the subject line to %{titleLength} characters and wrap the body at %{bodyLength} so they are readable in git."
msgstr ""
@@ -9708,6 +10014,9 @@ msgstr ""
msgid "The name %{entryName} is already taken in this directory."
msgstr ""
+msgid "The number of changes to be fetched from GitLab when cloning a repository. This can speed up Pipelines execution. Keep empty or set to 0 to disable shallow clone by default and make GitLab CI fetch all branches and tags each time."
+msgstr ""
+
msgid "The number of times an upload record could not find its file"
msgstr ""
@@ -9828,6 +10137,18 @@ msgstr ""
msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."
msgstr ""
+msgid "There are no GPG keys associated with this account."
+msgstr ""
+
+msgid "There are no GPG keys with access to your account."
+msgstr ""
+
+msgid "There are no SSH keys associated with this account."
+msgstr ""
+
+msgid "There are no SSH keys with access to your account."
+msgstr ""
+
msgid "There are no archived projects yet"
msgstr ""
@@ -9864,6 +10185,9 @@ msgstr ""
msgid "There was a problem communicating with your device."
msgstr ""
+msgid "There was a problem sending the confirmation email"
+msgstr ""
+
msgid "There was an error %{message} todo."
msgstr ""
@@ -9906,6 +10230,9 @@ msgstr ""
msgid "Third party offers"
msgstr ""
+msgid "This %{issuableDisplayName} is locked. Only project members can comment."
+msgstr ""
+
msgid "This %{issuable} is locked. Only <strong>project members</strong> can comment."
msgstr ""
@@ -9930,6 +10257,9 @@ msgstr ""
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr ""
+msgid "This certificate is automatically managed by Let's Encrypt"
+msgstr ""
+
msgid "This commit is part of merge request %{link_to_merge_request}. Comments created here will be created in the context of that merge request."
msgstr ""
@@ -9984,16 +10314,19 @@ msgstr ""
msgid "This is a delayed job to run in %{remainingTime}"
msgstr ""
+msgid "This is a list of devices that have logged into your account. Revoke any sessions that you do not recognize."
+msgstr ""
+
msgid "This is a security log of important events involving your account."
msgstr ""
msgid "This is the author's first Merge Request to this project."
msgstr ""
-msgid "This issue is confidential"
+msgid "This is your current session"
msgstr ""
-msgid "This issue is confidential and locked."
+msgid "This issue is confidential"
msgstr ""
msgid "This issue is locked."
@@ -10107,12 +10440,6 @@ msgstr ""
msgid "This runner will only run on pipelines triggered on protected branches"
msgstr ""
-msgid "This setting allows you to customize the appearance of the syntax."
-msgstr ""
-
-msgid "This setting allows you to customize the behavior of the system layout and default views."
-msgstr ""
-
msgid "This setting can be overridden in each project."
msgstr ""
@@ -10137,6 +10464,9 @@ msgstr ""
msgid "Thursday"
msgstr ""
+msgid "Time based: Yes"
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr ""
@@ -10321,6 +10651,9 @@ msgstr ""
msgid "Title"
msgstr ""
+msgid "Title:"
+msgstr ""
+
msgid "Titles and Filenames"
msgstr ""
@@ -10336,6 +10669,9 @@ msgstr ""
msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr ""
+msgid "To add the entry manually, provide the following details to the application on your phone."
+msgstr ""
+
msgid "To define internal users, first enable new users set to external"
msgstr ""
@@ -10549,6 +10885,15 @@ msgstr ""
msgid "Twitter"
msgstr ""
+msgid "Two-Factor Authentication"
+msgstr ""
+
+msgid "Two-factor Authentication"
+msgstr ""
+
+msgid "Two-factor Authentication Recovery codes"
+msgstr ""
+
msgid "Two-factor Authentication has been disabled for this user"
msgstr ""
@@ -10558,6 +10903,9 @@ msgstr ""
msgid "Type"
msgstr ""
+msgid "U2F Devices (%{length})"
+msgstr ""
+
msgid "U2F only works with HTTPS-enabled websites. Contact your administrator for more details."
msgstr ""
@@ -10696,6 +11044,9 @@ msgstr ""
msgid "UpdateProject|Project could not be updated!"
msgstr ""
+msgid "Updated"
+msgstr ""
+
msgid "Updating"
msgstr ""
@@ -10738,6 +11089,12 @@ msgstr ""
msgid "Use <code>%{native_redirect_uri}</code> for local tests"
msgstr ""
+msgid "Use a hardware device to add the second factor of authentication."
+msgstr ""
+
+msgid "Use an one time password authenticator on your mobile device or computer to enable two-factor authentication (2FA)."
+msgstr ""
+
msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr ""
@@ -10876,6 +11233,9 @@ msgstr ""
msgid "UserProfile|Your projects can be available publicly, internally, or privately, at your choice."
msgstr ""
+msgid "Username (optional)"
+msgstr ""
+
msgid "Username is already taken."
msgstr ""
@@ -11044,6 +11404,9 @@ msgstr ""
msgid "We heard back from your U2F device. You have been authenticated."
msgstr ""
+msgid "We sent you an email with reset password instructions"
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
@@ -11283,7 +11646,7 @@ msgstr ""
msgid "You are attempting to update a file that has changed since you started editing it."
msgstr ""
-msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgid "You are going to remove %{group_name}, this will also remove all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr ""
msgid "You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
@@ -11295,6 +11658,9 @@ msgstr ""
msgid "You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?"
msgstr ""
+msgid "You are not allowed to unlink your primary login account"
+msgstr ""
+
msgid "You are now impersonating %{username}"
msgstr ""
@@ -11406,6 +11772,9 @@ msgstr ""
msgid "You do not have permission to leave this %{namespaceType}."
msgstr ""
+msgid "You don't have any U2F devices registered yet."
+msgstr ""
+
msgid "You don't have any active chat names."
msgstr ""
@@ -11454,6 +11823,12 @@ msgstr ""
msgid "You must have permission to create a project in a namespace before forking."
msgstr ""
+msgid "You must provide a valid current password"
+msgstr ""
+
+msgid "You must provide your current password in order to change it."
+msgstr ""
+
msgid "You need permission."
msgstr ""
@@ -11529,24 +11904,45 @@ msgstr ""
msgid "You're receiving this email because of your account on %{host}. %{manage_notifications_link} &middot; %{help_link}"
msgstr ""
+msgid "You've already enabled two-factor authentication using one time password authenticators. In order to register a different device, you must first disable two-factor authentication."
+msgstr ""
+
msgid "YouTube"
msgstr ""
+msgid "Your Commit Email will be used for web based operations, such as edits and merges."
+msgstr ""
+
msgid "Your Conversational Development Index gives an overview of how you are using GitLab from a feature perspective. View how you compare with other organizations, discover features you are not using, and learn best practices through blog posts and white papers."
msgstr ""
+msgid "Your Default Notification Email will be used for account notifications if a %{openingTag}group-specific email address%{closingTag} is not set."
+msgstr ""
+
+msgid "Your GPG keys (%{count})"
+msgstr ""
+
msgid "Your Groups"
msgstr ""
msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure"
msgstr ""
+msgid "Your Primary Email will be used for avatar detection."
+msgstr ""
+
msgid "Your Projects (default)"
msgstr ""
msgid "Your Projects' Activity"
msgstr ""
+msgid "Your Public Email will be displayed on your public profile."
+msgstr ""
+
+msgid "Your SSH keys (%{count})"
+msgstr ""
+
msgid "Your Todos"
msgstr ""
@@ -11592,9 +11988,6 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
-msgid "Your comment will not be visible to the public."
-msgstr ""
-
msgid "Your deployment services will be broken, you will need to manually fix the services after renaming."
msgstr ""
@@ -11673,6 +12066,9 @@ msgstr ""
msgid "commented on %{link_to_project}"
msgstr ""
+msgid "commit %{commit_id}"
+msgstr ""
+
msgid "confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue."
msgstr ""
@@ -11688,6 +12084,9 @@ msgstr ""
msgid "customize"
msgstr ""
+msgid "date must not be after 9999-12-31"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] ""
@@ -11931,6 +12330,9 @@ msgstr ""
msgid "mrWidget|Merge failed."
msgstr ""
+msgid "mrWidget|Merge failed: %{mergeError}. Please try again."
+msgstr ""
+
msgid "mrWidget|Merge locally"
msgstr ""
@@ -12195,6 +12597,12 @@ msgstr ""
msgid "verify ownership"
msgstr ""
+msgid "via %{closed_via}"
+msgstr ""
+
+msgid "via merge request %{link}"
+msgstr ""
+
msgid "view it on GitLab"
msgstr ""
diff --git a/package.json b/package.json
index a4ec2dce0df..ab7268642ec 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,8 @@
"stylelint-create-utility-map": "node scripts/frontend/stylelint/stylelint-utility-map.js",
"test": "node scripts/frontend/test",
"webpack": "NODE_OPTIONS=\"--max-old-space-size=3584\" webpack --config config/webpack.config.js",
- "webpack-prod": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.js"
+ "webpack-prod": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.js",
+ "webpack-vrt": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.review_toolbar.js"
},
"dependencies": {
"@babel/core": "^7.4.4",
@@ -37,9 +38,11 @@
"@babel/preset-env": "^7.4.4",
"@gitlab/csslab": "^1.9.0",
"@gitlab/svgs": "^1.63.0",
- "@gitlab/ui": "^3.10.0",
+ "@gitlab/ui": "^3.11.0",
"apollo-cache-inmemory": "^1.5.1",
"apollo-client": "^2.5.1",
+ "apollo-link": "^1.2.11",
+ "apollo-link-batch-http": "^1.2.11",
"apollo-upload-client": "^10.0.0",
"at.js": "^1.5.4",
"autosize": "^4.0.0",
@@ -53,7 +56,7 @@
"clipboard": "^1.7.1",
"codesandbox-api": "^0.0.20",
"compression-webpack-plugin": "^2.0.0",
- "core-js": "^2.4.1",
+ "core-js": "^3.1.3",
"cropper": "^2.3.0",
"css-loader": "^1.0.0",
"d3": "^4.13.0",
@@ -143,7 +146,7 @@
},
"devDependencies": {
"@babel/plugin-transform-modules-commonjs": "^7.2.0",
- "@gitlab/eslint-config": "^1.5.0",
+ "@gitlab/eslint-config": "^1.6.0",
"@vue/test-utils": "^1.0.0-beta.25",
"axios-mock-adapter": "^1.15.0",
"babel-jest": "^24.1.0",
diff --git a/qa/Gemfile b/qa/Gemfile
index 64215b24cf1..12994b85322 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -10,3 +10,4 @@ gem 'airborne', '~> 0.2.13'
gem 'nokogiri', '~> 1.10.3'
gem 'rspec-retry', '~> 0.6.1'
gem 'faker', '~> 1.6', '>= 1.6.6'
+gem 'knapsack', '~> 1.17'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index a06c88b6f0a..6b0635ed0e2 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -39,6 +39,8 @@ GEM
domain_name (~> 0.5)
i18n (0.9.1)
concurrent-ruby (~> 1.0)
+ knapsack (1.17.1)
+ rake
launchy (2.4.3)
addressable (~> 2.3)
method_source (0.9.0)
@@ -102,6 +104,7 @@ DEPENDENCIES
capybara (~> 2.16.1)
capybara-screenshot (~> 1.0.18)
faker (~> 1.6, >= 1.6.6)
+ knapsack (~> 1.17)
nokogiri (~> 1.10.3)
pry-byebug (~> 3.5.1)
rake (~> 12.3.0)
diff --git a/qa/README.md b/qa/README.md
index f75205133e6..ef6f202464d 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -1,6 +1,6 @@
# GitLab QA - End-to-end tests for GitLab
-This directory contains [end-to-end tests](doc/development/testing_guide/end_to_end_tests.md)
+This directory contains [end-to-end tests](../../../doc/development/testing_guide/end_to_end/index.md)
for GitLab. It includes the test framework and the tests themselves.
The tests can be found in `qa/specs/features` (not to be confused with the unit
@@ -29,7 +29,7 @@ verify coupling between page objects implemented as a part of GitLab QA
and corresponding views / partials / selectors in CE / EE.
Whenever `qa:selectors` job fails in your merge request, you are supposed to
-fix [page objects](qa/page/README.md). You should also trigger end-to-end tests
+fix [page objects](../doc/development/testing_guide/end_to_end/page_objects.md). You should also trigger end-to-end tests
using `package-and-qa` manual action, to test if everything works fine.
## How can I use it?
@@ -49,10 +49,10 @@ will need to [modify your GDK setup](https://gitlab.com/gitlab-org/gitlab-qa/blo
### Writing tests
-- [Writing tests from scratch tutorial](docs/writing_tests_from_scratch.md)
- - [Best practices](docs/best_practices.md)
- - [Using page objects](qa/page/README.md)
- - [Guidelines](docs/guidelines.md)
+- [Writing tests from scratch tutorial](../doc/development/testing_guide/end_to_end/quick_start_guide.md)
+ - [Best practices](../doc/development/testing_guide/best_practices.md)
+ - [Using page objects](../doc/development/testing_guide/end_to_end/page_objects.md)
+ - [Guidelines](../doc/development/testing_guide/index.md)
### Running specific tests
diff --git a/qa/docs/writing_tests_from_scratch.md b/qa/docs/writing_tests_from_scratch.md
deleted file mode 100644
index 65e7a78a8b5..00000000000
--- a/qa/docs/writing_tests_from_scratch.md
+++ /dev/null
@@ -1,482 +0,0 @@
-# Writing end-to-end tests step-by-step
-
-In this tutorial, you will find different examples, and the steps involved, in the creation of end-to-end (_e2e_) tests for GitLab CE and GitLab EE, using GitLab QA.
-
-> When referring to end-to-end tests in this document, this means testing a specific feature end-to-end, such as a user logging in, the creation of a project, the management of labels, breaking down epics into sub-epics and issues, etc.
-
-## Important information before we start writing tests
-
-It's important to understand that end-to-end tests of isolated features, such as the ones described in the above note, doesn't mean that everything needs to happen through the GUI.
-
-If you don't exactly understand what we mean by **not everything needs to happen through the GUI,** please make sure you've read the [best practices](best_practices.md) before moving on.
-
-## This document covers the following items:
-
-0. Identifying if end-to-end tests are really needed
-1. Identifying the [DevOps stage](https://about.gitlab.com/stages-devops-lifecycle/) of the feature that you are going to cover with end-to-end tests
-2. Creating the skeleton of the test file (`*_spec.rb`)
-3. The [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of the test cases logic
-4. Extracting duplicated code into methods
-5. Tests' pre-conditions (`before :all` and `before`) using resources and [Page Objects](./qa/page/README.md)
-6. Optimizing the test suite
-7. Using and implementing resources
-8. Moving elements definitions and its methods to [Page Objects](./qa/page/README.md)
- - Adding testability to the application
-
-### 0. Are end-to-end tests needed?
-
-At GitLab we respect the [test pyramid](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/testing_guide/testing_levels.md), and so, we recommend to check the code coverage of a specific feature before writing end-to-end tests.
-
-Sometimes you may notice that there is already a good coverage in other test levels, and we can stay confident that if we break a feature, we will still have quick feedback about it, even without having end-to-end tests.
-
-If after this analysis you still think that end-to-end tests are needed, keep reading.
-
-### 1. Identifying the DevOps stage
-
-The GitLab QA end-to-end tests are organized by the different [stages in the DevOps lifecycle](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa/specs/features/browser_ui), and so, if you are creating tests for issue creation, for instance, you would locate the spec files under the `qa/qa/specs/features/browser_ui/2_plan/` directory since issue creation is part of the Plan stage.
-
- In another case of a test for listing merged merge requests (MRs), the test should go under the `qa/qa/specs/features/browser_ui/3_create/` directory since merge request is a feature from the Create stage.
-
-> There may be sub-directories inside the stages directories, for different features. For example: `.../browser_ui/2_plan/ee_epics/` and `.../browser_ui/2_plan/issues/`.
-
-Now, let's say we want to create tests for the [scoped labels](https://about.gitlab.com/2019/04/22/gitlab-11-10-released/#scoped-labels) feature, available on GitLab EE Premium (this feature is part of the Plan stage.)
-
-> Because these tests are for a feature available only on GitLab EE, we need to create them in the [EE repository](https://gitlab.com/gitlab-org/gitlab-ee).
-
-Since [there is no specific directory for this feature](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/qa/qa/specs/features/browser_ui/2_plan), we should create a sub-directory for it.
-
-Under `.../browser_ui/2_plan/`, let's create a sub-directory called `ee_scoped_labels/`.
-
-> Notice that since this feature is only available for GitLab EE we prefix the sub-directory with `ee_`.
-
-### 2. Test skeleton
-
-Inside the newly created sub-directory, let's create a file describing the test suite (e.g. `editing_scoped_labels_spec.rb`.)
-
-#### The `context` and `describe` blocks
-
-Specs have an outer `context` that indicates the DevOps stage. The next level is the `describe` block, that briefly states the subject of the test suite. See the following example:
-
-```ruby
-module QA
- context 'Plan' do
- describe 'Editing scoped labels properties on issues' do
- end
- end
-end
-```
-
-#### The `it` blocks
-
-Every test suite is composed by at least one `it` block, and a good way to start writing end-to-end tests is by typing test cases descriptions as `it` blocks. Take a look at the following example:
-
-```ruby
-module QA
- context 'Plan' do
- describe 'Editing scoped labels properties on issues' do
- it 'replaces an existing label if it has the same key' do
- end
-
- it 'keeps both scoped labels when adding a label with a different key' do
- end
- end
- end
-end
-```
-
-### 3. Test cases MVC
-
-For the [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of our test cases, let's say that we already have the application in the state needed for the tests, and then let's focus on the logic of the test cases only.
-
-To evolve the test cases drafted on step 2, let's imagine that the user is already logged in a GitLab EE instance, they already have at least a Premium license in use, there is already a project created, there is already an issue opened in the project, the issue already has a scoped label (e.g. `foo::bar`), there are other scoped labels (for the same scope and for a different scope, e.g. `foo::baz` and `bar::bah`), and finally, the user is already on the issue's page. Let's also suppose that for every test case the application is in a clean state, meaning that one test case won't affect another.
-
-> Note: there are different approaches to create an application state for end-to-end tests. Some of them are very time consuming and subject to failures, such as when using the GUI for all the pre-conditions of the tests. On the other hand, other approaches are more efficient, such as using the public APIs. The latter is more efficient since it doesn't depend on the GUI. We won't focus on this part yet, but it's good to keep it in mind.
-
-Let's now focus on the first test case.
-
-```ruby
-it 'keeps the latest scoped label when adding a label with the same key of an existing one, but with a different value' do
- # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects.
- page.find('.block.labels .edit-link').click
- page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['foo::baz', :enter]
- page.find('#content-body').click
- page.refresh
-
- scoped_label = page.find('.qa-labels-block .scoped-label-wrapper')
-
- expect(scoped_label).to have_content('foo::baz')
- expect(scoped_label).not_to have_content('foo::bar')
- expect(page).to have_content('added foo::baz label and removed foo::bar')
-end
-```
-
-> Notice that the test itself is simple. The most challenging part is the creation of the application state, which will be covered later.
-
-> The exemplified test cases' MVC is not enough for the change to be submitted in an MR, but they help on building up the test logic. The reason is that we do not want to use locators directly in the tests, and tests **must** use [Page Objects](./qa/page/README.md) before they can be merged.
-
-Below are the steps that the test covers:
-
-1. The test finds the 'Edit' link for the labels and clicks on it
-2. Then it fills in the 'Assign labels' input field with the value 'foo::baz' and press enter
-3. Then it clicks in the content body to apply the label and refreshes the page
-4. Finally the expectation that the previous scoped label was removed and that the new one was added happens
-
-Let's now see how the second test case would look like.
-
-```ruby
-it 'keeps both scoped labels when adding a label with a different key' do
- # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects.
- page.find('.block.labels .edit-link').click
- page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['bar::bah', :enter]
- page.find('#content-body').click
- page.refresh
-
- scoped_labels = page.all('.qa-labels-block .scoped-label-wrapper')
-
- expect(scoped_labels.first).to have_content('bar::bah')
- expect(scoped_labels.last).to have_content('foo::ba')
- expect(page).to have_content('added bar::bah')
- expect(page).to have_content('added foo::ba')
-end
-```
-
-> Note that elements are always located using CSS selectors, and a good practice is to add test specific attribute:value for elements (this is called adding testability to the application and we will talk more about it later.)
-
-Below are the steps that the test covers:
-
-1. The test finds the 'Edit' link for the labels and clicks on it
-2. Then it fills in the 'Assign labels' input field with the value 'bar::bah' and press enter
-3. Then it clicks in the content body to apply the label and refreshes the page
-4. Finally the expectation that the both scoped labels are present happens
-
-> Similar to the previous test, this one is also very straight forward, but there is some code duplication. Let's address it.
-
-### 4. Extracting duplicated code
-
-If we refactor the tests created on step 3 we could come up with something like this:
-
-```ruby
-it 'keeps the latest scoped label when adding a label with the same key of an existing one, but with a different value' do
- select_label_and_refresh 'foo::baz'
-
- expect(page).to have_content('added foo::baz')
- expect(page).to have_content('and removed foo::bar')
-
- scoped_label = page.find('.qa-labels-block .scoped-label-wrapper')
-
- expect(scoped_label).to have_content('foo::baz')
- expect(scoped_label).not_to have_content('foo::bar')
-end
-
-it 'keeps both scoped label when adding a label with a different key' do
- select_label_and_refresh 'bar::bah'
-
- expect(page).to have_content('added bar::bah')
- expect(page).to have_content('added foo::ba')
-
- scoped_labels = page.all('.qa-labels-block .scoped-label-wrapper')
-
- expect(scoped_labels.first).to have_content('bar::bah')
- expect(scoped_labels.last).to have_content('foo::ba')
-end
-
-def select_label_and_refresh(label)
- page.find('.block.labels .edit-link').click
- page.find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter]
- page.find('#content-body').click
- page.refresh
-end
-```
-
-By creating a reusable `select_label_and_refresh` method, we remove the code duplication, and later we can move this method to a Page Object class that will be created for easier maintenance purposes.
-
-> Notice that the reusable method is created in the bottom of the file. The reason for that is that reading the code should be similar to reading a newspaper, where high-level information is at the top, like the title and summary of the news, while low level, or more specific information, is at the bottom.
-
-### 5. Tests' pre-conditions using resources and Page Objects
-
-In this section, we will address the previously mentioned subject of creating the application state for the tests, using the `before :all` and `before` blocks, together with resources and Page Objects.
-
-#### `before :all`
-
-A pre-condition for the entire test suite is defined in the `before :all` block.
-
-For our test suite example, some things that could happen before the entire test suite starts are:
-
-- The user logging in;
-- A premium license already being set up;
-- A project being created with an issue and labels already setup.
-
-> In case of a test suite with only one `it` block it's ok to use only the `before` block (see below) with all the test's pre-conditions.
-
-#### `before`
-
-A pre-condition for each test case is defined in the `before` block.
-
-For our test cases samples, what we need is that for every test the issue page is opened, and there is only one scoped label applied to it.
-
-#### Implementation
-
-In the following code we will focus on the test suite and the test cases' pre-conditions only:
-
-```ruby
-module QA
- context 'Plan' do
- describe 'Editing scoped labels properties on issues' do
- before :all do
- project = Resource::Project.fabricate_via_api! do |resource|
- resource.name = 'scoped-labels-project'
- end
-
- @foo_bar_scoped_label = 'foo::bar'
-
- @issue = Resource::Issue.fabricate_via_api! do |issue|
- issue.project = project
- issue.title = 'Issue to test the scoped labels'
- issue.labels = @foo_bar_scoped_label
- end
-
- @labels = ['foo::baz', 'bar::bah']
- @labels.each do |label|
- Resource::Label.fabricate_via_api! do |l|
- l.project = project.id
- l.title = label
- end
- end
-
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
- end
-
- before do
- Page::Project::Issue::Show.perform do |issue_page|
- @issue.visit!
- end
- end
-
- it 'keeps the latest scoped label when adding a label with the same key of an existing one, but with a different value' do
- ...
- end
-
- it 'keeps both scoped labels when adding a label with a different key' do
- ...
- end
-
- def select_label_and_refresh(label)
- ...
- end
- end
- end
-end
-```
-
-In the `before :all` block we create all the application state needed for the tests to run. We do that by fabricating resources via APIs (`project`, `@issue`, and `@labels`), by using the `Runtime::Browser.visit` method to go to the login page, and by performing a `sign_in_using_credentials` from the `Login` Page Object.
-
-> When creating the resources, notice that when calling the `fabricate_via_api` method, we pass some attribute:values, like `name` for the `project` resource; `project`, `title`, and `labels` for the `issue` resource; and `project`, and `title` for `label` resources.
-
-> What's important to understand here is that by creating the application state mostly using the public APIs we save a lot of time in the test suite setup stage.
-
-> Soon we will cover the use of the already existing resources' methods and the creation of your own `fabricate_via_api` methods for resources where this is still not available, but first, let's optimize our implementation.
-
-### 6. Optimization
-
-As already mentioned in the [best practices](./BEST_PRACTICES.md) document, end-to-end tests are very costly in terms of execution time, and it's our responsibility as software engineers to ensure that we optimize them as much as possible.
-
-> Differently than unit tests, that exercise every little piece of the application in isolation, usually having only one assertion per test, and being very fast to run, end-to-end tests can have more actions and assertions in a single test to help on speeding up the test's feedback since they are much slower when comparing to unit tests.
-
-Some improvements that we could make in our test suite to optimize its time to run are:
-
-1. Having a single test case (an `it` block) that exercise both scenarios to avoid "wasting" time in the tests' pre-conditions, instead of having two different test cases.
-2. Moving all the pre-conditions to the `before` block since there will be only one `it` block.
-3. Making the selection of labels more performant by allowing for the selection of more than one label in the same reusable method.
-
-Let's look at a suggestion that addresses the above points, one by one:
-
-```ruby
-module QA
- context 'Plan' do
- describe 'Editing scoped labels properties on issues' do
- before do
- project = Resource::Project.fabricate_via_api! do |resource|
- resource.name = 'scoped-labels-project'
- end
-
- @foo_bar_scoped_label = 'foo::bar'
-
- @issue = Resource::Issue.fabricate_via_api! do |issue|
- issue.project = project
- issue.title = 'Issue to test the scoped labels'
- issue.labels = @foo_bar_scoped_label
- end
-
- @labels = ['foo::baz', 'bar::bah']
- @labels.each do |label|
- Resource::Label.fabricate_via_api! do |l|
- l.project = project.id
- l.title = label
- end
- end
-
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
- Page::Project::Issue::Show.perform do |issue_page|
- @issue.visit!
- end
- end
-
- it 'correctly applies the scoped labels depending if they are from the same or a different scope' do
- select_labels_and_refresh @labels
-
- scoped_labels = page.all('.qa-labels-block .scoped-label-wrapper')
-
- expect(page).to have_content("added #{@foo_bar_scoped_label}")
- expect(page).to have_content("added #{@labels[1]} #{@labels[0]} labels and removed #{@foo_bar_scoped_label}")
- expect(scoped_labels.count).to eq(2)
- expect(scoped_labels.first).to have_content(@labels[1])
- expect(scoped_labels.last).to have_content(@labels[0])
- end
-
- def select_labels_and_refresh(labels)
- find('.block.labels .edit-link').click
- labels.each do |label|
- find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter]
- end
- find('#content-body').click
- refresh
- end
- end
- end
- end
-```
-
-As you can see, now all the pre-conditions from the `before :all` block were moved to the `before` block, addressing point 2.
-
-To address point 1, we changed the test implementation from two `it` blocks into a single one that exercises both scenarios. Now the new test description is: `'correctly applies the scoped labels depending if they are from the same or a different scope'`. It's a long description, but it describes well what the test does.
-
-> Notice that the implementation of the new and unique `it` block had to change a little bit. Below we describe in details what it does.
-
-1. At the same time, it selects two scoped labels, one from the same scope of the one already applied in the issue during the setup phase (in the `before` block), and another one from a different scope.
-2. It runs the assertions that the labels where correctly added and removed; that only two labels are applied; and that those are the correct ones, and that they are shown in the right order.
-
-Finally, the `select_label_and_refresh` method is changed to `select_labels_and_refresh`, which accepts an array of labels instead of a single label, and it iterates on them for faster label selection (this is what is used in step 1 explained above.)
-
-### 7. Resources
-
-You can think of resources as anything that can be created on GitLab CE or EE, either through the GUI, the API, or the CLI.
-
-With that in mind, resources can be a project, an epic, an issue, a label, a commit, etc.
-
-As you saw in the tests' pre-conditions and the optimization sections, we're already creating some of these resources, and we are doing that by calling the `fabricate_via_api!` method.
-
-> We could be using the `fabricate!` method instead, which would use the `fabricate_via_api!` method if it exists, and fallback to GUI fabrication otherwise, but we recommend being explicit to make it clear what the test does. Also, we always recommend fabricating resources via API since this makes tests faster and more reliable.
-
-For our test suite example, the [project resource](https://gitlab.com/gitlab-org/gitlab-ee/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/project.rb#L55) already had a `fabricate_via_api!` method available, while other resources don't have it, so we will have to create them, like for the issue and label resources. Also, we will have to make a small change in the project resource to expose its `id` attribute so that we can refer to it when fabricating the issue.
-
-#### Implementation
-
-Following we describe the changes needed in every of the before-mentioned resource files.
-
-**Project resource**
-
-Let's start with the smallest change.
-
-In the [project resource](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/project.rb), let's expose its `id` attribute.
-
-Add the following `attribute :id` right below the [`attribute :description`](https://gitlab.com/gitlab-org/gitlab-ee/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/project.rb#L11).
-
-> This line is needed to allow for issues and labels to be automatically added to a project when fabricating them via API.
-
-**Issue resource**
-
-Now, let's make it possible to create an issue resource through the API.
-
-First, in the [issue resource](https://gitlab.com/gitlab-org/gitlab-ee/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/issue.rb), let's expose its labels attribute.
-
-Add the following `attribute :labels` right below the [`attribute :title`](https://gitlab.com/gitlab-org/gitlab-ee/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/issue.rb#L15).
-
-> This line is needed to allow for labels to be automatically added to an issue when fabricating it via API.
-
-Next, add the following code right below the [`fabricate!`](https://gitlab.com/gitlab-org/gitlab-ee/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/issue.rb#L27) method.
-
-```ruby
-def api_get_path
- "/projects/#{project.id}/issues/#{id}"
-end
-
-def api_post_path
- "/projects/#{project.id}/issues"
-end
-
-def api_post_body
- {
- title: title,
- labels: [labels]
- }
-end
-```
-
-By defining the `api_get_path` method, we allow the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to get a single issue.
-
-> This `GET` path can be found in the [public API documentation](https://docs.gitlab.com/ee/api/issues.html#single-issue).
-
-By defining the `api_post_path` method, we allow the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to create a new issue in a specific project.
-
-> This `POST` path can be found in the [public API documentation](https://docs.gitlab.com/ee/api/issues.html#new-issue).
-
-By defining the `api_post_body` method, we allow the [`ApiFabricator.api_post`](https://gitlab.com/gitlab-org/gitlab-ee/blob/a9177ca1812bac57e2b2fa4560e1d5dd8ffac38b/qa/qa/resource/api_fabricator.rb#L68) method to know which data to send when making the `POST` request.
-
-> Notice that we pass both `title` and `labels` attributes in the `api_post_body`, where `labels` receives an array of labels, and [`title` is required](https://docs.gitlab.com/ee/api/issues.html#new-issue).
-
-**Label resource**
-
-Finally, let's make it possible to create label resources through the API.
-
-Add the following code right below the [`fabricate!`](https://gitlab.com/gitlab-org/gitlab-ee/blob/a9177ca1812bac57e2b2fa4560e1d5dd8ffac38b/qa/qa/resource/label.rb#L36) method.
-
-```ruby
-def resource_web_url(resource)
- super
-rescue ResourceURLMissingError
- # this particular resource does not expose a web_url property
-end
-
-def api_get_path
- raise NotImplementedError, "The Labels API doesn't expose a single-resource endpoint so this method cannot be properly implemented."
-end
-
-def api_post_path
- "/projects/#{project}/labels"
-end
-
-def api_post_body
- {
- name: @title,
- color: @color
- }
-end
-```
-
-By defining the `resource_web_url(resource)` method, we override the one from the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb#L44) module. We do that to avoid failing the test due to this particular resource not exposing a `web_url` property.
-
-By defining the `api_get_path` method, we **would** allow for the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to get a single label, but since there's no path available for that in the publich API, we raise a `NotImplementedError` instead.
-
-By defining the `api_post_path` method, we allow for the [`ApiFabricator `](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to create a new label in a specific project.
-
-By defining the `api_post_body` method, we we allow for the [`ApiFabricator.api_post`](https://gitlab.com/gitlab-org/gitlab-ee/blob/a9177ca1812bac57e2b2fa4560e1d5dd8ffac38b/qa/qa/resource/api_fabricator.rb#L68) method to know which data to send when making the `POST` request.
-
-> Notice that we pass both `name` and `color` attributes in the `api_post_body` since [those are required](https://docs.gitlab.com/ee/api/labels.html#create-a-new-label).
-
-### 8. Page Objects
-
-> Page Objects are auto-loaded in the `qa/qa.rb` file and available in all the test files (`*_spec.rb`).
-
-Page Objects are used in end-to-end tests for maintenance reasons, where page's elements and methods are defined to be reused in any test.
-
-Take a look at [this document that specifically details the usage of Page Objects](./qa/page/README.md).
-
-Now, let's go back to our examples.
-
-...
-
-#### Adding testability
-
-TBD.
diff --git a/qa/knapsack/gitlab-ce/review-qa-all_master_report.json b/qa/knapsack/gitlab-ce/review-qa-all_master_report.json
new file mode 100644
index 00000000000..f147346ba0f
--- /dev/null
+++ b/qa/knapsack/gitlab-ce/review-qa-all_master_report.json
@@ -0,0 +1,42 @@
+{
+ "qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb": 9.697327613830566,
+ "qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb": 46.54227638244629,
+ "qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb": 10.214765310287476,
+ "qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb": 7.882027864456177,
+ "qa/specs/features/api/3_create/repository/files_spec.rb": 5.015859127044678,
+ "qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb": 12.772682905197144,
+ "qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb": 29.76174831390381,
+ "qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb": 22.800872802734375,
+ "qa/specs/features/browser_ui/1_manage/login/register_spec.rb": 22.320587396621704,
+ "qa/specs/features/api/1_manage/users_spec.rb": 0.6089541912078857,
+ "qa/specs/features/browser_ui/3_create/repository/clone_spec.rb": 0.9618203639984131,
+ "qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb": 13.403101205825806,
+ "qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb": 8.810423135757446,
+ "qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb": 7.730542182922363,
+ "qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb": 16.18057894706726,
+ "qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb": 8.31815505027771,
+ "qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb": 9.48607873916626,
+ "qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb": 19.552733182907104,
+ "qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb": 17.273863554000854,
+ "qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb": 8.281434059143066,
+ "qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb": 18.047621726989746,
+ "qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb": 7.422840595245361,
+ "qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb": 3.438166856765747,
+ "qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb": 18.679633855819702,
+ "qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb": 27.943300485610962,
+ "qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb": 39.17585229873657,
+ "qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb": 40.09336972236633,
+ "qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb": 3.705310821533203,
+ "qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb": 5.812374591827393,
+ "qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb": 92.46774697303772,
+ "qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb": 100.28881478309631,
+ "qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb": 23.710937023162842,
+ "qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb": 20.58603596687317,
+ "qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb": 25.460349321365356,
+ "qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb": 19.459370374679565,
+ "qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb": 6.731764793395996,
+ "qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb": 15.342933893203735,
+ "qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb": 11.280649185180664,
+ "qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb": 57.48992609977722,
+ "qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb": 32.5517954826355
+} \ No newline at end of file
diff --git a/qa/qa/ce/strategy.rb b/qa/qa/ce/strategy.rb
index 7e2d02424fe..6c1820ffdc8 100644
--- a/qa/qa/ce/strategy.rb
+++ b/qa/qa/ce/strategy.rb
@@ -10,9 +10,18 @@ module QA
end
def perform_before_hooks
+ retries ||= 0
+
# The login page could take some time to load the first time it is visited.
# We visit the login page and wait for it to properly load only once before the tests.
QA::Runtime::Browser.visit(:gitlab, QA::Page::Main::Login)
+ rescue QA::Page::Validatable::PageValidationError
+ if (retries += 1) < 3
+ Runtime::Logger.warn("The login page did not appear as expected. Retrying... (attempt ##{retries})")
+ retry
+ end
+
+ raise
end
end
end
diff --git a/qa/qa/fixtures/auto_devops_rack/Dockerfile b/qa/qa/fixtures/auto_devops_rack/Dockerfile
new file mode 100644
index 00000000000..1f59c23ea88
--- /dev/null
+++ b/qa/qa/fixtures/auto_devops_rack/Dockerfile
@@ -0,0 +1,9 @@
+FROM ruby:2.6.3-alpine
+ADD ./ /app/
+WORKDIR /app
+ENV RACK_ENV production
+ENV PORT 5000
+EXPOSE 5000
+
+RUN bundle install
+CMD ["bundle","exec", "rackup", "-p", "5000"]
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index 389f4e0032e..d247a273637 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -78,8 +78,12 @@ module QA
page.evaluate_script('xhr.status') == 200
end
- def find_element(name, text: nil, wait: Capybara.default_max_wait_time)
- find(element_selector_css(name), wait: wait, text: text)
+ def find_element(name, **kwargs)
+ find(element_selector_css(name), kwargs)
+ end
+
+ def active_element?(name)
+ find_element(name, class: 'active')
end
def all_elements(name)
@@ -132,6 +136,15 @@ module QA
has_no_css?('.fa-spinner', wait: Capybara.default_max_wait_time)
end
+ def wait_for_animated_element(name)
+ # It would be ideal if we could detect when the animation is complete
+ # but in some cases there's nothing we can easily access via capybara
+ # so instead we wait for the element, and then we wait a little longer
+ raise ElementNotFound, %Q(Couldn't find element named "#{name}") unless has_element?(name)
+
+ sleep 1
+ end
+
def within_element(name, text: nil)
page.within(element_selector_css(name), text: text) do
yield
diff --git a/qa/qa/page/file/form.rb b/qa/qa/page/file/form.rb
index dd9a9e054e1..e42de7d65c5 100644
--- a/qa/qa/page/file/form.rb
+++ b/qa/qa/page/file/form.rb
@@ -38,6 +38,8 @@ module QA
def commit_changes
click_on 'Commit changes'
+
+ finished_loading?
end
def select_template(template_type, template)
diff --git a/qa/qa/page/project/branches/show.rb b/qa/qa/page/project/branches/show.rb
index 762eacdab15..480fc7d78cb 100644
--- a/qa/qa/page/project/branches/show.rb
+++ b/qa/qa/page/project/branches/show.rb
@@ -28,9 +28,11 @@ module QA
finished_loading?
end
- def has_no_branch?(branch_name)
- within_element(:all_branches) do
- has_no_element?(:branch_name, text: branch_name, wait: Support::Waiter::DEFAULT_MAX_WAIT_TIME)
+ def has_no_branch?(branch_name, reload: false)
+ wait(reload: reload) do
+ within_element(:all_branches) do
+ has_no_element?(:branch_name, text: branch_name)
+ end
end
end
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
index 4f26ca5037c..defd85a5740 100644
--- a/qa/qa/page/project/new.rb
+++ b/qa/qa/page/project/new.rb
@@ -12,6 +12,7 @@ module QA
end
view 'app/views/projects/_new_project_fields.html.haml' do
+ element :initialize_with_readme_checkbox
element :project_namespace_select
element :project_namespace_field, 'namespaces_options' # rubocop:disable QA/ElementWithPattern
element :project_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
@@ -64,6 +65,10 @@ module QA
def click_github_link
click_link 'GitHub'
end
+
+ def enable_initialize_with_readme
+ check_element :initialize_with_readme_checkbox
+ end
end
end
end
diff --git a/qa/qa/page/project/operations/kubernetes/show.rb b/qa/qa/page/project/operations/kubernetes/show.rb
index 4096f57b7db..c81e13e9b91 100644
--- a/qa/qa/page/project/operations/kubernetes/show.rb
+++ b/qa/qa/page/project/operations/kubernetes/show.rb
@@ -8,8 +8,8 @@ module QA
class Show < Page::Base
view 'app/assets/javascripts/clusters/components/application_row.vue' do
element :application_row, 'js-cluster-application-row-${this.id}' # rubocop:disable QA/ElementWithPattern
- element :install_button, "s__('ClusterIntegration|Install')" # rubocop:disable QA/ElementWithPattern
- element :installed_button, "s__('ClusterIntegration|Installed')" # rubocop:disable QA/ElementWithPattern
+ element :install_button, "__('Install')" # rubocop:disable QA/ElementWithPattern
+ element :installed_button, "__('Installed')" # rubocop:disable QA/ElementWithPattern
end
view 'app/assets/javascripts/clusters/components/applications.vue' do
diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb
index ff7cc04e352..b5a36862389 100644
--- a/qa/qa/page/project/web_ide/edit.rb
+++ b/qa/qa/page/project/web_ide/edit.rb
@@ -7,6 +7,10 @@ module QA
class Edit < Page::Base
include Page::Component::DropdownFilter
+ view 'app/assets/javascripts/ide/components/activity_bar.vue' do
+ element :commit_mode_tab
+ end
+
view 'app/assets/javascripts/ide/components/ide_tree.vue' do
element :new_file
end
@@ -17,6 +21,7 @@ module QA
view 'app/assets/javascripts/ide/components/new_dropdown/modal.vue' do
element :full_file_path
+ element :new_file_modal
element :template_list
end
@@ -42,12 +47,19 @@ module QA
def create_new_file_from_template(file_name, template)
click_element :new_file
+
+ # Wait for the modal animation to complete before clicking on the file name
+ wait_for_animated_element(:new_file_modal)
+
within_element(:template_list) do
click_on file_name
rescue Capybara::ElementNotFound
raise ElementNotFound, %Q(Couldn't find file template named "#{file_name}". Please confirm that it is a valid option.)
end
+ # Wait for the modal to fade out too
+ has_no_element?(:new_file_modal)
+
wait(reload: false) do
within_element(:file_templates_bar) do
click_element :file_template_dropdown
@@ -63,10 +75,16 @@ module QA
end
def commit_changes
+ # Clicking :begin_commit_button the first time switches from the
+ # edit to the commit view
+ click_element :begin_commit_button
+ active_element? :commit_mode_tab
+
+ # We need to click :begin_commit_button again
click_element :begin_commit_button
- # After clicking :begin_commit_button there is an animation that
- # hides :begin_commit_button and shows :commit_button
+ # After clicking :begin_commit_button the 2nd time there is an
+ # animation that hides :begin_commit_button and shows :commit_button
#
# Wait for the animation to complete before clicking :commit_button
# otherwise the click will quietly do nothing.
@@ -75,7 +93,10 @@ module QA
has_element?(:commit_button)
end
- # Retry the attempt to click :commit_button just in case part of the
+ # At this point we're ready to commit and the button should be
+ # labelled "Stage & Commit"
+ #
+ # Click :commit_button and keep retrying just in case part of the
# animation is still in process even when the buttons have the
# expected visibility.
commit_success_msg_shown = retry_until do
diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb
index 523d92c7ef3..283fc6cdbcb 100644
--- a/qa/qa/resource/base.rb
+++ b/qa/qa/resource/base.rb
@@ -15,6 +15,38 @@ module QA
def_delegators :evaluator, :attribute
+ def self.fabricate!(*args, &prepare_block)
+ fabricate_via_api!(*args, &prepare_block)
+ rescue NotImplementedError
+ fabricate_via_browser_ui!(*args, &prepare_block)
+ end
+
+ def self.fabricate_via_browser_ui!(*args, &prepare_block)
+ options = args.extract_options!
+ resource = options.fetch(:resource) { new }
+ parents = options.fetch(:parents) { [] }
+
+ do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
+ log_fabrication(:browser_ui, resource, parents, args) { resource.fabricate!(*args) }
+
+ current_url
+ end
+ end
+
+ def self.fabricate_via_api!(*args, &prepare_block)
+ options = args.extract_options!
+ resource = options.fetch(:resource) { new }
+ parents = options.fetch(:parents) { [] }
+
+ raise NotImplementedError unless resource.api_support?
+
+ resource.eager_load_api_client!
+
+ do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
+ log_fabrication(:api, resource, parents, args) { resource.fabricate_via_api! }
+ end
+ end
+
def fabricate!(*_args)
raise NotImplementedError
end
@@ -55,38 +87,6 @@ module QA
QA::Runtime::Logger.info "<#{self.class}> Attribute #{name.inspect} has both API response `#{api_value}` and a block. API response will be picked. Block will be ignored."
end
- def self.fabricate!(*args, &prepare_block)
- fabricate_via_api!(*args, &prepare_block)
- rescue NotImplementedError
- fabricate_via_browser_ui!(*args, &prepare_block)
- end
-
- def self.fabricate_via_browser_ui!(*args, &prepare_block)
- options = args.extract_options!
- resource = options.fetch(:resource) { new }
- parents = options.fetch(:parents) { [] }
-
- do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
- log_fabrication(:browser_ui, resource, parents, args) { resource.fabricate!(*args) }
-
- current_url
- end
- end
-
- def self.fabricate_via_api!(*args, &prepare_block)
- options = args.extract_options!
- resource = options.fetch(:resource) { new }
- parents = options.fetch(:parents) { [] }
-
- raise NotImplementedError unless resource.api_support?
-
- resource.eager_load_api_client!
-
- do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
- log_fabrication(:api, resource, parents, args) { resource.fabricate_via_api! }
- end
- end
-
def self.do_fabricate!(resource:, prepare_block:, parents: [])
prepare_block.call(resource) if prepare_block
diff --git a/qa/qa/resource/file.rb b/qa/qa/resource/file.rb
index 57e82ac19ad..ca74654bf90 100644
--- a/qa/qa/resource/file.rb
+++ b/qa/qa/resource/file.rb
@@ -3,9 +3,12 @@
module QA
module Resource
class File < Base
- attr_accessor :name,
+ attr_accessor :author_email,
+ :author_name,
+ :branch,
:content,
- :commit_message
+ :commit_message,
+ :name
attribute :project do
Project.fabricate! do |resource|
@@ -31,6 +34,30 @@ module QA
page.commit_changes
end
end
+
+ def resource_web_url(resource)
+ super
+ rescue ResourceURLMissingError
+ # this particular resource does not expose a web_url property
+ end
+
+ def api_get_path
+ "/projects/#{CGI.escape(project.path_with_namespace)}/repository/files/#{CGI.escape(@name)}"
+ end
+
+ def api_post_path
+ api_get_path
+ end
+
+ def api_post_body
+ {
+ branch: @branch || "master",
+ author_email: @author_email || Runtime::User.default_email,
+ author_name: @author_name || Runtime::User.username,
+ content: content,
+ commit_message: commit_message
+ }
+ end
end
end
end
diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb
index c1a0cff86d8..d706439a891 100644
--- a/qa/qa/resource/project.rb
+++ b/qa/qa/resource/project.rb
@@ -7,6 +7,8 @@ module QA
class Project < Base
include Events::Project
+ attr_writer :initialize_with_readme
+
attribute :id
attribute :name
attribute :description
@@ -33,6 +35,7 @@ module QA
def initialize
@description = 'My awesome project'
+ @initialize_with_readme = false
end
def name=(raw_name)
@@ -49,6 +52,7 @@ module QA
page.choose_name(@name)
page.add_description(@description)
page.set_visibility('Public')
+ page.enable_initialize_with_readme if @initialize_with_readme
page.create_new_project
end
end
@@ -73,7 +77,8 @@ module QA
path: name,
name: name,
description: description,
- visibility: 'public'
+ visibility: 'public',
+ initialize_with_readme: @initialize_with_readme
}
end
diff --git a/qa/qa/resource/repository/project_push.rb b/qa/qa/resource/repository/project_push.rb
index cad89ebb0bb..e98880ce195 100644
--- a/qa/qa/resource/repository/project_push.rb
+++ b/qa/qa/resource/repository/project_push.rb
@@ -32,8 +32,8 @@ module QA
def fabricate!
super
- project.visit!
project.wait_for_push @commit_message if @wait_for_push
+ project.visit!
end
end
end
diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb
index 58de01705d7..40a3bc85195 100644
--- a/qa/qa/runtime/api/client.rb
+++ b/qa/qa/runtime/api/client.rb
@@ -25,15 +25,12 @@ module QA
private
def create_personal_access_token
- if @is_new_session
- Runtime::Browser.visit(@address, Page::Main::Login) { do_create_personal_access_token }
- else
- do_create_personal_access_token
- end
+ Runtime::Browser.visit(@address, Page::Main::Login) if @is_new_session
+ do_create_personal_access_token
end
def do_create_personal_access_token
- Page::Main::Login.act { sign_in_using_credentials }
+ Page::Main::Login.perform(&:sign_in_using_credentials)
Resource::PersonalAccessToken.fabricate!.access_token
end
end
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index 03cae3c1fe6..82510dfa03c 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -136,6 +136,10 @@ module QA
ENV['GITLAB_QA_PASSWORD_2']
end
+ def knapsack?
+ !!(ENV['KNAPSACK_GENERATE_REPORT'] || ENV['KNAPSACK_REPORT_PATH'] || ENV['KNAPSACK_TEST_FILE_PATTERN'])
+ end
+
def ldap_username
@ldap_username ||= ENV['GITLAB_LDAP_USERNAME']
end
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb
index f97b0e56ca2..530fc684437 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module QA
- # Failure issue: https://gitlab.com/gitlab-org/quality/staging/issues/53
- context 'Plan', :quarantine do
+ context 'Plan' do
describe 'issue suggestions' do
let(:issue_title) { 'Issue Lists are awesome' }
@@ -10,12 +9,12 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
- project = Resource::Project.fabricate! do |resource|
+ project = Resource::Project.fabricate_via_api! do |resource|
resource.name = 'project-for-issue-suggestions'
resource.description = 'project for issue suggestions'
end
- Resource::Issue.fabricate! do |issue|
+ Resource::Issue.fabricate_via_browser_ui! do |issue|
issue.title = issue_title
issue.project = project
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
index 10bba98f704..2750b171a85 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
@@ -17,14 +17,7 @@ module QA
@project = Resource::Project.fabricate! do |project|
project.name = 'file-template-project'
project.description = 'Add file templates via the Files view'
- end
-
- # There's no 'New File' dropdown when the project is blank, so we first
- # add a dummy file so that the dropdown will appear
- Resource::File.fabricate! do |file|
- file.project = @project
- file.name = 'README.md'
- file.content = '# Readme'
+ project.initialize_with_readme = true
end
Page::Main::Menu.perform(&:sign_out)
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
index cf6c24fa873..37a784248d4 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
@@ -84,7 +84,7 @@ module QA
page.refresh
Page::Project::Branches::Show.perform do |branches_view|
- expect(branches_view).to have_no_branch(second_branch)
+ expect(branches_view).to have_no_branch(second_branch, reload: true)
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
index 46346d1b984..d345fbfe995 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
@@ -12,7 +12,7 @@ module QA
file_content = 'QA Test - File content'
commit_message_for_create = 'QA Test - Create new file'
- Resource::File.fabricate! do |file|
+ Resource::File.fabricate_via_browser_ui! do |file|
file.name = file_name
file.content = file_content
file.commit_message = commit_message_for_create
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb
index a544efb35ee..5bfafdfa041 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb
@@ -26,6 +26,8 @@ module QA
Page::Main::Login.perform(&:sign_in_using_credentials)
set_file_size_limit('')
+
+ Page::Main::Menu.perform(&:sign_out)
end
it 'push successful when the file size is under the limit' do
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb
index b7400cdca97..680c5e21fa4 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb
@@ -3,13 +3,16 @@
module QA
context 'Create' do
# failure reported: https://gitlab.com/gitlab-org/quality/nightly/issues/42
- # also failing in staging until the fix is picked into the next release:
- # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24533
describe 'Commit data', :quarantine do
before(:context) do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
+ # Get the user's details to confirm they're included in the email patch
+ @user = Resource::User.fabricate_via_api! do |user|
+ user.username = Runtime::User.username
+ end
+
project_push = Resource::Repository::ProjectPush.fabricate! do |push|
push.file_name = 'README.md'
push.file_content = '# This is a test project'
@@ -21,12 +24,13 @@ module QA
# add second file to repo to enable diff from initial commit
@commit_message = 'Add second file'
- Page::Project::Show.perform(&:create_new_file!)
- Page::File::Form.perform do |f|
- f.add_name('second')
- f.add_content('second file content')
- f.add_commit_message(@commit_message)
- f.commit_changes
+ Resource::File.fabricate_via_api! do |file|
+ file.project = @project
+ file.name = 'second'
+ file.content = 'second file content'
+ file.commit_message = @commit_message
+ file.author_name = @user.name
+ file.author_email = @user.public_email
end
end
@@ -42,15 +46,11 @@ module QA
end
it 'user views raw email patch' do
- user = Resource::User.fabricate_via_api! do |user|
- user.username = Runtime::User.username
- end
-
view_commit
Page::Project::Commit::Show.perform(&:select_email_patches)
- expect(page).to have_content("From: #{user.name} <#{user.public_email}>")
+ expect(page).to have_content("From: #{@user.name} <#{@user.public_email}>")
expect(page).to have_content('Subject: [PATCH] Add second file')
expect(page).to have_content('diff --git a/second b/second')
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
index 66cd712afb0..078d3b2b5b1 100644
--- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
@@ -17,16 +17,7 @@ module QA
@project = Resource::Project.fabricate! do |project|
project.name = 'file-template-project'
project.description = 'Add file templates via the Web IDE'
- end
- @project.visit!
-
- # Add a file via the regular Files view because the Web IDE isn't
- # available unless there is a file present
- Page::Project::Show.perform(&:create_first_new_file!)
- Page::File::Form.perform do |page|
- page.add_name('dummy')
- page.add_content('Enable the Web IDE')
- page.commit_changes
+ project.initialize_with_readme = true
end
Page::Main::Menu.perform(&:sign_out)
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index 306913dafa6..f1cb9378de8 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
require 'rspec/core'
+require 'rspec/expectations'
+require 'knapsack'
module QA
module Specs
@@ -32,10 +34,25 @@ module QA
end
args.push(options)
- args.push(DEFAULT_TEST_PATH_ARGS) unless options.any? { |opt| opt =~ %r{/features/} }
Runtime::Browser.configure!
+ if Runtime::Env.knapsack?
+ allocator = Knapsack::AllocatorBuilder.new(Knapsack::Adapters::RSpecAdapter).allocator
+
+ QA::Runtime::Logger.info ''
+ QA::Runtime::Logger.info 'Report specs:'
+ QA::Runtime::Logger.info allocator.report_node_tests.join(', ')
+ QA::Runtime::Logger.info ''
+ QA::Runtime::Logger.info 'Leftover specs:'
+ QA::Runtime::Logger.info allocator.leftover_node_tests.join(', ')
+ QA::Runtime::Logger.info ''
+
+ args.push(['--', allocator.node_tests])
+ else
+ args.push(DEFAULT_TEST_PATH_ARGS) unless options.any? { |opt| opt =~ %r{/features/} }
+ end
+
RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
abort if status.nonzero?
end
diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb
index 3fe567d7757..02ebd96ad49 100644
--- a/qa/qa/support/page/logging.rb
+++ b/qa/qa/support/page/logging.rb
@@ -33,11 +33,8 @@ module QA
exists
end
- def find_element(name, text: nil, wait: Capybara.default_max_wait_time)
- msg = ["finding :#{name}"]
- msg << %Q(with text "#{text}") if text
- msg << "(wait: #{wait})"
- log(msg.compact.join(' '))
+ def find_element(name, **kwargs)
+ log("finding :#{name} with args #{kwargs}")
element = super
@@ -122,6 +119,12 @@ module QA
loaded
end
+ def wait_for_animated_element(name)
+ log("waiting for animated element: #{name}")
+
+ super
+ end
+
def within_element(name)
log("within element :#{name}")
diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb
index 99e96b81a51..092c6a17c9c 100644
--- a/qa/spec/page/logging_spec.rb
+++ b/qa/spec/page/logging_spec.rb
@@ -64,11 +64,21 @@ describe QA::Support::Page::Logging do
it 'logs find_element with text' do
expect { subject.find_element(:element, text: 'foo') }
- .to output(/finding :element with text "foo"/).to_stdout_from_any_process
+ .to output(/finding :element with args {:text=>"foo"}/).to_stdout_from_any_process
expect { subject.find_element(:element, text: 'foo') }
.to output(/found :element/).to_stdout_from_any_process
end
+ it 'logs find_element with wait' do
+ expect { subject.find_element(:element, wait: 0) }
+ .to output(/finding :element with args {:wait=>0}/).to_stdout_from_any_process
+ end
+
+ it 'logs find_element with class' do
+ expect { subject.find_element(:element, class: 'active') }
+ .to output(/finding :element with args {:class=>\"active\"}/).to_stdout_from_any_process
+ end
+
it 'logs click_element' do
expect { subject.click_element(:element) }
.to output(/clicking :element/).to_stdout_from_any_process
diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb
index 04085efe2ce..2560695ef2e 100644
--- a/qa/spec/runtime/env_spec.rb
+++ b/qa/spec/runtime/env_spec.rb
@@ -168,6 +168,30 @@ describe QA::Runtime::Env do
end
end
+ describe '.knapsack?' do
+ it 'returns true if KNAPSACK_GENERATE_REPORT is defined' do
+ stub_env('KNAPSACK_GENERATE_REPORT', 'true')
+
+ expect(described_class.knapsack?).to be_truthy
+ end
+
+ it 'returns true if KNAPSACK_REPORT_PATH is defined' do
+ stub_env('KNAPSACK_REPORT_PATH', '/a/path')
+
+ expect(described_class.knapsack?).to be_truthy
+ end
+
+ it 'returns true if KNAPSACK_TEST_FILE_PATTERN is defined' do
+ stub_env('KNAPSACK_TEST_FILE_PATTERN', '/a/**/pattern')
+
+ expect(described_class.knapsack?).to be_truthy
+ end
+
+ it 'returns false if neither KNAPSACK_GENERATE_REPORT nor KNAPSACK_REPORT_PATH nor KNAPSACK_TEST_FILE_PATTERN are defined' do
+ expect(described_class.knapsack?).to be_falsey
+ end
+ end
+
describe '.require_github_access_token!' do
it 'raises ArgumentError if GITHUB_ACCESS_TOKEN is not defined' do
stub_env('GITHUB_ACCESS_TOKEN', nil)
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
index a368ffba711..f25dbf3a8ab 100644
--- a/qa/spec/spec_helper.rb
+++ b/qa/spec/spec_helper.rb
@@ -3,6 +3,11 @@
require_relative '../qa'
require 'rspec/retry'
+if ENV['CI'] && QA::Runtime::Env.knapsack? && !ENV['NO_KNAPSACK']
+ require 'knapsack'
+ Knapsack::Adapters::RSpecAdapter.bind
+end
+
%w[helpers shared_examples].each do |d|
Dir[::File.join(__dir__, d, '**', '*.rb')].each { |f| require f }
end
diff --git a/spec/controllers/acme_challenges_controller_spec.rb b/spec/controllers/acme_challenges_controller_spec.rb
new file mode 100644
index 00000000000..cee06bed27b
--- /dev/null
+++ b/spec/controllers/acme_challenges_controller_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AcmeChallengesController do
+ describe '#show' do
+ let!(:acme_order) { create(:pages_domain_acme_order) }
+
+ def make_request(domain, token)
+ get(:show, params: { domain: domain, token: token })
+ end
+
+ before do
+ make_request(domain, token)
+ end
+
+ context 'with right domain and token' do
+ let(:domain) { acme_order.pages_domain.domain }
+ let(:token) { acme_order.challenge_token }
+
+ it 'renders acme challenge file content' do
+ expect(response.body).to eq(acme_order.challenge_file_content)
+ end
+ end
+
+ context 'when domain is invalid' do
+ let(:domain) { 'somewrongdomain.com' }
+ let(:token) { acme_order.challenge_token }
+
+ it 'renders not found' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when token is invalid' do
+ let(:domain) { acme_order.pages_domain.domain }
+ let(:token) { 'wrongtoken' }
+
+ it 'renders not found' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 5ecd1b6b7c8..40669ec5451 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -691,4 +691,38 @@ describe ApplicationController do
end
end
end
+
+ context 'Gitlab::Session' do
+ controller(described_class) do
+ prepend_before_action do
+ authenticate_sessionless_user!(:rss)
+ end
+
+ def index
+ if Gitlab::Session.current
+ head :created
+ else
+ head :not_found
+ end
+ end
+ end
+
+ it 'is set on web requests' do
+ sign_in(user)
+
+ get :index
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+
+ context 'with sessionless user' do
+ it 'is not set' do
+ personal_access_token = create(:personal_access_token, user: user)
+
+ get :index, format: :atom, params: { private_token: personal_access_token.token }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
end
diff --git a/spec/controllers/concerns/import_url_params_spec.rb b/spec/controllers/concerns/import_url_params_spec.rb
new file mode 100644
index 00000000000..adbe6e5d3bf
--- /dev/null
+++ b/spec/controllers/concerns/import_url_params_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ImportUrlParams do
+ let(:import_url_params) do
+ controller = OpenStruct.new(params: params).extend(described_class)
+ controller.import_url_params
+ end
+
+ context 'empty URL' do
+ let(:params) do
+ ActionController::Parameters.new(project: {
+ title: 'Test'
+ })
+ end
+
+ it 'returns empty hash' do
+ expect(import_url_params).to eq({})
+ end
+ end
+
+ context 'url and password separately provided' do
+ let(:params) do
+ ActionController::Parameters.new(project: {
+ import_url: 'https://url.com',
+ import_url_user: 'user', import_url_password: 'password'
+ })
+ end
+
+ describe '#import_url_params' do
+ it 'returns hash with import_url' do
+ expect(import_url_params).to eq(
+ import_url: "https://user:password@url.com"
+ )
+ end
+ end
+ end
+
+ context 'url with provided empty credentials' do
+ let(:params) do
+ ActionController::Parameters.new(project: {
+ import_url: 'https://user:password@url.com',
+ import_url_user: '', import_url_password: ''
+ })
+ end
+
+ describe '#import_url_params' do
+ it 'does not change the url' do
+ expect(import_url_params).to eq(
+ import_url: "https://user:password@url.com"
+ )
+ end
+ end
+ end
+end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 1cd08200552..47d7e278183 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -141,6 +141,28 @@ describe GroupsController do
end
describe 'POST #create' do
+ it 'allows creating a group' do
+ sign_in(user)
+
+ expect do
+ post :create, params: { group: { name: 'new_group', path: "new_group" } }
+ end.to change { Group.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+
+ context 'authorization' do
+ it 'allows an admin to create a group' do
+ sign_in(create(:admin))
+
+ expect do
+ post :create, params: { group: { name: 'new_group', path: "new_group" } }
+ end.to change { Group.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
context 'when creating subgroups', :nested_groups do
[true, false].each do |can_create_group_status|
context "and can_create_group is #{can_create_group_status}" do
diff --git a/spec/controllers/import/fogbugz_controller_spec.rb b/spec/controllers/import/fogbugz_controller_spec.rb
index f1e0923f316..f7c813576aa 100644
--- a/spec/controllers/import/fogbugz_controller_spec.rb
+++ b/spec/controllers/import/fogbugz_controller_spec.rb
@@ -11,6 +11,44 @@ describe Import::FogbugzController do
sign_in(user)
end
+ describe 'POST #callback' do
+ let(:token) { FFaker::Lorem.characters(8) }
+ let(:uri) { 'https://example.com' }
+ let(:xml_response) { %Q(<?xml version=\"1.0\" encoding=\"UTF-8\"?><response><token><![CDATA[#{token}]]></token></response>) }
+
+ it 'attempts to contact Fogbugz server' do
+ stub_request(:post, "https://example.com/api.asp").to_return(status: 200, body: xml_response, headers: {})
+
+ post :callback, params: { uri: uri, email: 'test@example.com', password: 'mypassword' }
+
+ expect(session[:fogbugz_token]).to eq(token)
+ expect(session[:fogbugz_uri]).to eq(uri)
+ expect(response).to redirect_to(new_user_map_import_fogbugz_path)
+ end
+ end
+
+ describe 'POST #create_user_map' do
+ let(:user_map) do
+ {
+ "2" => {
+ "name" => "Test User",
+ "email" => "testuser@example.com",
+ "gitlab_user" => "3"
+ }
+ }
+ end
+
+ it 'stores the user map in the session' do
+ client = double(user_map: {})
+ expect(controller).to receive(:client).and_return(client)
+
+ post :create_user_map, params: { users: user_map }
+
+ expect(session[:fogbugz_user_map]).to eq(user_map)
+ expect(response).to redirect_to(status_import_fogbugz_path)
+ end
+ end
+
describe 'GET status' do
before do
@repo = OpenStruct.new(name: 'vim')
diff --git a/spec/controllers/import/phabricator_controller_spec.rb b/spec/controllers/import/phabricator_controller_spec.rb
new file mode 100644
index 00000000000..85085a8e996
--- /dev/null
+++ b/spec/controllers/import/phabricator_controller_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Import::PhabricatorController do
+ let(:current_user) { create(:user) }
+
+ before do
+ sign_in current_user
+ end
+
+ describe 'GET #new' do
+ subject { get :new }
+
+ context 'when the import source is not available' do
+ before do
+ stub_feature_flags(phabricator_import: true)
+ stub_application_setting(import_sources: [])
+ end
+
+ it { is_expected.to have_gitlab_http_status(404) }
+ end
+
+ context 'when the feature is disabled' do
+ before do
+ stub_feature_flags(phabricator_import: false)
+ stub_application_setting(import_sources: ['phabricator'])
+ end
+
+ it { is_expected.to have_gitlab_http_status(404) }
+ end
+
+ context 'when the import is available' do
+ before do
+ stub_feature_flags(phabricator_import: true)
+ stub_application_setting(import_sources: ['phabricator'])
+ end
+
+ it { is_expected.to have_gitlab_http_status(200) }
+ end
+ end
+
+ describe 'POST #create' do
+ subject(:post_create) { post :create, params: params }
+
+ context 'with valid params' do
+ let(:params) do
+ { path: 'phab-import',
+ name: 'Phab import',
+ phabricator_server_url: 'https://phabricator.example.com',
+ api_token: 'hazaah',
+ namespace_id: current_user.namespace_id }
+ end
+
+ it 'creates a project to import' do
+ expect_next_instance_of(Gitlab::PhabricatorImport::Importer) do |importer|
+ expect(importer).to receive(:execute)
+ end
+
+ expect { post_create }.to change { current_user.namespace.projects.reload.size }.from(0).to(1)
+
+ expect(current_user.namespace.projects.last).to be_import
+ end
+ end
+
+ context 'when an import param is missing' do
+ let(:params) do
+ { path: 'phab-import',
+ name: 'Phab import',
+ phabricator_server_url: nil,
+ api_token: 'hazaah',
+ namespace_id: current_user.namespace_id }
+ end
+
+ it 'does not create the project' do
+ expect { post_create }.not_to change { current_user.namespace.projects.reload.size }
+ end
+ end
+
+ context 'when a project param is missing' do
+ let(:params) do
+ { phabricator_server_url: 'https://phabricator.example.com',
+ api_token: 'hazaah',
+ namespace_id: current_user.namespace_id }
+ end
+
+ it 'does not create the project' do
+ expect { post_create }.not_to change { current_user.namespace.projects.reload.size }
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/avatars_controller_spec.rb b/spec/controllers/projects/avatars_controller_spec.rb
index de1b9dc0bf3..d463619ad0b 100644
--- a/spec/controllers/projects/avatars_controller_spec.rb
+++ b/spec/controllers/projects/avatars_controller_spec.rb
@@ -39,7 +39,7 @@ describe Projects::AvatarsController do
end
context 'when the avatar is stored in lfs' do
- it_behaves_like 'repository lfs file load' do
+ it_behaves_like 'a controller that can serve LFS files' do
let(:filename) { 'lfs_object.iso' }
let(:filepath) { "files/lfs/#{filename}" }
end
diff --git a/spec/controllers/projects/ci/lints_controller_spec.rb b/spec/controllers/projects/ci/lints_controller_spec.rb
index 323a32575af..cc6ac83ca38 100644
--- a/spec/controllers/projects/ci/lints_controller_spec.rb
+++ b/spec/controllers/projects/ci/lints_controller_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Projects::Ci::LintsController do
+ include StubRequests
+
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
@@ -70,7 +72,7 @@ describe Projects::Ci::LintsController do
context 'with a valid gitlab-ci.yml' do
before do
- WebMock.stub_request(:get, remote_file_path).to_return(body: remote_file_content)
+ stub_full_request(remote_file_path).to_return(body: remote_file_content)
project.add_developer(user)
post :create, params: { namespace_id: project.namespace, project_id: project, content: content }
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index d5eea5b0439..9699f2952f2 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -433,20 +433,6 @@ describe Projects::EnvironmentsController do
end
context 'when only one time param is provided' do
- context 'when :metrics_time_window feature flag is disabled' do
- before do
- stub_feature_flags(metrics_time_window: false)
- expect(environment).to receive(:additional_metrics).with(no_args).and_return(nil)
- end
-
- it 'returns a time-window agnostic response' do
- additional_metrics(start: '1552647300.651094')
-
- expect(response).to have_gitlab_http_status(204)
- expect(json_response).to eq({})
- end
- end
-
it 'raises an error when start is missing' do
expect { additional_metrics(end: '1552647300.651094') }
.to raise_error(ActionController::ParameterMissing)
diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb
index 8d88ee7dfd6..bdc81efe3bc 100644
--- a/spec/controllers/projects/imports_controller_spec.rb
+++ b/spec/controllers/projects/imports_controller_spec.rb
@@ -122,4 +122,19 @@ describe Projects::ImportsController do
end
end
end
+
+ describe 'POST #create' do
+ let(:params) { { import_url: 'https://github.com/vim/vim.git', import_url_user: 'user', import_url_password: 'password' } }
+ let(:project) { create(:project) }
+
+ before do
+ allow(RepositoryImportWorker).to receive(:perform_async)
+
+ post :create, params: { project: params, namespace_id: project.namespace.to_param, project_id: project }
+ end
+
+ it 'sets import_url to the project' do
+ expect(project.reload.import_url).to eq('https://user:password@github.com/vim/vim.git')
+ end
+ end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 0c46b43f080..32607fc5f56 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -130,6 +130,37 @@ describe Projects::IssuesController do
end
end
+ context 'with relative_position sorting' do
+ let!(:issue_list) { create_list(:issue, 2, project: project) }
+
+ before do
+ sign_in(user)
+ project.add_developer(user)
+ allow(Kaminari.config).to receive(:default_per_page).and_return(1)
+ end
+
+ it 'overrides the number allowed on the page' do
+ get :index,
+ params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ sort: 'relative_position'
+ }
+
+ expect(assigns(:issues).count).to eq 2
+ end
+
+ it 'allows the default number on the page' do
+ get :index,
+ params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project
+ }
+
+ expect(assigns(:issues).count).to eq 1
+ end
+ end
+
context 'external authorization' do
before do
sign_in user
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 9ef00fff3b2..490e9841492 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -841,8 +841,8 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
it 'erases artifacts' do
- expect(job.artifacts_file.exists?).to be_falsey
- expect(job.artifacts_metadata.exists?).to be_falsey
+ expect(job.artifacts_file.present?).to be_falsey
+ expect(job.artifacts_metadata.present?).to be_falsey
end
it 'erases trace' do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index f4a18dcba51..f8c0ab55eb4 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -429,8 +429,9 @@ describe Projects::MergeRequestsController do
it 'sets the MR to merge when the pipeline succeeds' do
service = double(:merge_when_pipeline_succeeds_service)
+ allow(service).to receive(:available_for?) { true }
- expect(MergeRequests::MergeWhenPipelineSucceedsService)
+ expect(AutoMerge::MergeWhenPipelineSucceedsService)
.to receive(:new).with(project, anything, anything)
.and_return(service)
expect(service).to receive(:execute).with(merge_request)
@@ -713,9 +714,9 @@ describe Projects::MergeRequestsController do
end
end
- describe 'POST cancel_merge_when_pipeline_succeeds' do
+ describe 'POST cancel_auto_merge' do
subject do
- post :cancel_merge_when_pipeline_succeeds,
+ post :cancel_auto_merge,
params: {
format: :json,
namespace_id: merge_request.project.namespace.to_param,
@@ -725,14 +726,15 @@ describe Projects::MergeRequestsController do
xhr: true
end
- it 'calls MergeRequests::MergeWhenPipelineSucceedsService' do
- mwps_service = double
+ it 'calls AutoMergeService' do
+ auto_merge_service = double
- allow(MergeRequests::MergeWhenPipelineSucceedsService)
+ allow(AutoMergeService)
.to receive(:new)
- .and_return(mwps_service)
+ .and_return(auto_merge_service)
- expect(mwps_service).to receive(:cancel).with(merge_request)
+ allow(auto_merge_service).to receive(:available_strategies).with(merge_request)
+ expect(auto_merge_service).to receive(:cancel).with(merge_request)
subject
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index f8470a94f98..767cee7d54a 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -175,6 +175,40 @@ describe Projects::MilestonesController do
end
end
+ describe '#labels' do
+ render_views
+
+ context 'as json' do
+ let!(:guest) { create(:user, username: 'guest1') }
+ let!(:group) { create(:group, :public) }
+ let!(:project) { create(:project, :public, group: group) }
+ let!(:label) { create(:label, title: 'test_label_on_private_issue', project: project) }
+ let!(:confidential_issue) { create(:labeled_issue, confidential: true, project: project, milestone: milestone, labels: [label]) }
+
+ it 'does not render labels of private issues if user has no access' do
+ sign_in(guest)
+
+ get :labels, params: { namespace_id: group.id, project_id: project.id, id: milestone.iid }, format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.content_type).to eq 'application/json'
+
+ expect(json_response['html']).not_to include(label.title)
+ end
+
+ it 'does render labels of private issues if user has access' do
+ sign_in(user)
+
+ get :labels, params: { namespace_id: group.id, project_id: project.id, id: milestone.iid }, format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.content_type).to eq 'application/json'
+
+ expect(json_response['html']).to include(label.title)
+ end
+ end
+ end
+
context 'promotion succeeds' do
before do
group.add_developer(user)
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index 3a89d8ce032..97acd47b4da 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -42,7 +42,7 @@ describe Projects::RawController do
end
end
- it_behaves_like 'repository lfs file load' do
+ it_behaves_like 'a controller that can serve LFS files' do
let(:filename) { 'lfs_object.iso' }
let(:filepath) { "be93687/files/lfs/#{filename}" }
end
diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb
index 782f5f272d9..18c594acae0 100644
--- a/spec/controllers/projects/serverless/functions_controller_spec.rb
+++ b/spec/controllers/projects/serverless/functions_controller_spec.rb
@@ -8,9 +8,8 @@ describe Projects::Serverless::FunctionsController do
let(:user) { create(:user) }
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
let(:service) { cluster.platform_kubernetes }
- let(:project) { cluster.project}
+ let(:project) { cluster.project }
let(:namespace) do
create(:cluster_kubernetes_namespace,
@@ -30,17 +29,69 @@ describe Projects::Serverless::FunctionsController do
end
describe 'GET #index' do
- context 'empty cache' do
- it 'has no data' do
+ let(:expected_json) { { 'knative_installed' => knative_state, 'functions' => functions } }
+
+ context 'when cache is being read' do
+ let(:knative_state) { 'checking' }
+ let(:functions) { [] }
+
+ before do
get :index, params: params({ format: :json })
+ end
- expect(response).to have_gitlab_http_status(204)
+ it 'returns checking' do
+ expect(json_response).to eq expected_json
end
- it 'renders an html page' do
- get :index, params: params
+ it { expect(response).to have_gitlab_http_status(200) }
+ end
+
+ context 'when cache is ready' do
+ let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+ let(:knative_state) { true }
- expect(response).to have_gitlab_http_status(200)
+ before do
+ allow_any_instance_of(Clusters::Cluster)
+ .to receive(:knative_services_finder)
+ .and_return(knative_services_finder)
+ synchronous_reactive_cache(knative_services_finder)
+ stub_kubeclient_service_pods(
+ kube_response({ "kind" => "PodList", "items" => [] }),
+ namespace: namespace.namespace
+ )
+ end
+
+ context 'when no functions were found' do
+ let(:functions) { [] }
+
+ before do
+ stub_kubeclient_knative_services(
+ namespace: namespace.namespace,
+ response: kube_response({ "kind" => "ServiceList", "items" => [] })
+ )
+ get :index, params: params({ format: :json })
+ end
+
+ it 'returns checking' do
+ expect(json_response).to eq expected_json
+ end
+
+ it { expect(response).to have_gitlab_http_status(200) }
+ end
+
+ context 'when functions were found' do
+ let(:functions) { ["asdf"] }
+
+ before do
+ stub_kubeclient_knative_services(namespace: namespace.namespace)
+ get :index, params: params({ format: :json })
+ end
+
+ it 'returns functions' do
+ expect(json_response["functions"]).not_to be_empty
+ end
+
+ it { expect(response).to have_gitlab_http_status(200) }
end
end
end
@@ -56,11 +107,12 @@ describe Projects::Serverless::FunctionsController do
context 'valid data', :use_clean_rails_memory_store_caching do
before do
stub_kubeclient_service_pods
- stub_reactive_cache(knative,
+ stub_reactive_cache(cluster.knative_services_finder(project),
{
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
- })
+ },
+ *cluster.knative_services_finder(project).cache_args)
end
it 'has a valid function name' do
@@ -88,11 +140,12 @@ describe Projects::Serverless::FunctionsController do
describe 'GET #index with data', :use_clean_rails_memory_store_caching do
before do
stub_kubeclient_service_pods
- stub_reactive_cache(knative,
+ stub_reactive_cache(cluster.knative_services_finder(project),
{
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
- })
+ },
+ *cluster.knative_services_finder(project).cache_args)
end
it 'has data' do
@@ -100,11 +153,16 @@ describe Projects::Serverless::FunctionsController do
expect(response).to have_gitlab_http_status(200)
- expect(json_response).to contain_exactly(
- a_hash_including(
- "name" => project.name,
- "url" => "http://#{project.name}.#{namespace.namespace}.example.com"
- )
+ expect(json_response).to match(
+ {
+ "knative_installed" => "checking",
+ "functions" => [
+ a_hash_including(
+ "name" => project.name,
+ "url" => "http://#{project.name}.#{namespace.namespace}.example.com"
+ )
+ ]
+ }
)
end
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
index db53e5bc8a4..117b9cf7915 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -109,7 +109,7 @@ describe Projects::Settings::CiCdController do
end
context 'when updating the auto_devops settings' do
- let(:params) { { auto_devops_attributes: { enabled: '', domain: 'mepmep.md' } } }
+ let(:params) { { auto_devops_attributes: { enabled: '' } } }
context 'following the instance default' do
let(:params) { { auto_devops_attributes: { enabled: '' } } }
@@ -200,6 +200,21 @@ describe Projects::Settings::CiCdController do
expect(response).to redirect_to(namespace_project_settings_ci_cd_path)
end
end
+
+ context 'when default_git_depth is not specified' do
+ let(:params) { { ci_cd_settings_attributes: { default_git_depth: 10 } } }
+
+ before do
+ project.ci_cd_settings.update!(default_git_depth: nil)
+ end
+
+ it 'set specified git depth' do
+ subject
+
+ project.reload
+ expect(project.default_git_depth).to eq(10)
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 7f1bbebd128..8d2412f97ef 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -292,6 +292,18 @@ describe ProjectsController do
end
describe 'GET edit' do
+ it 'allows an admin user to access the page' do
+ sign_in(create(:user, :admin))
+
+ get :edit,
+ params: {
+ namespace_id: project.namespace.path,
+ id: project.path
+ }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
it 'sets the badge API endpoint' do
sign_in(user)
project.add_maintainer(user)
diff --git a/spec/controllers/sent_notifications_controller_spec.rb b/spec/controllers/sent_notifications_controller_spec.rb
index 2b9df71aa3a..89857a9d21b 100644
--- a/spec/controllers/sent_notifications_controller_spec.rb
+++ b/spec/controllers/sent_notifications_controller_spec.rb
@@ -4,15 +4,31 @@ require 'rails_helper'
describe SentNotificationsController do
let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:sent_notification) { create(:sent_notification, project: project, noteable: issue, recipient: user) }
+ let(:project) { create(:project, :public) }
+ let(:private_project) { create(:project, :private) }
+ let(:sent_notification) { create(:sent_notification, project: target_project, noteable: noteable, recipient: user) }
let(:issue) do
- create(:issue, project: project, author: user) do |issue|
- issue.subscriptions.create(user: user, project: project, subscribed: true)
+ create(:issue, project: target_project) do |issue|
+ issue.subscriptions.create(user: user, project: target_project, subscribed: true)
end
end
+ let(:confidential_issue) do
+ create(:issue, project: target_project, confidential: true) do |issue|
+ issue.subscriptions.create(user: user, project: target_project, subscribed: true)
+ end
+ end
+
+ let(:merge_request) do
+ create(:merge_request, source_project: target_project, target_project: target_project) do |mr|
+ mr.subscriptions.create(user: user, project: target_project, subscribed: true)
+ end
+ end
+
+ let(:noteable) { issue }
+ let(:target_project) { project }
+
describe 'GET unsubscribe' do
context 'when the user is not logged in' do
context 'when the force param is passed' do
@@ -34,20 +50,93 @@ describe SentNotificationsController do
end
context 'when the force param is not passed' do
+ render_views
+
before do
get(:unsubscribe, params: { id: sent_notification.reply_key })
end
- it 'does not unsubscribe the user' do
- expect(issue.subscribed?(user, project)).to be_truthy
+ shared_examples 'unsubscribing as anonymous' do
+ it 'does not unsubscribe the user' do
+ expect(noteable.subscribed?(user, target_project)).to be_truthy
+ end
+
+ it 'does not set the flash message' do
+ expect(controller).not_to set_flash[:notice]
+ end
+
+ it 'renders unsubscribe page' do
+ expect(response.status).to eq(200)
+ expect(response).to render_template :unsubscribe
+ end
end
- it 'does not set the flash message' do
- expect(controller).not_to set_flash[:notice]
+ context 'when project is public' do
+ context 'when unsubscribing from issue' do
+ let(:noteable) { issue }
+
+ it 'shows issue title' do
+ expect(response.body).to include(issue.title)
+ end
+
+ it_behaves_like 'unsubscribing as anonymous'
+ end
+
+ context 'when unsubscribing from confidential issue' do
+ let(:noteable) { confidential_issue }
+
+ it 'does not show issue title' do
+ expect(response.body).not_to include(confidential_issue.title)
+ expect(response.body).to include(confidential_issue.to_reference)
+ end
+
+ it_behaves_like 'unsubscribing as anonymous'
+ end
+
+ context 'when unsubscribing from merge request' do
+ let(:noteable) { merge_request }
+
+ it 'shows merge request title' do
+ expect(response.body).to include(merge_request.title)
+ end
+
+ it_behaves_like 'unsubscribing as anonymous'
+ end
end
- it 'redirects to the login page' do
- expect(response).to render_template :unsubscribe
+ context 'when project is not public' do
+ let(:target_project) { private_project }
+
+ context 'when unsubscribing from issue' do
+ let(:noteable) { issue }
+
+ it 'shows issue title' do
+ expect(response.body).not_to include(issue.title)
+ end
+
+ it_behaves_like 'unsubscribing as anonymous'
+ end
+
+ context 'when unsubscribing from confidential issue' do
+ let(:noteable) { confidential_issue }
+
+ it 'does not show issue title' do
+ expect(response.body).not_to include(confidential_issue.title)
+ expect(response.body).to include(confidential_issue.to_reference)
+ end
+
+ it_behaves_like 'unsubscribing as anonymous'
+ end
+
+ context 'when unsubscribing from merge request' do
+ let(:noteable) { merge_request }
+
+ it 'shows merge request title' do
+ expect(response.body).not_to include(merge_request.title)
+ end
+
+ it_behaves_like 'unsubscribing as anonymous'
+ end
end
end
end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 6bcff7f975c..9c4ddce5409 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -58,7 +58,26 @@ describe SessionsController do
it 'authenticates user correctly' do
post(:create, params: { user: user_params })
- expect(subject.current_user). to eq user
+ expect(subject.current_user).to eq user
+ end
+
+ context 'with password authentication disabled' do
+ before do
+ stub_application_setting(password_authentication_enabled_for_web: false)
+ end
+
+ it 'does not sign in the user' do
+ post(:create, params: { user: user_params })
+
+ expect(@request.env['warden']).not_to be_authenticated
+ expect(subject.current_user).to be_nil
+ end
+
+ it 'returns status 403' do
+ post(:create, params: { user: user_params })
+
+ expect(response.status).to eq 403
+ end
end
it 'creates an audit log record' do
@@ -153,6 +172,19 @@ describe SessionsController do
end
end
+ context 'with password authentication disabled' do
+ before do
+ stub_application_setting(password_authentication_enabled_for_web: false)
+ end
+
+ it 'allows 2FA stage of non-password login' do
+ authenticate_2fa(otp_attempt: user.current_otp)
+
+ expect(@request.env['warden']).to be_authenticated
+ expect(subject.current_user).to eq user
+ end
+ end
+
##
# See #14900 issue
#
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index f8c494c159e..a473136b57b 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -248,17 +248,6 @@ FactoryBot.define do
runner factory: :ci_runner
end
- trait :legacy_artifacts do
- after(:create) do |build, _|
- build.update!(
- legacy_artifacts_file: fixture_file_upload(
- Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip'),
- legacy_artifacts_metadata: fixture_file_upload(
- Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), 'application/x-gzip')
- )
- end
- end
-
trait :artifacts do
after(:create) do |build|
create(:ci_job_artifact, :archive, job: build, expire_at: build.artifacts_expire_at)
diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb
index 2c76c22ba69..542fa9775cd 100644
--- a/spec/factories/ci/job_artifacts.rb
+++ b/spec/factories/ci/job_artifacts.rb
@@ -45,9 +45,12 @@ FactoryBot.define do
file_type :archive
file_format :zip
- after(:build) do |artifact, _|
- artifact.file = fixture_file_upload(
- Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip')
+ transient do
+ file { fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip') }
+ end
+
+ after(:build) do |artifact, evaluator|
+ artifact.file = evaluator.file
end
end
@@ -61,9 +64,12 @@ FactoryBot.define do
file_type :metadata
file_format :gzip
- after(:build) do |artifact, _|
- artifact.file = fixture_file_upload(
- Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), 'application/x-gzip')
+ transient do
+ file { fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), 'application/x-gzip') }
+ end
+
+ after(:build) do |artifact, evaluator|
+ artifact.file = evaluator.file
end
end
diff --git a/spec/factories/ci/pipeline_schedule.rb b/spec/factories/ci/pipeline_schedule.rb
index b2b79807429..4b83ba2ac1b 100644
--- a/spec/factories/ci/pipeline_schedule.rb
+++ b/spec/factories/ci/pipeline_schedule.rb
@@ -7,6 +7,16 @@ FactoryBot.define do
description "pipeline schedule"
project
+ trait :every_minute do
+ cron '*/1 * * * *'
+ cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
+ end
+
+ trait :hourly do
+ cron '* */1 * * *'
+ cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
+ end
+
trait :nightly do
cron '0 1 * * *'
cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index e8df5094b83..0b6a43b13a9 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -95,7 +95,8 @@ FactoryBot.define do
end
trait :merge_when_pipeline_succeeds do
- merge_when_pipeline_succeeds true
+ auto_merge_enabled true
+ auto_merge_strategy AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS
merge_user { author }
end
diff --git a/spec/factories/pages_domain_acme_orders.rb b/spec/factories/pages_domain_acme_orders.rb
new file mode 100644
index 00000000000..7f9ee1c8f9c
--- /dev/null
+++ b/spec/factories/pages_domain_acme_orders.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :pages_domain_acme_order do
+ pages_domain
+ url { 'https://example.com/' }
+ expires_at { 1.day.from_now }
+ challenge_token { 'challenge_token' }
+ challenge_file_content { 'filecontent' }
+
+ private_key { OpenSSL::PKey::RSA.new(4096).to_pem }
+
+ trait :expired do
+ expires_at { 1.day.ago }
+ end
+ end
+end
diff --git a/spec/factories/project_auto_devops.rb b/spec/factories/project_auto_devops.rb
index 75ac7cc7687..1de42512402 100644
--- a/spec/factories/project_auto_devops.rb
+++ b/spec/factories/project_auto_devops.rb
@@ -2,7 +2,6 @@ FactoryBot.define do
factory :project_auto_devops do
project
enabled true
- domain "example.com"
deploy_strategy :continuous
trait :continuous_deployment do
diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb
index 83cd686818c..f6c498f7a4c 100644
--- a/spec/features/admin/admin_appearance_spec.rb
+++ b/spec/features/admin/admin_appearance_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Admin Appearance' do
diff --git a/spec/features/admin/admin_sees_project_statistics_spec.rb b/spec/features/admin/admin_sees_project_statistics_spec.rb
index 95d1fc5b57a..b5323a1c76d 100644
--- a/spec/features/admin/admin_sees_project_statistics_spec.rb
+++ b/spec/features/admin/admin_sees_project_statistics_spec.rb
@@ -15,7 +15,7 @@ describe "Admin > Admin sees project statistics" do
let(:project) { create(:project, :repository) }
it "shows project statistics" do
- expect(page).to have_content("Storage: 0 Bytes (0 Bytes repositories, 0 Bytes build artifacts, 0 Bytes LFS)")
+ expect(page).to have_content("Storage: 0 Bytes (0 Bytes repositories, 0 Bytes wikis, 0 Bytes build artifacts, 0 Bytes LFS)")
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index f9950b5b03f..93ccb03d822 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -302,22 +302,22 @@ describe 'Admin updates settings' do
group = create(:group)
page.within('.as-performance-bar') do
- check 'Enable the Performance Bar'
+ check 'Enable access to the Performance Bar'
fill_in 'Allowed group', with: group.path
click_on 'Save changes'
end
expect(page).to have_content "Application settings saved successfully"
- expect(find_field('Enable the Performance Bar')).to be_checked
+ expect(find_field('Enable access to the Performance Bar')).to be_checked
expect(find_field('Allowed group').value).to eq group.path
page.within('.as-performance-bar') do
- uncheck 'Enable the Performance Bar'
+ uncheck 'Enable access to the Performance Bar'
click_on 'Save changes'
end
expect(page).to have_content 'Application settings saved successfully'
- expect(find_field('Enable the Performance Bar')).not_to be_checked
+ expect(find_field('Enable access to the Performance Bar')).not_to be_checked
expect(find_field('Allowed group').value).to be_nil
end
@@ -332,16 +332,19 @@ describe 'Admin updates settings' do
end
context 'Network page' do
- it 'Enable outbound requests' do
+ it 'Changes Outbound requests settings' do
visit network_admin_application_settings_path
page.within('.as-outbound') do
check 'Allow requests to the local network from hooks and services'
+ # Enabled by default
+ uncheck 'Enforce DNS rebinding attack protection'
click_button 'Save changes'
end
expect(page).to have_content "Application settings saved successfully"
expect(Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services).to be true
+ expect(Gitlab::CurrentSettings.dns_rebinding_protection_enabled).to be false
end
end
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index dfa1c92ea49..d523e2992db 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe "Dashboard Issues Feed" do
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 5c6c1c4fd15..2adeb37c98a 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -89,7 +89,7 @@ describe 'Commits' do
context 'Download artifacts' do
before do
- build.update(legacy_artifacts_file: artifacts_file)
+ create(:ci_job_artifact, :archive, file: artifacts_file, job: build)
end
it do
@@ -119,7 +119,7 @@ describe 'Commits' do
context "when logged as reporter" do
before do
project.add_reporter(user)
- build.update(legacy_artifacts_file: artifacts_file)
+ create(:ci_job_artifact, :archive, file: artifacts_file, job: build)
visit pipeline_path(pipeline)
end
@@ -141,7 +141,7 @@ describe 'Commits' do
project.update(
visibility_level: Gitlab::VisibilityLevel::INTERNAL,
public_builds: false)
- build.update(legacy_artifacts_file: artifacts_file)
+ create(:ci_job_artifact, :archive, file: artifacts_file, job: build)
visit pipeline_path(pipeline)
end
diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb
index d7692181453..f2ab5373d3d 100644
--- a/spec/features/global_search_spec.rb
+++ b/spec/features/global_search_spec.rb
@@ -25,4 +25,18 @@ describe 'Global search' do
expect(page).to have_selector('.gl-pagination .next')
end
end
+
+ it 'closes the dropdown on blur', :js do
+ visit dashboard_projects_path
+
+ fill_in 'search', with: "a"
+ dropdown = find('.js-dashboard-search-options')
+
+ expect(dropdown[:class]).to include 'show'
+
+ find('#search').send_keys(:backspace)
+ find('body').click
+
+ expect(dropdown[:class]).not_to include 'show'
+ end
end
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index e1bc4eca619..59230d6891a 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Group merge requests page' do
diff --git a/spec/features/instance_statistics/conversational_development_index_spec.rb b/spec/features/instance_statistics/conversational_development_index_spec.rb
index d8be554d734..713cd944f8c 100644
--- a/spec/features/instance_statistics/conversational_development_index_spec.rb
+++ b/spec/features/instance_statistics/conversational_development_index_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Conversational Development Index' do
diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb
index 7b6e9cd66b2..225b858742d 100644
--- a/spec/features/issuables/issuable_list_spec.rb
+++ b/spec/features/issuables/issuable_list_spec.rb
@@ -76,7 +76,7 @@ describe 'issuable list' do
create(:issue, project: project, author: user)
else
create(:merge_request, source_project: project, source_branch: generate(:branch))
- source_branch = FFaker::Name.name
+ source_branch = FFaker::Lorem.characters(8)
pipeline = create(:ci_empty_pipeline, project: project, ref: source_branch, status: %w(running failed success).sample, sha: 'any')
create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: source_branch, head_pipeline: pipeline)
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index bc0ec58bd24..5ee9425c491 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Issues' do
diff --git a/spec/features/markdown/gitlab_flavored_markdown_spec.rb b/spec/features/markdown/gitlab_flavored_markdown_spec.rb
index 6997ca48427..8fda3c7193e 100644
--- a/spec/features/markdown/gitlab_flavored_markdown_spec.rb
+++ b/spec/features/markdown/gitlab_flavored_markdown_spec.rb
@@ -20,8 +20,7 @@ describe "GitLab Flavored Markdown" do
let(:commit) { project.commit }
before do
- allow_any_instance_of(Commit).to receive(:title)
- .and_return("fix #{issue.to_reference}\n\nask #{fred.to_reference} for details")
+ create_commit("fix #{issue.to_reference}\n\nask #{fred.to_reference} for details", project, user, 'master')
end
it "renders title in commits#index" do
diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb
index bcc11217389..d05ef2a8f12 100644
--- a/spec/features/merge_request/user_creates_merge_request_spec.rb
+++ b/spec/features/merge_request/user_creates_merge_request_spec.rb
@@ -8,8 +8,6 @@ describe "User creates a merge request", :js do
let(:user) { create(:user) }
before do
- stub_feature_flags(approval_rules: false)
-
project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/merge_request/user_merges_merge_request_spec.rb b/spec/features/merge_request/user_merges_merge_request_spec.rb
index 6539e6e9208..da15a4bda4b 100644
--- a/spec/features/merge_request/user_merges_merge_request_spec.rb
+++ b/spec/features/merge_request/user_merges_merge_request_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe "User merges a merge request", :js do
diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
index 6e54aa6006b..586b3ba170d 100644
--- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
@@ -52,7 +52,7 @@ describe 'Merge request > User merges when pipeline succeeds', :js do
# so we have to wait for asynchronous call to reload it
# and have_content expectation handles that.
#
- expect(page).to have_content "Pipeline ##{pipeline.id} running"
+ expect(page).to have_content "Pipeline ##{pipeline.id} (##{pipeline.iid}) running"
end
it_behaves_like 'Merge when pipeline succeeds activator'
@@ -74,11 +74,12 @@ describe 'Merge request > User merges when pipeline succeeds', :js do
source_project: project,
title: 'Bug NS-04',
author: user,
- merge_user: user,
- merge_params: { force_remove_source_branch: '1' })
+ merge_user: user)
end
before do
+ merge_request.merge_params['force_remove_source_branch'] = '0'
+ merge_request.save!
click_link "Cancel automatic merge"
end
@@ -102,11 +103,11 @@ describe 'Merge request > User merges when pipeline succeeds', :js do
context 'when merge when pipeline succeeds is enabled' do
let(:merge_request) do
- create(:merge_request_with_diffs, :simple, source_project: project,
- author: user,
- merge_user: user,
- title: 'MepMep',
- merge_when_pipeline_succeeds: true)
+ create(:merge_request_with_diffs, :simple, :merge_when_pipeline_succeeds,
+ source_project: project,
+ author: user,
+ merge_user: user,
+ title: 'MepMep')
end
let!(:build) do
create(:ci_build, pipeline: pipeline)
@@ -158,8 +159,8 @@ describe 'Merge request > User merges when pipeline succeeds', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- page.within('.mr-widget-body') do
- expect(page).to have_content('Something went wrong')
+ page.within('.mr-section-container') do
+ expect(page).to have_content('Merge failed: Something went wrong')
end
end
end
@@ -177,8 +178,8 @@ describe 'Merge request > User merges when pipeline succeeds', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- page.within('.mr-widget-body') do
- expect(page).to have_content('Something went wrong')
+ page.within('.mr-section-container') do
+ expect(page).to have_content('Merge failed: Something went wrong')
end
end
end
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index a32c6bdcf8f..0066e985fbb 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -160,7 +160,7 @@ describe 'Merge request > User sees merge widget', :js do
it 'shows head pipeline information' do
within '.ci-widget-content' do
- expect(page).to have_content("Pipeline ##{pipeline.id} pending " \
+ expect(page).to have_content("Pipeline ##{pipeline.id} (##{pipeline.iid}) pending " \
"for #{pipeline.short_sha} " \
"on #{pipeline.ref}")
end
@@ -189,7 +189,7 @@ describe 'Merge request > User sees merge widget', :js do
it 'shows head pipeline information' do
within '.ci-widget-content' do
- expect(page).to have_content("Pipeline ##{pipeline.id} pending " \
+ expect(page).to have_content("Pipeline ##{pipeline.id} (##{pipeline.iid}) pending " \
"for #{pipeline.short_sha} " \
"on #{merge_request.to_reference} " \
"with #{merge_request.source_branch}")
@@ -201,7 +201,7 @@ describe 'Merge request > User sees merge widget', :js do
it 'shows head pipeline information' do
within '.ci-widget-content' do
- expect(page).to have_content("Pipeline ##{pipeline.id} pending " \
+ expect(page).to have_content("Pipeline ##{pipeline.id} (##{pipeline.iid}) pending " \
"for #{pipeline.short_sha} " \
"on #{merge_request.to_reference} " \
"with #{merge_request.source_branch}")
@@ -234,7 +234,7 @@ describe 'Merge request > User sees merge widget', :js do
it 'shows head pipeline information' do
within '.ci-widget-content' do
- expect(page).to have_content("Pipeline ##{pipeline.id} pending " \
+ expect(page).to have_content("Pipeline ##{pipeline.id} (##{pipeline.iid}) pending " \
"for #{pipeline.short_sha} " \
"on #{merge_request.to_reference} " \
"with #{merge_request.source_branch} " \
@@ -248,7 +248,7 @@ describe 'Merge request > User sees merge widget', :js do
it 'shows head pipeline information' do
within '.ci-widget-content' do
- expect(page).to have_content("Pipeline ##{pipeline.id} pending " \
+ expect(page).to have_content("Pipeline ##{pipeline.id} (##{pipeline.iid}) pending " \
"for #{pipeline.short_sha} " \
"on #{merge_request.to_reference} " \
"with #{merge_request.source_branch} " \
@@ -314,7 +314,8 @@ describe 'Merge request > User sees merge widget', :js do
context 'view merge request with MWPS enabled but automatically merge fails' do
before do
merge_request.update(
- merge_when_pipeline_succeeds: true,
+ auto_merge_enabled: true,
+ auto_merge_strategy: AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS,
merge_user: merge_request.author,
merge_error: 'Something went wrong'
)
@@ -326,8 +327,8 @@ describe 'Merge request > User sees merge widget', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- page.within('.mr-widget-body') do
- expect(page).to have_content('Something went wrong')
+ page.within('.mr-section-container') do
+ expect(page).to have_content('Merge failed: Something went wrong')
end
end
end
@@ -347,8 +348,8 @@ describe 'Merge request > User sees merge widget', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- page.within('.mr-widget-body') do
- expect(page).to have_content('Something went wrong')
+ page.within('.mr-section-container') do
+ expect(page).to have_content('Merge failed: Something went wrong')
end
end
end
diff --git a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
index 5188dc3625f..dd8900a3698 100644
--- a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
+++ b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
@@ -27,7 +27,8 @@ describe 'Merge request < User sees mini pipeline graph', :js do
let(:artifacts_file2) { fixture_file_upload(File.join('spec/fixtures/dk.png'), 'image/png') }
before do
- create(:ci_build, :success, :trace_artifact, pipeline: pipeline, legacy_artifacts_file: artifacts_file1)
+ job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline)
+ create(:ci_job_artifact, :archive, file: artifacts_file1, job: job)
create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
end
@@ -35,7 +36,8 @@ describe 'Merge request < User sees mini pipeline graph', :js do
xit 'avoids repeated database queries' do
before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
- create(:ci_build, :success, :trace_artifact, pipeline: pipeline, legacy_artifacts_file: artifacts_file2)
+ job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline)
+ create(:ci_job_artifact, :archive, file: artifacts_file2, job: job)
create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
diff --git a/spec/features/project_variables_spec.rb b/spec/features/project_variables_spec.rb
index 76abc640077..95685a3c7ff 100644
--- a/spec/features/project_variables_spec.rb
+++ b/spec/features/project_variables_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Project variables', :js do
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index a85e7333ba8..ce382c19fc1 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Clusters', :js do
diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb
index 574a8aefd63..953517cdff9 100644
--- a/spec/features/projects/commits/user_browses_commits_spec.rb
+++ b/spec/features/projects/commits/user_browses_commits_spec.rb
@@ -61,7 +61,7 @@ describe 'User browses commits' do
it 'renders commit ci info' do
visit project_commit_path(project, sample_commit.id)
- expect(page).to have_content "Pipeline ##{pipeline.id} pending"
+ expect(page).to have_content "Pipeline ##{pipeline.id} (##{pipeline.iid}) pending"
end
end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index f76f9ba7577..9d74a96ab3d 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -12,7 +12,7 @@ describe 'Import/Export - project export integration test', :js do
let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
- let(:sensitive_words) { %w[pass secret token key encrypted] }
+ let(:sensitive_words) { %w[pass secret token key encrypted html] }
let(:safe_list) do
{
token: [ProjectHook, Ci::Trigger, CommitStatus],
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 28ae90bc0de..8d2b1fc7e30 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -47,7 +47,6 @@ describe 'Import/Export - project import integration test', :js do
expect(project.description).to eq("Foo Bar")
expect(project.issues).not_to be_empty
expect(project.merge_requests).not_to be_empty
- expect(project_hook_exists?(project)).to be true
expect(wiki_exists?(project)).to be true
expect(project.import_state.status).to eq('finished')
end
diff --git a/spec/features/projects/jobs/permissions_spec.rb b/spec/features/projects/jobs/permissions_spec.rb
index 6ce37297a7e..b5e711997a0 100644
--- a/spec/features/projects/jobs/permissions_spec.rb
+++ b/spec/features/projects/jobs/permissions_spec.rb
@@ -90,7 +90,7 @@ describe 'Project Jobs Permissions' do
before do
archive = fixture_file_upload('spec/fixtures/ci_build_artifacts.zip')
- job.update(legacy_artifacts_file: archive)
+ create(:ci_job_artifact, :archive, file: archive, job: job)
end
context 'when public access for jobs is disabled' do
diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb
index 908c616f2fc..54b462da87a 100644
--- a/spec/features/projects/jobs/user_browses_job_spec.rb
+++ b/spec/features/projects/jobs/user_browses_job_spec.rb
@@ -28,8 +28,8 @@ describe 'User browses a job', :js do
expect(page).to have_no_css('.artifacts')
expect(build).not_to have_trace
- expect(build.artifacts_file.exists?).to be_falsy
- expect(build.artifacts_metadata.exists?).to be_falsy
+ expect(build.artifacts_file.present?).to be_falsy
+ expect(build.artifacts_metadata.present?).to be_falsy
expect(page).to have_content('Job has been erased')
end
diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb
index ebc20d15d67..bd6c73f4b85 100644
--- a/spec/features/projects/jobs/user_browses_jobs_spec.rb
+++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb
@@ -16,6 +16,12 @@ describe 'User browses jobs' do
visit(project_jobs_path(project))
end
+ it 'shows pipeline id and IID' do
+ page.within('td.pipeline-link') do
+ expect(page).to have_content("##{pipeline.id} (##{pipeline.iid})")
+ end
+ end
+
it 'shows the coverage' do
page.within('td.coverage') do
expect(page).to have_content('99.9%')
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 9cf04fe13b4..03562bd382e 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -129,7 +129,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
visit project_job_path(project, job)
within '.js-pipeline-info' do
- expect(page).to have_content("Pipeline ##{pipeline.id} for #{pipeline.ref}")
+ expect(page).to have_content("Pipeline ##{pipeline.id} (##{pipeline.iid}) for #{pipeline.ref}")
end
end
@@ -314,7 +314,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
context "Download artifacts", :js do
before do
- job.update(legacy_artifacts_file: artifacts_file)
+ create(:ci_job_artifact, :archive, file: artifacts_file, job: job)
visit project_job_path(project, job)
end
@@ -338,8 +338,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
context 'Artifacts expire date', :js do
before do
- job.update(legacy_artifacts_file: artifacts_file,
- artifacts_expire_at: expire_at)
+ create(:ci_job_artifact, :archive, file: artifacts_file, expire_at: expire_at, job: job)
+ job.update!(artifacts_expire_at: expire_at)
visit project_job_path(project, job)
end
@@ -936,8 +936,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
find('.js-cancel-job').click
end
- it 'loads the page and shows no controls' do
- expect(page).not_to have_content 'Retry'
+ it 'loads the page and shows all needed controls' do
+ expect(page).to have_content 'Retry'
end
end
end
@@ -946,7 +946,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
context "Job from project", :js do
before do
job.run!
- job.drop!(:script_failure)
+ job.cancel!
visit project_job_path(project, job)
wait_for_requests
@@ -981,7 +981,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
describe "GET /:project/jobs/:id/download", :js do
before do
- job.update(legacy_artifacts_file: artifacts_file)
+ create(:ci_job_artifact, :archive, file: artifacts_file, job: job)
visit project_job_path(project, job)
click_link 'Download'
@@ -989,7 +989,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
context "Build from other project" do
before do
- job2.update(legacy_artifacts_file: artifacts_file)
+ create(:ci_job_artifact, :archive, file: artifacts_file, job: job2)
end
it do
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index b54ea929978..033e1afe866 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -63,6 +63,36 @@ describe 'New project' do
end
end
end
+
+ context 'when group visibility is private but default is internal' do
+ before do
+ stub_application_setting(default_project_visibility: Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'has private selected' do
+ group = create(:group, visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ visit new_project_path(namespace_id: group.id)
+
+ page.within('#blank-project-pane') do
+ expect(find_field("project_visibility_level_#{Gitlab::VisibilityLevel::PRIVATE}")).to be_checked
+ end
+ end
+ end
+
+ context 'when group visibility is public but user requests private' do
+ before do
+ stub_application_setting(default_project_visibility: Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'has private selected' do
+ group = create(:group, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ visit new_project_path(namespace_id: group.id, project: { visibility_level: Gitlab::VisibilityLevel::PRIVATE })
+
+ page.within('#blank-project-pane') do
+ expect(find_field("project_visibility_level_#{Gitlab::VisibilityLevel::PRIVATE}")).to be_checked
+ end
+ end
+ end
end
context 'Readme selector' do
diff --git a/spec/features/projects/pages_lets_encrypt_spec.rb b/spec/features/projects/pages_lets_encrypt_spec.rb
new file mode 100644
index 00000000000..baa217cbe58
--- /dev/null
+++ b/spec/features/projects/pages_lets_encrypt_spec.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe "Pages with Let's Encrypt", :https_pages_enabled do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:role) { :maintainer }
+ let(:certificate_pem) do
+ <<~PEM
+ -----BEGIN CERTIFICATE-----
+ MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0
+ LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ
+ MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
+ gYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2geNR1qlNFa
+ SvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLySNT438kdT
+ nY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEAAaNvMG0w
+ DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxl9WSxBprB0z0ibJs3rXEk0+95AwCwYD
+ VR0PBAQDAgXgMBEGCWCGSAGG+EIBAQQEAwIGQDAeBglghkgBhvhCAQ0EERYPeGNh
+ IGNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4GBAGC4T8SlFHK0yPSa+idGLQFQ
+ joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese
+ 5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg
+ YHi2yesCrOvVXt+lgPTd
+ -----END CERTIFICATE-----
+ PEM
+ end
+
+ let(:certificate_key) do
+ <<~KEY
+ -----BEGIN PRIVATE KEY-----
+ MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN
+ SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t
+ PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB
+ kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd
+ j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/
+ uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR
+ 5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O
+ AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K
+ EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh
+ Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C
+ m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH
+ EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx
+ 63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi
+ nNp/xedE1YxutQ==
+ -----END PRIVATE KEY-----
+ KEY
+ end
+
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ project.add_role(user, role)
+ sign_in(user)
+ project.namespace.update(owner: user)
+ allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
+ end
+
+ context 'when the page_auto_ssl feature flag is enabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl: true)
+ end
+
+ context 'when the auto SSL management is initially disabled' do
+ let(:domain) do
+ create(:pages_domain, auto_ssl_enabled: false, project: project)
+ end
+
+ it 'enables auto SSL and dynamically updates the form accordingly', :js do
+ visit edit_project_pages_domain_path(project, domain)
+
+ expect(domain.auto_ssl_enabled).to eq false
+
+ expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'false'
+ expect(page).to have_field 'Certificate (PEM)', type: 'textarea'
+ expect(page).to have_field 'Key (PEM)', type: 'textarea'
+
+ find('.js-auto-ssl-toggle-container .project-feature-toggle').click
+
+ expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'true'
+ expect(page).not_to have_field 'Certificate (PEM)', type: 'textarea'
+ expect(page).not_to have_field 'Key (PEM)', type: 'textarea'
+ expect(page).to have_content "The certificate will be shown here once it has been obtained from Let's Encrypt. This process may take up to an hour to complete."
+
+ click_on 'Save Changes'
+
+ expect(domain.reload.auto_ssl_enabled).to eq true
+ end
+ end
+
+ context 'when the auto SSL management is initially enabled' do
+ let(:domain) do
+ create(:pages_domain, auto_ssl_enabled: true, project: project)
+ end
+
+ it 'disables auto SSL and dynamically updates the form accordingly', :js do
+ visit edit_project_pages_domain_path(project, domain)
+
+ expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'true'
+ expect(page).to have_field 'Certificate (PEM)', type: 'textarea', disabled: true
+ expect(page).not_to have_field 'Key (PEM)', type: 'textarea'
+
+ find('.js-auto-ssl-toggle-container .project-feature-toggle').click
+
+ expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'false'
+ expect(page).to have_field 'Certificate (PEM)', type: 'textarea'
+ expect(page).to have_field 'Key (PEM)', type: 'textarea'
+
+ fill_in 'Certificate (PEM)', with: certificate_pem
+ fill_in 'Key (PEM)', with: certificate_key
+
+ click_on 'Save Changes'
+
+ expect(domain.reload.auto_ssl_enabled).to eq false
+ end
+ end
+ end
+
+ context 'when the page_auto_ssl feature flag is disabled' do
+ let(:domain) do
+ create(:pages_domain, auto_ssl_enabled: false, project: project)
+ end
+
+ before do
+ stub_feature_flags(pages_auto_ssl: false)
+
+ visit edit_project_pages_domain_path(project, domain)
+ end
+
+ it "does not render the Let's Encrypt field", :js do
+ expect(page).not_to have_selector '.js-auto-ssl-toggle-container'
+ end
+ end
+end
diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb
index f564ae34f11..9bb0ba81ef5 100644
--- a/spec/features/projects/pages_spec.rb
+++ b/spec/features/projects/pages_spec.rb
@@ -1,6 +1,7 @@
+# frozen_string_literal: true
require 'spec_helper'
-describe 'Pages' do
+shared_examples 'pages domain editing' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:role) { :maintainer }
@@ -287,10 +288,17 @@ describe 'Pages' do
:ci_build,
project: project,
pipeline: pipeline,
- ref: 'HEAD',
- legacy_artifacts_file: fixture_file_upload(File.join('spec/fixtures/pages.zip')),
- legacy_artifacts_metadata: fixture_file_upload(File.join('spec/fixtures/pages.zip.meta'))
- )
+ ref: 'HEAD')
+ end
+
+ let!(:artifact) do
+ create(:ci_job_artifact, :archive,
+ file: fixture_file_upload(File.join('spec/fixtures/pages.zip')), job: ci_build)
+ end
+
+ let!(:metadata) do
+ create(:ci_job_artifact, :metadata,
+ file: fixture_file_upload(File.join('spec/fixtures/pages.zip.meta')), job: ci_build)
end
before do
@@ -311,3 +319,21 @@ describe 'Pages' do
end
end
end
+
+describe 'Pages' do
+ context 'when pages_auto_ssl feature flag is disabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl: false)
+ end
+
+ include_examples 'pages domain editing'
+ end
+
+ context 'when pages_auto_ssl feature flag is enabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl: true)
+ end
+
+ include_examples 'pages domain editing'
+ end
+end
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index b1a705f09ce..24041a51383 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -225,7 +225,7 @@ describe 'Pipeline Schedules', :js do
context 'when active is true and next_run_at is NULL' do
before do
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
- pipeline_schedule.update_attribute(:cron, nil) # Consequently next_run_at will be nil
+ pipeline_schedule.update_attribute(:next_run_at, nil) # Consequently next_run_at will be nil
end
end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index a1115b514d3..1de153db41c 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Pipeline', :js do
@@ -115,11 +117,11 @@ describe 'Pipeline', :js do
end
end
- it 'cancels the running build and does not show retry button' do
+ it 'cancels the running build and shows retry button' do
find('#ci-badge-deploy .ci-action-icon-container').click
page.within('#ci-badge-deploy') do
- expect(page).not_to have_css('.js-icon-retry')
+ expect(page).to have_css('.js-icon-retry')
end
end
end
@@ -133,11 +135,11 @@ describe 'Pipeline', :js do
end
end
- it 'cancels the preparing build and does not show retry button' do
+ it 'cancels the preparing build and shows retry button' do
find('#ci-badge-deploy .ci-action-icon-container').click
page.within('#ci-badge-deploy') do
- expect(page).not_to have_css('.js-icon-retry')
+ expect(page).to have_css('.js-icon-retry')
end
end
end
@@ -328,6 +330,12 @@ describe 'Pipeline', :js do
expect(page).not_to have_link(pipeline.ref)
expect(page).to have_content(pipeline.ref)
end
+
+ it 'does not render render raw HTML to the pipeline ref' do
+ page.within '.pipeline-info' do
+ expect(page).not_to have_content('<span class="ref-name"')
+ end
+ end
end
context 'when pipeline is detached merge request pipeline' do
diff --git a/spec/features/projects/serverless/functions_spec.rb b/spec/features/projects/serverless/functions_spec.rb
index e14934b1672..9865dbbfb3c 100644
--- a/spec/features/projects/serverless/functions_spec.rb
+++ b/spec/features/projects/serverless/functions_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe 'Functions', :js do
include KubernetesHelpers
+ include ReactiveCachingHelpers
let(:project) { create(:project) }
let(:user) { create(:user) }
@@ -13,44 +14,70 @@ describe 'Functions', :js do
gitlab_sign_in(user)
end
- context 'when user does not have a cluster and visits the serverless page' do
+ shared_examples "it's missing knative installation" do
before do
visit project_serverless_functions_path(project)
end
- it 'sees an empty state' do
+ it 'sees an empty state require Knative installation' do
expect(page).to have_link('Install Knative')
expect(page).to have_selector('.empty-state')
end
end
+ context 'when user does not have a cluster and visits the serverless page' do
+ it_behaves_like "it's missing knative installation"
+ end
+
context 'when the user does have a cluster and visits the serverless page' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- before do
- visit project_serverless_functions_path(project)
- end
-
- it 'sees an empty state' do
- expect(page).to have_link('Install Knative')
- expect(page).to have_selector('.empty-state')
- end
+ it_behaves_like "it's missing knative installation"
end
context 'when the user has a cluster and knative installed and visits the serverless page' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:service) { cluster.platform_kubernetes }
- let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
- let(:project) { knative.cluster.project }
+ let(:project) { cluster.project }
+ let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+ let(:namespace) do
+ create(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ cluster_project: cluster.cluster_project,
+ project: cluster.cluster_project.project)
+ end
before do
- stub_kubeclient_knative_services
- stub_kubeclient_service_pods
+ allow_any_instance_of(Clusters::Cluster)
+ .to receive(:knative_services_finder)
+ .and_return(knative_services_finder)
+ synchronous_reactive_cache(knative_services_finder)
+ stub_kubeclient_knative_services(stub_get_services_options)
+ stub_kubeclient_service_pods(nil, namespace: namespace.namespace)
visit project_serverless_functions_path(project)
end
- it 'sees an empty listing of serverless functions' do
- expect(page).to have_selector('.empty-state')
+ context 'when there are no functions' do
+ let(:stub_get_services_options) do
+ {
+ namespace: namespace.namespace,
+ response: kube_response({ "kind" => "ServiceList", "items" => [] })
+ }
+ end
+
+ it 'sees an empty listing of serverless functions' do
+ expect(page).to have_selector('.empty-state')
+ expect(page).not_to have_selector('.content-list')
+ end
+ end
+
+ context 'when there are functions' do
+ let(:stub_get_services_options) { { namespace: namespace.namespace } }
+
+ it 'does not see an empty listing of serverless functions' do
+ expect(page).not_to have_selector('.empty-state')
+ expect(page).to have_selector('.content-list')
+ end
end
end
end
diff --git a/spec/features/projects/settings/forked_project_settings_spec.rb b/spec/features/projects/settings/forked_project_settings_spec.rb
index dc0278370aa..df33d215602 100644
--- a/spec/features/projects/settings/forked_project_settings_spec.rb
+++ b/spec/features/projects/settings/forked_project_settings_spec.rb
@@ -7,7 +7,6 @@ describe 'Projects > Settings > For a forked project', :js do
let(:forked_project) { fork_project(original_project, user) }
before do
- stub_feature_flags(approval_rules: false)
original_project.add_maintainer(user)
forked_project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
index 28d52f25f56..0739726f52c 100644
--- a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
@@ -51,7 +51,7 @@ describe 'Projects > Settings > User manages merge request settings' do
end
it 'shows the Merge Requests settings that do not depend on Builds feature' do
- expect(page).not_to have_content 'Pipelines must succeed'
+ expect(page).to have_content 'Pipelines must succeed'
expect(page).to have_content 'All discussions must be resolved'
within('.sharing-permissions-form') do
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 4fe45311b2d..b5112758475 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Project' do
@@ -6,7 +8,6 @@ describe 'Project' do
before do
stub_feature_flags(vue_file_list: false)
- stub_feature_flags(approval_rules: false)
end
describe 'creating from template' do
diff --git a/spec/features/security/profile_access_spec.rb b/spec/features/security/profile_access_spec.rb
index a198e65046f..044a47567be 100644
--- a/spec/features/security/profile_access_spec.rb
+++ b/spec/features/security/profile_access_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe "Profile access" do
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index 957c3cfc583..1a9caf0ffbb 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -25,6 +25,13 @@ describe 'Signup' do
expect(find('.username')).not_to have_css '.gl-field-error-outline'
end
+ it 'does not show an error border if the username length is not longer than 255 characters' do
+ fill_in 'new_user_username', with: 'u' * 255
+ wait_for_requests
+
+ expect(find('.username')).not_to have_css '.gl-field-error-outline'
+ end
+
it 'shows an error border if the username already exists' do
existing_user = create(:user)
@@ -41,6 +48,20 @@ describe 'Signup' do
expect(find('.username')).to have_css '.gl-field-error-outline'
end
+ it 'shows an error border if the username is longer than 255 characters' do
+ fill_in 'new_user_username', with: 'u' * 256
+ wait_for_requests
+
+ expect(find('.username')).to have_css '.gl-field-error-outline'
+ end
+
+ it 'shows an error message if the username is longer than 255 characters' do
+ fill_in 'new_user_username', with: 'u' * 256
+ wait_for_requests
+
+ expect(page).to have_content("Username is too long (maximum is 255 characters).")
+ end
+
it 'shows an error message on submit if the username contains special characters' do
fill_in 'new_user_username', with: 'new$user!username'
wait_for_requests
@@ -67,14 +88,35 @@ describe 'Signup' do
before do
visit root_path
click_link 'Register'
- simulate_input('#new_user_name', 'Ehsan 🦋')
+ end
+
+ it 'does not show an error border if the user\'s fullname length is not longer than 128 characters' do
+ fill_in 'new_user_name', with: 'u' * 128
+
+ expect(find('.name')).not_to have_css '.gl-field-error-outline'
end
it 'shows an error border if the user\'s fullname contains an emoji' do
+ simulate_input('#new_user_name', 'Ehsan 🦋')
+
+ expect(find('.name')).to have_css '.gl-field-error-outline'
+ end
+
+ it 'shows an error border if the user\'s fullname is longer than 128 characters' do
+ fill_in 'new_user_name', with: 'n' * 129
+
expect(find('.name')).to have_css '.gl-field-error-outline'
end
+ it 'shows an error message if the user\'s fullname is longer than 128 characters' do
+ fill_in 'new_user_name', with: 'n' * 129
+
+ expect(page).to have_content("Name is too long (maximum is 128 characters).")
+ end
+
it 'shows an error message if the username contains emojis' do
+ simulate_input('#new_user_name', 'Ehsan 🦋')
+
expect(page).to have_content("Invalid input, please avoid emojis")
end
end
diff --git a/spec/finders/clusters/knative_services_finder_spec.rb b/spec/finders/clusters/knative_services_finder_spec.rb
new file mode 100644
index 00000000000..b731c2bd6bf
--- /dev/null
+++ b/spec/finders/clusters/knative_services_finder_spec.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::KnativeServicesFinder do
+ include KubernetesHelpers
+ include ReactiveCachingHelpers
+
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:service) { cluster.platform_kubernetes }
+ let(:project) { cluster.cluster_project.project }
+ let(:namespace) do
+ create(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ cluster_project: cluster.cluster_project,
+ project: project)
+ end
+
+ before do
+ stub_kubeclient_knative_services(namespace: namespace.namespace)
+ stub_kubeclient_service_pods(
+ kube_response(
+ kube_knative_pods_body(
+ project.name, namespace.namespace
+ )
+ ),
+ namespace: namespace.namespace
+ )
+ end
+
+ shared_examples 'a cached data' do
+ it 'has an unintialized cache' do
+ is_expected.to be_blank
+ end
+
+ context 'when using synchronous reactive cache' do
+ before do
+ synchronous_reactive_cache(cluster.knative_services_finder(project))
+ end
+
+ context 'when there are functions for cluster namespace' do
+ it { is_expected.not_to be_blank }
+ end
+
+ context 'when there are no functions for cluster namespace' do
+ before do
+ stub_kubeclient_knative_services(
+ namespace: namespace.namespace,
+ response: kube_response({ "kind" => "ServiceList", "items" => [] })
+ )
+ stub_kubeclient_service_pods(
+ kube_response({ "kind" => "PodList", "items" => [] }),
+ namespace: namespace.namespace
+ )
+ end
+
+ it { is_expected.to be_blank }
+ end
+ end
+ end
+
+ describe '#service_pod_details' do
+ subject { cluster.knative_services_finder(project).service_pod_details(project.name) }
+
+ it_behaves_like 'a cached data'
+ end
+
+ describe '#services' do
+ subject { cluster.knative_services_finder(project).services }
+
+ it_behaves_like 'a cached data'
+ end
+
+ describe '#knative_detected' do
+ subject { cluster.knative_services_finder(project).knative_detected }
+ before do
+ synchronous_reactive_cache(cluster.knative_services_finder(project))
+ end
+
+ context 'when knative is installed' do
+ before do
+ stub_kubeclient_discover(service.api_url)
+ end
+
+ it { is_expected.to be_truthy }
+ it "discovers knative installation" do
+ expect { subject }
+ .to change { cluster.kubeclient.knative_client.discovered }
+ .from(false)
+ .to(true)
+ end
+ end
+
+ context 'when knative is not installed' do
+ before do
+ stub_kubeclient_discover_knative_not_found(service.api_url)
+ end
+
+ it { is_expected.to be_falsy }
+ it "does not discover knative installation" do
+ expect { subject }.not_to change { cluster.kubeclient.knative_client.discovered }
+ end
+ end
+ end
+end
diff --git a/spec/finders/projects/serverless/functions_finder_spec.rb b/spec/finders/projects/serverless/functions_finder_spec.rb
index 3ad38207da4..8aea45b457c 100644
--- a/spec/finders/projects/serverless/functions_finder_spec.rb
+++ b/spec/finders/projects/serverless/functions_finder_spec.rb
@@ -10,7 +10,7 @@ describe Projects::Serverless::FunctionsFinder do
let(:user) { create(:user) }
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:service) { cluster.platform_kubernetes }
- let(:project) { cluster.project}
+ let(:project) { cluster.project }
let(:namespace) do
create(:cluster_kubernetes_namespace,
@@ -23,9 +23,45 @@ describe Projects::Serverless::FunctionsFinder do
project.add_maintainer(user)
end
+ describe '#installed' do
+ it 'when reactive_caching is still fetching data' do
+ expect(described_class.new(project).knative_installed).to eq 'checking'
+ end
+
+ context 'when reactive_caching has finished' do
+ let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+
+ before do
+ allow_any_instance_of(Clusters::Cluster)
+ .to receive(:knative_services_finder)
+ .and_return(knative_services_finder)
+ synchronous_reactive_cache(knative_services_finder)
+ end
+
+ context 'when knative is not installed' do
+ it 'returns false' do
+ stub_kubeclient_discover_knative_not_found(service.api_url)
+
+ expect(described_class.new(project).knative_installed).to eq false
+ end
+ end
+
+ context 'reactive_caching is finished and knative is installed' do
+ let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+
+ it 'returns true' do
+ stub_kubeclient_knative_services(namespace: namespace.namespace)
+ stub_kubeclient_service_pods(nil, namespace: namespace.namespace)
+
+ expect(described_class.new(project).knative_installed).to be true
+ end
+ end
+ end
+ end
+
describe 'retrieve data from knative' do
- it 'does not have knative installed' do
- expect(described_class.new(project).execute).to be_empty
+ context 'does not have knative installed' do
+ it { expect(described_class.new(project).execute).to be_empty }
end
context 'has knative installed' do
@@ -38,22 +74,24 @@ describe Projects::Serverless::FunctionsFinder do
it 'there are functions', :use_clean_rails_memory_store_caching do
stub_kubeclient_service_pods
- stub_reactive_cache(knative,
+ stub_reactive_cache(cluster.knative_services_finder(project),
{
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
- })
+ },
+ *cluster.knative_services_finder(project).cache_args)
expect(finder.execute).not_to be_empty
end
it 'has a function', :use_clean_rails_memory_store_caching do
stub_kubeclient_service_pods
- stub_reactive_cache(knative,
+ stub_reactive_cache(cluster.knative_services_finder(project),
{
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
- })
+ },
+ *cluster.knative_services_finder(project).cache_args)
result = finder.service(cluster.environment_scope, cluster.project.name)
expect(result).not_to be_empty
@@ -84,20 +122,4 @@ describe Projects::Serverless::FunctionsFinder do
end
end
end
-
- describe 'verify if knative is installed' do
- context 'knative is not installed' do
- it 'does not have knative installed' do
- expect(described_class.new(project).installed?).to be false
- end
- end
-
- context 'knative is installed' do
- let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
-
- it 'does have knative installed' do
- expect(described_class.new(project).installed?).to be true
- end
- end
- end
end
diff --git a/spec/fixtures/phabricator_responses/auth_failed.json b/spec/fixtures/phabricator_responses/auth_failed.json
new file mode 100644
index 00000000000..50e57c0ba49
--- /dev/null
+++ b/spec/fixtures/phabricator_responses/auth_failed.json
@@ -0,0 +1 @@
+{"result":null,"error_code":"ERR-INVALID-AUTH","error_info":"API token \"api-token\" has the wrong length. API tokens should be 32 characters long."}
diff --git a/spec/fixtures/phabricator_responses/maniphest.search.json b/spec/fixtures/phabricator_responses/maniphest.search.json
new file mode 100644
index 00000000000..6a965007d0c
--- /dev/null
+++ b/spec/fixtures/phabricator_responses/maniphest.search.json
@@ -0,0 +1,98 @@
+{
+ "result": {
+ "data": [
+ {
+ "id": 283,
+ "type": "TASK",
+ "phid": "PHID-TASK-fswfs3wkowjb6cyyxtyx",
+ "fields": {
+ "name": "Things are slow",
+ "description": {
+ "raw": "Things are slow but should be fast!"
+ },
+ "authorPHID": "PHID-USER-nrtht5wijwbxquens3qr",
+ "ownerPHID": "PHID-USER-nrtht5wijwbxquens3qr",
+ "status": {
+ "value": "resolved",
+ "name": "Resolved",
+ "color": null
+ },
+ "priority": {
+ "value": 100,
+ "subpriority": 8589934592,
+ "name": "Super urgent",
+ "color": "pink"
+ },
+ "points": null,
+ "subtype": "default",
+ "closerPHID": "PHID-USER-nrtht5wijwbxquens3qr",
+ "dateClosed": 1374657042,
+ "spacePHID": null,
+ "dateCreated": 1374616241,
+ "dateModified": 1374657044,
+ "policy": {
+ "view": "users",
+ "interact": "users",
+ "edit": "users"
+ },
+ "custom.field-1": null,
+ "custom.field-2": null,
+ "custom.field-3": null
+ },
+ "attachments": {}
+ },
+ {
+ "id": 284,
+ "type": "TASK",
+ "phid": "PHID-TASK-5f73nyq5sjeh4cbmcsnb",
+ "fields": {
+ "name": "Things are broken",
+ "description": {
+ "raw": "Things are broken and should be fixed"
+ },
+ "authorPHID": "PHID-USER-nrtht5wijwbxquens3qr",
+ "ownerPHID": "PHID-USER-h425fsrixz4gjxiyr7ot",
+ "status": {
+ "value": "resolved",
+ "name": "Resolved",
+ "color": null
+ },
+ "priority": {
+ "value": 100,
+ "subpriority": 8589803520,
+ "name": "Super urgent",
+ "color": "pink"
+ },
+ "points": null,
+ "subtype": "default",
+ "closerPHID": "PHID-USER-h425fsrixz4gjxiyr7ot",
+ "dateClosed": 1375049556,
+ "spacePHID": null,
+ "dateCreated": 1374616578,
+ "dateModified": 1375049556,
+ "policy": {
+ "view": "users",
+ "interact": "users",
+ "edit": "users"
+ },
+ "custom.field-1": null,
+ "custom.field-2": null,
+ "custom.field-3": null
+ },
+ "attachments": {}
+ }
+ ],
+ "maps": {},
+ "query": {
+ "queryKey": null
+ },
+ "cursor": {
+ "limit": "2",
+ "after": "284",
+ "before": null,
+ "order": null
+ }
+ },
+ "error_code": null,
+ "error_info": null
+}
diff --git a/spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json
new file mode 100644
index 00000000000..1e62d020026
--- /dev/null
+++ b/spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json
@@ -0,0 +1,422 @@
+{
+ "version": "2.1",
+ "vulnerabilities": [
+ {
+ "category": "dependency_scanning",
+ "name": "Vulnerabilities in libxml2",
+ "message": "Vulnerabilities in libxml2 in nokogiri",
+ "description": " The version of libxml2 packaged with Nokogiri contains several vulnerabilities.\r\n Nokogiri has mitigated these issues by upgrading to libxml 2.9.5.\r\n\r\n It was discovered that a type confusion error existed in libxml2. An\r\n attacker could use this to specially construct XML data that\r\n could cause a denial of service or possibly execute arbitrary\r\n code. (CVE-2017-0663)\r\n\r\n It was discovered that libxml2 did not properly validate parsed entity\r\n references. An attacker could use this to specially construct XML\r\n data that could expose sensitive information. (CVE-2017-7375)\r\n\r\n It was discovered that a buffer overflow existed in libxml2 when\r\n handling HTTP redirects. An attacker could use this to specially\r\n construct XML data that could cause a denial of service or possibly\r\n execute arbitrary code. (CVE-2017-7376)\r\n\r\n Marcel Böhme and Van-Thuan Pham discovered a buffer overflow in\r\n libxml2 when handling elements. An attacker could use this to specially\r\n construct XML data that could cause a denial of service or possibly\r\n execute arbitrary code. (CVE-2017-9047)\r\n\r\n Marcel Böhme and Van-Thuan Pham discovered a buffer overread\r\n in libxml2 when handling elements. An attacker could use this\r\n to specially construct XML data that could cause a denial of\r\n service. (CVE-2017-9048)\r\n\r\n Marcel Böhme and Van-Thuan Pham discovered multiple buffer overreads\r\n in libxml2 when handling parameter-entity references. An attacker\r\n could use these to specially construct XML data that could cause a\r\n denial of service. (CVE-2017-9049, CVE-2017-9050)",
+ "cve": "rails/Gemfile.lock:nokogiri:gemnasium:06565b64-486d-4326-b906-890d9915804d",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest version.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-06565b64-486d-4326-b906-890d9915804d",
+ "value": "06565b64-486d-4326-b906-890d9915804d",
+ "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
+ },
+ {
+ "type": "usn",
+ "name": "USN-3424-1",
+ "value": "USN-3424-1",
+ "url": "https://usn.ubuntu.com/3424-1/"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://github.com/sparklemotion/nokogiri/issues/1673"
+ }
+ ]
+ },
+ {
+ "category": "dependency_scanning",
+ "name": "Infinite recursion in parameter entities",
+ "message": "Infinite recursion in parameter entities in nokogiri",
+ "description": "libxml2 incorrectly handles certain parameter entities. An attacker can leverage this with specially constructed XML data to cause libxml2 to consume resources, leading to a denial of service.",
+ "cve": "rails/Gemfile.lock:nokogiri:gemnasium:6a0d56f6-2441-492a-9b14-edb95ac31919",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest version.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-6a0d56f6-2441-492a-9b14-edb95ac31919",
+ "value": "6a0d56f6-2441-492a-9b14-edb95ac31919",
+ "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
+ },
+ {
+ "type": "cve",
+ "name": "CVE-2017-16932",
+ "value": "CVE-2017-16932",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-16932"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-16932"
+ },
+ {
+ "url": "https://github.com/sparklemotion/nokogiri/issues/1714"
+ },
+ {
+ "url": "https://people.canonical.com/~ubuntu-security/cve/2017/CVE-2017-16932.html"
+ },
+ {
+ "url": "https://usn.ubuntu.com/usn/usn-3504-1/"
+ }
+ ]
+ },
+ {
+ "category": "dependency_scanning",
+ "name": "Denial of Service",
+ "message": "Denial of Service in nokogiri",
+ "description": "libxml2 incorrectly handles certain files. An attacker can use this issue with specially constructed XML data to cause libxml2 to consume resources, leading to a denial of service.\r\n\r\n",
+ "cve": "rails/Gemfile.lock:nokogiri:gemnasium:78658378-bd8f-4d79-81c8-07c419302426",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest version.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-78658378-bd8f-4d79-81c8-07c419302426",
+ "value": "78658378-bd8f-4d79-81c8-07c419302426",
+ "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
+ },
+ {
+ "type": "cve",
+ "name": "CVE-2017-15412",
+ "value": "CVE-2017-15412",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15412"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15412"
+ },
+ {
+ "url": "https://github.com/sparklemotion/nokogiri/issues/1714"
+ },
+ {
+ "url": "https://people.canonical.com/~ubuntu-security/cve/2017/CVE-2017-15412.html"
+ }
+ ]
+ },
+ {
+ "category": "dependency_scanning",
+ "name": "Bypass of a protection mechanism in libxslt",
+ "message": "Bypass of a protection mechanism in libxslt in nokogiri",
+ "description": "libxslt through 1.1.33 allows bypass of a protection mechanism because callers of xsltCheckRead and xsltCheckWrite permit access even upon receiving a -1 error code. xsltCheckRead can return -1 for a crafted URL that is not actually invalid and is subsequently loaded. Vendored version of libxslt has been patched to remediate this vulnerability. Note that this patch is not yet (as of 2019-04-22) in an upstream release of libxslt.",
+ "cve": "rails/Gemfile.lock:nokogiri:gemnasium:1a2e2e6e-67ba-4142-bfa1-3391f5416e4c",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest version if using vendored version of libxslt OR update the system library libxslt to a fixed version",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-1a2e2e6e-67ba-4142-bfa1-3391f5416e4c",
+ "value": "1a2e2e6e-67ba-4142-bfa1-3391f5416e4c",
+ "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
+ },
+ {
+ "type": "cve",
+ "name": "CVE-2019-11068",
+ "value": "CVE-2019-11068",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11068"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11068"
+ },
+ {
+ "url": "https://github.com/sparklemotion/nokogiri/issues/1892"
+ },
+ {
+ "url": "https://people.canonical.com/~ubuntu-security/cve/CVE-2019-11068"
+ },
+ {
+ "url": "https://security-tracker.debian.org/tracker/CVE-2019-11068"
+ }
+ ]
+ },
+ {
+ "category": "dependency_scanning",
+ "name": "Regular Expression Denial of Service",
+ "message": "Regular Expression Denial of Service in debug",
+ "description": "The debug module is vulnerable to regular expression denial of service when untrusted user input is passed into the `o` formatter. It takes around 50k characters to block for 2 seconds making this a low severity issue.",
+ "cve": "yarn/yarn.lock:debug:gemnasium:37283ed4-0380-40d7-ada7-2d994afcc62a",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest versions.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "yarn/yarn.lock",
+ "dependency": {
+ "package": {
+ "name": "debug"
+ },
+ "version": "1.0.5"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-37283ed4-0380-40d7-ada7-2d994afcc62a",
+ "value": "37283ed4-0380-40d7-ada7-2d994afcc62a",
+ "url": "https://deps.sec.gitlab.com/packages/npm/debug/versions/1.0.5/advisories"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://github.com/visionmedia/debug/issues/501"
+ },
+ {
+ "url": "https://github.com/visionmedia/debug/pull/504"
+ },
+ {
+ "url": "https://nodesecurity.io/advisories/534"
+ }
+ ]
+ },
+ {
+ "category": "dependency_scanning",
+ "name": "Authentication bypass via incorrect DOM traversal and canonicalization",
+ "message": "Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js",
+ "description": "Some XML DOM traversal and canonicalization APIs may be inconsistent in handling of comments within XML nodes. Incorrect use of these APIs by some SAML libraries results in incorrect parsing of the inner text of XML nodes such that any inner text after the comment is lost prior to cryptographically signing the SAML message. Text after the comment therefore has no impact on the signature on the SAML message.\r\n\r\nA remote attacker can modify SAML content for a SAML service provider without invalidating the cryptographic signature, which may allow attackers to bypass primary authentication for the affected SAML service provider.",
+ "cve": "yarn/yarn.lock:saml2-js:gemnasium:9952e574-7b5b-46fa-a270-aeb694198a98",
+ "severity": "Unknown",
+ "solution": "Upgrade to fixed version.\r\n",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "yarn/yarn.lock",
+ "dependency": {
+ "package": {
+ "name": "saml2-js"
+ },
+ "version": "1.5.0"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-9952e574-7b5b-46fa-a270-aeb694198a98",
+ "value": "9952e574-7b5b-46fa-a270-aeb694198a98",
+ "url": "https://deps.sec.gitlab.com/packages/npm/saml2-js/versions/1.5.0/advisories"
+ },
+ {
+ "type": "cve",
+ "name": "CVE-2017-11429",
+ "value": "CVE-2017-11429",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11429"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://github.com/Clever/saml2/commit/3546cb61fd541f219abda364c5b919633609ef3d#diff-af730f9f738de1c9ad87596df3f6de84R279"
+ },
+ {
+ "url": "https://github.com/Clever/saml2/issues/127"
+ },
+ {
+ "url": "https://www.kb.cert.org/vuls/id/475445"
+ }
+ ]
+ }
+ ],
+ "remediations": [],
+ "dependency_files": [
+ {
+ "path": "rails/Gemfile.lock",
+ "package_manager": "bundler",
+ "dependencies": [
+ {
+ "package": {
+ "name": "mini_portile2"
+ },
+ "version": "2.2.0"
+ },
+ {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
+ ]
+ },
+ {
+ "path": "yarn/yarn.lock",
+ "package_manager": "yarn",
+ "dependencies": [
+ {
+ "package": {
+ "name": "async"
+ },
+ "version": "0.2.10"
+ },
+ {
+ "package": {
+ "name": "async"
+ },
+ "version": "1.5.2"
+ },
+ {
+ "package": {
+ "name": "debug"
+ },
+ "version": "1.0.5"
+ },
+ {
+ "package": {
+ "name": "ejs"
+ },
+ "version": "0.8.8"
+ },
+ {
+ "package": {
+ "name": "ms"
+ },
+ "version": "2.0.0"
+ },
+ {
+ "package": {
+ "name": "node-forge"
+ },
+ "version": "0.2.24"
+ },
+ {
+ "package": {
+ "name": "saml2-js"
+ },
+ "version": "1.5.0"
+ },
+ {
+ "package": {
+ "name": "sax"
+ },
+ "version": "1.2.4"
+ },
+ {
+ "package": {
+ "name": "underscore"
+ },
+ "version": "1.9.1"
+ },
+ {
+ "package": {
+ "name": "underscore"
+ },
+ "version": "1.6.0"
+ },
+ {
+ "package": {
+ "name": "xml-crypto"
+ },
+ "version": "0.8.5"
+ },
+ {
+ "package": {
+ "name": "xml-encryption"
+ },
+ "version": "0.7.4"
+ },
+ {
+ "package": {
+ "name": "xml2js"
+ },
+ "version": "0.4.19"
+ },
+ {
+ "package": {
+ "name": "xmlbuilder"
+ },
+ "version": "2.1.0"
+ },
+ {
+ "package": {
+ "name": "xmlbuilder"
+ },
+ "version": "9.0.7"
+ },
+ {
+ "package": {
+ "name": "xmldom"
+ },
+ "version": "0.1.19"
+ },
+ {
+ "package": {
+ "name": "xmldom"
+ },
+ "version": "0.1.27"
+ },
+ {
+ "package": {
+ "name": "xpath.js"
+ },
+ "version": "1.1.0"
+ },
+ {
+ "package": {
+ "name": "xpath"
+ },
+ "version": "0.0.5"
+ }
+ ]
+ }
+ ]
+}
diff --git a/spec/fixtures/security-reports/master/gl-dast-report.json b/spec/fixtures/security-reports/master/gl-dast-report.json
index 3a308bf047e..df459d9419d 100644
--- a/spec/fixtures/security-reports/master/gl-dast-report.json
+++ b/spec/fixtures/security-reports/master/gl-dast-report.json
@@ -1,40 +1,42 @@
{
- "site": {
- "alerts": [
- {
- "sourceid": "3",
- "wascid": "15",
- "cweid": "16",
- "reference": "<p>http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx</p><p>https://www.owasp.org/index.php/List_of_useful_HTTP_headers</p>",
- "otherinfo": "<p>This issue still applies to error type pages (401, 403, 500, etc) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.</p><p>At \"High\" threshold this scanner will not alert on client or server error responses.</p>",
- "solution": "<p>Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.</p><p>If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.</p>",
- "count": "2",
- "pluginid": "10021",
- "alert": "X-Content-Type-Options Header Missing",
- "name": "X-Content-Type-Options Header Missing",
- "riskcode": "1",
- "confidence": "2",
- "riskdesc": "Low (Medium)",
- "desc": "<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.</p>",
- "instances": [
- {
- "param": "X-Content-Type-Options",
- "method": "GET",
- "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
- },
- {
- "param": "X-Content-Type-Options",
- "method": "GET",
- "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io/"
- }
- ]
- }
- ],
- "@ssl": "false",
- "@port": "80",
- "@host": "bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io",
- "@name": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
- },
+ "site": [
+ {
+ "alerts": [
+ {
+ "sourceid": "3",
+ "wascid": "15",
+ "cweid": "16",
+ "reference": "<p>http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx</p><p>https://www.owasp.org/index.php/List_of_useful_HTTP_headers</p>",
+ "otherinfo": "<p>This issue still applies to error type pages (401, 403, 500, etc) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.</p><p>At \"High\" threshold this scanner will not alert on client or server error responses.</p>",
+ "solution": "<p>Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.</p><p>If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.</p>",
+ "count": "2",
+ "pluginid": "10021",
+ "alert": "X-Content-Type-Options Header Missing",
+ "name": "X-Content-Type-Options Header Missing",
+ "riskcode": "1",
+ "confidence": "2",
+ "riskdesc": "Low (Medium)",
+ "desc": "<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.</p>",
+ "instances": [
+ {
+ "param": "X-Content-Type-Options",
+ "method": "GET",
+ "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
+ },
+ {
+ "param": "X-Content-Type-Options",
+ "method": "GET",
+ "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io/"
+ }
+ ]
+ }
+ ],
+ "@ssl": "false",
+ "@port": "80",
+ "@host": "bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io",
+ "@name": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
+ }
+ ],
"@generated": "Fri, 13 Apr 2018 09:22:01",
"@version": "2.7.0"
}
diff --git a/spec/javascripts/activities_spec.js b/spec/frontend/activities_spec.js
index 23b6de7e4e0..d14be3a1f26 100644
--- a/spec/javascripts/activities_spec.js
+++ b/spec/frontend/activities_spec.js
@@ -1,7 +1,6 @@
/* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow */
import $ from 'jquery';
-import 'vendor/jquery.endless-scroll';
import Activities from '~/activities';
import Pager from '~/pager';
@@ -40,7 +39,7 @@ describe('Activities', () => {
beforeEach(() => {
loadFixtures(fixtureTemplate);
- spyOn(Pager, 'init').and.stub();
+ jest.spyOn(Pager, 'init').mockImplementation(() => {});
new Activities();
});
diff --git a/spec/javascripts/api_spec.js b/spec/frontend/api_spec.js
index 805bb10bda6..6010488d9e0 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -264,7 +264,7 @@ describe('Api', () => {
const namespace = 'some namespace';
const project = 'some project';
const labelData = { some: 'data' };
- const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/labels`;
+ const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/-/labels`;
const expectedData = {
label: labelData,
};
@@ -459,7 +459,7 @@ describe('Api', () => {
dummyProjectPath,
)}/repository/branches`;
- spyOn(axios, 'post').and.callThrough();
+ jest.spyOn(axios, 'post');
mock.onPost(expectedUrl).replyOnce(200, {
name: branch,
diff --git a/spec/javascripts/autosave_spec.js b/spec/frontend/autosave_spec.js
index dcb1c781591..4d9c8f96d62 100644
--- a/spec/javascripts/autosave_spec.js
+++ b/spec/frontend/autosave_spec.js
@@ -1,16 +1,19 @@
import $ from 'jquery';
import Autosave from '~/autosave';
import AccessorUtilities from '~/lib/utils/accessor';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
describe('Autosave', () => {
+ useLocalStorageSpy();
+
let autosave;
const field = $('<textarea></textarea>');
const key = 'key';
describe('class constructor', () => {
beforeEach(() => {
- spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.returnValue(true);
- spyOn(Autosave.prototype, 'restore');
+ jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockReturnValue(true);
+ jest.spyOn(Autosave.prototype, 'restore').mockImplementation(() => {});
});
it('should set .isLocalStorageAvailable', () => {
@@ -27,8 +30,6 @@ describe('Autosave', () => {
field,
key,
};
-
- spyOn(window.localStorage, 'getItem');
});
describe('if .isLocalStorageAvailable is `false`', () => {
@@ -55,7 +56,7 @@ describe('Autosave', () => {
});
it('triggers jquery event', () => {
- spyOn(autosave.field, 'trigger').and.callThrough();
+ jest.spyOn(autosave.field, 'trigger').mockImplementation(() => {});
Autosave.prototype.restore.call(autosave);
@@ -77,7 +78,7 @@ describe('Autosave', () => {
});
it('does not trigger event', () => {
- spyOn(field, 'trigger').and.callThrough();
+ jest.spyOn(field, 'trigger');
expect(field.trigger).not.toHaveBeenCalled();
});
@@ -86,11 +87,9 @@ describe('Autosave', () => {
describe('save', () => {
beforeEach(() => {
- autosave = jasmine.createSpyObj('autosave', ['reset']);
+ autosave = { reset: jest.fn() };
autosave.field = field;
field.val('value');
-
- spyOn(window.localStorage, 'setItem');
});
describe('if .isLocalStorageAvailable is `false`', () => {
@@ -123,8 +122,6 @@ describe('Autosave', () => {
autosave = {
key,
};
-
- spyOn(window.localStorage, 'removeItem');
});
describe('if .isLocalStorageAvailable is `false`', () => {
diff --git a/spec/frontend/boards/modal_store_spec.js b/spec/frontend/boards/modal_store_spec.js
index 3257a3fb8a3..4dd27e94d97 100644
--- a/spec/frontend/boards/modal_store_spec.js
+++ b/spec/frontend/boards/modal_store_spec.js
@@ -1,7 +1,7 @@
/* global ListIssue */
-import '~/vue_shared/models/label';
-import '~/vue_shared/models/assignee';
+import '~/boards/models/label';
+import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import Store from '~/boards/stores/modal_store';
diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js
index 66b22fa2681..6de06a9e2d5 100644
--- a/spec/frontend/clusters/clusters_bundle_spec.js
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -1,5 +1,10 @@
import Clusters from '~/clusters/clusters_bundle';
-import { APPLICATION_STATUS, INGRESS_DOMAIN_SUFFIX, APPLICATIONS } from '~/clusters/constants';
+import {
+ APPLICATION_STATUS,
+ INGRESS_DOMAIN_SUFFIX,
+ APPLICATIONS,
+ RUNNER,
+} from '~/clusters/constants';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { loadHTMLFixture } from 'helpers/fixtures';
@@ -353,4 +358,30 @@ describe('Clusters', () => {
});
});
});
+
+ describe('updateApplication', () => {
+ const params = { version: '1.0.0' };
+ let storeUpdateApplication;
+ let installApplication;
+
+ beforeEach(() => {
+ storeUpdateApplication = jest.spyOn(cluster.store, 'updateApplication');
+ installApplication = jest.spyOn(cluster.service, 'installApplication');
+
+ cluster.updateApplication({ id: RUNNER, params });
+ });
+
+ afterEach(() => {
+ storeUpdateApplication.mockRestore();
+ installApplication.mockRestore();
+ });
+
+ it('calls store updateApplication method', () => {
+ expect(storeUpdateApplication).toHaveBeenCalledWith(RUNNER);
+ });
+
+ it('sends installApplication request', () => {
+ expect(installApplication).toHaveBeenCalledWith(RUNNER, params);
+ });
+ });
});
diff --git a/spec/frontend/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js
index 7c781b72355..9f127ccb690 100644
--- a/spec/frontend/clusters/components/application_row_spec.js
+++ b/spec/frontend/clusters/components/application_row_spec.js
@@ -245,26 +245,26 @@ describe('Application Row', () => {
});
});
- describe('Upgrade button', () => {
+ describe('Update button', () => {
it('has indeterminate state on page load', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: null,
});
- const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
+ const updateBtn = vm.$el.querySelector('.js-cluster-application-update-button');
- expect(upgradeBtn).toBe(null);
+ expect(updateBtn).toBe(null);
});
- it('has enabled "Upgrade" when "upgradeAvailable" is true', () => {
+ it('has enabled "Update" when "updateAvailable" is true', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- upgradeAvailable: true,
+ updateAvailable: true,
});
- const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
+ const updateBtn = vm.$el.querySelector('.js-cluster-application-update-button');
- expect(upgradeBtn).not.toBe(null);
- expect(upgradeBtn.innerHTML).toContain('Upgrade');
+ expect(updateBtn).not.toBe(null);
+ expect(updateBtn.innerHTML).toContain('Update');
});
it('has enabled "Retry update" when update process fails', () => {
@@ -273,10 +273,10 @@ describe('Application Row', () => {
status: APPLICATION_STATUS.INSTALLED,
updateFailed: true,
});
- const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
+ const updateBtn = vm.$el.querySelector('.js-cluster-application-update-button');
- expect(upgradeBtn).not.toBe(null);
- expect(upgradeBtn.innerHTML).toContain('Retry update');
+ expect(updateBtn).not.toBe(null);
+ expect(updateBtn.innerHTML).toContain('Retry update');
});
it('has disabled "Updating" when APPLICATION_STATUS.UPDATING', () => {
@@ -284,53 +284,51 @@ describe('Application Row', () => {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATING,
});
- const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
+ const updateBtn = vm.$el.querySelector('.js-cluster-application-update-button');
- expect(upgradeBtn).not.toBe(null);
- expect(vm.isUpgrading).toBe(true);
- expect(upgradeBtn.innerHTML).toContain('Updating');
+ expect(updateBtn).not.toBe(null);
+ expect(vm.isUpdating).toBe(true);
+ expect(updateBtn.innerHTML).toContain('Updating');
});
- it('clicking upgrade button emits event', () => {
+ it('clicking update button emits event', () => {
jest.spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLED,
- upgradeAvailable: true,
+ updateAvailable: true,
});
- const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
+ const updateBtn = vm.$el.querySelector('.js-cluster-application-update-button');
- upgradeBtn.click();
+ updateBtn.click();
- expect(eventHub.$emit).toHaveBeenCalledWith('upgradeApplication', {
+ expect(eventHub.$emit).toHaveBeenCalledWith('updateApplication', {
id: DEFAULT_APPLICATION_STATE.id,
params: {},
});
});
- it('clicking disabled upgrade button emits nothing', () => {
+ it('clicking disabled update button emits nothing', () => {
jest.spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATING,
});
- const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
+ const updateBtn = vm.$el.querySelector('.js-cluster-application-update-button');
- upgradeBtn.click();
+ updateBtn.click();
expect(eventHub.$emit).not.toHaveBeenCalled();
});
- it('displays an error message if application upgrade failed', () => {
+ it('displays an error message if application update failed', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
title: 'GitLab Runner',
status: APPLICATION_STATUS.INSTALLED,
updateFailed: true,
});
- const failureMessage = vm.$el.querySelector(
- '.js-cluster-application-upgrade-failure-message',
- );
+ const failureMessage = vm.$el.querySelector('.js-cluster-application-update-details');
expect(failureMessage).not.toBe(null);
expect(failureMessage.innerHTML).toContain(
@@ -338,7 +336,7 @@ describe('Application Row', () => {
);
});
- it('displays a success toast message if application upgrade was successful', () => {
+ it('displays a success toast message if application update was successful', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
title: 'GitLab Runner',
@@ -349,13 +347,13 @@ describe('Application Row', () => {
vm.updateSuccessful = true;
return vm.$nextTick(() => {
- expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner upgraded successfully.');
+ expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner updated successfully.');
});
});
});
describe('Version', () => {
- it('displays a version number if application has been upgraded', () => {
+ it('displays a version number if application has been updated', () => {
const version = '0.1.45';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
@@ -363,15 +361,15 @@ describe('Application Row', () => {
updateSuccessful: true,
version,
});
- const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details');
- const versionEl = vm.$el.querySelector('.js-cluster-application-upgrade-version');
+ const updateDetails = vm.$el.querySelector('.js-cluster-application-update-details');
+ const versionEl = vm.$el.querySelector('.js-cluster-application-update-version');
- expect(upgradeDetails.innerHTML).toContain('Upgraded');
+ expect(updateDetails.innerHTML).toContain('Updated');
expect(versionEl).not.toBe(null);
expect(versionEl.innerHTML).toContain(version);
});
- it('contains a link to the chart repo if application has been upgraded', () => {
+ it('contains a link to the chart repo if application has been updated', () => {
const version = '0.1.45';
const chartRepo = 'https://gitlab.com/charts/gitlab-runner';
vm = mountComponent(ApplicationRow, {
@@ -381,13 +379,13 @@ describe('Application Row', () => {
chartRepo,
version,
});
- const versionEl = vm.$el.querySelector('.js-cluster-application-upgrade-version');
+ const versionEl = vm.$el.querySelector('.js-cluster-application-update-version');
expect(versionEl.href).toEqual(chartRepo);
expect(versionEl.target).toEqual('_blank');
});
- it('does not display a version number if application upgrade failed', () => {
+ it('does not display a version number if application update failed', () => {
const version = '0.1.45';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
@@ -395,10 +393,10 @@ describe('Application Row', () => {
updateFailed: true,
version,
});
- const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details');
- const versionEl = vm.$el.querySelector('.js-cluster-application-upgrade-version');
+ const updateDetails = vm.$el.querySelector('.js-cluster-application-update-details');
+ const versionEl = vm.$el.querySelector('.js-cluster-application-update-version');
- expect(upgradeDetails.innerHTML).toContain('failed');
+ expect(updateDetails.innerHTML).toContain('failed');
expect(versionEl).toBe(null);
});
});
diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js
index 8bcf02f0a34..221ebb143be 100644
--- a/spec/frontend/clusters/components/applications_spec.js
+++ b/spec/frontend/clusters/components/applications_spec.js
@@ -1,9 +1,11 @@
import Vue from 'vue';
import applications from '~/clusters/components/applications.vue';
import { CLUSTER_TYPE } from '~/clusters/constants';
-import eventHub from '~/clusters/event_hub';
import mountComponent from 'helpers/vue_mount_component_helper';
import { APPLICATIONS_MOCK_STATE } from '../services/mock_data';
+import eventHub from '~/clusters/event_hub';
+import { shallowMount } from '@vue/test-utils';
+import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
describe('Applications', () => {
let vm;
@@ -277,73 +279,48 @@ describe('Applications', () => {
});
describe('Knative application', () => {
- describe('when installed', () => {
- describe('with ip address', () => {
- const props = {
- applications: {
- ...APPLICATIONS_MOCK_STATE,
- knative: {
- title: 'Knative',
- hostname: 'example.com',
- status: 'installed',
- externalIp: '1.1.1.1',
- },
- },
- };
- it('renders ip address with a clipboard button', () => {
- vm = mountComponent(Applications, props);
+ const propsData = {
+ applications: {
+ ...APPLICATIONS_MOCK_STATE,
+ knative: {
+ title: 'Knative',
+ hostname: 'example.com',
+ status: 'installed',
+ externalIp: '1.1.1.1',
+ installed: true,
+ },
+ },
+ };
+ const newHostname = 'newhostname.com';
+ let wrapper;
+ let knativeDomainEditor;
- expect(vm.$el.querySelector('.js-knative-endpoint').value).toEqual('1.1.1.1');
-
- expect(
- vm.$el
- .querySelector('.js-knative-endpoint-clipboard-btn')
- .getAttribute('data-clipboard-text'),
- ).toEqual('1.1.1.1');
- });
-
- it('renders domain & allows editing', () => {
- expect(vm.$el.querySelector('.js-knative-domainname').value).toEqual('example.com');
- expect(vm.$el.querySelector('.js-knative-domainname').getAttribute('readonly')).toBe(
- null,
- );
- });
-
- it('renders an update/save Knative domain button', () => {
- expect(vm.$el.querySelector('.js-knative-save-domain-button')).not.toBe(null);
- });
+ beforeEach(() => {
+ wrapper = shallowMount(Applications, { propsData });
+ jest.spyOn(eventHub, '$emit');
- it('emits event when clicking Save changes button', () => {
- jest.spyOn(eventHub, '$emit');
- vm = mountComponent(Applications, props);
+ knativeDomainEditor = wrapper.find(KnativeDomainEditor);
+ });
- const saveButton = vm.$el.querySelector('.js-knative-save-domain-button');
+ afterEach(() => {
+ wrapper.destroy();
+ });
- saveButton.click();
+ it('emits saveKnativeDomain event when knative domain editor emits save event', () => {
+ knativeDomainEditor.vm.$emit('save', newHostname);
- expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', {
- id: 'knative',
- params: { hostname: 'example.com' },
- });
- });
+ expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', {
+ id: 'knative',
+ params: { hostname: newHostname },
});
+ });
- describe('without ip address', () => {
- it('renders an input text with a loading icon and an alert text', () => {
- vm = mountComponent(Applications, {
- applications: {
- ...APPLICATIONS_MOCK_STATE,
- knative: {
- title: 'Knative',
- hostname: 'example.com',
- status: 'installed',
- },
- },
- });
+ it('emits setKnativeHostname event when knative domain editor emits change event', () => {
+ wrapper.find(KnativeDomainEditor).vm.$emit('set', newHostname);
- expect(vm.$el.querySelector('.js-knative-ip-loading-icon')).not.toBe(null);
- expect(vm.$el.querySelector('.js-no-knative-endpoint-message')).not.toBe(null);
- });
+ expect(eventHub.$emit).toHaveBeenCalledWith('setKnativeHostname', {
+ id: 'knative',
+ hostname: newHostname,
});
});
});
diff --git a/spec/frontend/clusters/components/knative_domain_editor_spec.js b/spec/frontend/clusters/components/knative_domain_editor_spec.js
new file mode 100644
index 00000000000..242b5701f8b
--- /dev/null
+++ b/spec/frontend/clusters/components/knative_domain_editor_spec.js
@@ -0,0 +1,141 @@
+import { shallowMount } from '@vue/test-utils';
+import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { APPLICATION_STATUS } from '~/clusters/constants';
+
+const { UPDATING } = APPLICATION_STATUS;
+
+describe('KnativeDomainEditor', () => {
+ let wrapper;
+ let knative;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(KnativeDomainEditor, {
+ propsData: { ...props },
+ });
+ };
+
+ beforeEach(() => {
+ knative = {
+ title: 'Knative',
+ hostname: 'example.com',
+ installed: true,
+ };
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('knative has an assigned IP address', () => {
+ beforeEach(() => {
+ knative.externalIp = '1.1.1.1';
+ createComponent({ knative });
+ });
+
+ it('renders ip address with a clipboard button', () => {
+ expect(wrapper.find('.js-knative-endpoint').exists()).toBe(true);
+ expect(wrapper.find('.js-knative-endpoint').element.value).toEqual(knative.externalIp);
+ });
+
+ it('displays ip address clipboard button', () => {
+ expect(wrapper.find('.js-knative-endpoint-clipboard-btn').attributes('text')).toEqual(
+ knative.externalIp,
+ );
+ });
+
+ it('renders domain & allows editing', () => {
+ const domainNameInput = wrapper.find('.js-knative-domainname');
+
+ expect(domainNameInput.element.value).toEqual(knative.hostname);
+ expect(domainNameInput.attributes('readonly')).toBeFalsy();
+ });
+
+ it('renders an update/save Knative domain button', () => {
+ expect(wrapper.find('.js-knative-save-domain-button').exists()).toBe(true);
+ });
+ });
+
+ describe('knative without ip address', () => {
+ beforeEach(() => {
+ knative.externalIp = null;
+ createComponent({ knative });
+ });
+
+ it('renders an input text with a loading icon', () => {
+ expect(wrapper.find('.js-knative-ip-loading-icon').exists()).toBe(true);
+ });
+
+ it('renders message indicating there is not IP address assigned', () => {
+ expect(wrapper.find('.js-no-knative-endpoint-message').exists()).toBe(true);
+ });
+ });
+
+ describe('clicking save changes button', () => {
+ beforeEach(() => {
+ createComponent({ knative });
+ });
+
+ it('triggers save event and pass current knative hostname', () => {
+ wrapper.find(LoadingButton).vm.$emit('click');
+ expect(wrapper.emitted('save')[0]).toEqual([knative.hostname]);
+ });
+ });
+
+ describe('when knative domain name was saved successfully', () => {
+ beforeEach(() => {
+ createComponent({ knative });
+ });
+
+ it('displays toast indicating a successful update', () => {
+ wrapper.vm.$toast = { show: jest.fn() };
+ wrapper.setProps({ knative: Object.assign({ updateSuccessful: true }, knative) });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
+ 'Knative domain name was updated successfully.',
+ );
+ });
+ });
+ });
+
+ describe('when knative domain name input changes', () => {
+ it('emits "set" event with updated domain name', () => {
+ const newHostname = 'newhostname.com';
+
+ wrapper.setData({ knativeHostname: newHostname });
+
+ expect(wrapper.emitted('set')[0]).toEqual([newHostname]);
+ });
+ });
+
+ describe('when updating knative domain name failed', () => {
+ beforeEach(() => {
+ createComponent({ knative });
+ });
+
+ it('displays an error banner indicating the operation failure', () => {
+ wrapper.setProps({ knative: { updateFailed: true, ...knative } });
+
+ expect(wrapper.find('.js-cluster-knative-domain-name-failure-message').exists()).toBe(true);
+ });
+ });
+
+ describe(`when knative status is ${UPDATING}`, () => {
+ beforeEach(() => {
+ createComponent({ knative: { status: UPDATING, ...knative } });
+ });
+
+ it('renders loading spinner in save button', () => {
+ expect(wrapper.find(LoadingButton).props('loading')).toBe(true);
+ });
+
+ it('renders disabled save button', () => {
+ expect(wrapper.find(LoadingButton).props('disabled')).toBe(true);
+ });
+
+ it('renders save button with "Saving" label', () => {
+ expect(wrapper.find(LoadingButton).props('label')).toBe('Saving');
+ });
+ });
+});
diff --git a/spec/frontend/clusters/services/application_state_machine_spec.js b/spec/frontend/clusters/services/application_state_machine_spec.js
index e057e2ac955..c146ef79be7 100644
--- a/spec/frontend/clusters/services/application_state_machine_spec.js
+++ b/spec/frontend/clusters/services/application_state_machine_spec.js
@@ -127,7 +127,7 @@ describe('applicationStateMachine', () => {
describe(`current state is ${UPDATING}`, () => {
it.each`
expectedState | event | effects
- ${INSTALLED} | ${UPDATED} | ${{ updateSuccessful: true, updateAcknowledged: false }}
+ ${INSTALLED} | ${UPDATED} | ${{ updateSuccessful: true }}
${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
`(`transitions to $expectedState on $event event and applies $effects`, data => {
const { expectedState, event, effects } = data;
diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js
index aa926bb36d7..f2cc413512d 100644
--- a/spec/frontend/clusters/stores/clusters_store_spec.js
+++ b/spec/frontend/clusters/stores/clusters_store_spec.js
@@ -85,11 +85,10 @@ describe('Clusters Store', () => {
statusReason: mockResponseData.applications[2].status_reason,
requestReason: null,
version: mockResponseData.applications[2].version,
- upgradeAvailable: mockResponseData.applications[2].update_available,
+ updateAvailable: mockResponseData.applications[2].update_available,
chartRepo: 'https://gitlab.com/charts/gitlab-runner',
installed: false,
installFailed: false,
- updateAcknowledged: true,
updateFailed: false,
updateSuccessful: false,
uninstallable: false,
@@ -133,6 +132,8 @@ describe('Clusters Store', () => {
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
+ updateSuccessful: false,
+ updateFailed: false,
},
cert_manager: {
title: 'Cert-Manager',
diff --git a/spec/frontend/helpers/jest_helpers.js b/spec/frontend/helpers/jest_helpers.js
new file mode 100644
index 00000000000..4a150be9935
--- /dev/null
+++ b/spec/frontend/helpers/jest_helpers.js
@@ -0,0 +1,24 @@
+/* eslint-disable import/prefer-default-export */
+
+/*
+@module
+
+This method provides convenience functions to help migrating from Karma/Jasmine to Jest.
+
+Try not to use these in new tests - this module is provided primarily for convenience of migrating tests.
+ */
+
+/**
+ * Creates a plain JS object pre-populated with Jest spy functions. Useful for making simple mocks classes.
+ *
+ * @see https://jasmine.github.io/2.0/introduction.html#section-Spies:_%3Ccode%3EcreateSpyObj%3C/code%3E
+ * @param {string} baseName Human-readable name of the object. This is used for reporting purposes.
+ * @param methods {string[]} List of method names that will be added to the spy object.
+ */
+export function createSpyObj(baseName, methods) {
+ const obj = {};
+ methods.forEach(method => {
+ obj[method] = jest.fn().mockName(`${baseName}#${method}`);
+ });
+ return obj;
+}
diff --git a/spec/frontend/helpers/jquery.js b/spec/frontend/helpers/jquery.js
new file mode 100644
index 00000000000..6421a592c0c
--- /dev/null
+++ b/spec/frontend/helpers/jquery.js
@@ -0,0 +1,6 @@
+import $ from 'jquery';
+
+global.$ = $;
+global.jQuery = $;
+
+export default $;
diff --git a/spec/frontend/helpers/local_storage_helper.js b/spec/frontend/helpers/local_storage_helper.js
new file mode 100644
index 00000000000..48e66b11767
--- /dev/null
+++ b/spec/frontend/helpers/local_storage_helper.js
@@ -0,0 +1,41 @@
+/**
+ * Manage the instance of a custom `window.localStorage`
+ *
+ * This only encapsulates the setup / teardown logic so that it can easily be
+ * reused with different implementations (i.e. a spy or a [fake][1])
+ *
+ * [1]: https://stackoverflow.com/a/41434763/1708147
+ *
+ * @param {() => any} fn Function that returns the object to use for localStorage
+ */
+const useLocalStorage = fn => {
+ const origLocalStorage = window.localStorage;
+ let currentLocalStorage;
+
+ Object.defineProperty(window, 'localStorage', {
+ get: () => currentLocalStorage,
+ });
+
+ beforeEach(() => {
+ currentLocalStorage = fn();
+ });
+
+ afterEach(() => {
+ currentLocalStorage = origLocalStorage;
+ });
+};
+
+/**
+ * Create an object with the localStorage interface but `jest.fn()` implementations.
+ */
+export const createLocalStorageSpy = () => ({
+ clear: jest.fn(),
+ getItem: jest.fn(),
+ setItem: jest.fn(),
+ removeItem: jest.fn(),
+});
+
+/**
+ * Before each test, overwrite `window.localStorage` with a spy implementation.
+ */
+export const useLocalStorageSpy = () => useLocalStorage(createLocalStorageSpy);
diff --git a/spec/frontend/helpers/timeout.js b/spec/frontend/helpers/timeout.js
index b30b7f1ce1e..702ef0be5aa 100644
--- a/spec/frontend/helpers/timeout.js
+++ b/spec/frontend/helpers/timeout.js
@@ -1,13 +1,33 @@
const NS_PER_SEC = 1e9;
const NS_PER_MS = 1e6;
+const IS_DEBUGGING = process.execArgv.join(' ').includes('--inspect-brk');
let testTimeoutNS;
export const setTestTimeout = newTimeoutMS => {
- testTimeoutNS = newTimeoutMS * NS_PER_MS;
+ const newTimeoutNS = newTimeoutMS * NS_PER_MS;
+ // never accept a smaller timeout than the default
+ if (newTimeoutNS < testTimeoutNS) {
+ return;
+ }
+
+ testTimeoutNS = newTimeoutNS;
jest.setTimeout(newTimeoutMS);
};
+// Allows slow tests to set their own timeout.
+// Useful for tests with jQuery, which is very slow in big DOMs.
+let temporaryTimeoutNS = null;
+export const setTestTimeoutOnce = newTimeoutMS => {
+ const newTimeoutNS = newTimeoutMS * NS_PER_MS;
+ // never accept a smaller timeout than the default
+ if (newTimeoutNS < testTimeoutNS) {
+ return;
+ }
+
+ temporaryTimeoutNS = newTimeoutNS;
+};
+
export const initializeTestTimeout = defaultTimeoutMS => {
setTestTimeout(defaultTimeoutMS);
@@ -19,12 +39,20 @@ export const initializeTestTimeout = defaultTimeoutMS => {
});
afterEach(() => {
+ let timeoutNS = testTimeoutNS;
+ if (Number.isFinite(temporaryTimeoutNS)) {
+ timeoutNS = temporaryTimeoutNS;
+ temporaryTimeoutNS = null;
+ }
+
const [seconds, remainingNs] = process.hrtime(testStartTime);
const elapsedNS = seconds * NS_PER_SEC + remainingNs;
- if (elapsedNS > testTimeoutNS) {
+ // Disable the timeout error when debugging. It is meaningless because
+ // debugging always takes longer than the test timeout.
+ if (elapsedNS > timeoutNS && !IS_DEBUGGING) {
throw new Error(
- `Test took too long (${elapsedNS / NS_PER_MS}ms > ${testTimeoutNS / NS_PER_MS}ms)!`,
+ `Test took too long (${elapsedNS / NS_PER_MS}ms > ${timeoutNS / NS_PER_MS}ms)!`,
);
}
});
diff --git a/spec/frontend/helpers/vue_test_utils_helper.js b/spec/frontend/helpers/vue_test_utils_helper.js
index 19e27388eeb..121e99c9783 100644
--- a/spec/frontend/helpers/vue_test_utils_helper.js
+++ b/spec/frontend/helpers/vue_test_utils_helper.js
@@ -16,4 +16,6 @@ const vNodeContainsText = (vnode, text) =>
* @param {String} text
*/
export const shallowWrapperContainsSlotText = (shallowWrapper, slotName, text) =>
- !!shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length;
+ Boolean(
+ shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length,
+ );
diff --git a/spec/frontend/ide/stores/modules/commit/mutations_spec.js b/spec/frontend/ide/stores/modules/commit/mutations_spec.js
index 40d47aaad03..246500a2f34 100644
--- a/spec/frontend/ide/stores/modules/commit/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/mutations_spec.js
@@ -54,5 +54,20 @@ describe('IDE commit module mutations', () => {
expect(state.shouldCreateMR).toBe(false);
});
+
+ it('sets shouldCreateMR to given value when passed in', () => {
+ state.shouldCreateMR = false;
+ mutations.TOGGLE_SHOULD_CREATE_MR(state, false);
+
+ expect(state.shouldCreateMR).toBe(false);
+ });
+ });
+
+ describe('INTERACT_WITH_NEW_MR', () => {
+ it('sets interactedWithNewMR to true', () => {
+ mutations.INTERACT_WITH_NEW_MR(state);
+
+ expect(state.interactedWithNewMR).toBe(true);
+ });
});
});
diff --git a/spec/frontend/lib/utils/number_utility_spec.js b/spec/frontend/lib/utils/number_utility_spec.js
index 818404bad81..77d7478d317 100644
--- a/spec/frontend/lib/utils/number_utility_spec.js
+++ b/spec/frontend/lib/utils/number_utility_spec.js
@@ -5,6 +5,7 @@ import {
bytesToGiB,
numberToHumanSize,
sum,
+ isOdd,
} from '~/lib/utils/number_utils';
describe('Number Utils', () => {
@@ -98,4 +99,14 @@ describe('Number Utils', () => {
expect([1, 2, 3, 4, 5].reduce(sum)).toEqual(15);
});
});
+
+ describe('isOdd', () => {
+ it('should return 0 with a even number', () => {
+ expect(isOdd(2)).toEqual(0);
+ });
+
+ it('should return 1 with a odd number', () => {
+ expect(isOdd(1)).toEqual(1);
+ });
+ });
});
diff --git a/spec/javascripts/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 381c7b2d0a6..c771984a137 100644
--- a/spec/javascripts/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -1,5 +1,12 @@
import * as urlUtils from '~/lib/utils/url_utility';
+const setWindowLocation = value => {
+ Object.defineProperty(window, 'location', {
+ writable: true,
+ value,
+ });
+};
+
describe('URL utility', () => {
describe('webIDEUrl', () => {
afterEach(() => {
@@ -107,4 +114,113 @@ describe('URL utility', () => {
expect(url).toBe('/home/feature#install');
});
});
+
+ describe('getBaseURL', () => {
+ beforeEach(() => {
+ setWindowLocation({
+ protocol: 'https:',
+ host: 'gitlab.com',
+ });
+ });
+
+ it('returns correct base URL', () => {
+ expect(urlUtils.getBaseURL()).toBe('https://gitlab.com');
+ });
+ });
+
+ describe('isAbsoluteOrRootRelative', () => {
+ const validUrls = ['https://gitlab.com/', 'http://gitlab.com/', '/users/sign_in'];
+
+ const invalidUrls = [' https://gitlab.com/', './file/path', 'notanurl', '<a></a>'];
+
+ it.each(validUrls)(`returns true for %s`, url => {
+ expect(urlUtils.isAbsoluteOrRootRelative(url)).toBe(true);
+ });
+
+ it.each(invalidUrls)(`returns false for %s`, url => {
+ expect(urlUtils.isAbsoluteOrRootRelative(url)).toBe(false);
+ });
+ });
+
+ describe('isSafeUrl', () => {
+ const absoluteUrls = [
+ 'http://example.org',
+ 'http://example.org:8080',
+ 'https://example.org',
+ 'https://example.org:8080',
+ 'https://192.168.1.1',
+ ];
+
+ const rootRelativeUrls = ['/relative/link'];
+
+ const relativeUrls = ['./relative/link', '../relative/link'];
+
+ const urlsWithoutHost = ['http://', 'https://', 'https:https:https:'];
+
+ /* eslint-disable no-script-url */
+ const nonHttpUrls = [
+ 'javascript:',
+ 'javascript:alert("XSS")',
+ 'jav\tascript:alert("XSS");',
+ ' &#14; javascript:alert("XSS");',
+ 'ftp://192.168.1.1',
+ 'file:///',
+ 'file:///etc/hosts',
+ ];
+ /* eslint-enable no-script-url */
+
+ // javascript:alert('XSS')
+ const encodedJavaScriptUrls = [
+ '&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041',
+ '&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;',
+ '&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29',
+ '\\u006A\\u0061\\u0076\\u0061\\u0073\\u0063\\u0072\\u0069\\u0070\\u0074\\u003A\\u0061\\u006C\\u0065\\u0072\\u0074\\u0028\\u0027\\u0058\\u0053\\u0053\\u0027\\u0029',
+ ];
+
+ const safeUrls = [...absoluteUrls, ...rootRelativeUrls];
+ const unsafeUrls = [
+ ...relativeUrls,
+ ...urlsWithoutHost,
+ ...nonHttpUrls,
+ ...encodedJavaScriptUrls,
+ ];
+
+ describe('with URL constructor support', () => {
+ it.each(safeUrls)('returns true for %s', url => {
+ expect(urlUtils.isSafeURL(url)).toBe(true);
+ });
+
+ it.each(unsafeUrls)('returns false for %s', url => {
+ expect(urlUtils.isSafeURL(url)).toBe(false);
+ });
+ });
+ });
+
+ describe('getWebSocketProtocol', () => {
+ it.each`
+ protocol | expectation
+ ${'http:'} | ${'ws:'}
+ ${'https:'} | ${'wss:'}
+ `('returns "$expectation" with "$protocol" protocol', ({ protocol, expectation }) => {
+ setWindowLocation({
+ protocol,
+ host: 'example.com',
+ });
+
+ expect(urlUtils.getWebSocketProtocol()).toEqual(expectation);
+ });
+ });
+
+ describe('getWebSocketUrl', () => {
+ it('joins location host to path', () => {
+ setWindowLocation({
+ protocol: 'http:',
+ host: 'example.com',
+ });
+
+ const path = '/lorem/ipsum?a=bc';
+
+ expect(urlUtils.getWebSocketUrl(path)).toEqual('ws://example.com/lorem/ipsum?a=bc');
+ });
+ });
});
diff --git a/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap b/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap
index 5f9f13d591d..a2a7d0ee91e 100644
--- a/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap
+++ b/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap
@@ -3,6 +3,7 @@
exports[`MR Popover loaded state matches the snapshot 1`] = `
<glpopover-stub
boundary="viewport"
+ cssclasses=""
placement="top"
show=""
target=""
@@ -61,6 +62,7 @@ exports[`MR Popover loaded state matches the snapshot 1`] = `
exports[`MR Popover shows skeleton-loader while apollo is loading 1`] = `
<glpopover-stub
boundary="viewport"
+ cssclasses=""
placement="top"
show=""
target=""
diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/frontend/notes/components/note_app_spec.js
index ef876dc2941..ff833d2c899 100644
--- a/spec/javascripts/notes/components/note_app_spec.js
+++ b/spec/frontend/notes/components/note_app_spec.js
@@ -1,18 +1,47 @@
-import $ from 'jquery';
-import _ from 'underscore';
+import $ from 'helpers/jquery';
import Vue from 'vue';
import { mount, createLocalVue } from '@vue/test-utils';
import NotesApp from '~/notes/components/notes_app.vue';
import service from '~/notes/services/notes_service';
import createStore from '~/notes/stores';
import '~/behaviors/markdown/render_gfm';
-import * as mockData from '../mock_data';
+import { setTestTimeout } from 'helpers/timeout';
+// TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-ce/issues/62491)
+import * as mockData from '../../../javascripts/notes/mock_data';
+
+const originalInterceptors = [...Vue.http.interceptors];
+
+const emptyResponseInterceptor = (request, next) => {
+ next(
+ request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }),
+ );
+};
+
+setTestTimeout(1000);
describe('note_app', () => {
let mountComponent;
let wrapper;
let store;
+ /**
+ * waits for fetchNotes() to complete
+ */
+ const waitForDiscussionsRequest = () =>
+ new Promise(resolve => {
+ const { vm } = wrapper.find(NotesApp);
+ const unwatch = vm.$watch('isFetching', isFetching => {
+ if (isFetching) {
+ return;
+ }
+
+ unwatch();
+ resolve();
+ });
+ });
+
beforeEach(() => {
$('body').attr('data-page', 'projects:merge_requests:show');
@@ -33,6 +62,7 @@ describe('note_app', () => {
template: '<div class="js-vue-notes-event"><notes-app v-bind="$attrs" /></div>',
},
{
+ attachToDocument: true,
propsData,
store,
localVue,
@@ -44,24 +74,14 @@ describe('note_app', () => {
afterEach(() => {
wrapper.destroy();
+ Vue.http.interceptors = [...originalInterceptors];
});
describe('set data', () => {
- const responseInterceptor = (request, next) => {
- next(
- request.respondWith(JSON.stringify([]), {
- status: 200,
- }),
- );
- };
-
beforeEach(() => {
- Vue.http.interceptors.push(responseInterceptor);
+ Vue.http.interceptors.push(emptyResponseInterceptor);
wrapper = mountComponent();
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, responseInterceptor);
+ return waitForDiscussionsRequest();
});
it('should set notes data', () => {
@@ -87,29 +107,23 @@ describe('note_app', () => {
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
wrapper = mountComponent();
+ return waitForDiscussionsRequest();
});
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor);
- });
-
- it('should render list of notes', done => {
+ it('should render list of notes', () => {
const note =
mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[
'/gitlab-org/gitlab-ce/issues/26/discussions.json'
][0].notes[0];
- setTimeout(() => {
- expect(
- wrapper
- .find('.main-notes-list .note-header-author-name')
- .text()
- .trim(),
- ).toEqual(note.author.name);
+ expect(
+ wrapper
+ .find('.main-notes-list .note-header-author-name')
+ .text()
+ .trim(),
+ ).toEqual(note.author.name);
- expect(wrapper.find('.main-notes-list .note-text').html()).toContain(note.note_html);
- done();
- }, 0);
+ expect(wrapper.find('.main-notes-list .note-text').html()).toContain(note.note_html);
});
it('should render form', () => {
@@ -120,37 +134,42 @@ describe('note_app', () => {
});
it('should not render form when commenting is disabled', () => {
+ wrapper.destroy();
+
store.state.commentsDisabled = true;
wrapper = mountComponent();
-
- expect(wrapper.find('.js-main-target-form').exists()).toBe(false);
+ return waitForDiscussionsRequest().then(() => {
+ expect(wrapper.find('.js-main-target-form').exists()).toBe(false);
+ });
});
it('should render discussion filter note `commentsDisabled` is true', () => {
+ wrapper.destroy();
+
store.state.commentsDisabled = true;
wrapper = mountComponent();
-
- expect(wrapper.find('.js-discussion-filter-note').exists()).toBe(true);
+ return waitForDiscussionsRequest().then(() => {
+ expect(wrapper.find('.js-discussion-filter-note').exists()).toBe(true);
+ });
});
it('should render form comment button as disabled', () => {
expect(wrapper.find('.js-note-new-discussion').attributes('disabled')).toEqual('disabled');
});
- it('updates discussions badge', done => {
- setTimeout(() => {
- expect(document.querySelector('.js-discussions-count').textContent).toEqual('2');
-
- done();
- });
+ it('updates discussions badge', () => {
+ expect(document.querySelector('.js-discussions-count').textContent).toEqual('2');
});
});
describe('while fetching data', () => {
beforeEach(() => {
+ Vue.http.interceptors.push(emptyResponseInterceptor);
wrapper = mountComponent();
});
+ afterEach(() => waitForDiscussionsRequest());
+
it('renders skeleton notes', () => {
expect(wrapper.find('.animation-container').exists()).toBe(true);
});
@@ -165,78 +184,55 @@ describe('note_app', () => {
describe('update note', () => {
describe('individual note', () => {
- beforeEach(done => {
+ beforeEach(() => {
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
- spyOn(service, 'updateNote').and.callThrough();
+ jest.spyOn(service, 'updateNote');
wrapper = mountComponent();
- setTimeout(() => {
+ return waitForDiscussionsRequest().then(() => {
wrapper.find('.js-note-edit').trigger('click');
- Vue.nextTick(done);
- }, 0);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors,
- mockData.individualNoteInterceptor,
- );
+ });
});
it('renders edit form', () => {
expect(wrapper.find('.js-vue-issue-note-form').exists()).toBe(true);
});
- it('calls the service to update the note', done => {
+ it('calls the service to update the note', () => {
wrapper.find('.js-vue-issue-note-form').value = 'this is a note';
wrapper.find('.js-vue-issue-save').trigger('click');
expect(service.updateNote).toHaveBeenCalled();
- // Wait for the requests to finish before destroying
- setTimeout(() => {
- done();
- });
});
});
describe('discussion note', () => {
- beforeEach(done => {
+ beforeEach(() => {
Vue.http.interceptors.push(mockData.discussionNoteInterceptor);
- spyOn(service, 'updateNote').and.callThrough();
+ jest.spyOn(service, 'updateNote');
wrapper = mountComponent();
-
- setTimeout(() => {
+ return waitForDiscussionsRequest().then(() => {
wrapper.find('.js-note-edit').trigger('click');
- Vue.nextTick(done);
- }, 0);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors,
- mockData.discussionNoteInterceptor,
- );
+ });
});
it('renders edit form', () => {
expect(wrapper.find('.js-vue-issue-note-form').exists()).toBe(true);
});
- it('updates the note and resets the edit form', done => {
+ it('updates the note and resets the edit form', () => {
wrapper.find('.js-vue-issue-note-form').value = 'this is a note';
wrapper.find('.js-vue-issue-save').trigger('click');
expect(service.updateNote).toHaveBeenCalled();
- // Wait for the requests to finish before destroying
- setTimeout(() => {
- done();
- });
});
});
});
describe('new note form', () => {
beforeEach(() => {
+ Vue.http.interceptors.push(mockData.individualNoteInterceptor);
wrapper = mountComponent();
+ return waitForDiscussionsRequest();
});
it('should render markdown docs url', () => {
@@ -266,43 +262,37 @@ describe('note_app', () => {
beforeEach(() => {
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
wrapper = mountComponent();
+ return waitForDiscussionsRequest();
});
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor);
- });
+ it('should render markdown docs url', () => {
+ wrapper.find('.js-note-edit').trigger('click');
+ const { markdownDocsPath } = mockData.notesDataMock;
- it('should render markdown docs url', done => {
- setTimeout(() => {
- wrapper.find('.js-note-edit').trigger('click');
- const { markdownDocsPath } = mockData.notesDataMock;
-
- Vue.nextTick(() => {
- expect(
- wrapper
- .find(`.edit-note a[href="${markdownDocsPath}"]`)
- .text()
- .trim(),
- ).toEqual('Markdown is supported');
- done();
- });
- }, 0);
+ return Vue.nextTick().then(() => {
+ expect(
+ wrapper
+ .find(`.edit-note a[href="${markdownDocsPath}"]`)
+ .text()
+ .trim(),
+ ).toEqual('Markdown is supported');
+ });
});
- it('should not render quick actions docs url', done => {
- setTimeout(() => {
- wrapper.find('.js-note-edit').trigger('click');
- const { quickActionsDocsPath } = mockData.notesDataMock;
-
- Vue.nextTick(() => {
- expect(wrapper.find(`.edit-note a[href="${quickActionsDocsPath}"]`).exists()).toBe(false);
- done();
- });
- }, 0);
+ it('should not render quick actions docs url', () => {
+ wrapper.find('.js-note-edit').trigger('click');
+ const { quickActionsDocsPath } = mockData.notesDataMock;
+ expect(wrapper.find(`.edit-note a[href="${quickActionsDocsPath}"]`).exists()).toBe(false);
});
});
describe('emoji awards', () => {
+ beforeEach(() => {
+ Vue.http.interceptors.push(emptyResponseInterceptor);
+ wrapper = mountComponent();
+ return waitForDiscussionsRequest();
+ });
+
it('dispatches toggleAward after toggleAward event', () => {
const toggleAwardEvent = new CustomEvent('toggleAward', {
detail: {
@@ -310,17 +300,18 @@ describe('note_app', () => {
noteId: 1,
},
});
- const toggleAwardAction = jasmine.createSpy('toggleAward');
+ const toggleAwardAction = jest.fn().mockName('toggleAward');
wrapper.vm.$store.hotUpdate({
actions: {
toggleAward: toggleAwardAction,
+ stopPolling() {},
},
});
wrapper.vm.$parent.$el.dispatchEvent(toggleAwardEvent);
expect(toggleAwardAction).toHaveBeenCalledTimes(1);
- const [, payload] = toggleAwardAction.calls.argsFor(0);
+ const [, payload] = toggleAwardAction.mock.calls[0];
expect(payload).toEqual({
awardName: 'test',
diff --git a/spec/javascripts/notes_spec.js b/spec/frontend/notes/old_notes_spec.js
index 394e3343be6..b57041cf4d1 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/frontend/notes/old_notes_spec.js
@@ -1,84 +1,89 @@
-/* eslint-disable no-unused-expressions, no-var, object-shorthand */
+/* eslint-disable import/no-commonjs, no-new */
+
import $ from 'jquery';
import _ from 'underscore';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import 'autosize';
-import '~/gl_form';
-import '~/lib/utils/text_utility';
+import * as urlUtility from '~/lib/utils/url_utility';
import '~/behaviors/markdown/render_gfm';
-import Notes from '~/notes';
-import timeoutPromise from './helpers/set_timeout_promise_helper';
-
-window.gon || (window.gon = {});
+import { createSpyObj } from 'helpers/jest_helpers';
+import { setTestTimeoutOnce } from 'helpers/timeout';
+import { TEST_HOST } from 'helpers/test_constants';
+
+// These must be imported synchronously because they pull dependencies
+// from the DOM.
+window.jQuery = $;
+require('autosize');
+require('~/commons');
+require('~/notes');
+
+const { Notes } = window;
+const FLASH_TYPE_ALERT = 'alert';
+const NOTES_POST_PATH = /(.*)\/notes\?html=true$/;
+const fixture = 'snippets/show.html';
+let mockAxios;
+
+window.project_uploads_path = `${TEST_HOST}/uploads`;
+window.gon = window.gon || {};
window.gl = window.gl || {};
gl.utils = gl.utils || {};
+gl.utils.disableButtonIfEmptyField = () => {};
-const htmlEscape = comment => {
- const escapedString = comment.replace(/["&'<>]/g, a => {
- const escapedToken = {
- '&': '&amp;',
- '<': '&lt;',
- '>': '&gt;',
- '"': '&quot;',
- "'": '&#x27;',
- '`': '&#x60;',
- }[a];
-
- return escapedToken;
- });
+describe('Old Notes (~/notes.js)', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ loadFixtures(fixture);
+
+ // Re-declare this here so that test_setup.js#beforeEach() doesn't
+ // overwrite it.
+ mockAxios = new MockAdapter(axios);
- return escapedString;
-};
+ $.ajax = () => {
+ throw new Error('$.ajax should not be called through!');
+ };
-describe('Notes', function() {
- const FLASH_TYPE_ALERT = 'alert';
- const NOTES_POST_PATH = /(.*)\/notes\?html=true$/;
- var fixture = 'snippets/show.html';
- preloadFixtures(fixture);
+ // These jQuery+DOM tests are super flaky so increase the timeout to avoid
+ // random failures.
+ // It seems that running tests in parallel increases failure rate.
+ jest.setTimeout(4000);
+ setTestTimeoutOnce(4000);
+ });
- beforeEach(function() {
- loadFixtures(fixture);
- gl.utils.disableButtonIfEmptyField = _.noop;
- window.project_uploads_path = 'http://test.host/uploads';
- $('body').attr('data-page', 'projects:merge_requets:show');
+ afterEach(done => {
+ // The Notes component sets a polling interval. Clear it after every run.
+ // Make sure to use jest.runOnlyPendingTimers() instead of runAllTimers().
+ jest.clearAllTimers();
+
+ setImmediate(() => {
+ // Wait for any requests to resolve, otherwise we get failures about
+ // unmocked requests.
+ mockAxios.restore();
+ done();
+ });
});
- afterEach(() => {
- // Undo what we did to the shared <body>
- $('body').removeAttr('data-page');
+ it('loads the Notes class into the DOM', () => {
+ expect(Notes).toBeDefined();
+ expect(Notes.name).toBe('Notes');
});
describe('addBinding', () => {
it('calls postComment when comment button is clicked', () => {
- spyOn(Notes.prototype, 'postComment');
- this.notes = new Notes('', []);
+ jest.spyOn(Notes.prototype, 'postComment');
+ new window.Notes('', []);
$('.js-comment-button').click();
-
expect(Notes.prototype.postComment).toHaveBeenCalled();
});
});
- describe('task lists', function() {
- let mock;
-
- beforeEach(function() {
- spyOn(axios, 'patch').and.callFake(() => new Promise(() => {}));
- mock = new MockAdapter(axios);
- mock.onAny().reply(200, {});
-
- $('.js-comment-button').on('click', function(e) {
- e.preventDefault();
- });
- this.notes = new Notes('', []);
- });
-
- afterEach(() => {
- mock.restore();
+ describe('task lists', () => {
+ beforeEach(() => {
+ mockAxios.onAny().reply(200, {});
+ new Notes('', []);
});
- it('modifies the Markdown field', function() {
+ it('modifies the Markdown field', () => {
const changeEvent = document.createEvent('HTMLEvents');
changeEvent.initEvent('change', true, true);
$('input[type=checkbox]')
@@ -88,7 +93,9 @@ describe('Notes', function() {
expect($('.js-task-list-field.original-task-list').val()).toBe('- [x] Task List Item');
});
- it('submits an ajax request on tasklist:changed', function(done) {
+ it('submits an ajax request on tasklist:changed', () => {
+ jest.spyOn(axios, 'patch');
+
const lineNumber = 8;
const lineSource = '- [ ] item 8';
const index = 3;
@@ -99,76 +106,74 @@ describe('Notes', function() {
detail: { lineNumber, lineSource, index, checked },
});
- setTimeout(() => {
- expect(axios.patch).toHaveBeenCalledWith(undefined, {
- note: {
- note: '',
- lock_version: undefined,
- update_task: { index, checked, line_number: lineNumber, line_source: lineSource },
- },
- });
-
- done();
+ expect(axios.patch).toHaveBeenCalledWith(undefined, {
+ note: {
+ note: '',
+ lock_version: undefined,
+ update_task: { index, checked, line_number: lineNumber, line_source: lineSource },
+ },
});
});
});
- describe('comments', function() {
- var textarea = '.js-note-text';
-
- beforeEach(function() {
- this.notes = new Notes('', []);
+ describe('comments', () => {
+ let notes;
+ let autosizeSpy;
+ let textarea;
- this.autoSizeSpy = spyOnEvent($(textarea), 'autosize:update');
- spyOn(this.notes, 'renderNote').and.stub();
+ beforeEach(() => {
+ notes = new Notes('', []);
- $(textarea).data('autosave', {
- reset: function() {},
+ textarea = $('.js-note-text');
+ textarea.data('autosave', {
+ reset: () => {},
});
+ autosizeSpy = jest.fn();
+ $(textarea).on('autosize:update', autosizeSpy);
+
+ jest.spyOn(notes, 'renderNote');
$('.js-comment-button').on('click', e => {
const $form = $(this);
e.preventDefault();
- this.notes.addNote($form);
- this.notes.reenableTargetFormSubmitButton(e);
- this.notes.resetMainTargetForm(e);
+ notes.addNote($form, {});
+ notes.reenableTargetFormSubmitButton(e);
+ notes.resetMainTargetForm(e);
});
});
- it('autosizes after comment submission', function() {
- $(textarea).text('This is an example comment note');
-
- expect(this.autoSizeSpy).not.toHaveBeenTriggered();
-
+ it('autosizes after comment submission', () => {
+ textarea.text('This is an example comment note');
+ expect(autosizeSpy).not.toHaveBeenCalled();
$('.js-comment-button').click();
-
- expect(this.autoSizeSpy).toHaveBeenTriggered();
+ expect(autosizeSpy).toHaveBeenCalled();
});
- it('should not place escaped text in the comment box in case of error', function() {
+ it('should not place escaped text in the comment box in case of error', () => {
const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ jest.spyOn($, 'ajax').mockReturnValueOnce(deferred);
$(textarea).text('A comment with `markup`.');
deferred.reject();
$('.js-comment-button').click();
- expect($(textarea).val()).toEqual('A comment with `markup`.');
+ expect($(textarea).val()).toBe('A comment with `markup`.');
+
+ $.ajax.mockRestore();
+ expect($.ajax.mock).toBeUndefined();
});
});
describe('updateNote', () => {
- let sampleComment;
+ let notes;
let noteEntity;
- let $form;
let $notesContainer;
- let mock;
beforeEach(() => {
- this.notes = new Notes('', []);
+ notes = new Notes('', []);
window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator';
- sampleComment = 'foo';
+ const sampleComment = 'foo';
noteEntity = {
id: 1234,
html: `<li class="note note-row-1234 timeline-entry" id="note_1234">
@@ -177,35 +182,27 @@ describe('Notes', function() {
note: sampleComment,
valid: true,
};
- $form = $('form.js-main-target-form');
+
$notesContainer = $('ul.main-notes-list');
+ const $form = $('form.js-main-target-form');
$form.find('textarea.js-note-text').val(sampleComment);
- mock = new MockAdapter(axios);
- mock.onPost(NOTES_POST_PATH).reply(200, noteEntity);
- });
-
- afterEach(() => {
- mock.restore();
+ mockAxios.onPost(NOTES_POST_PATH).reply(200, noteEntity);
});
- it('updates note and resets edit form', done => {
- spyOn(this.notes, 'revertNoteEditForm');
- spyOn(this.notes, 'setupNewNote');
+ it('updates note and resets edit form', () => {
+ jest.spyOn(notes, 'revertNoteEditForm');
+ jest.spyOn(notes, 'setupNewNote');
$('.js-comment-button').click();
- setTimeout(() => {
- const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`);
- const updatedNote = Object.assign({}, noteEntity);
- updatedNote.note = 'bar';
- this.notes.updateNote(updatedNote, $targetNote);
-
- expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote);
- expect(this.notes.setupNewNote).toHaveBeenCalled();
+ const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`);
+ const updatedNote = Object.assign({}, noteEntity);
+ updatedNote.note = 'bar';
+ notes.updateNote(updatedNote, $targetNote);
- done();
- });
+ expect(notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote);
+ expect(notes.setupNewNote).toHaveBeenCalled();
});
});
@@ -215,32 +212,44 @@ describe('Notes', function() {
beforeEach(() => {
$note = $(`<div id="${hash}"></div>`);
- spyOn($note, 'filter').and.callThrough();
- spyOn($note, 'toggleClass').and.callThrough();
+ jest.spyOn($note, 'filter');
+ jest.spyOn($note, 'toggleClass');
+ });
+
+ afterEach(() => {
+ expect(typeof urlUtility.getLocationHash.mock).toBe('object');
+ urlUtility.getLocationHash.mockRestore();
+ expect(urlUtility.getLocationHash.mock).toBeUndefined();
+ expect(urlUtility.getLocationHash()).toBeNull();
});
+ // urlUtility is a dependency of the notes module. Its getLocatinHash() method should be called internally.
+
it('sets target when hash matches', () => {
- spyOnDependency(Notes, 'getLocationHash').and.returnValue(hash);
+ jest.spyOn(urlUtility, 'getLocationHash').mockReturnValueOnce(hash);
Notes.updateNoteTargetSelector($note);
+ expect(urlUtility.getLocationHash).toHaveBeenCalled();
expect($note.filter).toHaveBeenCalledWith(`#${hash}`);
expect($note.toggleClass).toHaveBeenCalledWith('target', true);
});
it('unsets target when hash does not match', () => {
- spyOnDependency(Notes, 'getLocationHash').and.returnValue('note_doesnotexist');
+ jest.spyOn(urlUtility, 'getLocationHash').mockReturnValueOnce('note_doesnotexist');
Notes.updateNoteTargetSelector($note);
+ expect(urlUtility.getLocationHash).toHaveBeenCalled();
expect($note.toggleClass).toHaveBeenCalledWith('target', false);
});
it('unsets target when there is not a hash fragment anymore', () => {
- spyOnDependency(Notes, 'getLocationHash').and.returnValue(null);
+ jest.spyOn(urlUtility, 'getLocationHash').mockReturnValueOnce(null);
Notes.updateNoteTargetSelector($note);
+ expect(urlUtility.getLocationHash).toHaveBeenCalled();
expect($note.toggleClass).toHaveBeenCalledWith('target', false);
});
});
@@ -257,28 +266,28 @@ describe('Notes', function() {
note: 'heya',
html: '<div>heya</div>',
};
- $notesList = jasmine.createSpyObj('$notesList', ['find', 'append']);
+ $notesList = createSpyObj('$notesList', ['find', 'append']);
- notes = jasmine.createSpyObj('notes', [
+ notes = createSpyObj('notes', [
'setupNewNote',
'refresh',
'collapseLongCommitList',
'updateNotesCount',
'putConflictEditWarningInPlace',
]);
- notes.taskList = jasmine.createSpyObj('tasklist', ['init']);
+ notes.taskList = createSpyObj('tasklist', ['init']);
notes.note_ids = [];
notes.updatedNotesTrackingMap = {};
- spyOn(Notes, 'isNewNote').and.callThrough();
- spyOn(Notes, 'isUpdatedNote').and.callThrough();
- spyOn(Notes, 'animateAppendNote').and.callThrough();
- spyOn(Notes, 'animateUpdateNote').and.callThrough();
+ jest.spyOn(Notes, 'isNewNote');
+ jest.spyOn(Notes, 'isUpdatedNote');
+ jest.spyOn(Notes, 'animateAppendNote');
+ jest.spyOn(Notes, 'animateUpdateNote');
});
describe('when adding note', () => {
it('should call .animateAppendNote', () => {
- Notes.isNewNote.and.returnValue(true);
+ Notes.isNewNote.mockReturnValueOnce(true);
Notes.prototype.renderNote.call(notes, note, null, $notesList);
expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList);
@@ -287,12 +296,12 @@ describe('Notes', function() {
describe('when note was edited', () => {
it('should call .animateUpdateNote', () => {
- Notes.isNewNote.and.returnValue(false);
- Notes.isUpdatedNote.and.returnValue(true);
+ Notes.isNewNote.mockReturnValueOnce(false);
+ Notes.isUpdatedNote.mockReturnValueOnce(true);
const $note = $('<div>');
- $notesList.find.and.returnValue($note);
+ $notesList.find.mockReturnValueOnce($note);
const $newNote = $(note.html);
- Notes.animateUpdateNote.and.returnValue($newNote);
+ Notes.animateUpdateNote.mockReturnValueOnce($newNote);
Notes.prototype.renderNote.call(notes, note, null, $notesList);
@@ -302,26 +311,26 @@ describe('Notes', function() {
describe('while editing', () => {
it('should update textarea if nothing has been touched', () => {
- Notes.isNewNote.and.returnValue(false);
- Notes.isUpdatedNote.and.returnValue(true);
+ Notes.isNewNote.mockReturnValueOnce(false);
+ Notes.isUpdatedNote.mockReturnValueOnce(true);
const $note = $(`<div class="is-editing">
<div class="original-note-content">initial</div>
<textarea class="js-note-text">initial</textarea>
</div>`);
- $notesList.find.and.returnValue($note);
+ $notesList.find.mockReturnValueOnce($note);
Notes.prototype.renderNote.call(notes, note, null, $notesList);
expect($note.find('.js-note-text').val()).toEqual(note.note);
});
it('should call .putConflictEditWarningInPlace', () => {
- Notes.isNewNote.and.returnValue(false);
- Notes.isUpdatedNote.and.returnValue(true);
+ Notes.isNewNote.mockReturnValueOnce(false);
+ Notes.isUpdatedNote.mockReturnValueOnce(true);
const $note = $(`<div class="is-editing">
<div class="original-note-content">initial</div>
<textarea class="js-note-text">different</textarea>
</div>`);
- $notesList.find.and.returnValue($note);
+ $notesList.find.mockReturnValueOnce($note);
Notes.prototype.renderNote.call(notes, note, null, $notesList);
expect(notes.putConflictEditWarningInPlace).toHaveBeenCalledWith(note, $note);
@@ -386,32 +395,32 @@ describe('Notes', function() {
discussion_resolvable: false,
diff_discussion_html: false,
};
- $form = jasmine.createSpyObj('$form', ['closest', 'find']);
+ $form = createSpyObj('$form', ['closest', 'find']);
$form.length = 1;
- row = jasmine.createSpyObj('row', ['prevAll', 'first', 'find']);
+ row = createSpyObj('row', ['prevAll', 'first', 'find']);
- notes = jasmine.createSpyObj('notes', ['isParallelView', 'updateNotesCount']);
+ notes = createSpyObj('notes', ['isParallelView', 'updateNotesCount']);
notes.note_ids = [];
- spyOn(Notes, 'isNewNote');
- spyOn(Notes, 'animateAppendNote');
- Notes.isNewNote.and.returnValue(true);
- notes.isParallelView.and.returnValue(false);
- row.prevAll.and.returnValue(row);
- row.first.and.returnValue(row);
- row.find.and.returnValue(row);
+ jest.spyOn(Notes, 'isNewNote');
+ jest.spyOn(Notes, 'animateAppendNote').mockImplementation();
+ Notes.isNewNote.mockReturnValue(true);
+ notes.isParallelView.mockReturnValue(false);
+ row.prevAll.mockReturnValue(row);
+ row.first.mockReturnValue(row);
+ row.find.mockReturnValue(row);
});
describe('Discussion root note', () => {
let body;
beforeEach(() => {
- body = jasmine.createSpyObj('body', ['attr']);
+ body = createSpyObj('body', ['attr']);
discussionContainer = { length: 0 };
- $form.closest.and.returnValues(row, $form);
- $form.find.and.returnValues(discussionContainer);
- body.attr.and.returnValue('');
+ $form.closest.mockReturnValueOnce(row).mockReturnValue($form);
+ $form.find.mockReturnValue(discussionContainer);
+ body.attr.mockReturnValue('');
});
it('should call Notes.animateAppendNote', () => {
@@ -432,7 +441,9 @@ describe('Notes', function() {
line.id = note.discussion_line_code;
document.body.appendChild(line);
- $form.closest.and.returnValues($form);
+ // Override mocks for this single test
+ $form.closest.mockReset();
+ $form.closest.mockReturnValue($form);
Notes.prototype.renderDiscussionNote.call(notes, note, $form);
@@ -444,8 +455,8 @@ describe('Notes', function() {
beforeEach(() => {
discussionContainer = { length: 1 };
- $form.closest.and.returnValues(row, $form);
- $form.find.and.returnValues(discussionContainer);
+ $form.closest.mockReturnValueOnce(row).mockReturnValueOnce($form);
+ $form.find.mockReturnValue(discussionContainer);
Notes.prototype.renderDiscussionNote.call(notes, note, $form);
});
@@ -463,7 +474,7 @@ describe('Notes', function() {
beforeEach(() => {
noteHTML = '<div></div>';
- $notesList = jasmine.createSpyObj('$notesList', ['append']);
+ $notesList = createSpyObj('$notesList', ['append']);
$resultantNote = Notes.animateAppendNote(noteHTML, $notesList);
});
@@ -484,7 +495,7 @@ describe('Notes', function() {
beforeEach(() => {
noteHTML = '<div></div>';
- $note = jasmine.createSpyObj('$note', ['replaceWith']);
+ $note = createSpyObj('$note', ['replaceWith']);
$updatedNote = Notes.animateUpdateNote(noteHTML, $note);
});
@@ -515,7 +526,6 @@ describe('Notes', function() {
describe('postComment & updateComment', () => {
const sampleComment = 'foo';
- const updatedComment = 'bar';
const note = {
id: 1234,
html: `<li class="note note-row-1234 timeline-entry" id="note_1234">
@@ -524,22 +534,20 @@ describe('Notes', function() {
note: sampleComment,
valid: true,
};
+ let notes;
let $form;
let $notesContainer;
- let mock;
function mockNotesPost() {
- mock.onPost(NOTES_POST_PATH).reply(200, note);
+ mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
}
function mockNotesPostError() {
- mock.onPost(NOTES_POST_PATH).networkError();
+ mockAxios.onPost(NOTES_POST_PATH).networkError();
}
beforeEach(() => {
- mock = new MockAdapter(axios);
-
- this.notes = new Notes('', []);
+ notes = new Notes('', []);
window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator';
$form = $('form.js-main-target-form');
@@ -547,10 +555,6 @@ describe('Notes', function() {
$form.find('textarea.js-note-text').val(sampleComment);
});
- afterEach(() => {
- mock.restore();
- });
-
it('should show placeholder note while new comment is being posted', () => {
mockNotesPost();
@@ -564,9 +568,8 @@ describe('Notes', function() {
$('.js-comment-button').click();
- setTimeout(() => {
+ setImmediate(() => {
expect($notesContainer.find('.note.being-posted').length).toEqual(0);
-
done();
});
});
@@ -580,12 +583,12 @@ describe('Notes', function() {
preventDefault() {},
target: $submitButton,
};
- mock.onPost(NOTES_POST_PATH).replyOnce(() => {
+ mockAxios.onPost(NOTES_POST_PATH).replyOnce(() => {
expect($submitButton).toBeDisabled();
return [200, note];
});
- this.notes
+ notes
.postComment(dummyEvent)
.then(() => {
expect($submitButton).not.toBeDisabled();
@@ -600,9 +603,8 @@ describe('Notes', function() {
$('.js-comment-button').click();
- setTimeout(() => {
+ setImmediate(() => {
expect($notesContainer.find(`#note_${note.id}`).length).toBeGreaterThan(0);
-
done();
});
});
@@ -612,48 +614,49 @@ describe('Notes', function() {
$('.js-comment-button').click();
- setTimeout(() => {
+ setImmediate(() => {
expect($form.find('textarea.js-note-text').val()).toEqual('');
-
done();
});
});
it('should show flash error message when new comment failed to be posted', done => {
mockNotesPostError();
+ jest.spyOn(notes, 'addFlash');
$('.js-comment-button').click();
- setTimeout(() => {
- expect(
- $notesContainer
- .parent()
- .find('.flash-container .flash-text')
- .is(':visible'),
- ).toEqual(true);
-
+ setImmediate(() => {
+ expect(notes.addFlash).toHaveBeenCalled();
+ // JSDom doesn't support the :visible selector yet
+ expect(notes.flashContainer.style.display).not.toBe('none');
done();
});
});
+ // This is a bad test carried over from the Karma -> Jest migration.
+ // The corresponding test in the Karma suite tests for
+ // elements and methods that don't actually exist, and gives a false
+ // positive pass.
+ /*
it('should show flash error message when comment failed to be updated', done => {
mockNotesPost();
+ jest.spyOn(notes, 'addFlash').mockName('addFlash');
$('.js-comment-button').click();
- timeoutPromise()
+ deferredPromise()
.then(() => {
const $noteEl = $notesContainer.find(`#note_${note.id}`);
$noteEl.find('.js-note-edit').click();
$noteEl.find('textarea.js-note-text').val(updatedComment);
- mock.restore();
-
mockNotesPostError();
$noteEl.find('.js-comment-save-button').click();
+ notes.updateComment({preventDefault: () => {}});
})
- .then(timeoutPromise)
+ .then(() => deferredPromise())
.then(() => {
const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`);
@@ -665,12 +668,13 @@ describe('Notes', function() {
.trim(),
).toEqual(sampleComment); // See if comment reverted back to original
- expect($('.flash-container').is(':visible')).toEqual(true); // Flash error message shown
-
+ expect(notes.addFlash).toHaveBeenCalled();
+ expect(notes.flashContainer.style.display).not.toBe('none');
done();
})
.catch(done.fail);
- }, 2000);
+ }, 5000);
+ */
});
describe('postComment with Slash commands', () => {
@@ -687,13 +691,11 @@ describe('Notes', function() {
};
let $form;
let $notesContainer;
- let mock;
beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onPost(NOTES_POST_PATH).reply(200, note);
+ mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
- this.notes = new Notes('', []);
+ new Notes('', []);
window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator';
gl.awardsHandler = {
@@ -710,17 +712,13 @@ describe('Notes', function() {
$form.find('textarea.js-note-text').val(sampleComment);
});
- afterEach(() => {
- mock.restore();
- });
-
it('should remove slash command placeholder when comment with slash commands is done posting', done => {
- spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough();
+ jest.spyOn(gl.awardsHandler, 'addAwardToEmojiBar');
$('.js-comment-button').click();
expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown
- setTimeout(() => {
+ setImmediate(() => {
expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed
done();
});
@@ -740,13 +738,11 @@ describe('Notes', function() {
};
let $form;
let $notesContainer;
- let mock;
beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onPost(NOTES_POST_PATH).reply(200, note);
+ mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
- this.notes = new Notes('', []);
+ new Notes('', []);
window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator';
$form = $('form.js-main-target-form');
@@ -754,14 +750,10 @@ describe('Notes', function() {
$form.find('textarea.js-note-text').html(sampleComment);
});
- afterEach(() => {
- mock.restore();
- });
-
it('should not render a script tag', done => {
$('.js-comment-button').click();
- setTimeout(() => {
+ setImmediate(() => {
const $noteEl = $notesContainer.find(`#note_${note.id}`);
$noteEl.find('.js-note-edit').click();
$noteEl.find('textarea.js-note-text').html(updatedComment);
@@ -786,9 +778,10 @@ describe('Notes', function() {
describe('getFormData', () => {
let $form;
let sampleComment;
+ let notes;
beforeEach(() => {
- this.notes = new Notes('', []);
+ notes = new Notes('', []);
$form = $('form');
sampleComment = 'foobar';
@@ -796,7 +789,7 @@ describe('Notes', function() {
it('should return form metadata object from form reference', () => {
$form.find('textarea.js-note-text').val(sampleComment);
- const { formData, formContent, formAction } = this.notes.getFormData($form);
+ const { formData, formContent, formAction } = notes.getFormData($form);
expect(formData.indexOf(sampleComment)).toBeGreaterThan(-1);
expect(formContent).toEqual(sampleComment);
@@ -804,12 +797,12 @@ describe('Notes', function() {
});
it('should return form metadata with sanitized formContent from form reference', () => {
- spyOn(_, 'escape').and.callFake(htmlEscape);
+ jest.spyOn(_, 'escape');
sampleComment = '<script>alert("Boom!");</script>';
$form.find('textarea.js-note-text').val(sampleComment);
- const { formContent } = this.notes.getFormData($form);
+ const { formContent } = notes.getFormData($form);
expect(_.escape).toHaveBeenCalledWith(sampleComment);
expect(formContent).toEqual('&lt;script&gt;alert(&quot;Boom!&quot;);&lt;/script&gt;');
@@ -817,27 +810,29 @@ describe('Notes', function() {
});
describe('hasQuickActions', () => {
+ let notes;
+
beforeEach(() => {
- this.notes = new Notes('', []);
+ notes = new Notes('', []);
});
it('should return true when comment begins with a quick action', () => {
const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
- const hasQuickActions = this.notes.hasQuickActions(sampleComment);
+ const hasQuickActions = notes.hasQuickActions(sampleComment);
expect(hasQuickActions).toBeTruthy();
});
it('should return false when comment does NOT begin with a quick action', () => {
const sampleComment = 'Hey, /unassign Merging this';
- const hasQuickActions = this.notes.hasQuickActions(sampleComment);
+ const hasQuickActions = notes.hasQuickActions(sampleComment);
expect(hasQuickActions).toBeFalsy();
});
it('should return false when comment does NOT have any quick actions', () => {
const sampleComment = 'Looking good, Awesome!';
- const hasQuickActions = this.notes.hasQuickActions(sampleComment);
+ const hasQuickActions = notes.hasQuickActions(sampleComment);
expect(hasQuickActions).toBeFalsy();
});
@@ -845,25 +840,25 @@ describe('Notes', function() {
describe('stripQuickActions', () => {
it('should strip quick actions from the comment which begins with a quick action', () => {
- this.notes = new Notes();
+ const notes = new Notes();
const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
- const stripedComment = this.notes.stripQuickActions(sampleComment);
+ const stripedComment = notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe('');
});
it('should strip quick actions from the comment but leaves plain comment if it is present', () => {
- this.notes = new Notes();
+ const notes = new Notes();
const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this';
- const stripedComment = this.notes.stripQuickActions(sampleComment);
+ const stripedComment = notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe('Merging this');
});
it('should NOT strip string that has slashes within', () => {
- this.notes = new Notes();
+ const notes = new Notes();
const sampleComment = 'http://127.0.0.1:3000/root/gitlab-shell/issues/1';
- const stripedComment = this.notes.stripQuickActions(sampleComment);
+ const stripedComment = notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe(sampleComment);
});
@@ -875,15 +870,16 @@ describe('Notes', function() {
{ name: 'title', description: 'Change title', params: [{}] },
{ name: 'estimate', description: 'Set time estimate', params: [{}] },
];
+ let notes;
beforeEach(() => {
- this.notes = new Notes();
+ notes = new Notes();
});
it('should return executing quick action description when note has single quick action', () => {
const sampleComment = '/close';
- expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe(
+ expect(notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe(
'Applying command to close this issue',
);
});
@@ -891,7 +887,7 @@ describe('Notes', function() {
it('should return generic multiple quick action description when note has multiple quick actions', () => {
const sampleComment = '/close\n/title [Duplicate] Issue foobar';
- expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe(
+ expect(notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe(
'Applying multiple commands',
);
});
@@ -899,7 +895,7 @@ describe('Notes', function() {
it('should return generic quick action description when available quick actions list is not populated', () => {
const sampleComment = '/close\n/title [Duplicate] Issue foobar';
- expect(this.notes.getQuickActionDescription(sampleComment)).toBe('Applying command');
+ expect(notes.getQuickActionDescription(sampleComment)).toBe('Applying command');
});
});
@@ -909,13 +905,14 @@ describe('Notes', function() {
const currentUsername = 'root';
const currentUserFullname = 'Administrator';
const currentUserAvatar = 'avatar_url';
+ let notes;
beforeEach(() => {
- this.notes = new Notes('', []);
+ notes = new Notes('', []);
});
it('should return constructed placeholder element for regular note based on form contents', () => {
- const $tempNote = this.notes.createPlaceholderNote({
+ const $tempNote = notes.createPlaceholderNote({
formContent: sampleComment,
uniqueId,
isDiscussionNote: false,
@@ -929,8 +926,8 @@ describe('Notes', function() {
expect($tempNote.attr('id')).toEqual(uniqueId);
expect($tempNote.hasClass('being-posted')).toBeTruthy();
expect($tempNote.hasClass('fade-in-half')).toBeTruthy();
- $tempNote.find('.timeline-icon > a, .note-header-info > a').each(function() {
- expect($(this).attr('href')).toEqual(`/${currentUsername}`);
+ $tempNote.find('.timeline-icon > a, .note-header-info > a').each((i, el) => {
+ expect(el.getAttribute('href')).toEqual(`/${currentUsername}`);
});
expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual(currentUserAvatar);
@@ -958,7 +955,7 @@ describe('Notes', function() {
});
it('should return constructed placeholder element for discussion note based on form contents', () => {
- const $tempNote = this.notes.createPlaceholderNote({
+ const $tempNote = notes.createPlaceholderNote({
formContent: sampleComment,
uniqueId,
isDiscussionNote: true,
@@ -972,7 +969,7 @@ describe('Notes', function() {
it('should return a escaped user name', () => {
const currentUserFullnameXSS = 'Foo <script>alert("XSS")</script>';
- const $tempNote = this.notes.createPlaceholderNote({
+ const $tempNote = notes.createPlaceholderNote({
formContent: sampleComment,
uniqueId,
isDiscussionNote: false,
@@ -994,14 +991,15 @@ describe('Notes', function() {
describe('createPlaceholderSystemNote', () => {
const sampleCommandDescription = 'Applying command to close this issue';
const uniqueId = 'b1234-a4567';
+ let notes;
beforeEach(() => {
- this.notes = new Notes('', []);
- spyOn(_, 'escape').and.callFake(htmlEscape);
+ notes = new Notes('', []);
+ jest.spyOn(_, 'escape');
});
it('should return constructed placeholder element for system note based on form contents', () => {
- const $tempNote = this.notes.createPlaceholderSystemNote({
+ const $tempNote = notes.createPlaceholderSystemNote({
formContent: sampleCommandDescription,
uniqueId,
});
@@ -1020,29 +1018,28 @@ describe('Notes', function() {
});
describe('appendFlash', () => {
- beforeEach(() => {
- this.notes = new Notes();
- });
-
it('shows a flash message', () => {
- this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0));
+ const notes = new Notes('', []);
+ notes.addFlash('Error message', FLASH_TYPE_ALERT, notes.parentTimeline.get(0));
- expect($('.flash-alert').is(':visible')).toBeTruthy();
+ const flash = $('.flash-alert')[0];
+ expect(document.contains(flash)).toBe(true);
+ expect(flash.parentNode.style.display).toBe('block');
});
});
describe('clearFlash', () => {
beforeEach(() => {
$(document).off('ajax:success');
- this.notes = new Notes();
});
it('hides visible flash message', () => {
- this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0));
-
- this.notes.clearFlash();
-
- expect($('.flash-alert').is(':visible')).toBeFalsy();
+ const notes = new Notes('', []);
+ notes.addFlash('Error message 1', FLASH_TYPE_ALERT, notes.parentTimeline.get(0));
+ const flash = $('.flash-alert')[0];
+ notes.clearFlash();
+ expect(flash.parentNode.style.display).toBe('none');
+ expect(notes.flashContainer).toBeNull();
});
});
});
diff --git a/spec/frontend/operation_settings/components/external_dashboard_spec.js b/spec/frontend/operation_settings/components/external_dashboard_spec.js
index de1dd219fe0..a881de8fbfe 100644
--- a/spec/frontend/operation_settings/components/external_dashboard_spec.js
+++ b/spec/frontend/operation_settings/components/external_dashboard_spec.js
@@ -1,30 +1,64 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import { GlButton, GlLink, GlFormGroup, GlFormInput } from '@gitlab/ui';
import ExternalDashboard from '~/operation_settings/components/external_dashboard.vue';
+import store from '~/operation_settings/store';
+import axios from '~/lib/utils/axios_utils';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import createFlash from '~/flash';
import { TEST_HOST } from 'helpers/test_constants';
+jest.mock('~/lib/utils/axios_utils');
+jest.mock('~/lib/utils/url_utility');
+jest.mock('~/flash');
+
describe('operation settings external dashboard component', () => {
let wrapper;
- const externalDashboardPath = `http://mock-external-domain.com/external/dashboard/path`;
+ const operationsSettingsEndpoint = `${TEST_HOST}/mock/ops/settings/endpoint`;
+ const externalDashboardUrl = `http://mock-external-domain.com/external/dashboard/url`;
const externalDashboardHelpPagePath = `${TEST_HOST}/help/page/path`;
-
- beforeEach(() => {
- wrapper = shallowMount(ExternalDashboard, {
- propsData: {
- externalDashboardPath,
- externalDashboardHelpPagePath,
+ const localVue = createLocalVue();
+ const mountComponent = (shallow = true) => {
+ const config = [
+ ExternalDashboard,
+ {
+ localVue,
+ store: store({
+ operationsSettingsEndpoint,
+ externalDashboardUrl,
+ externalDashboardHelpPagePath,
+ }),
},
- });
+ ];
+ wrapper = shallow ? shallowMount(...config) : mount(...config);
+ };
+
+ afterEach(() => {
+ if (wrapper.destroy) {
+ wrapper.destroy();
+ }
+ axios.patch.mockReset();
+ refreshCurrentPage.mockReset();
+ createFlash.mockReset();
});
it('renders header text', () => {
+ mountComponent();
expect(wrapper.find('.js-section-header').text()).toBe('External Dashboard');
});
+ describe('expand/collapse button', () => {
+ it('renders as an expand button by default', () => {
+ const button = wrapper.find(GlButton);
+
+ expect(button.text()).toBe('Expand');
+ });
+ });
+
describe('sub-header', () => {
let subHeader;
beforeEach(() => {
+ mountComponent();
subHeader = wrapper.find('.js-section-sub-header');
});
@@ -43,57 +77,89 @@ describe('operation settings external dashboard component', () => {
});
describe('form', () => {
- let form;
+ describe('input label', () => {
+ let formGroup;
- beforeEach(() => {
- form = wrapper.find('form');
- });
+ beforeEach(() => {
+ mountComponent();
+ formGroup = wrapper.find(GlFormGroup);
+ });
- describe('external dashboard url', () => {
- describe('input label', () => {
- let formGroup;
+ it('uses label text', () => {
+ expect(formGroup.attributes().label).toBe('Full dashboard URL');
+ });
- beforeEach(() => {
- formGroup = form.find(GlFormGroup);
- });
+ it('uses description text', () => {
+ expect(formGroup.attributes().description).toBe(
+ 'Enter the URL of the dashboard you want to link to',
+ );
+ });
+ });
- it('uses label text', () => {
- expect(formGroup.attributes().label).toBe('Full dashboard URL');
- });
+ describe('input field', () => {
+ let input;
- it('uses description text', () => {
- expect(formGroup.attributes().description).toBe(
- 'Enter the URL of the dashboard you want to link to',
- );
- });
+ beforeEach(() => {
+ mountComponent();
+ input = wrapper.find(GlFormInput);
});
- describe('input field', () => {
- let input;
-
- beforeEach(() => {
- input = form.find(GlFormInput);
- });
+ it('defaults to externalDashboardUrl', () => {
+ expect(input.attributes().value).toBe(externalDashboardUrl);
+ });
- it('defaults to externalDashboardPath prop', () => {
- expect(input.attributes().value).toBe(externalDashboardPath);
- });
+ it('uses a placeholder', () => {
+ expect(input.attributes().placeholder).toBe('https://my-org.gitlab.io/my-dashboards');
+ });
+ });
- it('uses a placeholder', () => {
- expect(input.attributes().placeholder).toBe('https://my-org.gitlab.io/my-dashboards');
- });
+ describe('submit button', () => {
+ const findSubmitButton = () => wrapper.find('.settings-content form').find(GlButton);
+
+ const endpointRequest = [
+ operationsSettingsEndpoint,
+ {
+ project: {
+ metrics_setting_attributes: {
+ external_dashboard_url: externalDashboardUrl,
+ },
+ },
+ },
+ ];
+
+ it('renders button label', () => {
+ mountComponent();
+ const submit = findSubmitButton();
+ expect(submit.text()).toBe('Save Changes');
});
- describe('submit button', () => {
- let submit;
+ it('submits form on click', () => {
+ mountComponent(false);
+ axios.patch.mockResolvedValue();
+ findSubmitButton().trigger('click');
- beforeEach(() => {
- submit = form.find(GlButton);
- });
+ expect(axios.patch).toHaveBeenCalledWith(...endpointRequest);
- it('renders button label', () => {
- expect(submit.text()).toBe('Save Changes');
- });
+ return wrapper.vm.$nextTick().then(() => expect(refreshCurrentPage).toHaveBeenCalled());
+ });
+
+ it('creates flash banner on error', () => {
+ mountComponent(false);
+ const message = 'mockErrorMessage';
+ axios.patch.mockRejectedValue({ response: { data: { message } } });
+ findSubmitButton().trigger('click');
+
+ expect(axios.patch).toHaveBeenCalledWith(...endpointRequest);
+
+ return wrapper.vm
+ .$nextTick()
+ .then(jest.runAllTicks)
+ .then(() =>
+ expect(createFlash).toHaveBeenCalledWith(
+ `There was an error saving your changes. ${message}`,
+ 'alert',
+ ),
+ );
});
});
});
diff --git a/spec/frontend/operation_settings/store/mutations_spec.js b/spec/frontend/operation_settings/store/mutations_spec.js
new file mode 100644
index 00000000000..1854142c89a
--- /dev/null
+++ b/spec/frontend/operation_settings/store/mutations_spec.js
@@ -0,0 +1,19 @@
+import mutations from '~/operation_settings/store/mutations';
+import createState from '~/operation_settings/store/state';
+
+describe('operation settings mutations', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = createState();
+ });
+
+ describe('SET_EXTERNAL_DASHBOARD_URL', () => {
+ it('sets externalDashboardUrl', () => {
+ const mockUrl = 'mockUrl';
+ mutations.SET_EXTERNAL_DASHBOARD_URL(localState, mockUrl);
+
+ expect(localState.externalDashboardUrl).toBe(mockUrl);
+ });
+ });
+});
diff --git a/spec/frontend/reports/components/report_section_spec.js b/spec/frontend/reports/components/report_section_spec.js
index 3b609484b9e..d4a3073374a 100644
--- a/spec/frontend/reports/components/report_section_spec.js
+++ b/spec/frontend/reports/components/report_section_spec.js
@@ -197,4 +197,44 @@ describe('Report section', () => {
expect(vm.$el.querySelector('.js-collapse-btn').textContent.trim()).toEqual('Expand');
});
});
+
+ describe('Success and Error slots', () => {
+ const createComponent = status => {
+ vm = mountComponentWithSlots(ReportSection, {
+ props: {
+ status,
+ hasIssues: true,
+ },
+ slots: {
+ success: ['This is a success'],
+ loading: ['This is loading'],
+ error: ['This is an error'],
+ },
+ });
+ };
+
+ it('only renders success slot when status is "SUCCESS"', () => {
+ createComponent('SUCCESS');
+
+ expect(vm.$el.textContent.trim()).toContain('This is a success');
+ expect(vm.$el.textContent.trim()).not.toContain('This is an error');
+ expect(vm.$el.textContent.trim()).not.toContain('This is loading');
+ });
+
+ it('only renders error slot when status is "ERROR"', () => {
+ createComponent('ERROR');
+
+ expect(vm.$el.textContent.trim()).toContain('This is an error');
+ expect(vm.$el.textContent.trim()).not.toContain('This is a success');
+ expect(vm.$el.textContent.trim()).not.toContain('This is loading');
+ });
+
+ it('only renders loading slot when status is "LOADING"', () => {
+ createComponent('LOADING');
+
+ expect(vm.$el.textContent.trim()).toContain('This is loading');
+ expect(vm.$el.textContent.trim()).not.toContain('This is an error');
+ expect(vm.$el.textContent.trim()).not.toContain('This is a success');
+ });
+ });
});
diff --git a/spec/frontend/repository/components/table/parent_row_spec.js b/spec/frontend/repository/components/table/parent_row_spec.js
new file mode 100644
index 00000000000..7020055271f
--- /dev/null
+++ b/spec/frontend/repository/components/table/parent_row_spec.js
@@ -0,0 +1,64 @@
+import { shallowMount, RouterLinkStub } from '@vue/test-utils';
+import ParentRow from '~/repository/components/table/parent_row.vue';
+
+let vm;
+let $router;
+
+function factory(path) {
+ $router = {
+ push: jest.fn(),
+ };
+
+ vm = shallowMount(ParentRow, {
+ propsData: {
+ commitRef: 'master',
+ path,
+ },
+ stubs: {
+ RouterLink: RouterLinkStub,
+ },
+ mocks: {
+ $router,
+ },
+ });
+}
+
+describe('Repository parent row component', () => {
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ it.each`
+ path | to
+ ${'app'} | ${'/tree/master/'}
+ ${'app/assets'} | ${'/tree/master/app'}
+ `('renders link in $path to $to', ({ path, to }) => {
+ factory(path);
+
+ expect(vm.find(RouterLinkStub).props().to).toEqual({
+ path: to,
+ });
+ });
+
+ it('pushes new router when clicking row', () => {
+ factory('app/assets');
+
+ vm.find('td').trigger('click');
+
+ expect($router.push).toHaveBeenCalledWith({
+ path: '/tree/master/app',
+ });
+ });
+
+ // We test that it does not get called when clicking any internal
+ // links as this was causing multipe routes to get pushed
+ it('does not trigger router.push when clicking link', () => {
+ factory('app/assets');
+
+ vm.find('a').trigger('click');
+
+ expect($router.push).not.toHaveBeenCalledWith({
+ path: '/tree/master/app',
+ });
+ });
+});
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index 6b4508c418e..a70dc7bb866 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -86,4 +86,16 @@ describe('Repository table row component', () => {
expect(vm.find('.commit-sha').text()).toContain('1');
});
+
+ it('renders link with href', () => {
+ factory({
+ id: '1',
+ path: 'test',
+ type: 'blob',
+ url: 'https://test.com',
+ currentPath: '/',
+ });
+
+ expect(vm.find('a').attributes('href')).toEqual('https://test.com');
+ });
});
diff --git a/spec/frontend/serverless/components/environment_row_spec.js b/spec/frontend/serverless/components/environment_row_spec.js
index 161a637dd75..0ad85e218dc 100644
--- a/spec/frontend/serverless/components/environment_row_spec.js
+++ b/spec/frontend/serverless/components/environment_row_spec.js
@@ -14,7 +14,7 @@ describe('environment row component', () => {
beforeEach(() => {
localVue = createLocalVue();
- vm = createComponent(localVue, translate(mockServerlessFunctions)['*'], '*');
+ vm = createComponent(localVue, translate(mockServerlessFunctions.functions)['*'], '*');
});
afterEach(() => vm.$destroy());
@@ -48,7 +48,11 @@ describe('environment row component', () => {
beforeEach(() => {
localVue = createLocalVue();
- vm = createComponent(localVue, translate(mockServerlessFunctionsDiffEnv).test, 'test');
+ vm = createComponent(
+ localVue,
+ translate(mockServerlessFunctionsDiffEnv.functions).test,
+ 'test',
+ );
});
afterEach(() => vm.$destroy());
diff --git a/spec/frontend/serverless/components/functions_spec.js b/spec/frontend/serverless/components/functions_spec.js
index 6924fb9e91f..d8a80f8031e 100644
--- a/spec/frontend/serverless/components/functions_spec.js
+++ b/spec/frontend/serverless/components/functions_spec.js
@@ -34,11 +34,11 @@ describe('functionsComponent', () => {
});
it('should render empty state when Knative is not installed', () => {
+ store.dispatch('receiveFunctionsSuccess', { knative_installed: false });
component = shallowMount(functionsComponent, {
localVue,
store,
propsData: {
- installed: false,
clustersPath: '',
helpPath: '',
statusPath: '',
@@ -55,7 +55,6 @@ describe('functionsComponent', () => {
localVue,
store,
propsData: {
- installed: true,
clustersPath: '',
helpPath: '',
statusPath: '',
@@ -67,12 +66,11 @@ describe('functionsComponent', () => {
});
it('should render empty state when there is no function data', () => {
- store.dispatch('receiveFunctionsNoDataSuccess');
+ store.dispatch('receiveFunctionsNoDataSuccess', { knative_installed: true });
component = shallowMount(functionsComponent, {
localVue,
store,
propsData: {
- installed: true,
clustersPath: '',
helpPath: '',
statusPath: '',
@@ -91,12 +89,31 @@ describe('functionsComponent', () => {
);
});
+ it('should render functions and a loader when functions are partially fetched', () => {
+ store.dispatch('receiveFunctionsPartial', {
+ ...mockServerlessFunctions,
+ knative_installed: 'checking',
+ });
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ clustersPath: '',
+ helpPath: '',
+ statusPath: '',
+ },
+ sync: false,
+ });
+
+ expect(component.find('.js-functions-wrapper').exists()).toBe(true);
+ expect(component.find('.js-functions-loader').exists()).toBe(true);
+ });
+
it('should render the functions list', () => {
component = shallowMount(functionsComponent, {
localVue,
store,
propsData: {
- installed: true,
clustersPath: 'clustersPath',
helpPath: 'helpPath',
statusPath,
diff --git a/spec/frontend/serverless/mock_data.js b/spec/frontend/serverless/mock_data.js
index a2c18616324..ef616ceb37f 100644
--- a/spec/frontend/serverless/mock_data.js
+++ b/spec/frontend/serverless/mock_data.js
@@ -1,56 +1,62 @@
-export const mockServerlessFunctions = [
- {
- name: 'testfunc1',
- namespace: 'tm-example',
- environment_scope: '*',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
- podcount: null,
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc1.tm-example.apps.example.com',
- description: 'A test service',
- image: 'knative-test-container-buildtemplate',
- },
- {
- name: 'testfunc2',
- namespace: 'tm-example',
- environment_scope: '*',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
- podcount: null,
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc2.tm-example.apps.example.com',
- description: 'A second test service\nThis one with additional descriptions',
- image: 'knative-test-echo-buildtemplate',
- },
-];
+export const mockServerlessFunctions = {
+ knative_installed: true,
+ functions: [
+ {
+ name: 'testfunc1',
+ namespace: 'tm-example',
+ environment_scope: '*',
+ cluster_id: 46,
+ detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
+ podcount: null,
+ created_at: '2019-02-05T01:01:23Z',
+ url: 'http://testfunc1.tm-example.apps.example.com',
+ description: 'A test service',
+ image: 'knative-test-container-buildtemplate',
+ },
+ {
+ name: 'testfunc2',
+ namespace: 'tm-example',
+ environment_scope: '*',
+ cluster_id: 46,
+ detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
+ podcount: null,
+ created_at: '2019-02-05T01:01:23Z',
+ url: 'http://testfunc2.tm-example.apps.example.com',
+ description: 'A second test service\nThis one with additional descriptions',
+ image: 'knative-test-echo-buildtemplate',
+ },
+ ],
+};
-export const mockServerlessFunctionsDiffEnv = [
- {
- name: 'testfunc1',
- namespace: 'tm-example',
- environment_scope: '*',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
- podcount: null,
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc1.tm-example.apps.example.com',
- description: 'A test service',
- image: 'knative-test-container-buildtemplate',
- },
- {
- name: 'testfunc2',
- namespace: 'tm-example',
- environment_scope: 'test',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
- podcount: null,
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc2.tm-example.apps.example.com',
- description: 'A second test service\nThis one with additional descriptions',
- image: 'knative-test-echo-buildtemplate',
- },
-];
+export const mockServerlessFunctionsDiffEnv = {
+ knative_installed: true,
+ functions: [
+ {
+ name: 'testfunc1',
+ namespace: 'tm-example',
+ environment_scope: '*',
+ cluster_id: 46,
+ detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
+ podcount: null,
+ created_at: '2019-02-05T01:01:23Z',
+ url: 'http://testfunc1.tm-example.apps.example.com',
+ description: 'A test service',
+ image: 'knative-test-container-buildtemplate',
+ },
+ {
+ name: 'testfunc2',
+ namespace: 'tm-example',
+ environment_scope: 'test',
+ cluster_id: 46,
+ detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
+ podcount: null,
+ created_at: '2019-02-05T01:01:23Z',
+ url: 'http://testfunc2.tm-example.apps.example.com',
+ description: 'A second test service\nThis one with additional descriptions',
+ image: 'knative-test-echo-buildtemplate',
+ },
+ ],
+};
export const mockServerlessFunction = {
name: 'testfunc1',
diff --git a/spec/frontend/serverless/store/getters_spec.js b/spec/frontend/serverless/store/getters_spec.js
index fb549c8f153..92853fda37c 100644
--- a/spec/frontend/serverless/store/getters_spec.js
+++ b/spec/frontend/serverless/store/getters_spec.js
@@ -32,7 +32,7 @@ describe('Serverless Store Getters', () => {
describe('getFunctions', () => {
it('should translate the raw function array to group the functions per environment scope', () => {
- state.functions = mockServerlessFunctions;
+ state.functions = mockServerlessFunctions.functions;
const funcs = getters.getFunctions(state);
diff --git a/spec/frontend/serverless/store/mutations_spec.js b/spec/frontend/serverless/store/mutations_spec.js
index ca3053e5c38..e2771c7e5fd 100644
--- a/spec/frontend/serverless/store/mutations_spec.js
+++ b/spec/frontend/serverless/store/mutations_spec.js
@@ -19,13 +19,13 @@ describe('ServerlessMutations', () => {
expect(state.isLoading).toEqual(false);
expect(state.hasFunctionData).toEqual(true);
- expect(state.functions).toEqual(mockServerlessFunctions);
+ expect(state.functions).toEqual(mockServerlessFunctions.functions);
});
it('should ensure loading has stopped and hasFunctionData is false when there are no functions available', () => {
const state = {};
- mutations[types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state);
+ mutations[types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state, { knative_installed: true });
expect(state.isLoading).toEqual(false);
expect(state.hasFunctionData).toEqual(false);
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index c24f0bc4776..7e7cc1488b8 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -15,7 +15,7 @@ afterEach(() =>
}),
);
-initializeTestTimeout(500);
+initializeTestTimeout(process.env.CI ? 5000 : 500);
// fail tests for unmocked requests
beforeEach(done => {
diff --git a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
index b356ea85cad..0f5d47b3bfe 100644
--- a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
+++ b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
@@ -4,7 +4,7 @@ describe('getStateKey', () => {
it('should return proper state name', () => {
const context = {
mergeStatus: 'checked',
- mergeWhenPipelineSucceeds: false,
+ autoMergeEnabled: false,
canMerge: true,
onlyAllowMergeIfPipelineSucceeds: false,
isPipelineFailed: false,
@@ -31,9 +31,9 @@ describe('getStateKey', () => {
expect(bound()).toEqual('notAllowedToMerge');
- context.mergeWhenPipelineSucceeds = true;
+ context.autoMergeEnabled = true;
- expect(bound()).toEqual('mergeWhenPipelineSucceeds');
+ expect(bound()).toEqual('autoMergeEnabled');
context.isSHAMismatch = true;
@@ -80,7 +80,7 @@ describe('getStateKey', () => {
it('returns rebased state key', () => {
const context = {
mergeStatus: 'checked',
- mergeWhenPipelineSucceeds: false,
+ autoMergeEnabled: false,
canMerge: true,
onlyAllowMergeIfPipelineSucceeds: true,
isPipelineFailed: true,
diff --git a/spec/frontend/vue_shared/components/issue/issue_warning_spec.js b/spec/frontend/vue_shared/components/issue/issue_warning_spec.js
index 4a8de5fc4f1..63880b85625 100644
--- a/spec/frontend/vue_shared/components/issue/issue_warning_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_warning_spec.js
@@ -15,31 +15,37 @@ function formatWarning(string) {
describe('Issue Warning Component', () => {
describe('isLocked', () => {
it('should render locked issue warning information', () => {
- const vm = mountComponent(IssueWarning, {
+ const props = {
isLocked: true,
- });
+ lockedIssueDocsPath: 'docs/issues/locked',
+ };
+ const vm = mountComponent(IssueWarning, props);
expect(
vm.$el.querySelector('.icon use').getAttributeNS('http://www.w3.org/1999/xlink', 'href'),
).toMatch(/lock$/);
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual(
- 'This issue is locked. Only project members can comment.',
+ 'This issue is locked. Only project members can comment. Learn more',
);
+ expect(vm.$el.querySelector('a').href).toContain(props.lockedIssueDocsPath);
});
});
describe('isConfidential', () => {
it('should render confidential issue warning information', () => {
- const vm = mountComponent(IssueWarning, {
+ const props = {
isConfidential: true,
- });
+ confidentialIssueDocsPath: '/docs/issues/confidential',
+ };
+ const vm = mountComponent(IssueWarning, props);
expect(
vm.$el.querySelector('.icon use').getAttributeNS('http://www.w3.org/1999/xlink', 'href'),
).toMatch(/eye-slash$/);
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual(
- 'This is a confidential issue. Your comment will not be visible to the public.',
+ 'This is a confidential issue. People without permission will never get a notification. Learn more',
);
+ expect(vm.$el.querySelector('a').href).toContain(props.confidentialIssueDocsPath);
});
});
diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
new file mode 100644
index 00000000000..f1943861523
--- /dev/null
+++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import modalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+
+describe('modal copy button', () => {
+ const Component = Vue.extend(modalCopyButton);
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ beforeEach(() => {
+ wrapper = shallowMount(Component, {
+ propsData: {
+ text: 'copy me',
+ title: 'Copy this value into Clipboard!',
+ },
+ });
+ });
+
+ describe('clipboard', () => {
+ it('should fire a `success` event on click', () => {
+ document.execCommand = jest.fn(() => true);
+ window.getSelection = jest.fn(() => ({
+ toString: jest.fn(() => 'test'),
+ removeAllRanges: jest.fn(),
+ }));
+ wrapper.trigger('click');
+ expect(wrapper.emitted().success).not.toBeEmpty();
+ expect(document.execCommand).toHaveBeenCalledWith('copy');
+ });
+ it("should propagate the clipboard error event if execCommand doesn't work", () => {
+ document.execCommand = jest.fn(() => false);
+ wrapper.trigger('click');
+ expect(wrapper.emitted().error).not.toBeEmpty();
+ expect(document.execCommand).toHaveBeenCalledWith('copy');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/droplab_dropdown_button_spec.js b/spec/frontend/vue_shared/droplab_dropdown_button_spec.js
new file mode 100644
index 00000000000..22295721328
--- /dev/null
+++ b/spec/frontend/vue_shared/droplab_dropdown_button_spec.js
@@ -0,0 +1,136 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+
+import DroplabDropdownButton from '~/vue_shared/components/droplab_dropdown_button.vue';
+
+const mockActions = [
+ {
+ title: 'Foo',
+ description: 'Some foo action',
+ },
+ {
+ title: 'Bar',
+ description: 'Some bar action',
+ },
+];
+
+const createComponent = ({
+ size = '',
+ dropdownClass = '',
+ actions = mockActions,
+ defaultAction = 0,
+}) => {
+ const localVue = createLocalVue();
+
+ return mount(DroplabDropdownButton, {
+ localVue,
+ propsData: {
+ size,
+ dropdownClass,
+ actions,
+ defaultAction,
+ },
+ });
+};
+
+describe('DroplabDropdownButton', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent({});
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('data', () => {
+ it('contains `selectedAction` representing value of `defaultAction` prop', () => {
+ expect(wrapper.vm.selectedAction).toBe(0);
+ });
+ });
+
+ describe('computed', () => {
+ describe('selectedActionTitle', () => {
+ it('returns string containing title of selected action', () => {
+ wrapper.setData({ selectedAction: 0 });
+
+ expect(wrapper.vm.selectedActionTitle).toBe(mockActions[0].title);
+
+ wrapper.setData({ selectedAction: 1 });
+
+ expect(wrapper.vm.selectedActionTitle).toBe(mockActions[1].title);
+ });
+ });
+
+ describe('buttonSizeClass', () => {
+ it('returns string containing button sizing class based on `size` prop', done => {
+ const wrapperWithSize = createComponent({
+ size: 'sm',
+ });
+
+ wrapperWithSize.vm.$nextTick(() => {
+ expect(wrapperWithSize.vm.buttonSizeClass).toBe('btn-sm');
+
+ done();
+ wrapperWithSize.destroy();
+ });
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('handlePrimaryActionClick', () => {
+ it('emits `onActionClick` event on component with selectedAction object as param', () => {
+ jest.spyOn(wrapper.vm, '$emit');
+
+ wrapper.setData({ selectedAction: 0 });
+ wrapper.vm.handlePrimaryActionClick();
+
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('onActionClick', mockActions[0]);
+ });
+ });
+
+ describe('handleActionClick', () => {
+ it('emits `onActionSelect` event on component with selectedAction index as param', () => {
+ jest.spyOn(wrapper.vm, '$emit');
+
+ wrapper.vm.handleActionClick(1);
+
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('onActionSelect', 1);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders default action button', () => {
+ const defaultButton = wrapper.findAll('.btn').at(0);
+
+ expect(defaultButton.text()).toBe(mockActions[0].title);
+ });
+
+ it('renders dropdown button', () => {
+ const dropdownButton = wrapper.findAll('.dropdown-toggle').at(0);
+
+ expect(dropdownButton.isVisible()).toBe(true);
+ });
+
+ it('renders dropdown actions', () => {
+ const dropdownActions = wrapper.findAll('.dropdown-menu li button');
+
+ Array(dropdownActions.length)
+ .fill()
+ .forEach((_, index) => {
+ const actionContent = dropdownActions.at(index).find('.description');
+
+ expect(actionContent.find('strong').text()).toBe(mockActions[index].title);
+ expect(actionContent.find('p').text()).toBe(mockActions[index].description);
+ });
+ });
+
+ it('renders divider between dropdown actions', () => {
+ const dropdownDivider = wrapper.find('.dropdown-menu .divider');
+
+ expect(dropdownDivider.isVisible()).toBe(true);
+ });
+ });
+});
diff --git a/spec/graphql/features/authorization_spec.rb b/spec/graphql/features/authorization_spec.rb
index f5eb628a982..c427893f9cc 100644
--- a/spec/graphql/features/authorization_spec.rb
+++ b/spec/graphql/features/authorization_spec.rb
@@ -282,7 +282,7 @@ describe 'Gitlab::Graphql::Authorization' do
issue_ids = issue_edges.map { |issue_edge| issue_edge['node']&.fetch('id') }
expect(issue_edges.size).to eq(visible_issues.size)
- expect(issue_ids).to eq(visible_issues.map { |i| i.id.to_s })
+ expect(issue_ids).to eq(visible_issues.map { |i| i.to_global_id.to_s })
end
it 'does not check access on fields that will not be rendered' do
diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb
index e9149f4250f..4076c1f824b 100644
--- a/spec/graphql/gitlab_schema_spec.rb
+++ b/spec/graphql/gitlab_schema_spec.rb
@@ -107,6 +107,64 @@ describe GitlabSchema do
end
end
+ describe '.id_from_object' do
+ it 'returns a global id' do
+ expect(described_class.id_from_object(build(:project, id: 1))).to be_a(GlobalID)
+ end
+
+ it "raises a meaningful error if a global id couldn't be generated" do
+ expect { described_class.id_from_object(build(:commit)) }
+ .to raise_error(RuntimeError, /include `GlobalID::Identification` into/i)
+ end
+ end
+
+ describe '.object_from_id' do
+ context 'for subclasses of `ApplicationRecord`' do
+ it 'returns the correct record' do
+ user = create(:user)
+
+ result = described_class.object_from_id(user.to_global_id.to_s)
+
+ expect(result.__sync).to eq(user)
+ end
+
+ it 'batchloads the queries' do
+ user1 = create(:user)
+ user2 = create(:user)
+
+ expect do
+ [described_class.object_from_id(user1.to_global_id),
+ described_class.object_from_id(user2.to_global_id)].map(&:__sync)
+ end.not_to exceed_query_limit(1)
+ end
+ end
+
+ context 'for other classes' do
+ # We cannot use an anonymous class here as `GlobalID` expects `.name` not
+ # to return `nil`
+ class TestGlobalId
+ include GlobalID::Identification
+ attr_accessor :id
+
+ def initialize(id)
+ @id = id
+ end
+ end
+
+ it 'falls back to a regular find' do
+ result = TestGlobalId.new(123)
+
+ expect(TestGlobalId).to receive(:find).with("123").and_return(result)
+
+ expect(described_class.object_from_id(result.to_global_id)).to eq(result)
+ end
+ end
+
+ it 'raises the correct error on invalid input' do
+ expect { described_class.object_from_id("bogus id") }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ end
+ end
+
def field_instrumenters
described_class.instrumenters[:field] + described_class.instrumenters[:field_after_built_ins]
end
diff --git a/spec/graphql/resolvers/base_resolver_spec.rb b/spec/graphql/resolvers/base_resolver_spec.rb
index 9982288e206..c162fdbbb47 100644
--- a/spec/graphql/resolvers/base_resolver_spec.rb
+++ b/spec/graphql/resolvers/base_resolver_spec.rb
@@ -29,18 +29,20 @@ describe Resolvers::BaseResolver do
end
end
- it 'increases complexity based on arguments' do
- field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 1)
+ context 'when field is a connection' do
+ it 'increases complexity based on arguments' do
+ field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 1)
- expect(field.to_graphql.complexity.call({}, { sort: 'foo' }, 1)).to eq 3
- expect(field.to_graphql.complexity.call({}, { search: 'foo' }, 1)).to eq 7
- end
+ expect(field.to_graphql.complexity.call({}, { sort: 'foo' }, 1)).to eq 3
+ expect(field.to_graphql.complexity.call({}, { search: 'foo' }, 1)).to eq 7
+ end
- it 'does not increase complexity when filtering by iids' do
- field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 100)
+ it 'does not increase complexity when filtering by iids' do
+ field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 100)
- expect(field.to_graphql.complexity.call({}, { sort: 'foo' }, 1)).to eq 6
- expect(field.to_graphql.complexity.call({}, { sort: 'foo', iid: 1 }, 1)).to eq 3
- expect(field.to_graphql.complexity.call({}, { sort: 'foo', iids: [1, 2, 3] }, 1)).to eq 3
+ expect(field.to_graphql.complexity.call({}, { sort: 'foo' }, 1)).to eq 6
+ expect(field.to_graphql.complexity.call({}, { sort: 'foo', iid: 1 }, 1)).to eq 3
+ expect(field.to_graphql.complexity.call({}, { sort: 'foo', iids: [1, 2, 3] }, 1)).to eq 3
+ end
end
end
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb
index bffcdbfe915..798fe00de97 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -121,7 +121,7 @@ describe Resolvers::IssuesResolver do
end
it 'increases field complexity based on arguments' do
- field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 100)
+ field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 100)
expect(field.to_graphql.complexity.call({}, {}, 1)).to eq 4
expect(field.to_graphql.complexity.call({}, { labelName: 'foo' }, 1)).to eq 8
diff --git a/spec/graphql/resolvers/namespace_projects_resolver_spec.rb b/spec/graphql/resolvers/namespace_projects_resolver_spec.rb
new file mode 100644
index 00000000000..20e197e9f73
--- /dev/null
+++ b/spec/graphql/resolvers/namespace_projects_resolver_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::NamespaceProjectsResolver, :nested_groups do
+ include GraphqlHelpers
+
+ let(:current_user) { create(:user) }
+
+ context "with a group" do
+ let(:group) { create(:group) }
+ let(:namespace) { group }
+ let(:project1) { create(:project, namespace: namespace) }
+ let(:project2) { create(:project, namespace: namespace) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:nested_project) { create(:project, group: nested_group) }
+
+ before do
+ project1.add_developer(current_user)
+ project2.add_developer(current_user)
+ nested_project.add_developer(current_user)
+ end
+
+ describe '#resolve' do
+ it 'finds all projects' do
+ expect(resolve_projects).to contain_exactly(project1, project2)
+ end
+
+ it 'finds all projects including the subgroups' do
+ expect(resolve_projects(include_subgroups: true)).to contain_exactly(project1, project2, nested_project)
+ end
+
+ context 'with an user namespace' do
+ let(:namespace) { current_user.namespace }
+
+ it 'finds all projects' do
+ expect(resolve_projects).to contain_exactly(project1, project2)
+ end
+
+ it 'finds all projects including the subgroups' do
+ expect(resolve_projects(include_subgroups: true)).to contain_exactly(project1, project2)
+ end
+ end
+ end
+ end
+
+ context "when passing a non existent, batch loaded namespace" do
+ let(:namespace) do
+ BatchLoader.for("non-existent-path").batch do |_fake_paths, loader, _|
+ loader.call("non-existent-path", nil)
+ end
+ end
+
+ it "returns nil without breaking" do
+ expect(resolve_projects).to be_empty
+ end
+ end
+
+ it 'has an high complexity regardless of arguments' do
+ field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 100)
+
+ expect(field.to_graphql.complexity.call({}, {}, 1)).to eq 24
+ expect(field.to_graphql.complexity.call({}, { include_subgroups: true }, 1)).to eq 24
+ end
+
+ def resolve_projects(args = { include_subgroups: false }, context = { current_user: current_user })
+ resolve(described_class, obj: namespace, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/types/base_field_spec.rb b/spec/graphql/types/base_field_spec.rb
index 4fe426e2447..0d3c3e37daf 100644
--- a/spec/graphql/types/base_field_spec.rb
+++ b/spec/graphql/types/base_field_spec.rb
@@ -6,7 +6,7 @@ describe Types::BaseField do
context 'when considering complexity' do
let(:resolver) do
Class.new(described_class) do
- def self.resolver_complexity(args)
+ def self.resolver_complexity(args, child_complexity:)
2 if args[:foo]
end
@@ -28,18 +28,29 @@ describe Types::BaseField do
expect(field.to_graphql.complexity).to eq 12
end
- it 'sets complexity depending on arguments for resolvers' do
- field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, max_page_size: 100, null: true)
+ context 'when field has a resolver proc' do
+ context 'and is a connection' do
+ let(:field) { described_class.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: resolver, max_page_size: 100, null: true) }
- expect(field.to_graphql.complexity.call({}, {}, 2)).to eq 4
- expect(field.to_graphql.complexity.call({}, { first: 50 }, 2)).to eq 3
- end
+ it 'sets complexity depending on arguments for resolvers' do
+ expect(field.to_graphql.complexity.call({}, {}, 2)).to eq 4
+ expect(field.to_graphql.complexity.call({}, { first: 50 }, 2)).to eq 3
+ end
- it 'sets complexity depending on number load limits for resolvers' do
- field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, max_page_size: 100, null: true)
+ it 'sets complexity depending on number load limits for resolvers' do
+ expect(field.to_graphql.complexity.call({}, { first: 1 }, 2)).to eq 2
+ expect(field.to_graphql.complexity.call({}, { first: 1, foo: true }, 2)).to eq 4
+ end
+ end
- expect(field.to_graphql.complexity.call({}, { first: 1 }, 2)).to eq 2
- expect(field.to_graphql.complexity.call({}, { first: 1, foo: true }, 2)).to eq 4
+ context 'and is not a connection' do
+ it 'sets complexity as normal' do
+ field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, max_page_size: 100, null: true)
+
+ expect(field.to_graphql.complexity.call({}, {}, 2)).to eq 2
+ expect(field.to_graphql.complexity.call({}, { first: 50 }, 2)).to eq 2
+ end
+ end
end
end
end
diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb
index dc37b15001f..bae560829cc 100644
--- a/spec/graphql/types/issue_type_spec.rb
+++ b/spec/graphql/types/issue_type_spec.rb
@@ -6,4 +6,10 @@ describe GitlabSchema.types['Issue'] do
it { expect(described_class.graphql_name).to eq('Issue') }
it { expect(described_class).to require_graphql_authorizations(:read_issue) }
+
+ it 'has specific fields' do
+ %i[relative_position web_path web_url reference].each do |field_name|
+ expect(described_class).to have_graphql_field(field_name)
+ end
+ end
end
diff --git a/spec/graphql/types/namespace_type.rb b/spec/graphql/types/namespace_type_spec.rb
index 7cd6a79ae5d..b4144cc4121 100644
--- a/spec/graphql/types/namespace_type.rb
+++ b/spec/graphql/types/namespace_type_spec.rb
@@ -4,4 +4,6 @@ require 'spec_helper'
describe GitlabSchema.types['Namespace'] do
it { expect(described_class.graphql_name).to eq('Namespace') }
+
+ it { expect(described_class).to have_graphql_field(:projects) }
end
diff --git a/spec/graphql/types/project_statistics_type_spec.rb b/spec/graphql/types/project_statistics_type_spec.rb
new file mode 100644
index 00000000000..e9feac57a36
--- /dev/null
+++ b/spec/graphql/types/project_statistics_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['ProjectStatistics'] do
+ it "has all the required fields" do
+ is_expected.to have_graphql_fields(:storage_size, :repository_size, :lfs_objects_size,
+ :build_artifacts_size, :packages_size, :commit_count,
+ :wiki_size)
+ end
+end
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 075fa7c7e43..cb5ac2e3cb1 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -19,4 +19,6 @@ describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_field(:pipelines) }
it { is_expected.to have_graphql_field(:repository) }
+
+ it { is_expected.to have_graphql_field(:statistics) }
end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index b4626955816..af1972a2513 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -5,7 +5,17 @@ describe GitlabSchema.types['Query'] do
expect(described_class.graphql_name).to eq('Query')
end
- it { is_expected.to have_graphql_fields(:project, :group, :echo, :metadata) }
+ it { is_expected.to have_graphql_fields(:project, :namespace, :group, :echo, :metadata) }
+
+ describe 'namespace field' do
+ subject { described_class.fields['namespace'] }
+
+ it 'finds namespaces by full path' do
+ is_expected.to have_graphql_arguments(:full_path)
+ is_expected.to have_graphql_type(Types::NamespaceType)
+ is_expected.to have_graphql_resolver(Resolvers::NamespaceResolver)
+ end
+ end
describe 'project field' do
subject { described_class.fields['project'] }
diff --git a/spec/graphql/types/tree/blob_type_spec.rb b/spec/graphql/types/tree/blob_type_spec.rb
index fa29bb5fff7..b12e214ca84 100644
--- a/spec/graphql/types/tree/blob_type_spec.rb
+++ b/spec/graphql/types/tree/blob_type_spec.rb
@@ -5,5 +5,5 @@ require 'spec_helper'
describe Types::Tree::BlobType do
it { expect(described_class.graphql_name).to eq('Blob') }
- it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path) }
+ it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path, :web_url) }
end
diff --git a/spec/graphql/types/tree/tree_entry_type_spec.rb b/spec/graphql/types/tree/tree_entry_type_spec.rb
index 397cabde8e5..ea1b6426034 100644
--- a/spec/graphql/types/tree/tree_entry_type_spec.rb
+++ b/spec/graphql/types/tree/tree_entry_type_spec.rb
@@ -5,5 +5,5 @@ require 'spec_helper'
describe Types::Tree::TreeEntryType do
it { expect(described_class.graphql_name).to eq('TreeEntry') }
- it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path) }
+ it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path, :web_url) }
end
diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb
index 0434af25866..e6aacb5b92b 100644
--- a/spec/helpers/emails_helper_spec.rb
+++ b/spec/helpers/emails_helper_spec.rb
@@ -8,19 +8,19 @@ describe EmailsHelper do
context "and format is text" do
it "returns plain text" do
- expect(closure_reason_text(merge_request, format: :text)).to eq(" via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})")
+ expect(closure_reason_text(merge_request, format: :text)).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})")
end
end
context "and format is HTML" do
it "returns HTML" do
- expect(closure_reason_text(merge_request, format: :html)).to eq(" via merge request #{link_to(merge_request.to_reference, merge_request_presenter.web_url)}")
+ expect(closure_reason_text(merge_request, format: :html)).to eq("via merge request #{link_to(merge_request.to_reference, merge_request_presenter.web_url)}")
end
end
context "and format is unknown" do
it "returns plain text" do
- expect(closure_reason_text(merge_request, format: :text)).to eq(" via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})")
+ expect(closure_reason_text(merge_request, format: :text)).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})")
end
end
end
@@ -29,7 +29,7 @@ describe EmailsHelper do
let(:closed_via) { "5a0eb6fd7e0f133044378c662fcbbc0d0c16dbfa" }
it "returns plain text" do
- expect(closure_reason_text(closed_via)).to eq(" via #{closed_via}")
+ expect(closure_reason_text(closed_via)).to eq("via #{closed_via}")
end
end
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
index 143b28728a3..027480143bd 100644
--- a/spec/helpers/gitlab_routing_helper_spec.rb
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -101,7 +101,7 @@ describe GitlabRoutingHelper do
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")
+ expect(edit_milestone_path(milestone)).to eq("/#{milestone.project.full_path}/-/milestones/#{milestone.iid}/edit")
end
end
end
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index c3956ba08fd..597c8f836a9 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -78,7 +78,8 @@ describe MarkupHelper do
let(:link) { '/commits/0a1b2c3d' }
let(:issues) { create_list(:issue, 2, project: project) }
- it 'handles references nested in links with all the text' do
+ # Clean the cache to make sure the title is re-rendered from the stubbed one
+ it 'handles references nested in links with all the text', :clean_gitlab_redis_cache do
allow(commit).to receive(:title).and_return("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real")
actual = helper.link_to_markdown_field(commit, :title, link)
diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb
index 50c74a7c2f9..62c00964524 100644
--- a/spec/helpers/storage_helper_spec.rb
+++ b/spec/helpers/storage_helper_spec.rb
@@ -26,11 +26,12 @@ describe StorageHelper do
namespace: namespace,
statistics: build(:project_statistics,
repository_size: 10.kilobytes,
+ wiki_size: 10.bytes,
lfs_objects_size: 20.gigabytes,
build_artifacts_size: 30.megabytes))
end
- let(:message) { '10 KB repositories, 30 MB build artifacts, 20 GB LFS' }
+ let(:message) { '10 KB repositories, 10 Bytes wikis, 30 MB build artifacts, 20 GB LFS' }
it 'works on ProjectStatistics' do
expect(helper.storage_counters_details(project.statistics)).to eq(message)
diff --git a/spec/helpers/tracking_helper_spec.rb b/spec/helpers/tracking_helper_spec.rb
new file mode 100644
index 00000000000..71505e8ea69
--- /dev/null
+++ b/spec/helpers/tracking_helper_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe TrackingHelper do
+ describe '#tracking_attrs' do
+ it 'returns an empty hash' do
+ expect(helper.tracking_attrs('a', 'b', 'c')).to eq({})
+ end
+ end
+end
diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb
index e565ac8c530..25a2fcf5a81 100644
--- a/spec/helpers/visibility_level_helper_spec.rb
+++ b/spec/helpers/visibility_level_helper_spec.rb
@@ -162,4 +162,49 @@ describe VisibilityLevelHelper do
end
end
end
+
+ describe "selected_visibility_level" do
+ let(:group) { create(:group, :public) }
+ let!(:project) { create(:project, :internal, group: group) }
+ let!(:forked_project) { fork_project(project) }
+
+ using RSpec::Parameterized::TableSyntax
+
+ PUBLIC = Gitlab::VisibilityLevel::PUBLIC
+ INTERNAL = Gitlab::VisibilityLevel::INTERNAL
+ PRIVATE = Gitlab::VisibilityLevel::PRIVATE
+
+ # This is a subset of all the permutations
+ where(:requested_level, :max_allowed, :global_default_level, :restricted_levels, :expected) do
+ PUBLIC | PUBLIC | PUBLIC | [] | PUBLIC
+ PUBLIC | PUBLIC | PUBLIC | [PUBLIC] | INTERNAL
+ INTERNAL | PUBLIC | PUBLIC | [] | INTERNAL
+ INTERNAL | PRIVATE | PRIVATE | [] | PRIVATE
+ PRIVATE | PUBLIC | PUBLIC | [] | PRIVATE
+ PUBLIC | PRIVATE | INTERNAL | [] | PRIVATE
+ PUBLIC | INTERNAL | PUBLIC | [] | INTERNAL
+ PUBLIC | PRIVATE | PUBLIC | [] | PRIVATE
+ PUBLIC | INTERNAL | INTERNAL | [] | INTERNAL
+ PUBLIC | PUBLIC | INTERNAL | [] | PUBLIC
+ end
+
+ before do
+ stub_application_setting(restricted_visibility_levels: restricted_levels,
+ default_project_visibility: global_default_level)
+ end
+
+ with_them do
+ it "provides correct visibility level for forked project" do
+ project.update(visibility_level: max_allowed)
+
+ expect(selected_visibility_level(forked_project, requested_level)).to eq(expected)
+ end
+
+ it "provides correct visibiility level for project in group" do
+ project.group.update(visibility_level: max_allowed)
+
+ expect(selected_visibility_level(project, requested_level)).to eq(expected)
+ end
+ end
+ end
end
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
index e1017130bed..13b708a03d5 100644
--- a/spec/javascripts/boards/board_card_spec.js
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -7,8 +7,8 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import eventHub from '~/boards/eventhub';
-import '~/vue_shared/models/label';
-import '~/vue_shared/models/assignee';
+import '~/boards/models/label';
+import '~/boards/models/assignee';
import '~/boards/models/list';
import boardsStore from '~/boards/stores/boards_store';
import boardCard from '~/boards/components/board_card.vue';
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index 22f192bc7f3..e81115e10c9 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -6,12 +6,13 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Cookies from 'js-cookie';
-import '~/vue_shared/models/label';
-import '~/vue_shared/models/assignee';
+import '~/boards/models/label';
+import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import '~/boards/services/board_service';
import boardsStore from '~/boards/stores/boards_store';
+import eventHub from '~/boards/eventhub';
import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data';
describe('Store', () => {
@@ -44,6 +45,48 @@ describe('Store', () => {
expect(boardsStore.state.lists.length).toBe(0);
});
+ describe('addList', () => {
+ it('sorts by position', () => {
+ boardsStore.addList({ position: 2 });
+ boardsStore.addList({ position: 1 });
+
+ expect(boardsStore.state.lists[0].position).toBe(1);
+ });
+ });
+
+ describe('toggleFilter', () => {
+ const dummyFilter = 'x=42';
+ let updateTokensSpy;
+
+ beforeEach(() => {
+ updateTokensSpy = jasmine.createSpy('updateTokens');
+ eventHub.$once('updateTokens', updateTokensSpy);
+
+ // prevent using window.history
+ spyOn(boardsStore, 'updateFiltersUrl').and.callFake(() => {});
+ });
+
+ it('adds the filter if it is not present', () => {
+ boardsStore.filter.path = 'something';
+
+ boardsStore.toggleFilter(dummyFilter);
+
+ expect(boardsStore.filter.path).toEqual(`something&${dummyFilter}`);
+ expect(updateTokensSpy).toHaveBeenCalled();
+ expect(boardsStore.updateFiltersUrl).toHaveBeenCalled();
+ });
+
+ it('removes the filter if it is present', () => {
+ boardsStore.filter.path = `something&${dummyFilter}`;
+
+ boardsStore.toggleFilter(dummyFilter);
+
+ expect(boardsStore.filter.path).toEqual('something');
+ expect(updateTokensSpy).toHaveBeenCalled();
+ expect(boardsStore.updateFiltersUrl).toHaveBeenCalled();
+ });
+ });
+
describe('lists', () => {
it('creates new list without persisting to DB', () => {
boardsStore.addList(listObj);
@@ -268,4 +311,48 @@ describe('Store', () => {
});
});
});
+
+ describe('setListDetail', () => {
+ it('sets the list detail', () => {
+ boardsStore.detail.list = 'not a list';
+
+ const dummyValue = 'new list';
+ boardsStore.setListDetail(dummyValue);
+
+ expect(boardsStore.detail.list).toEqual(dummyValue);
+ });
+ });
+
+ describe('clearDetailIssue', () => {
+ it('resets issue details', () => {
+ boardsStore.detail.issue = 'something';
+
+ boardsStore.clearDetailIssue();
+
+ expect(boardsStore.detail.issue).toEqual({});
+ });
+ });
+
+ describe('setIssueDetail', () => {
+ it('sets issue details', () => {
+ boardsStore.detail.issue = 'some details';
+
+ const dummyValue = 'new details';
+ boardsStore.setIssueDetail(dummyValue);
+
+ expect(boardsStore.detail.issue).toEqual(dummyValue);
+ });
+ });
+
+ describe('startMoving', () => {
+ it('stores list and issue', () => {
+ const dummyIssue = 'some issue';
+ const dummyList = 'some list';
+
+ boardsStore.startMoving(dummyList, dummyIssue);
+
+ expect(boardsStore.moving.issue).toEqual(dummyIssue);
+ expect(boardsStore.moving.list).toEqual(dummyList);
+ });
+ });
});
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index a5bf97bdcc2..8a20911cc66 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -4,8 +4,8 @@
import Vue from 'vue';
-import '~/vue_shared/models/label';
-import '~/vue_shared/models/assignee';
+import '~/boards/models/label';
+import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import IssueCardInner from '~/boards/components/issue_card_inner.vue';
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index e4ff3eb381f..bb7abe52eae 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -1,8 +1,8 @@
/* global ListIssue */
import Vue from 'vue';
-import '~/vue_shared/models/label';
-import '~/vue_shared/models/assignee';
+import '~/boards/models/label';
+import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import '~/boards/services/board_service';
diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js
index bb6fc6c693d..15c9ff6dfb4 100644
--- a/spec/javascripts/boards/list_spec.js
+++ b/spec/javascripts/boards/list_spec.js
@@ -4,8 +4,8 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import _ from 'underscore';
-import '~/vue_shared/models/label';
-import '~/vue_shared/models/assignee';
+import '~/boards/models/label';
+import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import '~/boards/services/board_service';
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index 93a0f29af0a..9854cf49e97 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -47,7 +47,7 @@ export const BoardsMockData = {
},
],
},
- '/test/issue-boards/milestones.json': [
+ '/test/issue-boards/-/milestones.json': [
{
id: 1,
title: 'test',
@@ -58,10 +58,10 @@ export const BoardsMockData = {
'/test/-/boards/1/lists': listObj,
},
PUT: {
- '/test/issue-boards/board/1/lists{/id}': {},
+ '/test/issue-boards/-/board/1/lists{/id}': {},
},
DELETE: {
- '/test/issue-boards/board/1/lists{/id}': {},
+ '/test/issue-boards/-/board/1/lists{/id}': {},
},
};
@@ -71,7 +71,7 @@ export const boardsMockInterceptor = config => {
};
export const mockBoardService = (opts = {}) => {
- const boardsEndpoint = opts.boardsEndpoint || '/test/issue-boards/boards.json';
+ const boardsEndpoint = opts.boardsEndpoint || '/test/issue-boards/-/boards.json';
const listsEndpoint = opts.listsEndpoint || '/test/-/boards/1/lists';
const bulkUpdatePath = opts.bulkUpdatePath || '';
const boardId = opts.boardId || '1';
diff --git a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
index 2839922fbd3..e6a969bd855 100644
--- a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
+++ b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
@@ -32,6 +32,7 @@ describe('AjaxFormVariableList', () => {
saveButton,
errorBox,
saveEndpoint: container.dataset.saveEndpoint,
+ maskableRegex: container.dataset.maskableRegex,
});
spyOn(ajaxVariableList, 'updateRowsWithPersistedVariables').and.callThrough();
@@ -220,4 +221,11 @@ describe('AjaxFormVariableList', () => {
expect(row.dataset.isPersisted).toEqual('true');
});
});
+
+ describe('maskableRegex', () => {
+ it('takes in the regex provided by the data attribute', () => {
+ expect(container.dataset.maskableRegex).toBe('^[a-zA-Z0-9_+=/-]{8,}$');
+ expect(ajaxVariableList.maskableRegex).toBe(container.dataset.maskableRegex);
+ });
+ });
});
diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
index 394e60fc22c..064113e879a 100644
--- a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
+++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
@@ -150,6 +150,65 @@ describe('VariableList', () => {
.then(done)
.catch(done.fail);
});
+
+ describe('validateMaskability', () => {
+ let $row;
+
+ const maskingErrorElement = '.js-row:last-child .masking-validation-error';
+
+ beforeEach(() => {
+ $row = $wrapper.find('.js-row:last-child');
+ $row.find('.ci-variable-masked-item .js-project-feature-toggle').click();
+ });
+
+ it('has a regex provided via a data attribute', () => {
+ expect($wrapper.attr('data-maskable-regex')).toBe('^[a-zA-Z0-9_+=/-]{8,}$');
+ });
+
+ it('allows values that are 8 characters long', done => {
+ $row.find('.js-ci-variable-input-value').val('looooong');
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect($wrapper.find(maskingErrorElement)).toHaveClass('hide');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('rejects values that are shorter than 8 characters', done => {
+ $row.find('.js-ci-variable-input-value').val('short');
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect($wrapper.find(maskingErrorElement)).toBeVisible();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('allows values with base 64 characters', done => {
+ $row.find('.js-ci-variable-input-value').val('abcABC123_+=/-');
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect($wrapper.find(maskingErrorElement)).toHaveClass('hide');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('rejects values with other special characters', done => {
+ $row.find('.js-ci-variable-input-value').val('1234567$');
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect($wrapper.find(maskingErrorElement)).toBeVisible();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
});
describe('toggleEnableRow method', () => {
diff --git a/spec/javascripts/diffs/components/commit_item_spec.js b/spec/javascripts/diffs/components/commit_item_spec.js
index 8fc9b10dd0b..cfe0c4bad71 100644
--- a/spec/javascripts/diffs/components/commit_item_spec.js
+++ b/spec/javascripts/diffs/components/commit_item_spec.js
@@ -8,7 +8,7 @@ import getDiffWithCommit from '../mock_data/diff_with_commit';
const TEST_AUTHOR_NAME = 'test';
const TEST_AUTHOR_EMAIL = 'test+test@gitlab.com';
-const TEST_AUTHOR_GRAVATAR = `${TEST_HOST}/avatar/test?s=36`;
+const TEST_AUTHOR_GRAVATAR = `${TEST_HOST}/avatar/test?s=40`;
const TEST_SIGNATURE_HTML = '<a>Legit commit</a>';
const TEST_PIPELINE_STATUS_PATH = `${TEST_HOST}/pipeline/status`;
@@ -65,7 +65,7 @@ describe('diffs/components/commit_item', () => {
const imgElement = avatarElement.querySelector('img');
expect(avatarElement).toHaveAttr('href', commit.author.web_url);
- expect(imgElement).toHaveClass('s36');
+ expect(imgElement).toHaveClass('s40');
expect(imgElement).toHaveAttr('alt', commit.author.name);
expect(imgElement).toHaveAttr('src', commit.author.avatar_url);
});
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index 6309a8823d7..f129fbb57a3 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -396,6 +396,7 @@ describe('DiffsStoreActions', () => {
});
describe('loadCollapsedDiff', () => {
+ const state = { showWhitespace: true };
it('should fetch data and call mutation with response and the give parameter', done => {
const file = { hash: 123, load_collapsed_diff_url: '/load/collapsed/diff/url' };
const data = { hash: 123, parallelDiffLines: [{ lineCode: 1 }] };
@@ -403,7 +404,7 @@ describe('DiffsStoreActions', () => {
const commit = jasmine.createSpy('commit');
mock.onGet(file.loadCollapsedDiffUrl).reply(200, data);
- loadCollapsedDiff({ commit, getters: { commitId: null } }, file)
+ loadCollapsedDiff({ commit, getters: { commitId: null }, state }, file)
.then(() => {
expect(commit).toHaveBeenCalledWith(types.ADD_COLLAPSED_DIFFS, { file, data });
@@ -421,10 +422,10 @@ describe('DiffsStoreActions', () => {
spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} }));
- loadCollapsedDiff({ commit() {}, getters }, file);
+ loadCollapsedDiff({ commit() {}, getters, state }, file);
expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, {
- params: { commit_id: null },
+ params: { commit_id: null, w: '0' },
});
});
@@ -436,10 +437,10 @@ describe('DiffsStoreActions', () => {
spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} }));
- loadCollapsedDiff({ commit() {}, getters }, file);
+ loadCollapsedDiff({ commit() {}, getters, state }, file);
expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, {
- params: { commit_id: '123' },
+ params: { commit_id: '123', w: '0' },
});
});
});
diff --git a/spec/javascripts/helpers/vue_test_utils_helper.js b/spec/javascripts/helpers/vue_test_utils_helper.js
index 19e27388eeb..121e99c9783 100644
--- a/spec/javascripts/helpers/vue_test_utils_helper.js
+++ b/spec/javascripts/helpers/vue_test_utils_helper.js
@@ -16,4 +16,6 @@ const vNodeContainsText = (vnode, text) =>
* @param {String} text
*/
export const shallowWrapperContainsSlotText = (shallowWrapper, slotName, text) =>
- !!shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length;
+ Boolean(
+ shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length,
+ );
diff --git a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
index 23e6a055518..b903abe63fc 100644
--- a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
@@ -73,47 +73,4 @@ describe('IDE commit sidebar actions', () => {
expect(vm.commitToCurrentBranchText).not.toContain(injectedSrc);
});
});
-
- describe('create new MR checkbox', () => {
- it('disables `createMR` button when an MR already exists and committing to current branch', () => {
- createComponent({ hasMR: true, commitAction: consts.COMMIT_TO_CURRENT_BRANCH });
-
- expect(vm.$el.querySelector('input[type="checkbox"]').disabled).toBe(true);
- });
-
- it('does not disable checkbox if MR does not exist', () => {
- createComponent({ hasMR: false });
-
- expect(vm.$el.querySelector('input[type="checkbox"]').disabled).toBe(false);
- });
-
- it('does not disable checkbox when creating a new branch', () => {
- createComponent({ commitAction: consts.COMMIT_TO_NEW_BRANCH });
-
- expect(vm.$el.querySelector('input[type="checkbox"]').disabled).toBe(false);
- });
-
- it('toggles off new MR when switching back to commit to current branch and MR exists', () => {
- createComponent({
- commitAction: consts.COMMIT_TO_NEW_BRANCH,
- shouldCreateMR: true,
- });
- const currentBranchRadio = vm.$el.querySelector(
- `input[value="${consts.COMMIT_TO_CURRENT_BRANCH}"`,
- );
- currentBranchRadio.click();
-
- vm.$nextTick(() => {
- expect(vm.$store.state.commit.shouldCreateMR).toBe(false);
- });
- });
-
- it('toggles `shouldCreateMR` when clicking checkbox', () => {
- createComponent();
- const el = vm.$el.querySelector('input[type="checkbox"]');
- el.dispatchEvent(new Event('change'));
-
- expect(vm.$store.state.commit.shouldCreateMR).toBe(true);
- });
- });
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js b/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js
new file mode 100644
index 00000000000..7017bfcd6a6
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js
@@ -0,0 +1,73 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import consts from '~/ide/stores/modules/commit/constants';
+import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { projectData } from 'spec/ide/mock_data';
+import { resetStore } from 'spec/ide/helpers';
+
+describe('create new MR checkbox', () => {
+ let vm;
+ const createComponent = ({
+ hasMR = false,
+ commitAction = consts.COMMIT_TO_NEW_BRANCH,
+ currentBranchId = 'master',
+ } = {}) => {
+ const Component = Vue.extend(NewMergeRequestOption);
+
+ vm = createComponentWithStore(Component, store);
+
+ vm.$store.state.currentBranchId = currentBranchId;
+ vm.$store.state.currentProjectId = 'abcproject';
+ vm.$store.state.commit.commitAction = commitAction;
+ Vue.set(vm.$store.state.projects, 'abcproject', { ...projectData });
+
+ if (hasMR) {
+ vm.$store.state.currentMergeRequestId = '1';
+ vm.$store.state.projects[store.state.currentProjectId].mergeRequests[
+ store.state.currentMergeRequestId
+ ] = { foo: 'bar' };
+ }
+
+ return vm.$mount();
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('is hidden when an MR already exists and committing to current branch', () => {
+ createComponent({
+ hasMR: true,
+ commitAction: consts.COMMIT_TO_CURRENT_BRANCH,
+ currentBranchId: 'feature',
+ });
+
+ expect(vm.$el.textContent).toBe('');
+ });
+
+ it('does not hide checkbox if MR does not exist', () => {
+ createComponent({ hasMR: false });
+
+ expect(vm.$el.querySelector('input[type="checkbox"]').hidden).toBe(false);
+ });
+
+ it('does not hide checkbox when creating a new branch', () => {
+ createComponent({ commitAction: consts.COMMIT_TO_NEW_BRANCH });
+
+ expect(vm.$el.querySelector('input[type="checkbox"]').hidden).toBe(false);
+ });
+
+ it('dispatches toggleShouldCreateMR when clicking checkbox', () => {
+ createComponent();
+ const el = vm.$el.querySelector('input[type="checkbox"]');
+ spyOn(vm.$store, 'dispatch');
+ el.dispatchEvent(new Event('change'));
+
+ expect(vm.$store.dispatch.calls.allArgs()).toEqual(
+ jasmine.arrayContaining([['commit/toggleShouldCreateMR', jasmine.any(Object)]]),
+ );
+ });
+});
diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js
index 4fe826943b2..570a396c5e3 100644
--- a/spec/javascripts/ide/mock_data.js
+++ b/spec/javascripts/ide/mock_data.js
@@ -16,6 +16,7 @@ export const projectData = {
},
mergeRequests: {},
merge_requests_enabled: true,
+ default_branch: 'master',
};
export const pipelines = [
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index e6fb08bcc49..dd2313dc800 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -719,4 +719,20 @@ describe('IDE store file actions', () => {
.catch(done.fail);
});
});
+
+ describe('triggerFilesChange', () => {
+ beforeEach(() => {
+ spyOn(eventHub, '$emit');
+ });
+
+ it('emits event that files have changed', done => {
+ store
+ .dispatch('triggerFilesChange')
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('ide.files.change');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index 04e236fb042..37354283cab 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -488,7 +488,7 @@ describe('Multi-file store actions', () => {
'path',
store.state,
[{ type: types.DELETE_ENTRY, payload: 'path' }],
- [{ type: 'burstUnusedSeal' }],
+ [{ type: 'burstUnusedSeal' }, { type: 'triggerFilesChange' }],
done,
);
});
@@ -510,7 +510,7 @@ describe('Multi-file store actions', () => {
payload: { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' },
},
],
- [{ type: 'deleteEntry', payload: 'test' }],
+ [{ type: 'deleteEntry', payload: 'test' }, { type: 'triggerFilesChange' }],
done,
);
});
@@ -558,6 +558,7 @@ describe('Multi-file store actions', () => {
},
},
{ type: 'deleteEntry', payload: 'test' },
+ { type: 'triggerFilesChange' },
],
done,
);
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
index 9c135661997..735bbd47f55 100644
--- a/spec/javascripts/ide/stores/getters_spec.js
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -180,6 +180,38 @@ describe('IDE store getters', () => {
});
});
+ describe('isOnDefaultBranch', () => {
+ it('returns false when no project exists', () => {
+ const localGetters = {
+ currentProject: undefined,
+ };
+
+ expect(getters.isOnDefaultBranch({}, localGetters)).toBeFalsy();
+ });
+
+ it("returns true when project's default branch matches current branch", () => {
+ const localGetters = {
+ currentProject: {
+ default_branch: 'master',
+ },
+ branchName: 'master',
+ };
+
+ expect(getters.isOnDefaultBranch({}, localGetters)).toBeTruthy();
+ });
+
+ it("returns false when project's default branch doesn't match current branch", () => {
+ const localGetters = {
+ currentProject: {
+ default_branch: 'master',
+ },
+ branchName: 'feature',
+ };
+
+ expect(getters.isOnDefaultBranch({}, localGetters)).toBeFalsy();
+ });
+ });
+
describe('packageJson', () => {
it('returns package.json entry', () => {
localState.entries['package.json'] = { name: 'package.json' };
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
index 4413a12fac4..5f7272311c8 100644
--- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -1,9 +1,12 @@
-import actions from '~/ide/stores/actions';
+import rootActions from '~/ide/stores/actions';
import store from '~/ide/stores';
import service from '~/ide/services';
import router from '~/ide/ide_router';
import eventHub from '~/ide/eventhub';
import consts from '~/ide/stores/modules/commit/constants';
+import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types';
+import * as actions from '~/ide/stores/modules/commit/actions';
+import testAction from '../../../../helpers/vuex_action_helper';
import { commitActionTypes } from '~/ide/constants';
import { resetStore, file } from 'spec/ide/helpers';
@@ -225,7 +228,7 @@ describe('IDE commit module actions', () => {
let visitUrl;
beforeEach(() => {
- visitUrl = spyOnDependency(actions, 'visitUrl');
+ visitUrl = spyOnDependency(rootActions, 'visitUrl');
document.body.innerHTML += '<div class="flash-container"></div>';
@@ -523,4 +526,154 @@ describe('IDE commit module actions', () => {
});
});
});
+
+ describe('toggleShouldCreateMR', () => {
+ it('commits both toggle and interacting with MR checkbox actions', done => {
+ testAction(
+ actions.toggleShouldCreateMR,
+ {},
+ store.state,
+ [
+ { type: mutationTypes.TOGGLE_SHOULD_CREATE_MR },
+ { type: mutationTypes.INTERACT_WITH_NEW_MR },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setShouldCreateMR', () => {
+ beforeEach(() => {
+ store.state.projects = {
+ project: {
+ default_branch: 'master',
+ branches: {
+ master: {
+ name: 'master',
+ },
+ feature: {
+ name: 'feature',
+ },
+ },
+ },
+ };
+
+ store.state.currentProjectId = 'project';
+ });
+
+ it('sets to false when the current branch already has an MR', done => {
+ store.state.commit.currentMergeRequestId = 1;
+ store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
+ store.state.currentMergeRequestId = '1';
+ store.state.currentBranchId = 'feature';
+ spyOn(store, 'commit').and.callThrough();
+
+ store
+ .dispatch('commit/setShouldCreateMR')
+ .then(() => {
+ expect(store.commit.calls.allArgs()[0]).toEqual(
+ jasmine.arrayContaining([`commit/${mutationTypes.TOGGLE_SHOULD_CREATE_MR}`, false]),
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('changes to false when current branch is the default branch and user has not interacted', done => {
+ store.state.commit.interactedWithNewMR = false;
+ store.state.currentBranchId = 'master';
+ store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
+ spyOn(store, 'commit').and.callThrough();
+
+ store
+ .dispatch('commit/setShouldCreateMR')
+ .then(() => {
+ expect(store.commit.calls.allArgs()[0]).toEqual(
+ jasmine.arrayContaining([`commit/${mutationTypes.TOGGLE_SHOULD_CREATE_MR}`, false]),
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('changes to true when "create new branch" is selected and user has not interacted', done => {
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ store.state.commit.interactedWithNewMR = false;
+ spyOn(store, 'commit').and.callThrough();
+
+ store
+ .dispatch('commit/setShouldCreateMR')
+ .then(() => {
+ expect(store.commit.calls.allArgs()[0]).toEqual(
+ jasmine.arrayContaining([`commit/${mutationTypes.TOGGLE_SHOULD_CREATE_MR}`, true]),
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not change anything if user has interacted and comitting to new branch', done => {
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ store.state.commit.interactedWithNewMR = true;
+ spyOn(store, 'commit').and.callThrough();
+
+ store
+ .dispatch('commit/setShouldCreateMR')
+ .then(() => {
+ expect(store.commit).not.toHaveBeenCalled();
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not change anything if user has interacted and comitting to branch without MR', done => {
+ store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
+ store.state.commit.currentMergeRequestId = null;
+ store.state.commit.interactedWithNewMR = true;
+ spyOn(store, 'commit').and.callThrough();
+
+ store
+ .dispatch('commit/setShouldCreateMR')
+ .then(() => {
+ expect(store.commit).not.toHaveBeenCalled();
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('still changes to false if hiding the checkbox', done => {
+ store.state.currentBranchId = 'feature';
+ store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
+ store.state.currentMergeRequestId = '1';
+ store.state.commit.interactedWithNewMR = true;
+ spyOn(store, 'commit').and.callThrough();
+
+ store
+ .dispatch('commit/setShouldCreateMR')
+ .then(() => {
+ expect(store.commit.calls.allArgs()[0]).toEqual(
+ jasmine.arrayContaining([`commit/${mutationTypes.TOGGLE_SHOULD_CREATE_MR}`, false]),
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not change to false when on master and user has interacted even if MR exists', done => {
+ store.state.currentBranchId = 'master';
+ store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
+ store.state.currentMergeRequestId = '1';
+ store.state.commit.interactedWithNewMR = true;
+ spyOn(store, 'commit').and.callThrough();
+
+ store
+ .dispatch('commit/setShouldCreateMR')
+ .then(() => {
+ expect(store.commit).not.toHaveBeenCalled();
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
index e00fd7199d7..6e71a790deb 100644
--- a/spec/javascripts/ide/stores/modules/commit/getters_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
@@ -144,33 +144,4 @@ describe('IDE commit module getters', () => {
});
});
});
-
- describe('shouldDisableNewMrOption', () => {
- it('returns false if commitAction `COMMIT_TO_NEW_BRANCH`', () => {
- state.commitAction = consts.COMMIT_TO_NEW_BRANCH;
- const rootState = {
- currentMergeRequest: { foo: 'bar' },
- };
-
- expect(getters.shouldDisableNewMrOption(state, null, null, rootState)).toBeFalsy();
- });
-
- it('returns false if there is no current merge request', () => {
- state.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
- const rootState = {
- currentMergeRequest: null,
- };
-
- expect(getters.shouldDisableNewMrOption(state, null, null, rootState)).toBeFalsy();
- });
-
- it('returns true an MR exists and commit action is `COMMIT_TO_CURRENT_BRANCH`', () => {
- state.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
- const rootState = {
- currentMergeRequest: { foo: 'bar' },
- };
-
- expect(getters.shouldDisableNewMrOption(state, null, null, rootState)).toBeTruthy();
- });
- });
});
diff --git a/spec/javascripts/jobs/components/stages_dropdown_spec.js b/spec/javascripts/jobs/components/stages_dropdown_spec.js
index 52bb5161123..e98639bf21e 100644
--- a/spec/javascripts/jobs/components/stages_dropdown_spec.js
+++ b/spec/javascripts/jobs/components/stages_dropdown_spec.js
@@ -9,6 +9,7 @@ describe('Stages Dropdown', () => {
const mockPipelineData = {
id: 28029444,
+ iid: 123,
details: {
status: {
details_path: '/gitlab-org/gitlab-ce/pipelines/28029444',
@@ -77,8 +78,8 @@ describe('Stages Dropdown', () => {
expect(vm.$el.querySelector('.dropdown .js-selected-stage').textContent).toContain('deploy');
});
- it(`renders the pipeline info text like "Pipeline #123 for source_branch"`, () => {
- const expected = `Pipeline #${pipeline.id} for ${pipeline.ref.name}`;
+ it(`renders the pipeline info text like "Pipeline #123 (#12) for source_branch"`, () => {
+ const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) for ${pipeline.ref.name}`;
const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText);
expect(actual).toBe(expected);
@@ -100,10 +101,10 @@ describe('Stages Dropdown', () => {
});
});
- it(`renders the pipeline info text like "Pipeline #123 for !456 with source_branch into target_branch"`, () => {
- const expected = `Pipeline #${pipeline.id} for !${pipeline.merge_request.iid} with ${
- pipeline.merge_request.source_branch
- } into ${pipeline.merge_request.target_branch}`;
+ it(`renders the pipeline info text like "Pipeline #123 (#12) for !456 with source_branch into target_branch"`, () => {
+ const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) for !${
+ pipeline.merge_request.iid
+ } with ${pipeline.merge_request.source_branch} into ${pipeline.merge_request.target_branch}`;
const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText);
expect(actual).toBe(expected);
@@ -143,10 +144,10 @@ describe('Stages Dropdown', () => {
});
});
- it(`renders the pipeline info like "Pipeline #123 for !456 with source_branch"`, () => {
- const expected = `Pipeline #${pipeline.id} for !${pipeline.merge_request.iid} with ${
- pipeline.merge_request.source_branch
- }`;
+ it(`renders the pipeline info like "Pipeline #123 (#12) for !456 with source_branch"`, () => {
+ const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) for !${
+ pipeline.merge_request.iid
+ } with ${pipeline.merge_request.source_branch}`;
const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText);
expect(actual).toBe(expected);
diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js
index 3d40e94d219..88b0bb206ee 100644
--- a/spec/javascripts/jobs/mock_data.js
+++ b/spec/javascripts/jobs/mock_data.js
@@ -960,6 +960,7 @@ export default {
},
pipeline: {
id: 140,
+ iid: 13,
user: {
name: 'Root',
username: 'root',
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index 0cd077a6099..296ee85089f 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -852,20 +852,20 @@ describe('common_utils', () => {
describe('roundOffFloat', () => {
it('Rounds off decimal places of a float number with provided precision', () => {
- expect(commonUtils.roundOffFloat(3.141592, 3)).toBe(3.142);
+ expect(commonUtils.roundOffFloat(3.141592, 3)).toBeCloseTo(3.142);
});
it('Rounds off a float number to a whole number when provided precision is zero', () => {
- expect(commonUtils.roundOffFloat(3.141592, 0)).toBe(3);
- expect(commonUtils.roundOffFloat(3.5, 0)).toBe(4);
+ expect(commonUtils.roundOffFloat(3.141592, 0)).toBeCloseTo(3);
+ expect(commonUtils.roundOffFloat(3.5, 0)).toBeCloseTo(4);
});
it('Rounds off float number to nearest 0, 10, 100, 1000 and so on when provided precision is below 0', () => {
- expect(commonUtils.roundOffFloat(34567.14159, -1)).toBe(34570);
- expect(commonUtils.roundOffFloat(34567.14159, -2)).toBe(34600);
- expect(commonUtils.roundOffFloat(34567.14159, -3)).toBe(35000);
- expect(commonUtils.roundOffFloat(34567.14159, -4)).toBe(30000);
- expect(commonUtils.roundOffFloat(34567.14159, -5)).toBe(0);
+ expect(commonUtils.roundOffFloat(34567.14159, -1)).toBeCloseTo(34570);
+ expect(commonUtils.roundOffFloat(34567.14159, -2)).toBeCloseTo(34600);
+ expect(commonUtils.roundOffFloat(34567.14159, -3)).toBeCloseTo(35000);
+ expect(commonUtils.roundOffFloat(34567.14159, -4)).toBeCloseTo(30000);
+ expect(commonUtils.roundOffFloat(34567.14159, -5)).toBeCloseTo(0);
});
});
diff --git a/spec/javascripts/matchers.js b/spec/javascripts/matchers.js
index 406527b08a3..7d1921cabcf 100644
--- a/spec/javascripts/matchers.js
+++ b/spec/javascripts/matchers.js
@@ -28,7 +28,7 @@ export default {
reference.getAttribute('xlink:href').endsWith(`#${iconName}`),
);
const result = {
- pass: !!matchingIcon,
+ pass: Boolean(matchingIcon),
};
if (result.pass) {
diff --git a/spec/javascripts/monitoring/charts/single_stat_spec.js b/spec/javascripts/monitoring/charts/single_stat_spec.js
new file mode 100644
index 00000000000..12b73002f97
--- /dev/null
+++ b/spec/javascripts/monitoring/charts/single_stat_spec.js
@@ -0,0 +1,28 @@
+import { shallowMount } from '@vue/test-utils';
+import SingleStatChart from '~/monitoring/components/charts/single_stat.vue';
+
+describe('Single Stat Chart component', () => {
+ let singleStatChart;
+
+ beforeEach(() => {
+ singleStatChart = shallowMount(SingleStatChart, {
+ propsData: {
+ title: 'Time to render',
+ value: 1,
+ unit: 'sec',
+ },
+ });
+ });
+
+ afterEach(() => {
+ singleStatChart.destroy();
+ });
+
+ describe('computed', () => {
+ describe('valueWithUnit', () => {
+ it('should interpolate the value and unit props', () => {
+ expect(singleStatChart.vm.valueWithUnit).toBe('1sec');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index 58bcd916739..1a371c3adaf 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -38,6 +38,7 @@ describe('Dashboard', () => {
let DashboardComponent;
let mock;
let store;
+ let component;
beforeEach(() => {
setFixtures(`
@@ -59,14 +60,15 @@ describe('Dashboard', () => {
});
afterEach(() => {
+ component.$destroy();
mock.restore();
});
describe('no metrics are available yet', () => {
it('shows a getting started empty state when no metrics are present', () => {
- const component = new DashboardComponent({
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData: { ...propsData, showTimeWindowDropdown: false },
+ propsData: { ...propsData },
store,
});
@@ -81,9 +83,9 @@ describe('Dashboard', () => {
});
it('shows up a loading state', done => {
- const component = new DashboardComponent({
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData: { ...propsData, hasMetrics: true, showTimeWindowDropdown: false },
+ propsData: { ...propsData, hasMetrics: true },
store,
});
@@ -94,13 +96,12 @@ describe('Dashboard', () => {
});
it('hides the legend when showLegend is false', done => {
- const component = new DashboardComponent({
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
hasMetrics: true,
showLegend: false,
- showTimeWindowDropdown: false,
},
store,
});
@@ -114,13 +115,12 @@ describe('Dashboard', () => {
});
it('hides the group panels when showPanels is false', done => {
- const component = new DashboardComponent({
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
hasMetrics: true,
showPanels: false,
- showTimeWindowDropdown: false,
},
store,
});
@@ -134,13 +134,12 @@ describe('Dashboard', () => {
});
it('renders the environments dropdown with a number of environments', done => {
- const component = new DashboardComponent({
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
hasMetrics: true,
showPanels: false,
- showTimeWindowDropdown: false,
},
store,
});
@@ -165,21 +164,16 @@ describe('Dashboard', () => {
});
it('hides the environments dropdown list when there is no environments', done => {
- const component = new DashboardComponent({
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
hasMetrics: true,
showPanels: false,
- showTimeWindowDropdown: false,
},
store,
});
- component.$store.commit(
- `monitoringDashboard/${types.SET_ENVIRONMENTS_ENDPOINT}`,
- '/environments',
- );
component.$store.commit(`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, []);
component.$store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
@@ -199,22 +193,17 @@ describe('Dashboard', () => {
});
it('renders the environments dropdown with a single active element', done => {
- const component = new DashboardComponent({
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
hasMetrics: true,
showPanels: false,
- showTimeWindowDropdown: false,
},
store,
});
component.$store.commit(
- `monitoringDashboard/${types.SET_ENVIRONMENTS_ENDPOINT}`,
- '/environments',
- );
- component.$store.commit(
`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
environmentData,
);
@@ -236,14 +225,13 @@ describe('Dashboard', () => {
});
it('hides the dropdown', done => {
- const component = new DashboardComponent({
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
hasMetrics: true,
showPanels: false,
environmentsEndpoint: '',
- showTimeWindowDropdown: false,
},
store,
});
@@ -256,59 +244,72 @@ describe('Dashboard', () => {
});
});
- it('does not show the time window dropdown when the feature flag is not set', done => {
- const component = new DashboardComponent({
+ it('renders the time window dropdown with a set of options', done => {
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
hasMetrics: true,
showPanels: false,
- showTimeWindowDropdown: false,
},
store,
});
+ const numberOfTimeWindows = Object.keys(timeWindows).length;
setTimeout(() => {
const timeWindowDropdown = component.$el.querySelector('.js-time-window-dropdown');
+ const timeWindowDropdownEls = component.$el.querySelectorAll(
+ '.js-time-window-dropdown .dropdown-item',
+ );
- expect(timeWindowDropdown).toBeNull();
+ expect(timeWindowDropdown).not.toBeNull();
+ expect(timeWindowDropdownEls.length).toEqual(numberOfTimeWindows);
done();
});
});
- it('renders the time window dropdown with a set of options', done => {
- const component = new DashboardComponent({
+ it('fetches the metrics data with proper time window', done => {
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
hasMetrics: true,
showPanels: false,
- showTimeWindowDropdown: true,
},
store,
});
- const numberOfTimeWindows = Object.keys(timeWindows).length;
- setTimeout(() => {
- const timeWindowDropdown = component.$el.querySelector('.js-time-window-dropdown');
- const timeWindowDropdownEls = component.$el.querySelectorAll(
- '.js-time-window-dropdown .dropdown-item',
- );
+ spyOn(component.$store, 'dispatch').and.stub();
+ const getTimeDiffSpy = spyOnDependency(Dashboard, 'getTimeDiff');
- expect(timeWindowDropdown).not.toBeNull();
- expect(timeWindowDropdownEls.length).toEqual(numberOfTimeWindows);
+ component.$store.commit(
+ `monitoringDashboard/${types.SET_ENVIRONMENTS_ENDPOINT}`,
+ '/environments',
+ );
+ component.$store.commit(
+ `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
+ environmentData,
+ );
- done();
- });
+ component.$mount();
+
+ Vue.nextTick()
+ .then(() => {
+ expect(component.$store.dispatch).toHaveBeenCalled();
+ expect(getTimeDiffSpy).toHaveBeenCalledWith(component.selectedTimeWindow);
+
+ done();
+ })
+ .catch(done.fail);
});
it('shows a specific time window selected from the url params', done => {
spyOnDependency(Dashboard, 'getParameterValues').and.returnValue(['thirtyMinutes']);
- const component = new DashboardComponent({
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData: { ...propsData, hasMetrics: true, showTimeWindowDropdown: true },
+ propsData: { ...propsData, hasMetrics: true },
store,
});
@@ -327,9 +328,9 @@ describe('Dashboard', () => {
'<script>alert("XSS")</script>',
]);
- const component = new DashboardComponent({
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData: { ...propsData, hasMetrics: true, showTimeWindowDropdown: true },
+ propsData: { ...propsData, hasMetrics: true },
store,
});
@@ -352,13 +353,12 @@ describe('Dashboard', () => {
});
it('sets elWidth to page width when the sidebar is resized', done => {
- const component = new DashboardComponent({
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
hasMetrics: true,
showPanels: false,
- showTimeWindowDropdown: false,
},
store,
});
@@ -382,16 +382,10 @@ describe('Dashboard', () => {
});
describe('external dashboard link', () => {
- let component;
-
beforeEach(() => {
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
});
- afterEach(() => {
- component.$destroy();
- });
-
describe('with feature flag enabled', () => {
beforeEach(() => {
component = new DashboardComponent({
@@ -400,8 +394,7 @@ describe('Dashboard', () => {
...propsData,
hasMetrics: true,
showPanels: false,
- showTimeWindowDropdown: false,
- externalDashboardPath: '/mockPath',
+ externalDashboardUrl: '/mockUrl',
},
store,
});
@@ -426,8 +419,7 @@ describe('Dashboard', () => {
...propsData,
hasMetrics: true,
showPanels: false,
- showTimeWindowDropdown: false,
- externalDashboardPath: '',
+ externalDashboardUrl: '',
},
store,
});
diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js
index 0cfcc994234..1f2c07385a7 100644
--- a/spec/javascripts/notes/components/note_actions_spec.js
+++ b/spec/javascripts/notes/components/note_actions_spec.js
@@ -58,6 +58,7 @@ describe('noteActions', () => {
it('should render emoji link', () => {
expect(wrapper.find('.js-add-award').exists()).toBe(true);
+ expect(wrapper.find('.js-add-award').attributes('data-position')).toBe('right');
});
describe('actions dropdown', () => {
@@ -65,7 +66,7 @@ describe('noteActions', () => {
expect(wrapper.find('.js-note-edit').exists()).toBe(true);
});
- it('should be possible to report abuse to GitLab', () => {
+ it('should be possible to report abuse to admin', () => {
expect(wrapper.find(`a[href="${props.reportAbusePath}"]`).exists()).toBe(true);
});
diff --git a/spec/javascripts/pipelines/mock_data.js b/spec/javascripts/pipelines/mock_data.js
index 03ead6cd8ba..8eef9166b8d 100644
--- a/spec/javascripts/pipelines/mock_data.js
+++ b/spec/javascripts/pipelines/mock_data.js
@@ -1,5 +1,6 @@
export const pipelineWithStages = {
id: 20333396,
+ iid: 304399,
user: {
id: 128633,
name: 'Rémy Coutable',
diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js
index aa196af2f33..88c0137dc58 100644
--- a/spec/javascripts/pipelines/pipeline_url_spec.js
+++ b/spec/javascripts/pipelines/pipeline_url_spec.js
@@ -13,6 +13,7 @@ describe('Pipeline Url Component', () => {
propsData: {
pipeline: {
id: 1,
+ iid: 1,
path: 'foo',
flags: {},
},
@@ -28,6 +29,7 @@ describe('Pipeline Url Component', () => {
propsData: {
pipeline: {
id: 1,
+ iid: 1,
path: 'foo',
flags: {},
},
@@ -47,6 +49,7 @@ describe('Pipeline Url Component', () => {
propsData: {
pipeline: {
id: 1,
+ iid: 1,
path: 'foo',
flags: {
latest: true,
@@ -78,6 +81,7 @@ describe('Pipeline Url Component', () => {
propsData: {
pipeline: {
id: 1,
+ iid: 1,
path: 'foo',
flags: {
latest: true,
@@ -100,6 +104,7 @@ describe('Pipeline Url Component', () => {
propsData: {
pipeline: {
id: 1,
+ iid: 1,
path: 'foo',
flags: {
failure_reason: true,
diff --git a/spec/javascripts/projects/project_new_spec.js b/spec/javascripts/projects/project_new_spec.js
index b61e0ac872f..106a3ba94e4 100644
--- a/spec/javascripts/projects/project_new_spec.js
+++ b/spec/javascripts/projects/project_new_spec.js
@@ -10,7 +10,17 @@ describe('New Project', () => {
setFixtures(`
<div class='toggle-import-form'>
<div class='import-url-data'>
- <input id="project_import_url" />
+ <div class="form-group">
+ <input id="project_import_url" />
+ </div>
+ <div id="import-url-auth-method">
+ <div class="form-group">
+ <input id="project-import-url-user" />
+ </div>
+ <div class="form-group">
+ <input id="project_import_url_password" />
+ </div>
+ </div>
<input id="project_name" />
<input id="project_path" />
</div>
@@ -119,7 +129,7 @@ describe('New Project', () => {
});
it('changes project path for HTTPS URL in $projectImportUrl', () => {
- $projectImportUrl.val('https://username:password@gitlab.company.com/group/project.git');
+ $projectImportUrl.val('https://gitlab.company.com/group/project.git');
projectNew.deriveProjectPathFromUrl($projectImportUrl);
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
index 75017d20473..a2308b0dfdb 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -103,7 +103,7 @@ describe('MRWidgetPipeline', () => {
it('should render pipeline ID', () => {
expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual(
- `#${mockData.pipeline.id}`,
+ `#${mockData.pipeline.id} (#${mockData.pipeline.iid})`,
);
});
@@ -150,7 +150,7 @@ describe('MRWidgetPipeline', () => {
it('should render pipeline ID', () => {
expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual(
- `#${mockData.pipeline.id}`,
+ `#${mockData.pipeline.id} (#${mockData.pipeline.iid})`,
);
});
@@ -222,9 +222,9 @@ describe('MRWidgetPipeline', () => {
sourceBranchLink: mockCopy.source_branch_link,
});
- const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${
- pipeline.commit.short_id
- } on ${mockCopy.source_branch_link}`;
+ const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) ${
+ pipeline.details.status.label
+ } for ${pipeline.commit.short_id} on ${mockCopy.source_branch_link}`;
const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
@@ -247,11 +247,11 @@ describe('MRWidgetPipeline', () => {
sourceBranchLink: mockCopy.source_branch_link,
});
- const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${
- pipeline.commit.short_id
- } on !${pipeline.merge_request.iid} with ${pipeline.merge_request.source_branch} into ${
- pipeline.merge_request.target_branch
- }`;
+ const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) ${
+ pipeline.details.status.label
+ } for ${pipeline.commit.short_id} on !${pipeline.merge_request.iid} with ${
+ pipeline.merge_request.source_branch
+ } into ${pipeline.merge_request.target_branch}`;
const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
@@ -274,9 +274,11 @@ describe('MRWidgetPipeline', () => {
sourceBranchLink: mockCopy.source_branch_link,
});
- const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${
- pipeline.commit.short_id
- } on !${pipeline.merge_request.iid} with ${pipeline.merge_request.source_branch}`;
+ const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) ${
+ pipeline.details.status.label
+ } for ${pipeline.commit.short_id} on !${pipeline.merge_request.iid} with ${
+ pipeline.merge_request.source_branch
+ }`;
const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
index b9718a78fa4..8e0415b813b 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
@@ -21,7 +21,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
canCancelAutomaticMerge: true,
mergeUserId: 1,
currentUserId: 1,
- setToMWPSBy: {},
+ setToAutoMergeBy: {},
sha,
targetBranchPath,
targetBranch,
@@ -106,7 +106,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
expect(vm.service.merge).toHaveBeenCalledWith({
sha,
- merge_when_pipeline_succeeds: true,
+ auto_merge_strategy: 'merge_when_pipeline_succeeds',
should_remove_source_branch: true,
});
done();
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 368c997d318..3ae773b6ccb 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
@@ -80,7 +80,7 @@ describe('ReadyToMerge', () => {
it('should have default data', () => {
expect(vm.mergeWhenBuildSucceeds).toBeFalsy();
expect(vm.useCommitMessageWithDescription).toBeFalsy();
- expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy();
+ expect(vm.autoMergeStrategy).toBeUndefined();
expect(vm.showCommitMessageEditor).toBeFalsy();
expect(vm.isMakingRequest).toBeFalsy();
expect(vm.isMergingImmediately).toBeFalsy();
@@ -91,17 +91,17 @@ describe('ReadyToMerge', () => {
});
describe('computed', () => {
- describe('shouldShowMergeWhenPipelineSucceedsText', () => {
+ describe('shouldShowAutoMergeText', () => {
it('should return true with active pipeline', () => {
vm.mr.isPipelineActive = true;
- expect(vm.shouldShowMergeWhenPipelineSucceedsText).toBeTruthy();
+ expect(vm.shouldShowAutoMergeText).toBeTruthy();
});
it('should return false with inactive pipeline', () => {
vm.mr.isPipelineActive = false;
- expect(vm.shouldShowMergeWhenPipelineSucceedsText).toBeFalsy();
+ expect(vm.shouldShowAutoMergeText).toBeFalsy();
});
});
@@ -325,16 +325,20 @@ describe('ReadyToMerge', () => {
vm.handleMergeButtonClick(true);
setTimeout(() => {
- expect(vm.setToMergeWhenPipelineSucceeds).toBeTruthy();
+ expect(vm.autoMergeStrategy).toBe('merge_when_pipeline_succeeds');
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
const params = vm.service.merge.calls.argsFor(0)[0];
- expect(params.sha).toEqual(vm.mr.sha);
- expect(params.commit_message).toEqual(vm.mr.commitMessage);
- expect(params.should_remove_source_branch).toBeFalsy();
- expect(params.merge_when_pipeline_succeeds).toBeTruthy();
+ expect(params).toEqual(
+ jasmine.objectContaining({
+ sha: vm.mr.sha,
+ commit_message: vm.mr.commitMessage,
+ should_remove_source_branch: false,
+ auto_merge_strategy: 'merge_when_pipeline_succeeds',
+ }),
+ );
done();
}, 333);
});
@@ -345,7 +349,7 @@ describe('ReadyToMerge', () => {
vm.handleMergeButtonClick(false, true);
setTimeout(() => {
- expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy();
+ expect(vm.autoMergeStrategy).toBeUndefined();
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined);
@@ -363,7 +367,7 @@ describe('ReadyToMerge', () => {
vm.handleMergeButtonClick();
setTimeout(() => {
- expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy();
+ expect(vm.autoMergeStrategy).toBeUndefined();
expect(vm.isMakingRequest).toBeTruthy();
expect(vm.initiateMergePolling).toHaveBeenCalled();
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index bec16b0aab0..edbd0d54151 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -62,6 +62,7 @@ export default {
"Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
pipeline: {
id: 172,
+ iid: 32,
user: {
name: 'Administrator',
username: 'root',
@@ -241,6 +242,8 @@ export default {
export const mockStore = {
pipeline: {
id: 0,
+ iid: 0,
+ path: '/root/acets-app/pipelines/0',
details: {
status: {
details_path: '/root/review-app-tester/pipelines/66',
@@ -258,6 +261,8 @@ export const mockStore = {
},
mergePipeline: {
id: 1,
+ iid: 1,
+ path: '/root/acets-app/pipelines/0',
details: {
status: {
details_path: '/root/review-app-tester/pipelines/66',
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index a0628fdcebe..918717c4547 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -21,7 +21,6 @@ describe('mrWidgetOptions', () => {
const COLLABORATION_MESSAGE = 'Allows commits from members who can merge to the target branch';
beforeEach(() => {
- gon.features = { approvalRules: false };
// Prevent component mounting
delete mrWidgetOptions.el;
@@ -32,7 +31,6 @@ describe('mrWidgetOptions', () => {
});
afterEach(() => {
- gon.features = null;
vm.$destroy();
});
@@ -600,6 +598,7 @@ describe('mrWidgetOptions', () => {
];
const deploymentMockData = {
id: 15,
+ iid: 7,
name: 'review/diplo',
url: '/root/acets-review-apps/environments/15',
stop_url: '/root/acets-review-apps/environments/15/stop',
@@ -646,6 +645,7 @@ describe('mrWidgetOptions', () => {
vm.mr.state = 'merged';
vm.mr.mergePipeline = {
id: 127,
+ iid: 35,
user: {
id: 1,
name: 'Administrator',
diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb
index 6e215ea1561..c788da55cd2 100644
--- a/spec/lib/api/helpers/pagination_spec.rb
+++ b/spec/lib/api/helpers/pagination_spec.rb
@@ -2,8 +2,12 @@ require 'spec_helper'
describe API::Helpers::Pagination do
let(:resource) { Project.all }
- let(:incoming_api_projects_url) { "#{Gitlab.config.gitlab.url}:8080/api/v4/projects" }
- let(:canonical_api_projects_url) { "#{Gitlab.config.gitlab.url}/api/v4/projects" }
+ let(:custom_port) { 8080 }
+ let(:incoming_api_projects_url) { "#{Gitlab.config.gitlab.url}:#{custom_port}/api/v4/projects" }
+
+ before do
+ stub_config_setting(port: custom_port)
+ end
subject do
Class.new.include(described_class).new
@@ -48,7 +52,7 @@ describe API::Helpers::Pagination do
it 'adds appropriate headers' do
expect_header('X-Per-Page', '2')
- expect_header('X-Next-Page', "#{canonical_api_projects_url}?#{query.merge(ks_prev_id: projects[1].id).to_query}")
+ expect_header('X-Next-Page', "#{incoming_api_projects_url}?#{query.merge(ks_prev_id: projects[1].id).to_query}")
expect_header('Link', anything) do |_key, val|
expect(val).to include('rel="next"')
@@ -71,7 +75,7 @@ describe API::Helpers::Pagination do
it 'adds appropriate headers' do
expect_header('X-Per-Page', '2')
- expect_header('X-Next-Page', "#{canonical_api_projects_url}?#{query.merge(ks_prev_id: projects[2].id).to_query}")
+ expect_header('X-Next-Page', "#{incoming_api_projects_url}?#{query.merge(ks_prev_id: projects[2].id).to_query}")
expect_header('Link', anything) do |_key, val|
expect(val).to include('rel="next"')
@@ -171,7 +175,7 @@ describe API::Helpers::Pagination do
it 'returns the right link to the next page' do
expect_header('X-Per-Page', '2')
- expect_header('X-Next-Page', "#{canonical_api_projects_url}?#{query.merge(ks_prev_id: projects[6].id, ks_prev_name: projects[6].name).to_query}")
+ expect_header('X-Next-Page', "#{incoming_api_projects_url}?#{query.merge(ks_prev_id: projects[6].id, ks_prev_name: projects[6].name).to_query}")
expect_header('Link', anything) do |_key, val|
expect(val).to include('rel="next"')
end
@@ -224,9 +228,9 @@ describe API::Helpers::Pagination do
expect_header('X-Prev-Page', '')
expect_header('Link', anything) do |_key, val|
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last"))
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
expect(val).not_to include('rel="prev"')
end
@@ -290,8 +294,8 @@ describe API::Helpers::Pagination do
expect_header('X-Prev-Page', '')
expect_header('Link', anything) do |_key, val|
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
expect(val).not_to include('rel="last"')
expect(val).not_to include('rel="prev"')
end
@@ -318,9 +322,9 @@ describe API::Helpers::Pagination do
expect_header('X-Prev-Page', '1')
expect_header('Link', anything) do |_key, val|
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last"))
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="prev"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="prev"))
expect(val).not_to include('rel="next"')
end
@@ -367,8 +371,8 @@ describe API::Helpers::Pagination do
expect_header('X-Prev-Page', '')
expect_header('Link', anything) do |_key, val|
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="last"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="last"))
expect(val).not_to include('rel="prev"')
expect(val).not_to include('rel="next"')
expect(val).not_to include('page=0')
diff --git a/spec/lib/banzai/commit_renderer_spec.rb b/spec/lib/banzai/commit_renderer_spec.rb
index 1f53657c59c..316dbf052c3 100644
--- a/spec/lib/banzai/commit_renderer_spec.rb
+++ b/spec/lib/banzai/commit_renderer_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Banzai::CommitRenderer do
- describe '.render' do
+ describe '.render', :clean_gitlab_redis_cache do
it 'renders a commit description and title' do
user = build(:user)
project = create(:project, :repository)
@@ -13,7 +13,7 @@ describe Banzai::CommitRenderer do
described_class::ATTRIBUTES.each do |attr|
expect_any_instance_of(Banzai::ObjectRenderer).to receive(:render).with([project.commit], attr).once.and_call_original
- expect(Banzai::Renderer).to receive(:cacheless_render_field).with(project.commit, attr, {})
+ expect(Banzai::Renderer).to receive(:cacheless_render_field).with(project.commit, attr, { skip_project_check: false }).and_call_original
end
described_class.render([project.commit], project, user)
diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
index 43222ddb5e2..7c94cf37e32 100644
--- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
@@ -155,6 +155,13 @@ describe Banzai::Filter::ExternalIssueReferenceFilter do
it_behaves_like "external issue tracker"
end
+
+ context "with a lowercase prefix" do
+ let(:issue) { ExternalIssue.new("gl-030", project) }
+ let(:reference) { issue.to_reference }
+
+ it_behaves_like "external issue tracker"
+ end
end
context "jira project" do
diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
index b9059b85fdc..cce1cd0b284 100644
--- a/spec/lib/banzai/filter/wiki_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
@@ -70,5 +70,47 @@ describe Banzai::Filter::WikiLinkFilter do
expect(filtered_link.attribute('href').value).to eq(invalid_link)
end
end
+
+ context "when the slug is deemed unsafe or invalid" do
+ let(:link) { "alert(1);" }
+
+ invalid_slugs = [
+ "javascript:",
+ "JaVaScRiPt:",
+ "\u0001java\u0003script:",
+ "javascript :",
+ "javascript: ",
+ "javascript : ",
+ ":javascript:",
+ "javascript&#58;",
+ "javascript&#0058;",
+ "javascript&#x3A;",
+ "javascript&#x003A;",
+ "java\0script:",
+ " &#14; javascript:"
+ ]
+
+ invalid_slugs.each do |slug|
+ context "with the slug #{slug}" do
+ it "doesn't rewrite a (.) relative link" do
+ filtered_link = filter(
+ "<a href='.#{link}'>Link</a>",
+ project_wiki: wiki,
+ page_slug: slug).children[0]
+
+ expect(filtered_link.attribute('href').value).not_to include(slug)
+ end
+
+ it "doesn't rewrite a (..) relative link" do
+ filtered_link = filter(
+ "<a href='..#{link}'>Link</a>",
+ project_wiki: wiki,
+ page_slug: slug).children[0]
+
+ expect(filtered_link.attribute('href').value).not_to include(slug)
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
index 3b52f6666d0..7b855251a74 100644
--- a/spec/lib/banzai/object_renderer_spec.rb
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -11,7 +11,7 @@ describe Banzai::ObjectRenderer do
)
end
- let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16) }
+ let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16) }
describe '#render' do
context 'with cache' do
@@ -60,24 +60,38 @@ describe Banzai::ObjectRenderer do
end
context 'without cache' do
- let(:commit) { project.commit }
+ let(:cacheless_class) do
+ Class.new do
+ attr_accessor :title, :redacted_title_html, :project
+
+ def banzai_render_context(field)
+ { project: project, pipeline: :single_line }
+ end
+ end
+ end
+ let(:cacheless_thing) do
+ cacheless_class.new.tap do |thing|
+ thing.title = "Merge branch 'branch-merged' into 'master'"
+ thing.project = project
+ end
+ end
it 'renders and redacts an Array of objects' do
- renderer.render([commit], :title)
+ renderer.render([cacheless_thing], :title)
- expect(commit.redacted_title_html).to eq("Merge branch 'branch-merged' into 'master'")
+ expect(cacheless_thing.redacted_title_html).to eq("Merge branch 'branch-merged' into 'master'")
end
it 'calls Banzai::Redactor to perform redaction' do
expect_any_instance_of(Banzai::Redactor).to receive(:redact).and_call_original
- renderer.render([commit], :title)
+ renderer.render([cacheless_thing], :title)
end
it 'retrieves field content using Banzai::Renderer.cacheless_render_field' do
- expect(Banzai::Renderer).to receive(:cacheless_render_field).with(commit, :title, {}).and_call_original
+ expect(Banzai::Renderer).to receive(:cacheless_render_field).with(cacheless_thing, :title, {}).and_call_original
- renderer.render([commit], :title)
+ renderer.render([cacheless_thing], :title)
end
end
end
diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb
index aaeec953e4b..718649e0e10 100644
--- a/spec/lib/banzai/redactor_spec.rb
+++ b/spec/lib/banzai/redactor_spec.rb
@@ -13,10 +13,10 @@ describe Banzai::Redactor do
it 'redacts an array of documents' do
doc1 = Nokogiri::HTML
- .fragment('<a class="gfm" data-reference-type="issue">foo</a>')
+ .fragment('<a class="gfm" href="https://www.gitlab.com" data-reference-type="issue">foo</a>')
doc2 = Nokogiri::HTML
- .fragment('<a class="gfm" data-reference-type="issue">bar</a>')
+ .fragment('<a class="gfm" href="https://www.gitlab.com" data-reference-type="issue">bar</a>')
redacted_data = redactor.redact([doc1, doc2])
@@ -27,7 +27,7 @@ describe Banzai::Redactor do
end
it 'replaces redacted reference with inner HTML' do
- doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue'>foo</a>")
+ doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue'>foo</a>")
redactor.redact([doc])
expect(doc.to_html).to eq('foo')
end
@@ -35,20 +35,24 @@ describe Banzai::Redactor do
context 'when data-original attribute provided' do
let(:original_content) { '<code>foo</code>' }
it 'replaces redacted reference with original content' do
- doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-original='#{original_content}'>bar</a>")
+ doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue' data-original='#{original_content}'>bar</a>")
redactor.redact([doc])
expect(doc.to_html).to eq(original_content)
end
- end
-
- it 'returns <a> tag with original href if it is originally a link reference' do
- href = 'http://localhost:3000'
- doc = Nokogiri::HTML
- .fragment("<a class='gfm' data-reference-type='issue' data-original=#{href} data-link-reference='true'>#{href}</a>")
- redactor.redact([doc])
+ it 'does not replace redacted reference with original content if href is given' do
+ html = "<a href='https://www.gitlab.com' data-link-reference='true' class='gfm' data-reference-type='issue' data-reference-type='issue' data-original='Marge'>Marge</a>"
+ doc = Nokogiri::HTML.fragment(html)
+ redactor.redact([doc])
+ expect(doc.to_html).to eq('<a href="https://www.gitlab.com">Marge</a>')
+ end
- expect(doc.to_html).to eq('<a href="http://localhost:3000">http://localhost:3000</a>')
+ it 'uses the original content as the link content if given' do
+ html = "<a href='https://www.gitlab.com' data-link-reference='true' class='gfm' data-reference-type='issue' data-reference-type='issue' data-original='Homer'>Marge</a>"
+ doc = Nokogiri::HTML.fragment(html)
+ redactor.redact([doc])
+ expect(doc.to_html).to eq('<a href="https://www.gitlab.com">Homer</a>')
+ end
end
end
@@ -61,7 +65,7 @@ describe Banzai::Redactor do
end
it 'redacts an issue attached' do
- doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-issue='#{issue.id}'>foo</a>")
+ doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue' data-issue='#{issue.id}'>foo</a>")
redactor.redact([doc])
@@ -69,7 +73,7 @@ describe Banzai::Redactor do
end
it 'redacts an external issue' do
- doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-external-issue='#{issue.id}' data-project='#{project.id}'>foo</a>")
+ doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue' data-external-issue='#{issue.id}' data-project='#{project.id}'>foo</a>")
redactor.redact([doc])
diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb
index 650cecfc778..aa828e2f0e9 100644
--- a/spec/lib/banzai/renderer_spec.rb
+++ b/spec/lib/banzai/renderer_spec.rb
@@ -11,16 +11,24 @@ describe Banzai::Renderer do
object
end
+ def fake_cacheless_object
+ object = double('cacheless object')
+
+ allow(object).to receive(:respond_to?).with(:cached_markdown_fields).and_return(false)
+
+ object
+ end
+
describe '#render_field' do
let(:renderer) { described_class }
context 'without cache' do
- let(:commit) { create(:project, :repository).commit }
+ let(:commit) { fake_cacheless_object }
it 'returns cacheless render field' do
- expect(renderer).to receive(:cacheless_render_field).with(commit, :title, {})
+ expect(renderer).to receive(:cacheless_render_field).with(commit, :field, {})
- renderer.render_field(commit, :title)
+ renderer.render_field(commit, :field)
end
end
diff --git a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb
index 27281333348..0a5b99d27e7 100644
--- a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb
+++ b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb
@@ -3,6 +3,12 @@ require 'spec_helper'
# rubocop:disable RSpec/FactoriesInMigrationSpecs
describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, :sidekiq, schema: 20180619121030 do
describe '#perform' do
+ before do
+ # This migration was created before we introduced ProjectCiCdSetting#default_git_depth
+ allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth=).and_return(0)
+ allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth).and_return(nil)
+ end
+
context 'when diff files can be deleted' do
let(:merge_request) { create(:merge_request, :merged) }
let!(:merge_request_diff) do
diff --git a/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb b/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb
index 3e009fed0f1..c6bc3db88a3 100644
--- a/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb
@@ -9,6 +9,9 @@ describe Gitlab::BackgroundMigration::PopulateExternalPipelineSource, :migration
before do
# This migration was created before we introduced metadata configs
stub_feature_flags(ci_build_metadata_config: false)
+ # This migration was created before we introduced ProjectCiCdSetting#default_git_depth
+ allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth).and_return(nil)
+ allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth=).and_return(0)
end
let!(:internal_pipeline) { create(:ci_pipeline, source: :web) }
diff --git a/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb b/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb
new file mode 100644
index 00000000000..740781f1aa5
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Gitlab::BackgroundMigration::ResetMergeStatus, :migration, schema: 20190528180441 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') }
+ let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') }
+ let(:merge_requests) { table(:merge_requests) }
+
+ def create_merge_request(id, extra_params = {})
+ params = {
+ id: id,
+ target_project_id: project.id,
+ target_branch: 'master',
+ source_project_id: project.id,
+ source_branch: 'mr name',
+ title: "mr name#{id}"
+ }.merge(extra_params)
+
+ merge_requests.create!(params)
+ end
+
+ it 'correctly updates opened mergeable MRs to unchecked' do
+ create_merge_request(1, state: 'opened', merge_status: 'can_be_merged')
+ create_merge_request(2, state: 'opened', merge_status: 'can_be_merged')
+ create_merge_request(3, state: 'opened', merge_status: 'can_be_merged')
+ create_merge_request(4, state: 'merged', merge_status: 'can_be_merged')
+ create_merge_request(5, state: 'opened', merge_status: 'cannot_be_merged')
+
+ subject.perform(1, 5)
+
+ expected_rows = [
+ { id: 1, state: 'opened', merge_status: 'unchecked' },
+ { id: 2, state: 'opened', merge_status: 'unchecked' },
+ { id: 3, state: 'opened', merge_status: 'unchecked' },
+ { id: 4, state: 'merged', merge_status: 'can_be_merged' },
+ { id: 5, state: 'opened', merge_status: 'cannot_be_merged' }
+ ]
+
+ rows = merge_requests.order(:id).map do |row|
+ row.attributes.slice('id', 'state', 'merge_status').symbolize_keys
+ end
+
+ expect(rows).to eq(expected_rows)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb b/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb
new file mode 100644
index 00000000000..d494ce68c5b
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190527194900_schedule_calculate_wiki_sizes.rb')
+
+describe ScheduleCalculateWikiSizes, :migration, :sidekiq do
+ let(:migration_class) { Gitlab::BackgroundMigration::CalculateWikiSizes }
+ let(:migration_name) { migration_class.to_s.demodulize }
+
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:project_statistics) { table(:project_statistics) }
+
+ context 'when missing wiki sizes exist' do
+ before do
+ namespaces.create!(id: 1, name: 'wiki-migration', path: 'wiki-migration')
+ projects.create!(id: 1, name: 'wiki-project-1', path: 'wiki-project-1', namespace_id: 1)
+ projects.create!(id: 2, name: 'wiki-project-2', path: 'wiki-project-2', namespace_id: 1)
+ projects.create!(id: 3, name: 'wiki-project-3', path: 'wiki-project-3', namespace_id: 1)
+ project_statistics.create!(id: 1, project_id: 1, namespace_id: 1, wiki_size: 1000)
+ project_statistics.create!(id: 2, project_id: 2, namespace_id: 1, wiki_size: nil)
+ project_statistics.create!(id: 3, project_id: 3, namespace_id: 1, wiki_size: nil)
+ end
+
+ it 'schedules a background migration' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(migration_name).to be_scheduled_delayed_migration(5.minutes, 2, 3)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 1
+ end
+ end
+ end
+
+ it 'calculates missing wiki sizes' do
+ expect(project_statistics.find_by(id: 2).wiki_size).to be_nil
+ expect(project_statistics.find_by(id: 3).wiki_size).to be_nil
+
+ migrate!
+
+ expect(project_statistics.find_by(id: 2).wiki_size).not_to be_nil
+ expect(project_statistics.find_by(id: 3).wiki_size).not_to be_nil
+ end
+ end
+
+ context 'when missing wiki sizes do not exist' do
+ before do
+ namespaces.create!(id: 1, name: 'wiki-migration', path: 'wiki-migration')
+ projects.create!(id: 1, name: 'wiki-project-1', path: 'wiki-project-1', namespace_id: 1)
+ project_statistics.create!(id: 1, project_id: 1, namespace_id: 1, wiki_size: 1000)
+ end
+
+ it 'does not schedule a background migration' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq 0
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index a02c00e3340..2e90f6c7f71 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -5,6 +5,7 @@ describe Gitlab::BitbucketImport::Importer do
before do
stub_omniauth_provider('bitbucket')
+ stub_feature_flags(stricter_mr_branch_name: false)
end
let(:statuses) do
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index 0560eb42e4d..e0552ae8c57 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -94,7 +94,7 @@ describe Gitlab::Ci::Config::Entry::Job do
it 'returns error about wrong value type' do
expect(entry).not_to be_valid
- expect(entry.errors).to include "job extends should be a string"
+ expect(entry.errors).to include "job extends should be an array of strings or a string"
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb
index d5bd139b5f1..d31866a1987 100644
--- a/spec/lib/gitlab/ci/config/entry/service_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb
@@ -112,6 +112,16 @@ describe Gitlab::Ci::Config::Entry::Service do
it 'is valid' do
expect(entry).to be_valid
end
+
+ context 'when unknown port keys detected' do
+ let(:ports) { [{ number: 80, invalid_key: 'foo' }] }
+
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors.first)
+ .to match /port config contains unknown keys: invalid_key/
+ end
+ end
end
describe '#ports' do
diff --git a/spec/lib/gitlab/ci/config/extendable/entry_spec.rb b/spec/lib/gitlab/ci/config/extendable/entry_spec.rb
index 0a148375d11..d63612053b6 100644
--- a/spec/lib/gitlab/ci/config/extendable/entry_spec.rb
+++ b/spec/lib/gitlab/ci/config/extendable/entry_spec.rb
@@ -44,12 +44,12 @@ describe Gitlab::Ci::Config::Extendable::Entry do
end
end
- describe '#extends_key' do
+ describe '#extends_keys' do
context 'when entry is extensible' do
it 'returns symbolized extends key value' do
entry = described_class.new(:test, test: { extends: 'something' })
- expect(entry.extends_key).to eq :something
+ expect(entry.extends_keys).to eq [:something]
end
end
@@ -57,7 +57,7 @@ describe Gitlab::Ci::Config::Extendable::Entry do
it 'returns nil' do
entry = described_class.new(:test, test: 'something')
- expect(entry.extends_key).to be_nil
+ expect(entry.extends_keys).to be_nil
end
end
end
@@ -76,7 +76,7 @@ describe Gitlab::Ci::Config::Extendable::Entry do
end
end
- describe '#base_hash!' do
+ describe '#base_hashes!' do
subject { described_class.new(:test, hash) }
context 'when base hash is not extensible' do
@@ -87,8 +87,8 @@ describe Gitlab::Ci::Config::Extendable::Entry do
}
end
- it 'returns unchanged base hash' do
- expect(subject.base_hash!).to eq(script: 'rspec')
+ it 'returns unchanged base hashes' do
+ expect(subject.base_hashes!).to eq([{ script: 'rspec' }])
end
end
@@ -101,12 +101,12 @@ describe Gitlab::Ci::Config::Extendable::Entry do
}
end
- it 'extends the base hash first' do
- expect(subject.base_hash!).to eq(extends: 'first', script: 'rspec')
+ it 'extends the base hashes first' do
+ expect(subject.base_hashes!).to eq([{ extends: 'first', script: 'rspec' }])
end
it 'mutates original context' do
- subject.base_hash!
+ subject.base_hashes!
expect(hash.fetch(:second)).to eq(extends: 'first', script: 'rspec')
end
@@ -171,6 +171,34 @@ describe Gitlab::Ci::Config::Extendable::Entry do
end
end
+ context 'when extending multiple hashes correctly' do
+ let(:hash) do
+ {
+ first: { script: 'my value', image: 'ubuntu' },
+ second: { image: 'alpine' },
+ test: { extends: %w(first second) }
+ }
+ end
+
+ let(:result) do
+ {
+ first: { script: 'my value', image: 'ubuntu' },
+ second: { image: 'alpine' },
+ test: { extends: %w(first second), script: 'my value', image: 'alpine' }
+ }
+ end
+
+ it 'returns extended part of the hash' do
+ expect(subject.extend!).to eq result[:test]
+ end
+
+ it 'mutates original context' do
+ subject.extend!
+
+ expect(hash).to eq result
+ end
+ end
+
context 'when hash is not extensible' do
let(:hash) do
{
diff --git a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
index d8a61618e77..46d68097fff 100644
--- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Gitlab::Ci::Config::External::File::Remote do
+ include StubRequests
+
let(:context) { described_class::Context.new(nil, '12345', nil, Set.new) }
let(:params) { { remote: location } }
let(:remote_file) { described_class.new(params, context) }
@@ -46,7 +48,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
describe "#valid?" do
context 'when is a valid remote url' do
before do
- WebMock.stub_request(:get, location).to_return(body: remote_file_content)
+ stub_full_request(location).to_return(body: remote_file_content)
end
it 'returns true' do
@@ -92,7 +94,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
describe "#content" do
context 'with a valid remote file' do
before do
- WebMock.stub_request(:get, location).to_return(body: remote_file_content)
+ stub_full_request(location).to_return(body: remote_file_content)
end
it 'returns the content of the file' do
@@ -114,7 +116,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
let(:location) { 'https://asdasdasdaj48ggerexample.com' }
before do
- WebMock.stub_request(:get, location).to_raise(SocketError.new('Some HTTP error'))
+ stub_full_request(location).to_raise(SocketError.new('Some HTTP error'))
end
it 'is nil' do
@@ -144,7 +146,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'when timeout error has been raised' do
before do
- WebMock.stub_request(:get, location).to_timeout
+ stub_full_request(location).to_timeout
end
it 'returns error message about a timeout' do
@@ -154,7 +156,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'when HTTP error has been raised' do
before do
- WebMock.stub_request(:get, location).to_raise(Gitlab::HTTP::Error)
+ stub_full_request(location).to_raise(Gitlab::HTTP::Error)
end
it 'returns error message about a HTTP error' do
@@ -164,7 +166,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'when response has 404 status' do
before do
- WebMock.stub_request(:get, location).to_return(body: remote_file_content, status: 404)
+ stub_full_request(location).to_return(body: remote_file_content, status: 404)
end
it 'returns error message about a timeout' do
diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
index 136974569de..e068b786b02 100644
--- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Gitlab::Ci::Config::External::Mapper do
+ include StubRequests
+
set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
@@ -18,7 +20,7 @@ describe Gitlab::Ci::Config::External::Mapper do
end
before do
- WebMock.stub_request(:get, remote_url).to_return(body: file_content)
+ stub_full_request(remote_url).to_return(body: file_content)
end
describe '#process' do
diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb
index 0f58a4f1d44..856187371e1 100644
--- a/spec/lib/gitlab/ci/config/external/processor_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Gitlab::Ci::Config::External::Processor do
+ include StubRequests
+
set(:project) { create(:project, :repository) }
set(:another_project) { create(:project, :repository) }
set(:user) { create(:user) }
@@ -42,7 +44,7 @@ describe Gitlab::Ci::Config::External::Processor do
let(:values) { { include: remote_file, image: 'ruby:2.2' } }
before do
- WebMock.stub_request(:get, remote_file).to_raise(SocketError.new('Some HTTP error'))
+ stub_full_request(remote_file).and_raise(SocketError.new('Some HTTP error'))
end
it 'raises an error' do
@@ -75,7 +77,7 @@ describe Gitlab::Ci::Config::External::Processor do
end
before do
- WebMock.stub_request(:get, remote_file).to_return(body: external_file_content)
+ stub_full_request(remote_file).to_return(body: external_file_content)
end
it 'appends the file to the values' do
@@ -145,7 +147,7 @@ describe Gitlab::Ci::Config::External::Processor do
allow_any_instance_of(Gitlab::Ci::Config::External::File::Local)
.to receive(:fetch_local_content).and_return(local_file_content)
- WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content)
+ stub_full_request(remote_file).to_return(body: remote_file_content)
end
it 'appends the files to the values' do
@@ -191,7 +193,8 @@ describe Gitlab::Ci::Config::External::Processor do
end
it 'takes precedence' do
- WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content)
+ stub_full_request(remote_file).to_return(body: remote_file_content)
+
expect(processor.perform[:image]).to eq('ruby:2.2')
end
end
@@ -231,7 +234,8 @@ describe Gitlab::Ci::Config::External::Processor do
HEREDOC
end
- WebMock.stub_request(:get, 'http://my.domain.com/config.yml').to_return(body: 'remote_build: { script: echo Hello World }')
+ stub_full_request('http://my.domain.com/config.yml')
+ .to_return(body: 'remote_build: { script: echo Hello World }')
end
context 'when project is public' do
@@ -273,8 +277,10 @@ describe Gitlab::Ci::Config::External::Processor do
context 'when config includes an external configuration file via SSL web request' do
before do
- stub_request(:get, 'https://sha256.badssl.com/fake.yml').to_return(body: 'image: ruby:2.6', status: 200)
- stub_request(:get, 'https://self-signed.badssl.com/fake.yml')
+ stub_full_request('https://sha256.badssl.com/fake.yml', ip_address: '8.8.8.8')
+ .to_return(body: 'image: ruby:2.6', status: 200)
+
+ stub_full_request('https://self-signed.badssl.com/fake.yml', ip_address: '8.8.8.9')
.to_raise(OpenSSL::SSL::SSLError.new('SSL_connect returned=1 errno=0 state=error: certificate verify failed (self signed certificate)'))
end
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index 092e9f242b7..7f336ee853e 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Gitlab::Ci::Config do
+ include StubRequests
+
set(:user) { create(:user) }
let(:config) do
@@ -216,8 +218,7 @@ describe Gitlab::Ci::Config do
end
before do
- WebMock.stub_request(:get, remote_location)
- .to_return(body: remote_file_content)
+ stub_full_request(remote_location).to_return(body: remote_file_content)
allow(project.repository)
.to receive(:blob_data_at).and_return(local_file_content)
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb
new file mode 100644
index 00000000000..006ce4d8078
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb
@@ -0,0 +1,77 @@
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexeme::And do
+ let(:left) { double('left', evaluate: nil) }
+ let(:right) { double('right', evaluate: nil) }
+
+ describe '.build' do
+ it 'creates a new instance of the token' do
+ expect(described_class.build('&&', left, right)).to be_a(described_class)
+ end
+
+ context 'with non-evaluable operands' do
+ let(:left) { double('left') }
+ let(:right) { double('right') }
+
+ it 'raises an operator error' do
+ expect { described_class.build('&&', left, right) }.to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError
+ end
+ end
+ end
+
+ describe '.type' do
+ it 'is an operator' do
+ expect(described_class.type).to eq :operator
+ end
+ end
+
+ describe '.precedence' do
+ it 'has a precedence' do
+ expect(described_class.precedence).to be_an Integer
+ end
+ end
+
+ describe '#evaluate' do
+ let(:operator) { described_class.new(left, right) }
+
+ subject { operator.evaluate }
+
+ before do
+ allow(left).to receive(:evaluate).and_return(left_value)
+ allow(right).to receive(:evaluate).and_return(right_value)
+ end
+
+ context 'when left and right are truthy' do
+ where(:left_value, :right_value) do
+ [true, 1, 'a'].permutation(2).to_a
+ end
+
+ with_them do
+ it { is_expected.to be_truthy }
+ it { is_expected.to eq(right_value) }
+ end
+ end
+
+ context 'when left or right is falsey' do
+ where(:left_value, :right_value) do
+ [true, false, nil].permutation(2).to_a
+ end
+
+ with_them do
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when left and right are falsey' do
+ where(:left_value, :right_value) do
+ [false, nil].permutation(2).to_a
+ end
+
+ with_them do
+ it { is_expected.to be_falsey }
+ it { is_expected.to eq(left_value) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb
index 019a2ed184d..fcbd2863289 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb
@@ -5,9 +5,21 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Equals do
let(:right) { double('right') }
describe '.build' do
- it 'creates a new instance of the token' do
- expect(described_class.build('==', left, right))
- .to be_a(described_class)
+ context 'with non-evaluable operands' do
+ it 'creates a new instance of the token' do
+ expect { described_class.build('==', left, right) }
+ .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError
+ end
+ end
+
+ context 'with evaluable operands' do
+ it 'creates a new instance of the token' do
+ allow(left).to receive(:evaluate).and_return('my-string')
+ allow(right).to receive(:evaluate).and_return('my-string')
+
+ expect(described_class.build('==', left, right))
+ .to be_a(described_class)
+ end
end
end
@@ -17,23 +29,40 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Equals do
end
end
+ describe '.precedence' do
+ it 'has a precedence' do
+ expect(described_class.precedence).to be_an Integer
+ end
+ end
+
describe '#evaluate' do
- it 'returns false when left and right are not equal' do
- allow(left).to receive(:evaluate).and_return(1)
- allow(right).to receive(:evaluate).and_return(2)
+ let(:operator) { described_class.new(left, right) }
- operator = described_class.new(left, right)
+ subject { operator.evaluate }
- expect(operator.evaluate(VARIABLE: 3)).to eq false
+ before do
+ allow(left).to receive(:evaluate).and_return(left_value)
+ allow(right).to receive(:evaluate).and_return(right_value)
end
- it 'returns true when left and right are equal' do
- allow(left).to receive(:evaluate).and_return(1)
- allow(right).to receive(:evaluate).and_return(1)
+ context 'when left and right are equal' do
+ where(:left_value, :right_value) do
+ [%w(string string)]
+ end
+
+ with_them do
+ it { is_expected.to eq(true) }
+ end
+ end
- operator = described_class.new(left, right)
+ context 'when left and right are not equal' do
+ where(:left_value, :right_value) do
+ ['one string', 'two string'].permutation(2).to_a
+ end
- expect(operator.evaluate(VARIABLE: 3)).to eq true
+ with_them do
+ it { is_expected.to eq(false) }
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
index 49e5af52f4d..97da66d2bcc 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
@@ -6,9 +6,21 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do
let(:right) { double('right') }
describe '.build' do
- it 'creates a new instance of the token' do
- expect(described_class.build('=~', left, right))
- .to be_a(described_class)
+ context 'with non-evaluable operands' do
+ it 'creates a new instance of the token' do
+ expect { described_class.build('=~', left, right) }
+ .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError
+ end
+ end
+
+ context 'with evaluable operands' do
+ it 'creates a new instance of the token' do
+ allow(left).to receive(:evaluate).and_return('my-string')
+ allow(right).to receive(:evaluate).and_return('/my-string/')
+
+ expect(described_class.build('=~', left, right))
+ .to be_a(described_class)
+ end
end
end
@@ -18,63 +30,93 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do
end
end
+ describe '.precedence' do
+ it 'has a precedence' do
+ expect(described_class.precedence).to be_an Integer
+ end
+ end
+
describe '#evaluate' do
- it 'returns false when left and right do not match' do
- allow(left).to receive(:evaluate).and_return('my-string')
- allow(right).to receive(:evaluate)
- .and_return(Gitlab::UntrustedRegexp.new('something'))
+ let(:operator) { described_class.new(left, right) }
- operator = described_class.new(left, right)
+ subject { operator.evaluate }
- expect(operator.evaluate).to eq false
+ before do
+ allow(left).to receive(:evaluate).and_return(left_value)
+ allow(right).to receive(:evaluate).and_return(right_value)
end
- it 'returns true when left and right match' do
- allow(left).to receive(:evaluate).and_return('my-awesome-string')
- allow(right).to receive(:evaluate)
- .and_return(Gitlab::UntrustedRegexp.new('awesome.string$'))
+ context 'when left and right do not match' do
+ let(:left_value) { 'my-string' }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('something') }
- operator = described_class.new(left, right)
-
- expect(operator.evaluate).to eq true
+ it { is_expected.to eq(nil) }
end
- it 'supports matching against a nil value' do
- allow(left).to receive(:evaluate).and_return(nil)
- allow(right).to receive(:evaluate)
- .and_return(Gitlab::UntrustedRegexp.new('pattern'))
+ context 'when left and right match' do
+ let(:left_value) { 'my-awesome-string' }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('awesome.string$') }
+
+ it { is_expected.to eq(3) }
+ end
- operator = described_class.new(left, right)
+ context 'when left is nil' do
+ let(:left_value) { nil }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('pattern') }
- expect(operator.evaluate).to eq false
+ it { is_expected.to eq(nil) }
end
- it 'supports multiline strings' do
- allow(left).to receive(:evaluate).and_return <<~TEXT
- My awesome contents
+ context 'when left is a multiline string and matches right' do
+ let(:left_value) do
+ <<~TEXT
+ My awesome contents
+
+ My-text-string!
+ TEXT
+ end
+
+ let(:right_value) { Gitlab::UntrustedRegexp.new('text-string') }
+
+ it { is_expected.to eq(24) }
+ end
- My-text-string!
- TEXT
+ context 'when left is a multiline string and does not match right' do
+ let(:left_value) do
+ <<~TEXT
+ My awesome contents
- allow(right).to receive(:evaluate)
- .and_return(Gitlab::UntrustedRegexp.new('text-string'))
+ My-terrible-string!
+ TEXT
+ end
- operator = described_class.new(left, right)
+ let(:right_value) { Gitlab::UntrustedRegexp.new('text-string') }
- expect(operator.evaluate).to eq true
+ it { is_expected.to eq(nil) }
end
- it 'supports regexp flags' do
- allow(left).to receive(:evaluate).and_return <<~TEXT
- My AWESOME content
- TEXT
+ context 'when a matching pattern uses regex flags' do
+ let(:left_value) do
+ <<~TEXT
+ My AWESOME content
+ TEXT
+ end
+
+ let(:right_value) { Gitlab::UntrustedRegexp.new('(?i)awesome') }
+
+ it { is_expected.to eq(3) }
+ end
- allow(right).to receive(:evaluate)
- .and_return(Gitlab::UntrustedRegexp.new('(?i)awesome'))
+ context 'when a non-matching pattern uses regex flags' do
+ let(:left_value) do
+ <<~TEXT
+ My AWESOME content
+ TEXT
+ end
- operator = described_class.new(left, right)
+ let(:right_value) { Gitlab::UntrustedRegexp.new('(?i)terrible') }
- expect(operator.evaluate).to eq true
+ it { is_expected.to eq(nil) }
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb
index 9aa2f4efd67..38d30c9035a 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb
@@ -5,9 +5,21 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotEquals do
let(:right) { double('right') }
describe '.build' do
- it 'creates a new instance of the token' do
- expect(described_class.build('!=', left, right))
- .to be_a(described_class)
+ context 'with non-evaluable operands' do
+ it 'creates a new instance of the token' do
+ expect { described_class.build('!=', left, right) }
+ .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError
+ end
+ end
+
+ context 'with evaluable operands' do
+ it 'creates a new instance of the token' do
+ allow(left).to receive(:evaluate).and_return('my-string')
+ allow(right).to receive(:evaluate).and_return('my-string')
+
+ expect(described_class.build('!=', left, right))
+ .to be_a(described_class)
+ end
end
end
@@ -17,23 +29,45 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotEquals do
end
end
+ describe '.precedence' do
+ it 'has a precedence' do
+ expect(described_class.precedence).to be_an Integer
+ end
+ end
+
describe '#evaluate' do
- it 'returns true when left and right are not equal' do
- allow(left).to receive(:evaluate).and_return(1)
- allow(right).to receive(:evaluate).and_return(2)
+ let(:operator) { described_class.new(left, right) }
- operator = described_class.new(left, right)
+ subject { operator.evaluate }
- expect(operator.evaluate(VARIABLE: 3)).to eq true
+ before do
+ allow(left).to receive(:evaluate).and_return(left_value)
+ allow(right).to receive(:evaluate).and_return(right_value)
end
- it 'returns false when left and right are equal' do
- allow(left).to receive(:evaluate).and_return(1)
- allow(right).to receive(:evaluate).and_return(1)
+ context 'when left and right are equal' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:left_value, :right_value) do
+ 'string' | 'string'
+ 1 | 1
+ '' | ''
+ nil | nil
+ end
+
+ with_them do
+ it { is_expected.to eq(false) }
+ end
+ end
- operator = described_class.new(left, right)
+ context 'when left and right are not equal' do
+ where(:left_value, :right_value) do
+ ['one string', 'two string', 1, 2, '', nil, false, true].permutation(2).to_a
+ end
- expect(operator.evaluate(VARIABLE: 3)).to eq false
+ with_them do
+ it { is_expected.to eq(true) }
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
index fa3b9651fb4..99110ff8d88 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
@@ -6,9 +6,21 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches do
let(:right) { double('right') }
describe '.build' do
- it 'creates a new instance of the token' do
- expect(described_class.build('!~', left, right))
- .to be_a(described_class)
+ context 'with non-evaluable operands' do
+ it 'creates a new instance of the token' do
+ expect { described_class.build('!~', left, right) }
+ .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError
+ end
+ end
+
+ context 'with evaluable operands' do
+ it 'creates a new instance of the token' do
+ allow(left).to receive(:evaluate).and_return('my-string')
+ allow(right).to receive(:evaluate).and_return('my-string')
+
+ expect(described_class.build('!~', left, right))
+ .to be_a(described_class)
+ end
end
end
@@ -18,63 +30,93 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches do
end
end
+ describe '.precedence' do
+ it 'has a precedence' do
+ expect(described_class.precedence).to be_an Integer
+ end
+ end
+
describe '#evaluate' do
- it 'returns true when left and right do not match' do
- allow(left).to receive(:evaluate).and_return('my-string')
- allow(right).to receive(:evaluate)
- .and_return(Gitlab::UntrustedRegexp.new('something'))
+ let(:operator) { described_class.new(left, right) }
- operator = described_class.new(left, right)
+ subject { operator.evaluate }
- expect(operator.evaluate).to eq true
+ before do
+ allow(left).to receive(:evaluate).and_return(left_value)
+ allow(right).to receive(:evaluate).and_return(right_value)
end
- it 'returns false when left and right match' do
- allow(left).to receive(:evaluate).and_return('my-awesome-string')
- allow(right).to receive(:evaluate)
- .and_return(Gitlab::UntrustedRegexp.new('awesome.string$'))
+ context 'when left and right do not match' do
+ let(:left_value) { 'my-string' }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('something') }
- operator = described_class.new(left, right)
-
- expect(operator.evaluate).to eq false
+ it { is_expected.to eq(true) }
end
- it 'supports matching against a nil value' do
- allow(left).to receive(:evaluate).and_return(nil)
- allow(right).to receive(:evaluate)
- .and_return(Gitlab::UntrustedRegexp.new('pattern'))
+ context 'when left and right match' do
+ let(:left_value) { 'my-awesome-string' }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('awesome.string$') }
+
+ it { is_expected.to eq(false) }
+ end
- operator = described_class.new(left, right)
+ context 'when left is nil' do
+ let(:left_value) { nil }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('pattern') }
- expect(operator.evaluate).to eq true
+ it { is_expected.to eq(true) }
end
- it 'supports multiline strings' do
- allow(left).to receive(:evaluate).and_return <<~TEXT
- My awesome contents
+ context 'when left is a multiline string and matches right' do
+ let(:left_value) do
+ <<~TEXT
+ My awesome contents
+
+ My-text-string!
+ TEXT
+ end
+
+ let(:right_value) { Gitlab::UntrustedRegexp.new('text-string') }
+
+ it { is_expected.to eq(false) }
+ end
- My-text-string!
- TEXT
+ context 'when left is a multiline string and does not match right' do
+ let(:left_value) do
+ <<~TEXT
+ My awesome contents
- allow(right).to receive(:evaluate)
- .and_return(Gitlab::UntrustedRegexp.new('text-string'))
+ My-terrible-string!
+ TEXT
+ end
- operator = described_class.new(left, right)
+ let(:right_value) { Gitlab::UntrustedRegexp.new('text-string') }
- expect(operator.evaluate).to eq false
+ it { is_expected.to eq(true) }
end
- it 'supports regexp flags' do
- allow(left).to receive(:evaluate).and_return <<~TEXT
- My AWESOME content
- TEXT
+ context 'when a matching pattern uses regex flags' do
+ let(:left_value) do
+ <<~TEXT
+ My AWESOME content
+ TEXT
+ end
+
+ let(:right_value) { Gitlab::UntrustedRegexp.new('(?i)awesome') }
+
+ it { is_expected.to eq(false) }
+ end
- allow(right).to receive(:evaluate)
- .and_return(Gitlab::UntrustedRegexp.new('(?i)awesome'))
+ context 'when a non-matching pattern uses regex flags' do
+ let(:left_value) do
+ <<~TEXT
+ My AWESOME content
+ TEXT
+ end
- operator = described_class.new(left, right)
+ let(:right_value) { Gitlab::UntrustedRegexp.new('(?i)terrible') }
- expect(operator.evaluate).to eq false
+ it { is_expected.to eq(true) }
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb
new file mode 100644
index 00000000000..d542eebc613
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb
@@ -0,0 +1,77 @@
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexeme::Or do
+ let(:left) { double('left', evaluate: nil) }
+ let(:right) { double('right', evaluate: nil) }
+
+ describe '.build' do
+ it 'creates a new instance of the token' do
+ expect(described_class.build('||', left, right)).to be_a(described_class)
+ end
+
+ context 'with non-evaluable operands' do
+ let(:left) { double('left') }
+ let(:right) { double('right') }
+
+ it 'raises an operator error' do
+ expect { described_class.build('||', left, right) }.to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError
+ end
+ end
+ end
+
+ describe '.type' do
+ it 'is an operator' do
+ expect(described_class.type).to eq :operator
+ end
+ end
+
+ describe '.precedence' do
+ it 'has a precedence' do
+ expect(described_class.precedence).to be_an Integer
+ end
+ end
+
+ describe '#evaluate' do
+ let(:operator) { described_class.new(left, right) }
+
+ subject { operator.evaluate }
+
+ before do
+ allow(left).to receive(:evaluate).and_return(left_value)
+ allow(right).to receive(:evaluate).and_return(right_value)
+ end
+
+ context 'when left and right are truthy' do
+ where(:left_value, :right_value) do
+ [true, 1, 'a'].permutation(2).to_a
+ end
+
+ with_them do
+ it { is_expected.to be_truthy }
+ it { is_expected.to eq(left_value) }
+ end
+ end
+
+ context 'when left or right is truthy' do
+ where(:left_value, :right_value) do
+ [true, false, 'a'].permutation(2).to_a
+ end
+
+ with_them do
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when left and right are falsey' do
+ where(:left_value, :right_value) do
+ [false, nil].permutation(2).to_a
+ end
+
+ with_them do
+ it { is_expected.to be_falsey }
+ it { is_expected.to eq(right_value) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb
index cff7f57ceff..30ea3f3e28e 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb
@@ -1,4 +1,4 @@
-require 'fast_spec_helper'
+require 'spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do
describe '.build' do
@@ -30,16 +30,6 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do
.to eq Gitlab::UntrustedRegexp.new('pattern')
end
- it 'is a greedy scanner for regexp boundaries' do
- scanner = StringScanner.new('/some .* / pattern/')
-
- token = described_class.scan(scanner)
-
- expect(token).not_to be_nil
- expect(token.build.evaluate)
- .to eq Gitlab::UntrustedRegexp.new('some .* / pattern')
- end
-
it 'does not allow to use an empty pattern' do
scanner = StringScanner.new(%(//))
@@ -68,12 +58,90 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do
.to eq Gitlab::UntrustedRegexp.new('(?im)pattern')
end
- it 'does not support arbitrary flags' do
+ it 'ignores unsupported flags' do
scanner = StringScanner.new('/pattern/x')
token = described_class.scan(scanner)
- expect(token).to be_nil
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('pattern')
+ end
+
+ it 'is a eager scanner for regexp boundaries' do
+ scanner = StringScanner.new('/some .* / pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('some .* ')
+ end
+
+ it 'does not match on escaped regexp boundaries' do
+ scanner = StringScanner.new('/some .* \/ pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('some .* / pattern')
+ end
+
+ it 'recognizes \ as an escape character for /' do
+ scanner = StringScanner.new('/some numeric \/$ pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('some numeric /$ pattern')
+ end
+
+ it 'does not recognize \ as an escape character for $' do
+ scanner = StringScanner.new('/some numeric \$ pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('some numeric \$ pattern')
+ end
+
+ context 'with the ci_variables_complex_expressions feature flag disabled' do
+ before do
+ stub_feature_flags(ci_variables_complex_expressions: false)
+ end
+
+ it 'is a greedy scanner for regexp boundaries' do
+ scanner = StringScanner.new('/some .* / pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('some .* / pattern')
+ end
+
+ it 'does not recognize the \ escape character for /' do
+ scanner = StringScanner.new('/some .* \/ pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('some .* \/ pattern')
+ end
+
+ it 'does not recognize the \ escape character for $' do
+ scanner = StringScanner.new('/some numeric \$ pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('some numeric \$ pattern')
+ end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
index 3f11b3f7673..d8db9c262a1 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
@@ -58,6 +58,56 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do
expect { lexer.tokens }
.to raise_error described_class::SyntaxError
end
+
+ context 'with complex expressions' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { described_class.new(expression).tokens.map(&:value) }
+
+ where(:expression, :tokens) do
+ '$PRESENT_VARIABLE =~ /my var/ && $EMPTY_VARIABLE =~ /nope/' | ['$PRESENT_VARIABLE', '=~', '/my var/', '&&', '$EMPTY_VARIABLE', '=~', '/nope/']
+ '$EMPTY_VARIABLE == "" && $PRESENT_VARIABLE' | ['$EMPTY_VARIABLE', '==', '""', '&&', '$PRESENT_VARIABLE']
+ '$EMPTY_VARIABLE == "" && $PRESENT_VARIABLE != "nope"' | ['$EMPTY_VARIABLE', '==', '""', '&&', '$PRESENT_VARIABLE', '!=', '"nope"']
+ '$PRESENT_VARIABLE && $EMPTY_VARIABLE' | ['$PRESENT_VARIABLE', '&&', '$EMPTY_VARIABLE']
+ '$PRESENT_VARIABLE =~ /my var/ || $EMPTY_VARIABLE =~ /nope/' | ['$PRESENT_VARIABLE', '=~', '/my var/', '||', '$EMPTY_VARIABLE', '=~', '/nope/']
+ '$EMPTY_VARIABLE == "" || $PRESENT_VARIABLE' | ['$EMPTY_VARIABLE', '==', '""', '||', '$PRESENT_VARIABLE']
+ '$EMPTY_VARIABLE == "" || $PRESENT_VARIABLE != "nope"' | ['$EMPTY_VARIABLE', '==', '""', '||', '$PRESENT_VARIABLE', '!=', '"nope"']
+ '$PRESENT_VARIABLE || $EMPTY_VARIABLE' | ['$PRESENT_VARIABLE', '||', '$EMPTY_VARIABLE']
+ '$PRESENT_VARIABLE && null || $EMPTY_VARIABLE == ""' | ['$PRESENT_VARIABLE', '&&', 'null', '||', '$EMPTY_VARIABLE', '==', '""']
+ end
+
+ with_them do
+ it { is_expected.to eq(tokens) }
+ end
+ end
+
+ context 'with the ci_variables_complex_expressions feature flag turned off' do
+ before do
+ stub_feature_flags(ci_variables_complex_expressions: false)
+ end
+
+ it 'incorrectly tokenizes conjunctive match statements as one match statement' do
+ tokens = described_class.new('$PRESENT_VARIABLE =~ /my var/ && $EMPTY_VARIABLE =~ /nope/').tokens
+
+ expect(tokens.map(&:value)).to eq(['$PRESENT_VARIABLE', '=~', '/my var/ && $EMPTY_VARIABLE =~ /nope/'])
+ end
+
+ it 'incorrectly tokenizes disjunctive match statements as one statement' do
+ tokens = described_class.new('$PRESENT_VARIABLE =~ /my var/ || $EMPTY_VARIABLE =~ /nope/').tokens
+
+ expect(tokens.map(&:value)).to eq(['$PRESENT_VARIABLE', '=~', '/my var/ || $EMPTY_VARIABLE =~ /nope/'])
+ end
+
+ it 'raises an error about && operators' do
+ expect { described_class.new('$EMPTY_VARIABLE == "" && $PRESENT_VARIABLE').tokens }
+ .to raise_error(Gitlab::Ci::Pipeline::Expression::Lexer::SyntaxError).with_message('Unknown lexeme found!')
+ end
+
+ it 'raises an error about || operators' do
+ expect { described_class.new('$EMPTY_VARIABLE == "" || $PRESENT_VARIABLE').tokens }
+ .to raise_error(Gitlab::Ci::Pipeline::Expression::Lexer::SyntaxError).with_message('Unknown lexeme found!')
+ end
+ end
end
describe '#lexemes' do
diff --git a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
index 2b78b1dd4a7..e88ec5561b6 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
@@ -2,25 +2,67 @@ require 'fast_spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Parser do
describe '#tree' do
- context 'when using operators' do
+ context 'when using two operators' do
+ it 'returns a reverse descent parse tree' do
+ expect(described_class.seed('$VAR1 == "123"').tree)
+ .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals
+ end
+ end
+
+ context 'when using three operators' do
it 'returns a reverse descent parse tree' do
expect(described_class.seed('$VAR1 == "123" == $VAR2').tree)
.to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals
end
end
- context 'when using a single token' do
+ context 'when using a single variable token' do
it 'returns a single token instance' do
expect(described_class.seed('$VAR').tree)
.to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Variable
end
end
+ context 'when using a single string token' do
+ it 'returns a single token instance' do
+ expect(described_class.seed('"some value"').tree)
+ .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::String
+ end
+ end
+
context 'when expression is empty' do
it 'returns a null token' do
- expect(described_class.seed('').tree)
+ expect { described_class.seed('').tree }
+ .to raise_error Gitlab::Ci::Pipeline::Expression::Parser::ParseError
+ end
+ end
+
+ context 'when expression is null' do
+ it 'returns a null token' do
+ expect(described_class.seed('null').tree)
.to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Null
end
end
+
+ context 'when two value tokens have no operator' do
+ it 'raises a parsing error' do
+ expect { described_class.seed('$VAR "text"').tree }
+ .to raise_error Gitlab::Ci::Pipeline::Expression::Parser::ParseError
+ end
+ end
+
+ context 'when an operator has no left side' do
+ it 'raises an OperatorError' do
+ expect { described_class.seed('== "123"').tree }
+ .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError
+ end
+ end
+
+ context 'when an operator has no right side' do
+ it 'raises an OperatorError' do
+ expect { described_class.seed('$VAR ==').tree }
+ .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
index a9fd809409b..057e2f3fbe8 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
@@ -1,4 +1,6 @@
-require 'fast_spec_helper'
+# TODO switch this back after the "ci_variables_complex_expressions" feature flag is removed
+# require 'fast_spec_helper'
+require 'spec_helper'
require 'rspec-parameterized'
describe Gitlab::Ci::Pipeline::Expression::Statement do
@@ -7,8 +9,12 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
end
let(:variables) do
- { 'PRESENT_VARIABLE' => 'my variable',
- EMPTY_VARIABLE: '' }
+ {
+ 'PRESENT_VARIABLE' => 'my variable',
+ 'PATH_VARIABLE' => 'a/path/variable/value',
+ 'FULL_PATH_VARIABLE' => '/a/full/path/variable/value',
+ 'EMPTY_VARIABLE' => ''
+ }
end
describe '.new' do
@@ -21,105 +27,158 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
end
end
- describe '#parse_tree' do
- context 'when expression is empty' do
- let(:text) { '' }
-
- it 'raises an error' do
- expect { subject.parse_tree }
- .to raise_error described_class::StatementError
- end
- end
+ describe '#evaluate' do
+ using RSpec::Parameterized::TableSyntax
- context 'when expression grammar is incorrect' do
- table = [
- '$VAR "text"', # missing operator
- '== "123"', # invalid left side
- '"some string"', # only string provided
- '$VAR ==', # invalid right side
- 'null', # missing lexemes
- '' # empty statement
- ]
-
- table.each do |syntax|
- context "when expression grammar is #{syntax.inspect}" do
- let(:text) { syntax }
-
- it 'raises a statement error exception' do
- expect { subject.parse_tree }
- .to raise_error described_class::StatementError
- end
-
- it 'is an invalid statement' do
- expect(subject).not_to be_valid
- end
- end
- end
+ where(:expression, :value) do
+ '$PRESENT_VARIABLE == "my variable"' | true
+ '"my variable" == $PRESENT_VARIABLE' | true
+ '$PRESENT_VARIABLE == null' | false
+ '$EMPTY_VARIABLE == null' | false
+ '"" == $EMPTY_VARIABLE' | true
+ '$EMPTY_VARIABLE' | ''
+ '$UNDEFINED_VARIABLE == null' | true
+ 'null == $UNDEFINED_VARIABLE' | true
+ '$PRESENT_VARIABLE' | 'my variable'
+ '$UNDEFINED_VARIABLE' | nil
+ "$PRESENT_VARIABLE =~ /var.*e$/" | 3
+ '$PRESENT_VARIABLE =~ /va\r.*e$/' | nil
+ '$PRESENT_VARIABLE =~ /va\/r.*e$/' | nil
+ "$PRESENT_VARIABLE =~ /var.*e$/" | 3
+ "$PRESENT_VARIABLE =~ /^var.*/" | nil
+ "$EMPTY_VARIABLE =~ /var.*/" | nil
+ "$UNDEFINED_VARIABLE =~ /var.*/" | nil
+ "$PRESENT_VARIABLE =~ /VAR.*/i" | 3
+ '$PATH_VARIABLE =~ /path\/variable/' | 2
+ '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/' | 0
+ '$FULL_PATH_VARIABLE =~ /\\/path\\/variable\\/value$/' | 7
+ '$PRESENT_VARIABLE != "my variable"' | false
+ '"my variable" != $PRESENT_VARIABLE' | false
+ '$PRESENT_VARIABLE != null' | true
+ '$EMPTY_VARIABLE != null' | true
+ '"" != $EMPTY_VARIABLE' | false
+ '$UNDEFINED_VARIABLE != null' | false
+ 'null != $UNDEFINED_VARIABLE' | false
+ "$PRESENT_VARIABLE !~ /var.*e$/" | false
+ "$PRESENT_VARIABLE !~ /^var.*/" | true
+ '$PRESENT_VARIABLE !~ /^v\ar.*/' | true
+ '$PRESENT_VARIABLE !~ /^v\/ar.*/' | true
+ "$EMPTY_VARIABLE !~ /var.*/" | true
+ "$UNDEFINED_VARIABLE !~ /var.*/" | true
+ "$PRESENT_VARIABLE !~ /VAR.*/i" | false
+
+ '$PRESENT_VARIABLE && "string"' | 'string'
+ '$PRESENT_VARIABLE && $PRESENT_VARIABLE' | 'my variable'
+ '$PRESENT_VARIABLE && $EMPTY_VARIABLE' | ''
+ '$PRESENT_VARIABLE && null' | nil
+ '"string" && $PRESENT_VARIABLE' | 'my variable'
+ '$EMPTY_VARIABLE && $PRESENT_VARIABLE' | 'my variable'
+ 'null && $PRESENT_VARIABLE' | nil
+ '$EMPTY_VARIABLE && "string"' | 'string'
+ '$EMPTY_VARIABLE && $EMPTY_VARIABLE' | ''
+ '"string" && $EMPTY_VARIABLE' | ''
+ '"string" && null' | nil
+ 'null && "string"' | nil
+ '"string" && "string"' | 'string'
+ 'null && null' | nil
+
+ '$PRESENT_VARIABLE =~ /my var/ && $EMPTY_VARIABLE =~ /nope/' | nil
+ '$EMPTY_VARIABLE == "" && $PRESENT_VARIABLE' | 'my variable'
+ '$EMPTY_VARIABLE == "" && $PRESENT_VARIABLE != "nope"' | true
+ '$PRESENT_VARIABLE && $EMPTY_VARIABLE' | ''
+ '$PRESENT_VARIABLE && $UNDEFINED_VARIABLE' | nil
+ '$UNDEFINED_VARIABLE && $EMPTY_VARIABLE' | nil
+ '$UNDEFINED_VARIABLE && $PRESENT_VARIABLE' | nil
+
+ '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/ && $PATH_VARIABLE =~ /path\/variable/' | 2
+ '$FULL_PATH_VARIABLE =~ /^\/a\/bad\/path\/variable\/value$/ && $PATH_VARIABLE =~ /path\/variable/' | nil
+ '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/ && $PATH_VARIABLE =~ /bad\/path\/variable/' | nil
+ '$FULL_PATH_VARIABLE =~ /^\/a\/bad\/path\/variable\/value$/ && $PATH_VARIABLE =~ /bad\/path\/variable/' | nil
+
+ '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/ || $PATH_VARIABLE =~ /path\/variable/' | 0
+ '$FULL_PATH_VARIABLE =~ /^\/a\/bad\/path\/variable\/value$/ || $PATH_VARIABLE =~ /path\/variable/' | 2
+ '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/ || $PATH_VARIABLE =~ /bad\/path\/variable/' | 0
+ '$FULL_PATH_VARIABLE =~ /^\/a\/bad\/path\/variable\/value$/ || $PATH_VARIABLE =~ /bad\/path\/variable/' | nil
+
+ '$PRESENT_VARIABLE =~ /my var/ || $EMPTY_VARIABLE =~ /nope/' | 0
+ '$EMPTY_VARIABLE == "" || $PRESENT_VARIABLE' | true
+ '$PRESENT_VARIABLE != "nope" || $EMPTY_VARIABLE == ""' | true
+
+ '$PRESENT_VARIABLE && null || $EMPTY_VARIABLE == ""' | true
+ '$PRESENT_VARIABLE || $UNDEFINED_VARIABLE' | 'my variable'
+ '$UNDEFINED_VARIABLE || $PRESENT_VARIABLE' | 'my variable'
+ '$UNDEFINED_VARIABLE == null || $PRESENT_VARIABLE' | true
+ '$PRESENT_VARIABLE || $UNDEFINED_VARIABLE == null' | 'my variable'
end
- context 'when expression grammar is correct' do
- context 'when using an operator' do
- let(:text) { '$VAR == "value"' }
-
- it 'returns a reverse descent parse tree' do
- expect(subject.parse_tree)
- .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals
- end
+ with_them do
+ let(:text) { expression }
- it 'is a valid statement' do
- expect(subject).to be_valid
- end
+ it "evaluates to `#{params[:value].inspect}`" do
+ expect(subject.evaluate).to eq(value)
end
- context 'when using a single token' do
- let(:text) { '$PRESENT_VARIABLE' }
+ # This test is used to ensure that our parser
+ # returns exactly the same results as if we
+ # were evaluating using ruby's `eval`
+ context 'when using Ruby eval' do
+ let(:expression_ruby) do
+ expression
+ .gsub(/null/, 'nil')
+ .gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)/) { "variables['#{Regexp.last_match(1)}']" }
+ end
- it 'returns a single token instance' do
- expect(subject.parse_tree)
- .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Variable
+ it 'behaves exactly the same' do
+ expect(instance_eval(expression_ruby)).to eq(subject.evaluate)
end
end
end
- end
- describe '#evaluate' do
- using RSpec::Parameterized::TableSyntax
+ context 'with the ci_variables_complex_expressions feature flag disabled' do
+ before do
+ stub_feature_flags(ci_variables_complex_expressions: false)
+ end
- where(:expression, :value) do
- '$PRESENT_VARIABLE == "my variable"' | true
- '"my variable" == $PRESENT_VARIABLE' | true
- '$PRESENT_VARIABLE == null' | false
- '$EMPTY_VARIABLE == null' | false
- '"" == $EMPTY_VARIABLE' | true
- '$EMPTY_VARIABLE' | ''
- '$UNDEFINED_VARIABLE == null' | true
- 'null == $UNDEFINED_VARIABLE' | true
- '$PRESENT_VARIABLE' | 'my variable'
- '$UNDEFINED_VARIABLE' | nil
- "$PRESENT_VARIABLE =~ /var.*e$/" | true
- "$PRESENT_VARIABLE =~ /^var.*/" | false
- "$EMPTY_VARIABLE =~ /var.*/" | false
- "$UNDEFINED_VARIABLE =~ /var.*/" | false
- "$PRESENT_VARIABLE =~ /VAR.*/i" | true
- '$PRESENT_VARIABLE != "my variable"' | false
- '"my variable" != $PRESENT_VARIABLE' | false
- '$PRESENT_VARIABLE != null' | true
- '$EMPTY_VARIABLE != null' | true
- '"" != $EMPTY_VARIABLE' | false
- '$UNDEFINED_VARIABLE != null' | false
- 'null != $UNDEFINED_VARIABLE' | false
- "$PRESENT_VARIABLE !~ /var.*e$/" | false
- "$PRESENT_VARIABLE !~ /^var.*/" | true
- "$EMPTY_VARIABLE !~ /var.*/" | true
- "$UNDEFINED_VARIABLE !~ /var.*/" | true
- "$PRESENT_VARIABLE !~ /VAR.*/i" | false
- end
+ where(:expression, :value) do
+ '$PRESENT_VARIABLE == "my variable"' | true
+ '"my variable" == $PRESENT_VARIABLE' | true
+ '$PRESENT_VARIABLE == null' | false
+ '$EMPTY_VARIABLE == null' | false
+ '"" == $EMPTY_VARIABLE' | true
+ '$EMPTY_VARIABLE' | ''
+ '$UNDEFINED_VARIABLE == null' | true
+ 'null == $UNDEFINED_VARIABLE' | true
+ '$PRESENT_VARIABLE' | 'my variable'
+ '$UNDEFINED_VARIABLE' | nil
+ "$PRESENT_VARIABLE =~ /var.*e$/" | true
+ "$PRESENT_VARIABLE =~ /^var.*/" | false
+ "$EMPTY_VARIABLE =~ /var.*/" | false
+ "$UNDEFINED_VARIABLE =~ /var.*/" | false
+ "$PRESENT_VARIABLE =~ /VAR.*/i" | true
+ '$PATH_VARIABLE =~ /path/variable/' | true
+ '$PATH_VARIABLE =~ /path\/variable/' | true
+ '$FULL_PATH_VARIABLE =~ /^/a/full/path/variable/value$/' | true
+ '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/' | true
+ '$PRESENT_VARIABLE != "my variable"' | false
+ '"my variable" != $PRESENT_VARIABLE' | false
+ '$PRESENT_VARIABLE != null' | true
+ '$EMPTY_VARIABLE != null' | true
+ '"" != $EMPTY_VARIABLE' | false
+ '$UNDEFINED_VARIABLE != null' | false
+ 'null != $UNDEFINED_VARIABLE' | false
+ "$PRESENT_VARIABLE !~ /var.*e$/" | false
+ "$PRESENT_VARIABLE !~ /^var.*/" | true
+ "$EMPTY_VARIABLE !~ /var.*/" | true
+ "$UNDEFINED_VARIABLE !~ /var.*/" | true
+ "$PRESENT_VARIABLE !~ /VAR.*/i" | false
+ end
- with_them do
- let(:text) { expression }
+ with_them do
+ let(:text) { expression }
- it "evaluates to `#{params[:value].inspect}`" do
- expect(subject.evaluate).to eq value
+ it "evaluates to `#{params[:value].inspect}`" do
+ expect(subject.evaluate).to eq value
+ end
end
end
end
@@ -137,6 +196,8 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
'$INVALID = 1' | false
"$PRESENT_VARIABLE =~ /var.*/" | true
"$UNDEFINED_VARIABLE =~ /var.*/" | false
+ "$PRESENT_VARIABLE !~ /var.*/" | false
+ "$UNDEFINED_VARIABLE !~ /var.*/" | true
end
with_them do
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index 025439f1b6e..b6231510b91 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -163,11 +163,11 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
- .to eq [Gitlab::Ci::Status::Build::Canceled]
+ .to eq [Gitlab::Ci::Status::Build::Canceled, Gitlab::Ci::Status::Build::Retryable]
end
- it 'does not fabricate a retryable build status' do
- expect(status).not_to be_a Gitlab::Ci::Status::Build::Retryable
+ it 'fabricates a retryable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
end
it 'fabricates status with correct details' do
@@ -177,7 +177,7 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.illustration).to include(:image, :size, :title)
expect(status.label).to eq 'canceled'
expect(status).to have_details
- expect(status).not_to have_action
+ expect(status).to have_action
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 0d998d89d73..635b4e556e8 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -3,6 +3,8 @@ require 'spec_helper'
module Gitlab
module Ci
describe YamlProcessor do
+ include StubRequests
+
subject { described_class.new(config, user: nil) }
describe '#build_attributes' do
@@ -648,7 +650,7 @@ module Gitlab
end
before do
- WebMock.stub_request(:get, 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml')
+ stub_full_request('https://gitlab.com/awesome-project/raw/master/.before-script-template.yml')
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
@@ -1468,7 +1470,7 @@ module Gitlab
expect { Gitlab::Ci::YamlProcessor.new(config) }
.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'rspec: unknown key in `extends`')
+ 'rspec: unknown keys in `extends` (something)')
end
end
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index 32b90041c64..f7642182a17 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -2,7 +2,6 @@
require 'fast_spec_helper'
require 'rspec-parameterized'
-require 'webmock/rspec'
require 'gitlab/danger/helper'
@@ -19,39 +18,6 @@ describe Gitlab::Danger::Helper do
end
end
- let(:teammate_json) do
- <<~JSON
- [
- {
- "username": "in-gitlab-ce",
- "name": "CE maintainer",
- "projects":{ "gitlab-ce": "maintainer backend" }
- },
- {
- "username": "in-gitlab-ee",
- "name": "EE reviewer",
- "projects":{ "gitlab-ee": "reviewer frontend" }
- }
- ]
- JSON
- end
-
- let(:ce_teammate_matcher) do
- satisfy do |teammate|
- teammate.username == 'in-gitlab-ce' &&
- teammate.name == 'CE maintainer' &&
- teammate.projects == { 'gitlab-ce' => 'maintainer backend' }
- end
- end
-
- let(:ee_teammate_matcher) do
- satisfy do |teammate|
- teammate.username == 'in-gitlab-ee' &&
- teammate.name == 'EE reviewer' &&
- teammate.projects == { 'gitlab-ee' => 'reviewer frontend' }
- end
- end
-
let(:fake_git) { double('fake-git') }
subject(:helper) { FakeDanger.new(git: fake_git) }
@@ -119,69 +85,6 @@ describe Gitlab::Danger::Helper do
end
end
- describe '#team' do
- subject(:team) { helper.team }
-
- context 'HTTP failure' do
- before do
- WebMock
- .stub_request(:get, 'https://about.gitlab.com/roulette.json')
- .to_return(status: 404)
- end
-
- it 'raises a pretty error' do
- expect { team }.to raise_error(/Failed to read/)
- end
- end
-
- context 'JSON failure' do
- before do
- WebMock
- .stub_request(:get, 'https://about.gitlab.com/roulette.json')
- .to_return(body: 'INVALID JSON')
- end
-
- it 'raises a pretty error' do
- expect { team }.to raise_error(/Failed to parse/)
- end
- end
-
- context 'success' do
- before do
- WebMock
- .stub_request(:get, 'https://about.gitlab.com/roulette.json')
- .to_return(body: teammate_json)
- end
-
- it 'returns an array of teammates' do
- is_expected.to contain_exactly(ce_teammate_matcher, ee_teammate_matcher)
- end
-
- it 'memoizes the result' do
- expect(team.object_id).to eq(helper.team.object_id)
- end
- end
- end
-
- describe '#project_team' do
- subject { helper.project_team }
-
- before do
- WebMock
- .stub_request(:get, 'https://about.gitlab.com/roulette.json')
- .to_return(body: teammate_json)
- end
-
- it 'filters team by project_name' do
- expect(helper)
- .to receive(:project_name)
- .at_least(:once)
- .and_return('gitlab-ce')
-
- is_expected.to contain_exactly(ce_teammate_matcher)
- end
- end
-
describe '#changes_by_category' do
it 'categorizes changed files' do
expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/foo qa/foo ee/changelogs/foo.yml] }
diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/lib/gitlab/danger/roulette_spec.rb
new file mode 100644
index 00000000000..40dce0c5378
--- /dev/null
+++ b/spec/lib/gitlab/danger/roulette_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'webmock/rspec'
+
+require 'gitlab/danger/roulette'
+
+describe Gitlab::Danger::Roulette do
+ let(:teammate_json) do
+ <<~JSON
+ [
+ {
+ "username": "in-gitlab-ce",
+ "name": "CE maintainer",
+ "projects":{ "gitlab-ce": "maintainer backend" }
+ },
+ {
+ "username": "in-gitlab-ee",
+ "name": "EE reviewer",
+ "projects":{ "gitlab-ee": "reviewer frontend" }
+ }
+ ]
+ JSON
+ end
+
+ let(:ce_teammate_matcher) do
+ satisfy do |teammate|
+ teammate.username == 'in-gitlab-ce' &&
+ teammate.name == 'CE maintainer' &&
+ teammate.projects == { 'gitlab-ce' => 'maintainer backend' }
+ end
+ end
+
+ let(:ee_teammate_matcher) do
+ satisfy do |teammate|
+ teammate.username == 'in-gitlab-ee' &&
+ teammate.name == 'EE reviewer' &&
+ teammate.projects == { 'gitlab-ee' => 'reviewer frontend' }
+ end
+ end
+
+ subject(:roulette) { Object.new.extend(described_class) }
+
+ describe '#team' do
+ subject(:team) { roulette.team }
+
+ context 'HTTP failure' do
+ before do
+ WebMock
+ .stub_request(:get, described_class::ROULETTE_DATA_URL)
+ .to_return(status: 404)
+ end
+
+ it 'raises a pretty error' do
+ expect { team }.to raise_error(/Failed to read/)
+ end
+ end
+
+ context 'JSON failure' do
+ before do
+ WebMock
+ .stub_request(:get, described_class::ROULETTE_DATA_URL)
+ .to_return(body: 'INVALID JSON')
+ end
+
+ it 'raises a pretty error' do
+ expect { team }.to raise_error(/Failed to parse/)
+ end
+ end
+
+ context 'success' do
+ before do
+ WebMock
+ .stub_request(:get, described_class::ROULETTE_DATA_URL)
+ .to_return(body: teammate_json)
+ end
+
+ it 'returns an array of teammates' do
+ is_expected.to contain_exactly(ce_teammate_matcher, ee_teammate_matcher)
+ end
+
+ it 'memoizes the result' do
+ expect(team.object_id).to eq(roulette.team.object_id)
+ end
+ end
+ end
+
+ describe '#project_team' do
+ subject { roulette.project_team('gitlab-ce') }
+
+ before do
+ WebMock
+ .stub_request(:get, described_class::ROULETTE_DATA_URL)
+ .to_return(body: teammate_json)
+ end
+
+ it 'filters team by project_name' do
+ is_expected.to contain_exactly(ce_teammate_matcher)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb
index 4bc0a4c1398..6a6cf1429c8 100644
--- a/spec/lib/gitlab/danger/teammate_spec.rb
+++ b/spec/lib/gitlab/danger/teammate_spec.rb
@@ -1,39 +1,70 @@
# frozen_string_literal: true
+require 'fast_spec_helper'
+
+require 'gitlab/danger/teammate'
+
describe Gitlab::Danger::Teammate do
- subject { described_class.new({ 'projects' => projects }) }
+ subject { described_class.new(options) }
+ let(:options) { { 'projects' => projects, 'role' => role } }
let(:projects) { { project => capabilities } }
+ let(:role) { 'Engineer, Manage' }
+ let(:labels) { [] }
let(:project) { double }
- describe 'multiple roles project project' do
- let(:capabilities) { ['reviewer backend', 'maintainer frontend', 'trainee_maintainer database'] }
+ context 'when having multiple capabilities' do
+ let(:capabilities) { ['reviewer backend', 'maintainer frontend', 'trainee_maintainer qa'] }
it '#reviewer? supports multiple roles per project' do
- expect(subject.reviewer?(project, 'backend')).to be_truthy
+ expect(subject.reviewer?(project, :backend, labels)).to be_truthy
end
it '#traintainer? supports multiple roles per project' do
- expect(subject.traintainer?(project, 'database')).to be_truthy
+ expect(subject.traintainer?(project, :qa, labels)).to be_truthy
end
it '#maintainer? supports multiple roles per project' do
- expect(subject.maintainer?(project, 'frontend')).to be_truthy
+ expect(subject.maintainer?(project, :frontend, labels)).to be_truthy
+ end
+
+ context 'when labels contain Create and the category is test' do
+ let(:labels) { ['Create'] }
+
+ context 'when role is Test Automation Engineer, Create' do
+ let(:role) { 'Test Automation Engineer, Create' }
+
+ it '#reviewer? returns true' do
+ expect(subject.reviewer?(project, :test, labels)).to be_truthy
+ end
+
+ it '#maintainer? returns false' do
+ expect(subject.maintainer?(project, :test, labels)).to be_falsey
+ end
+ end
+
+ context 'when role is Test Automation Engineer, Manage' do
+ let(:role) { 'Test Automation Engineer, Manage' }
+
+ it '#reviewer? returns false' do
+ expect(subject.reviewer?(project, :test, labels)).to be_falsey
+ end
+ end
end
end
- describe 'one role project project' do
+ context 'when having single capability' do
let(:capabilities) { 'reviewer backend' }
it '#reviewer? supports one role per project' do
- expect(subject.reviewer?(project, 'backend')).to be_truthy
+ expect(subject.reviewer?(project, :backend, labels)).to be_truthy
end
it '#traintainer? supports one role per project' do
- expect(subject.traintainer?(project, 'database')).to be_falsey
+ expect(subject.traintainer?(project, :database, labels)).to be_falsey
end
it '#maintainer? supports one role per project' do
- expect(subject.maintainer?(project, 'frontend')).to be_falsey
+ expect(subject.maintainer?(project, :frontend, labels)).to be_falsey
end
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 7644d83992f..e72fb9c6fbc 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -29,51 +29,6 @@ describe Gitlab::Git::Repository, :seed_helper do
let(:storage_path) { TestEnv.repos_path }
let(:user) { build(:user) }
- describe '.create_hooks' do
- let(:repo_path) { File.join(storage_path, 'hook-test.git') }
- let(:hooks_dir) { File.join(repo_path, 'hooks') }
- let(:target_hooks_dir) { Gitlab::Shell.new.hooks_path }
- let(:existing_target) { File.join(repo_path, 'foobar') }
-
- before do
- FileUtils.rm_rf(repo_path)
- FileUtils.mkdir_p(repo_path)
- end
-
- context 'hooks is a directory' do
- let(:existing_file) { File.join(hooks_dir, 'my-file') }
-
- before do
- FileUtils.mkdir_p(hooks_dir)
- FileUtils.touch(existing_file)
- described_class.create_hooks(repo_path, target_hooks_dir)
- end
-
- it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) }
- it { expect(Dir[File.join(repo_path, "hooks.old.*/my-file")].count).to eq(1) }
- end
-
- context 'hooks is a valid symlink' do
- before do
- FileUtils.mkdir_p existing_target
- File.symlink(existing_target, hooks_dir)
- described_class.create_hooks(repo_path, target_hooks_dir)
- end
-
- it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) }
- end
-
- context 'hooks is a broken symlink' do
- before do
- FileUtils.rm_f(existing_target)
- File.symlink(existing_target, hooks_dir)
- described_class.create_hooks(repo_path, target_hooks_dir)
- end
-
- it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) }
- end
- end
-
describe "Respond to" do
subject { repository }
@@ -231,6 +186,18 @@ describe Gitlab::Git::Repository, :seed_helper do
it { is_expected.to be < 2 }
end
+ describe '#object_directory_size' do
+ before do
+ allow(repository.gitaly_repository_client)
+ .to receive(:get_object_directory_size)
+ .and_return(2)
+ end
+
+ subject { repository.object_directory_size }
+
+ it { is_expected.to eq 2048 }
+ end
+
describe '#empty?' do
it { expect(repository).not_to be_empty }
end
@@ -1959,13 +1926,6 @@ describe Gitlab::Git::Repository, :seed_helper do
expect { imported_repo.fsck }.not_to raise_exception
end
- it 'creates a symlink to the global hooks dir' do
- imported_repo.create_from_bundle(valid_bundle_path)
- hooks_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { File.join(imported_repo.path, 'hooks') }
-
- expect(File.readlink(hooks_path)).to eq(Gitlab::Shell.new.hooks_path)
- end
-
it 'raises an error if the bundle is an attempted malicious payload' do
expect do
imported_repo.create_from_bundle(malicious_bundle_path)
diff --git a/spec/lib/gitlab/git_ref_validator_spec.rb b/spec/lib/gitlab/git_ref_validator_spec.rb
index 3ab04a1c46d..b63389af29f 100644
--- a/spec/lib/gitlab/git_ref_validator_spec.rb
+++ b/spec/lib/gitlab/git_ref_validator_spec.rb
@@ -1,31 +1,69 @@
require 'spec_helper'
describe Gitlab::GitRefValidator do
- it { expect(described_class.validate('feature/new')).to be_truthy }
- it { expect(described_class.validate('implement_@all')).to be_truthy }
- it { expect(described_class.validate('my_new_feature')).to be_truthy }
- it { expect(described_class.validate('my-branch')).to be_truthy }
- it { expect(described_class.validate('#1')).to be_truthy }
- it { expect(described_class.validate('feature/refs/heads/foo')).to be_truthy }
- it { expect(described_class.validate('feature/~new/')).to be_falsey }
- it { expect(described_class.validate('feature/^new/')).to be_falsey }
- it { expect(described_class.validate('feature/:new/')).to be_falsey }
- it { expect(described_class.validate('feature/?new/')).to be_falsey }
- it { expect(described_class.validate('feature/*new/')).to be_falsey }
- it { expect(described_class.validate('feature/[new/')).to be_falsey }
- it { expect(described_class.validate('feature/new/')).to be_falsey }
- it { expect(described_class.validate('feature/new.')).to be_falsey }
- it { expect(described_class.validate('feature\@{')).to be_falsey }
- it { expect(described_class.validate('feature\new')).to be_falsey }
- it { expect(described_class.validate('feature//new')).to be_falsey }
- it { expect(described_class.validate('feature new')).to be_falsey }
- it { expect(described_class.validate('refs/heads/')).to be_falsey }
- it { expect(described_class.validate('refs/remotes/')).to be_falsey }
- it { expect(described_class.validate('refs/heads/feature')).to be_falsey }
- it { expect(described_class.validate('refs/remotes/origin')).to be_falsey }
- it { expect(described_class.validate('-')).to be_falsey }
- it { expect(described_class.validate('-branch')).to be_falsey }
- it { expect(described_class.validate('.tag')).to be_falsey }
- it { expect(described_class.validate('my branch')).to be_falsey }
- it { expect(described_class.validate("\xA0\u0000\xB0")).to be_falsey }
+ using RSpec::Parameterized::TableSyntax
+
+ context '.validate' do
+ it { expect(described_class.validate('feature/new')).to be true }
+ it { expect(described_class.validate('implement_@all')).to be true }
+ it { expect(described_class.validate('my_new_feature')).to be true }
+ it { expect(described_class.validate('my-branch')).to be true }
+ it { expect(described_class.validate('#1')).to be true }
+ it { expect(described_class.validate('feature/refs/heads/foo')).to be true }
+ it { expect(described_class.validate('feature/~new/')).to be false }
+ it { expect(described_class.validate('feature/^new/')).to be false }
+ it { expect(described_class.validate('feature/:new/')).to be false }
+ it { expect(described_class.validate('feature/?new/')).to be false }
+ it { expect(described_class.validate('feature/*new/')).to be false }
+ it { expect(described_class.validate('feature/[new/')).to be false }
+ it { expect(described_class.validate('feature/new/')).to be false }
+ it { expect(described_class.validate('feature/new.')).to be false }
+ it { expect(described_class.validate('feature\@{')).to be false }
+ it { expect(described_class.validate('feature\new')).to be false }
+ it { expect(described_class.validate('feature//new')).to be false }
+ it { expect(described_class.validate('feature new')).to be false }
+ it { expect(described_class.validate('refs/heads/')).to be false }
+ it { expect(described_class.validate('refs/remotes/')).to be false }
+ it { expect(described_class.validate('refs/heads/feature')).to be false }
+ it { expect(described_class.validate('refs/remotes/origin')).to be false }
+ it { expect(described_class.validate('-')).to be false }
+ it { expect(described_class.validate('-branch')).to be false }
+ it { expect(described_class.validate('+foo:bar')).to be false }
+ it { expect(described_class.validate('foo:bar')).to be false }
+ it { expect(described_class.validate('.tag')).to be false }
+ it { expect(described_class.validate('my branch')).to be false }
+ it { expect(described_class.validate("\xA0\u0000\xB0")).to be false }
+ end
+
+ context '.validate_merge_request_branch' do
+ it { expect(described_class.validate_merge_request_branch('HEAD')).to be true }
+ it { expect(described_class.validate_merge_request_branch('feature/new')).to be true }
+ it { expect(described_class.validate_merge_request_branch('implement_@all')).to be true }
+ it { expect(described_class.validate_merge_request_branch('my_new_feature')).to be true }
+ it { expect(described_class.validate_merge_request_branch('my-branch')).to be true }
+ it { expect(described_class.validate_merge_request_branch('#1')).to be true }
+ it { expect(described_class.validate_merge_request_branch('feature/refs/heads/foo')).to be true }
+ it { expect(described_class.validate_merge_request_branch('feature/~new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/^new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/:new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/?new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/*new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/[new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/new.')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature\@{')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature\new')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature//new')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature new')).to be false }
+ it { expect(described_class.validate_merge_request_branch('refs/heads/master')).to be true }
+ it { expect(described_class.validate_merge_request_branch('refs/heads/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('refs/remotes/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('-')).to be false }
+ it { expect(described_class.validate_merge_request_branch('-branch')).to be false }
+ it { expect(described_class.validate_merge_request_branch('+foo:bar')).to be false }
+ it { expect(described_class.validate_merge_request_branch('foo:bar')).to be false }
+ it { expect(described_class.validate_merge_request_branch('.tag')).to be false }
+ it { expect(described_class.validate_merge_request_branch('my branch')).to be false }
+ it { expect(described_class.validate_merge_request_branch("\xA0\u0000\xB0")).to be false }
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index 09de7ca6afd..a3808adb376 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -73,6 +73,17 @@ describe Gitlab::GitalyClient::RepositoryService do
end
end
+ describe '#get_object_directory_size' do
+ it 'sends a get_object_directory_size message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:get_object_directory_size)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(size: 0)
+
+ client.get_object_directory_size
+ end
+ end
+
describe '#apply_gitattributes' do
let(:revision) { 'master' }
diff --git a/spec/lib/gitlab/github_import/parallel_importer_spec.rb b/spec/lib/gitlab/github_import/parallel_importer_spec.rb
index f5df38c9aaf..ecab64a372a 100644
--- a/spec/lib/gitlab/github_import/parallel_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/parallel_importer_spec.rb
@@ -25,18 +25,9 @@ describe Gitlab::GithubImport::ParallelImporter do
end
it 'sets the JID in Redis' do
- expect(Gitlab::SidekiqStatus)
- .to receive(:set)
- .with("github-importer/#{project.id}", StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
- .and_call_original
+ expect(Gitlab::Import::SetAsyncJid).to receive(:set_jid).with(project).and_call_original
importer.execute
end
-
- it 'updates the import JID of the project' do
- importer.execute
-
- expect(project.import_state.reload.jid).to eq("github-importer/#{project.id}")
- end
end
end
diff --git a/spec/lib/gitlab/graphql/loaders/batch_project_statistics_loader_spec.rb b/spec/lib/gitlab/graphql/loaders/batch_project_statistics_loader_spec.rb
new file mode 100644
index 00000000000..ec2fcad31e5
--- /dev/null
+++ b/spec/lib/gitlab/graphql/loaders/batch_project_statistics_loader_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::Loaders::BatchProjectStatisticsLoader do
+ describe '#find' do
+ it 'only queries once for project statistics' do
+ stats = create_list(:project_statistics, 2)
+ project1 = stats.first.project
+ project2 = stats.last.project
+
+ expect do
+ described_class.new(project1.id).find
+ described_class.new(project2.id).find
+ end.not_to exceed_query_limit(1)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb b/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb
new file mode 100644
index 00000000000..66033736e01
--- /dev/null
+++ b/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer do
+ subject { described_class.new }
+
+ describe '#analyze?' do
+ context 'feature flag disabled' do
+ before do
+ stub_feature_flags(graphql_logging: false)
+ end
+
+ it 'disables the analyzer' do
+ expect(subject.analyze?(anything)).to be_falsey
+ end
+ end
+
+ context 'feature flag enabled by default' do
+ it 'enables the analyzer' do
+ expect(subject.analyze?(anything)).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql/representation/tree_entry_spec.rb b/spec/lib/gitlab/graphql/representation/tree_entry_spec.rb
new file mode 100644
index 00000000000..d45e690160c
--- /dev/null
+++ b/spec/lib/gitlab/graphql/representation/tree_entry_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::Representation::TreeEntry do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+
+ describe '.decorate' do
+ it 'returns NilClass when given nil' do
+ expect(described_class.decorate(nil, repository)).to be_nil
+ end
+
+ it 'returns array of TreeEntry' do
+ entries = described_class.decorate(repository.tree.blobs, repository)
+
+ expect(entries.first).to be_a(described_class)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql_logger_spec.rb b/spec/lib/gitlab/graphql_logger_spec.rb
new file mode 100644
index 00000000000..4977f98b83e
--- /dev/null
+++ b/spec/lib/gitlab/graphql_logger_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::GraphqlLogger do
+ subject { described_class.new('/dev/null') }
+
+ let(:now) { Time.now }
+
+ it 'builds a logger once' do
+ expect(::Logger).to receive(:new).and_call_original
+
+ subject.info('hello world')
+ subject.error('hello again')
+ end
+
+ context 'logging a GraphQL query' do
+ let(:query) { File.read(Rails.root.join('spec/fixtures/api/graphql/introspection.graphql')) }
+
+ it 'logs a query from JSON' do
+ analyzer_memo = {
+ query_string: query,
+ variables: {},
+ complexity: 181,
+ depth: 0,
+ duration: 7
+ }
+
+ output = subject.format_message('INFO', now, 'test', analyzer_memo)
+
+ data = JSON.parse(output)
+ expect(data['severity']).to eq('INFO')
+ expect(data['time']).to eq(now.utc.iso8601(3))
+ expect(data['complexity']).to eq(181)
+ expect(data['variables']).to eq({})
+ expect(data['depth']).to eq(0)
+ expect(data['duration']).to eq(7)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/http_connection_adapter_spec.rb b/spec/lib/gitlab/http_connection_adapter_spec.rb
new file mode 100644
index 00000000000..930d1f62272
--- /dev/null
+++ b/spec/lib/gitlab/http_connection_adapter_spec.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::HTTPConnectionAdapter do
+ describe '#connection' do
+ context 'when local requests are not allowed' do
+ it 'sets up the connection' do
+ uri = URI('https://example.org')
+
+ connection = described_class.new(uri).connection
+
+ expect(connection).to be_a(Net::HTTP)
+ expect(connection.address).to eq('93.184.216.34')
+ expect(connection.hostname_override).to eq('example.org')
+ expect(connection.addr_port).to eq('example.org')
+ expect(connection.port).to eq(443)
+ end
+
+ it 'raises error when it is a request to local address' do
+ uri = URI('http://172.16.0.0/12')
+
+ expect { described_class.new(uri).connection }
+ .to raise_error(Gitlab::HTTP::BlockedUrlError,
+ "URL 'http://172.16.0.0/12' is blocked: Requests to the local network are not allowed")
+ end
+
+ it 'raises error when it is a request to localhost address' do
+ uri = URI('http://127.0.0.1')
+
+ expect { described_class.new(uri).connection }
+ .to raise_error(Gitlab::HTTP::BlockedUrlError,
+ "URL 'http://127.0.0.1' is blocked: Requests to localhost are not allowed")
+ end
+
+ context 'when port different from URL scheme is used' do
+ it 'sets up the addr_port accordingly' do
+ uri = URI('https://example.org:8080')
+
+ connection = described_class.new(uri).connection
+
+ expect(connection.address).to eq('93.184.216.34')
+ expect(connection.hostname_override).to eq('example.org')
+ expect(connection.addr_port).to eq('example.org:8080')
+ expect(connection.port).to eq(8080)
+ end
+ end
+ end
+
+ context 'when DNS rebinding protection is disabled' do
+ it 'sets up the connection' do
+ stub_application_setting(dns_rebinding_protection_enabled: false)
+
+ uri = URI('https://example.org')
+
+ connection = described_class.new(uri).connection
+
+ expect(connection).to be_a(Net::HTTP)
+ expect(connection.address).to eq('example.org')
+ expect(connection.hostname_override).to eq(nil)
+ expect(connection.addr_port).to eq('example.org')
+ expect(connection.port).to eq(443)
+ end
+ end
+
+ context 'when http(s) environment variable is set' do
+ it 'sets up the connection' do
+ stub_env('https_proxy' => 'https://my.proxy')
+
+ uri = URI('https://example.org')
+
+ connection = described_class.new(uri).connection
+
+ expect(connection).to be_a(Net::HTTP)
+ expect(connection.address).to eq('example.org')
+ expect(connection.hostname_override).to eq(nil)
+ expect(connection.addr_port).to eq('example.org')
+ expect(connection.port).to eq(443)
+ end
+ end
+
+ context 'when local requests are allowed' do
+ it 'sets up the connection' do
+ uri = URI('https://example.org')
+
+ connection = described_class.new(uri, allow_local_requests: true).connection
+
+ expect(connection).to be_a(Net::HTTP)
+ expect(connection.address).to eq('93.184.216.34')
+ expect(connection.hostname_override).to eq('example.org')
+ expect(connection.addr_port).to eq('example.org')
+ expect(connection.port).to eq(443)
+ end
+
+ it 'sets up the connection when it is a local network' do
+ uri = URI('http://172.16.0.0/12')
+
+ connection = described_class.new(uri, allow_local_requests: true).connection
+
+ expect(connection).to be_a(Net::HTTP)
+ expect(connection.address).to eq('172.16.0.0')
+ expect(connection.hostname_override).to be(nil)
+ expect(connection.addr_port).to eq('172.16.0.0')
+ expect(connection.port).to eq(80)
+ end
+
+ it 'sets up the connection when it is localhost' do
+ uri = URI('http://127.0.0.1')
+
+ connection = described_class.new(uri, allow_local_requests: true).connection
+
+ expect(connection).to be_a(Net::HTTP)
+ expect(connection.address).to eq('127.0.0.1')
+ expect(connection.hostname_override).to be(nil)
+ expect(connection.addr_port).to eq('127.0.0.1')
+ expect(connection.port).to eq(80)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb
index 6c37c157f5d..158f77cab2c 100644
--- a/spec/lib/gitlab/http_spec.rb
+++ b/spec/lib/gitlab/http_spec.rb
@@ -1,6 +1,28 @@
require 'spec_helper'
describe Gitlab::HTTP do
+ include StubRequests
+
+ context 'when allow_local_requests' do
+ it 'sends the request to the correct URI' do
+ stub_full_request('https://example.org:8080', ip_address: '8.8.8.8').to_return(status: 200)
+
+ described_class.get('https://example.org:8080', allow_local_requests: false)
+
+ expect(WebMock).to have_requested(:get, 'https://8.8.8.8:8080').once
+ end
+ end
+
+ context 'when not allow_local_requests' do
+ it 'sends the request to the correct URI' do
+ stub_full_request('https://example.org:8080')
+
+ described_class.get('https://example.org:8080', allow_local_requests: true)
+
+ expect(WebMock).to have_requested(:get, 'https://8.8.8.9:8080').once
+ end
+ end
+
describe 'allow_local_requests_from_hooks_and_services is' do
before do
WebMock.stub_request(:get, /.*/).to_return(status: 200, body: 'Success')
@@ -21,6 +43,8 @@ describe Gitlab::HTTP do
context 'if allow_local_requests set to true' do
it 'override the global value and allow requests to localhost or private network' do
+ stub_full_request('http://localhost:3003')
+
expect { described_class.get('http://localhost:3003', allow_local_requests: true) }.not_to raise_error
end
end
@@ -32,6 +56,8 @@ describe Gitlab::HTTP do
end
it 'allow requests to localhost' do
+ stub_full_request('http://localhost:3003')
+
expect { described_class.get('http://localhost:3003') }.not_to raise_error
end
@@ -49,7 +75,7 @@ describe Gitlab::HTTP do
describe 'handle redirect loops' do
before do
- WebMock.stub_request(:any, "http://example.org").to_raise(HTTParty::RedirectionTooDeep.new("Redirection Too Deep"))
+ stub_full_request("http://example.org", method: :any).to_raise(HTTParty::RedirectionTooDeep.new("Redirection Too Deep"))
end
it 'handles GET requests' do
diff --git a/spec/lib/gitlab/import/set_async_jid_spec.rb b/spec/lib/gitlab/import/set_async_jid_spec.rb
new file mode 100644
index 00000000000..51397280138
--- /dev/null
+++ b/spec/lib/gitlab/import/set_async_jid_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::Import::SetAsyncJid do
+ describe '.set_jid', :clean_gitlab_redis_shared_state do
+ let(:project) { create(:project, :import_scheduled) }
+
+ it 'sets the JID in Redis' do
+ expect(Gitlab::SidekiqStatus)
+ .to receive(:set)
+ .with("async-import/#{project.id}", StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
+ .and_call_original
+
+ described_class.set_jid(project)
+ end
+
+ it 'updates the import JID of the project' do
+ described_class.set_jid(project)
+
+ expect(project.import_state.reload.jid).to eq("async-import/#{project.id}")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
index 7c4ac62790e..21a227335cd 100644
--- a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
+++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do
+ include StubRequests
+
let(:example_url) { 'http://www.example.com' }
let(:strategy) { subject.new(url: example_url, http_method: 'post') }
let!(:project) { create(:project, :with_export) }
@@ -35,7 +37,7 @@ describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do
context 'when upload fails' do
it 'stores the export error' do
- stub_request(:post, example_url).to_return(status: [404, 'Page not found'])
+ stub_full_request(example_url, method: :post).to_return(status: [404, 'Page not found'])
strategy.execute(user, project)
diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
index 536cc359d39..99669285d5b 100644
--- a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
@@ -18,7 +18,11 @@ describe Gitlab::ImportExport::AttributeCleaner do
'notid' => 99,
'import_source' => 'whatever',
'import_type' => 'whatever',
- 'non_existent_attr' => 'whatever'
+ 'non_existent_attr' => 'whatever',
+ 'some_html' => '<p>dodgy html</p>',
+ 'legit_html' => '<p>legit html</p>',
+ '_html' => '<p>perfectly ordinary html</p>',
+ 'cached_markdown_version' => 12345
}
end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 4a7accc4c52..fb7bddb386c 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -158,6 +158,8 @@
{
"id": 351,
"note": "Quo reprehenderit aliquam qui dicta impedit cupiditate eligendi.",
+ "note_html": "<p>something else entirely</p>",
+ "cached_markdown_version": 917504,
"noteable_type": "Issue",
"author_id": 26,
"created_at": "2016-06-14T15:02:47.770Z",
@@ -2363,6 +2365,8 @@
{
"id": 671,
"note": "Sit voluptatibus eveniet architecto quidem.",
+ "note_html": "<p>something else entirely</p>",
+ "cached_markdown_version": 917504,
"noteable_type": "MergeRequest",
"author_id": 26,
"created_at": "2016-06-14T15:02:56.632Z",
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 651aa600fb2..ca46006ea58 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -58,6 +58,26 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(Milestone.find_by_description('test milestone').issues.count).to eq(2)
end
+ context 'when importing a project with cached_markdown_version and note_html' do
+ context 'for an Issue' do
+ it 'does not import note_html' do
+ note_content = 'Quo reprehenderit aliquam qui dicta impedit cupiditate eligendi'
+ issue_note = Issue.find_by(description: 'Aliquam enim illo et possimus.').notes.select { |n| n.note.match(/#{note_content}/)}.first
+
+ expect(issue_note.note_html).to match(/#{note_content}/)
+ end
+ end
+
+ context 'for a Merge Request' do
+ it 'does not import note_html' do
+ note_content = 'Sit voluptatibus eveniet architecto quidem'
+ merge_request_note = MergeRequest.find_by(title: 'MR1').notes.select { |n| n.note.match(/#{note_content}/)}.first
+
+ expect(merge_request_note.note_html).to match(/#{note_content}/)
+ end
+ end
+ end
+
it 'creates a valid pipeline note' do
expect(Ci::Pipeline.find_by_sha('sha-notes').notes).not_to be_empty
end
diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
index 8a699eb1461..e2ffb2adb9b 100644
--- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -34,11 +34,5 @@ describe Gitlab::ImportExport::RepoRestorer do
it 'restores the repo successfully' do
expect(restorer.restore).to be_truthy
end
-
- it 'has the webhooks' do
- restorer.restore
-
- expect(project_hook_exists?(project)).to be true
- end
end
end
diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb
index 94abf9679c4..8060b5d4448 100644
--- a/spec/lib/gitlab/import_sources_spec.rb
+++ b/spec/lib/gitlab/import_sources_spec.rb
@@ -14,7 +14,8 @@ describe Gitlab::ImportSources do
'Repo by URL' => 'git',
'GitLab export' => 'gitlab_project',
'Gitea' => 'gitea',
- 'Manifest file' => 'manifest'
+ 'Manifest file' => 'manifest',
+ 'Phabricator' => 'phabricator'
}
expect(described_class.options).to eq(expected)
@@ -35,6 +36,7 @@ describe Gitlab::ImportSources do
gitlab_project
gitea
manifest
+ phabricator
)
expect(described_class.values).to eq(expected)
@@ -53,6 +55,7 @@ describe Gitlab::ImportSources do
fogbugz
gitlab_project
gitea
+ phabricator
)
expect(described_class.importer_names).to eq(expected)
@@ -70,7 +73,8 @@ describe Gitlab::ImportSources do
'git' => nil,
'gitlab_project' => Gitlab::ImportExport::Importer,
'gitea' => Gitlab::LegacyGithubImport::Importer,
- 'manifest' => nil
+ 'manifest' => nil,
+ 'phabricator' => Gitlab::PhabricatorImport::Importer
}
import_sources.each do |name, klass|
@@ -91,7 +95,8 @@ describe Gitlab::ImportSources do
'git' => 'Repo by URL',
'gitlab_project' => 'GitLab export',
'gitea' => 'Gitea',
- 'manifest' => 'Manifest file'
+ 'manifest' => 'Manifest file',
+ 'phabricator' => 'Phabricator'
}
import_sources.each do |name, title|
@@ -102,7 +107,7 @@ describe Gitlab::ImportSources do
end
describe 'imports_repository? checker' do
- let(:allowed_importers) { %w[github gitlab_project bitbucket_server] }
+ let(:allowed_importers) { %w[github gitlab_project bitbucket_server phabricator] }
it 'fails if any importer other than the allowed ones implements this method' do
current_importers = described_class.values.select { |kind| described_class.importer(kind).try(:imports_repository?) }
diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb
index 24ce397ec3d..0de809833e6 100644
--- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb
@@ -36,6 +36,8 @@ describe Gitlab::Kubernetes::Helm::Api do
describe '#uninstall' do
before do
allow(client).to receive(:create_pod).and_return(nil)
+ allow(client).to receive(:get_config_map).and_return(nil)
+ allow(client).to receive(:create_config_map).and_return(nil)
allow(client).to receive(:delete_pod).and_return(nil)
allow(namespace).to receive(:ensure_exists!).once
end
@@ -53,6 +55,28 @@ describe Gitlab::Kubernetes::Helm::Api do
subject.uninstall(command)
end
+
+ context 'with a ConfigMap' do
+ let(:resource) { Gitlab::Kubernetes::ConfigMap.new(application_name, files).generate }
+
+ it 'creates a ConfigMap on kubeclient' do
+ expect(client).to receive(:create_config_map).with(resource).once
+
+ subject.install(command)
+ end
+
+ context 'config map already exists' do
+ before do
+ expect(client).to receive(:get_config_map).with("values-content-configuration-#{application_name}", gitlab_namespace).and_return(resource)
+ end
+
+ it 'updates the config map' do
+ expect(client).to receive(:update_config_map).with(resource).once
+
+ subject.install(command)
+ end
+ end
+ end
end
describe '#install' do
diff --git a/spec/lib/gitlab/lets_encrypt/challenge_spec.rb b/spec/lib/gitlab/lets_encrypt/challenge_spec.rb
index 74622f356de..fcd92586362 100644
--- a/spec/lib/gitlab/lets_encrypt/challenge_spec.rb
+++ b/spec/lib/gitlab/lets_encrypt/challenge_spec.rb
@@ -3,23 +3,11 @@
require 'spec_helper'
describe ::Gitlab::LetsEncrypt::Challenge do
- delegated_methods = {
- url: 'https://example.com/',
- status: 'pending',
- token: 'tokenvalue',
- file_content: 'hereisfilecontent',
- request_validation: true
- }
+ include LetsEncryptHelpers
- let(:acme_challenge) do
- acme_challenge = instance_double('Acme::Client::Resources::Challenge')
- allow(acme_challenge).to receive_messages(delegated_methods)
- acme_challenge
- end
-
- let(:challenge) { described_class.new(acme_challenge) }
+ let(:challenge) { described_class.new(acme_challenge_double) }
- delegated_methods.each do |method, value|
+ LetsEncryptHelpers::ACME_CHALLENGE_METHODS.each do |method, value|
describe "##{method}" do
it 'delegates to Acme::Client::Resources::Challenge' do
expect(challenge.public_send(method)).to eq(value)
diff --git a/spec/lib/gitlab/lets_encrypt/client_spec.rb b/spec/lib/gitlab/lets_encrypt/client_spec.rb
index d63a2fbee04..5454d9c1af4 100644
--- a/spec/lib/gitlab/lets_encrypt/client_spec.rb
+++ b/spec/lib/gitlab/lets_encrypt/client_spec.rb
@@ -5,14 +5,12 @@ require 'spec_helper'
describe ::Gitlab::LetsEncrypt::Client do
include LetsEncryptHelpers
- set(:private_key) { OpenSSL::PKey::RSA.new(4096).to_pem }
let(:client) { described_class.new }
before do
stub_application_setting(
lets_encrypt_notification_email: 'myemail@test.example.com',
- lets_encrypt_terms_of_service_accepted: true,
- lets_encrypt_private_key: private_key
+ lets_encrypt_terms_of_service_accepted: true
)
end
@@ -28,6 +26,36 @@ describe ::Gitlab::LetsEncrypt::Client do
)
end
+ it 'generates and stores private key and initialize acme client with it' do
+ expect(Gitlab::CurrentSettings.lets_encrypt_private_key).to eq(nil)
+
+ subject
+
+ saved_private_key = Gitlab::CurrentSettings.lets_encrypt_private_key
+
+ expect(saved_private_key).to be
+ expect(Acme::Client).to have_received(:new).with(
+ hash_including(private_key: eq_pem(saved_private_key))
+ )
+ end
+
+ context 'when private key is saved in settings' do
+ let!(:saved_private_key) do
+ key = OpenSSL::PKey::RSA.new(4096).to_pem
+ Gitlab::CurrentSettings.current_application_settings.update(lets_encrypt_private_key: key)
+ key
+ end
+
+ it 'uses current value of private key' do
+ subject
+
+ expect(Acme::Client).to have_received(:new).with(
+ hash_including(private_key: eq_pem(saved_private_key))
+ )
+ expect(Gitlab::CurrentSettings.lets_encrypt_private_key).to eq(saved_private_key)
+ end
+ end
+
context 'when acme integration is disabled' do
before do
stub_application_setting(lets_encrypt_terms_of_service_accepted: false)
@@ -94,6 +122,18 @@ describe ::Gitlab::LetsEncrypt::Client do
context 'when terms of service are accepted' do
it { is_expected.to eq(true) }
+ context "when private_key isn't present and database is read only" do
+ before do
+ allow(::Gitlab::Database).to receive(:read_only?).and_return(true)
+ end
+
+ it 'returns false' do
+ expect(::Gitlab::CurrentSettings.lets_encrypt_private_key).to eq(nil)
+
+ is_expected.to eq(false)
+ end
+ end
+
context 'when feature flag is disabled' do
before do
stub_feature_flags(pages_auto_ssl: false)
diff --git a/spec/lib/gitlab/lets_encrypt/order_spec.rb b/spec/lib/gitlab/lets_encrypt/order_spec.rb
index ee7058baf8d..1a759103c44 100644
--- a/spec/lib/gitlab/lets_encrypt/order_spec.rb
+++ b/spec/lib/gitlab/lets_encrypt/order_spec.rb
@@ -3,20 +3,13 @@
require 'spec_helper'
describe ::Gitlab::LetsEncrypt::Order do
- delegated_methods = {
- url: 'https://example.com/',
- status: 'valid'
- }
-
- let(:acme_order) do
- acme_order = instance_double('Acme::Client::Resources::Order')
- allow(acme_order).to receive_messages(delegated_methods)
- acme_order
- end
+ include LetsEncryptHelpers
+
+ let(:acme_order) { acme_order_double }
let(:order) { described_class.new(acme_order) }
- delegated_methods.each do |method, value|
+ LetsEncryptHelpers::ACME_ORDER_METHODS.each do |method, value|
describe "##{method}" do
it 'delegates to Acme::Client::Resources::Order' do
expect(order.public_send(method)).to eq(value)
@@ -25,15 +18,24 @@ describe ::Gitlab::LetsEncrypt::Order do
end
describe '#new_challenge' do
- before do
- challenge = instance_double('Acme::Client::Resources::Challenges::HTTP01')
- authorization = instance_double('Acme::Client::Resources::Authorization')
- allow(authorization).to receive(:http).and_return(challenge)
- allow(acme_order).to receive(:authorizations).and_return([authorization])
- end
-
it 'returns challenge' do
expect(order.new_challenge).to be_a(::Gitlab::LetsEncrypt::Challenge)
end
end
+
+ describe '#request_certificate' do
+ let(:private_key) do
+ OpenSSL::PKey::RSA.new(4096).to_pem
+ end
+
+ it 'generates csr and finalizes order' do
+ expect(acme_order).to receive(:finalize) do |csr:|
+ expect do
+ csr.csr # it's being evaluated lazily
+ end.not_to raise_error
+ end
+
+ order.request_certificate(domain: 'example.com', private_key: private_key)
+ end
+ end
end
diff --git a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb
new file mode 100644
index 00000000000..18052b1991c
--- /dev/null
+++ b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb
@@ -0,0 +1,179 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::MarkdownCache::ActiveRecord::Extension do
+ let(:klass) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'issues'
+ include CacheMarkdownField
+ cache_markdown_field :title, whitelisted: true
+ cache_markdown_field :description, pipeline: :single_line
+
+ attr_accessor :author, :project
+ end
+ end
+
+ let(:cache_version) { Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16 }
+ let(:thing) { klass.new(title: markdown, title_html: html, cached_markdown_version: cache_version) }
+
+ let(:markdown) { '`Foo`' }
+ let(:html) { '<p data-sourcepos="1:1-1:5" dir="auto"><code>Foo</code></p>' }
+
+ let(:updated_markdown) { '`Bar`' }
+ let(:updated_html) { '<p data-sourcepos="1:1-1:5" dir="auto"><code>Bar</code></p>' }
+
+ context 'an unchanged markdown field' do
+ let(:thing) { klass.new(title: markdown) }
+
+ before do
+ thing.title = thing.title
+ thing.save
+ end
+
+ it { expect(thing.title).to eq(markdown) }
+ it { expect(thing.title_html).to eq(html) }
+ it { expect(thing.title_html_changed?).not_to be_truthy }
+ it { expect(thing.cached_markdown_version).to eq(cache_version) }
+ end
+
+ context 'a changed markdown field' do
+ let(:thing) { klass.new(title: markdown, title_html: html, cached_markdown_version: cache_version) }
+
+ before do
+ thing.title = updated_markdown
+ thing.save
+ end
+
+ it { expect(thing.title_html).to eq(updated_html) }
+ it { expect(thing.cached_markdown_version).to eq(cache_version) }
+ end
+
+ context 'when a markdown field is set repeatedly to an empty string' do
+ it do
+ expect(thing).to receive(:refresh_markdown_cache).once
+ thing.title = ''
+ thing.save
+ thing.title = ''
+ thing.save
+ end
+ end
+
+ context 'when a markdown field is set repeatedly to a string which renders as empty html' do
+ it do
+ expect(thing).to receive(:refresh_markdown_cache).once
+ thing.title = '[//]: # (This is also a comment.)'
+ thing.save
+ thing.title = '[//]: # (This is also a comment.)'
+ thing.save
+ end
+ end
+
+ context 'a non-markdown field changed' do
+ let(:thing) { klass.new(title: markdown, title_html: html, cached_markdown_version: cache_version) }
+
+ before do
+ thing.state = 'closed'
+ thing.save
+ end
+
+ it { expect(thing.state).to eq('closed') }
+ it { expect(thing.title).to eq(markdown) }
+ it { expect(thing.title_html).to eq(html) }
+ it { expect(thing.cached_markdown_version).to eq(cache_version) }
+ end
+
+ context 'version is out of date' do
+ let(:thing) { klass.new(title: updated_markdown, title_html: html, cached_markdown_version: nil) }
+
+ before do
+ thing.save
+ end
+
+ it { expect(thing.title_html).to eq(updated_html) }
+ it { expect(thing.cached_markdown_version).to eq(cache_version) }
+ end
+
+ context 'when an invalidating field is changed' do
+ it 'invalidates the cache when project changes' do
+ thing.project = :new_project
+ allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
+
+ thing.save
+
+ expect(thing.title_html).to eq(updated_html)
+ expect(thing.description_html).to eq(updated_html)
+ expect(thing.cached_markdown_version).to eq(cache_version)
+ end
+
+ it 'invalidates the cache when author changes' do
+ thing.author = :new_author
+ allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
+
+ thing.save
+
+ expect(thing.title_html).to eq(updated_html)
+ expect(thing.description_html).to eq(updated_html)
+ expect(thing.cached_markdown_version).to eq(cache_version)
+ end
+ end
+
+ describe '.attributes' do
+ it 'excludes cache attributes that is blacklisted by default' do
+ expect(thing.attributes.keys.sort).not_to include(%w[description_html])
+ end
+ end
+
+ describe '#cached_html_up_to_date?' do
+ let(:thing) { klass.create(title: updated_markdown, title_html: html, cached_markdown_version: nil) }
+ subject { thing.cached_html_up_to_date?(:title) }
+
+ it 'returns false if markdown has been changed but html has not' do
+ thing.title = "changed!"
+
+ is_expected.to be_falsy
+ end
+
+ it 'returns true if markdown has not been changed but html has' do
+ thing.title_html = updated_html
+
+ is_expected.to be_truthy
+ end
+
+ it 'returns true if markdown and html have both been changed' do
+ thing.title = updated_markdown
+ thing.title_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.title_html = nil
+
+ is_expected.to be_falsy
+ end
+ end
+
+ describe '#refresh_markdown_cache!' do
+ before do
+ thing.title = updated_markdown
+ end
+
+ it 'skips saving if not persisted' do
+ expect(thing).to receive(:persisted?).and_return(false)
+ expect(thing).not_to receive(:update_columns)
+
+ thing.refresh_markdown_cache!
+ end
+
+ it 'saves the changes' do
+ expect(thing).to receive(:persisted?).and_return(true)
+
+ expect(thing).to receive(:update_columns)
+ .with("title_html" => updated_html,
+ "description_html" => "",
+ "cached_markdown_version" => cache_version)
+
+ thing.refresh_markdown_cache!
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown_cache/field_data_spec.rb b/spec/lib/gitlab/markdown_cache/field_data_spec.rb
new file mode 100644
index 00000000000..393bf85aa43
--- /dev/null
+++ b/spec/lib/gitlab/markdown_cache/field_data_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::MarkdownCache::FieldData do
+ subject(:field_data) { described_class.new }
+
+ before do
+ field_data[:description] = { project: double('project in context') }
+ end
+
+ it 'translates a markdown field name into a html field name' do
+ expect(field_data.html_field(:description)).to eq("description_html")
+ end
+end
diff --git a/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb b/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb
new file mode 100644
index 00000000000..b6a781de426
--- /dev/null
+++ b/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::MarkdownCache::Redis::Extension, :clean_gitlab_redis_cache do
+ let(:klass) do
+ Class.new do
+ include CacheMarkdownField
+
+ def initialize(title: nil, description: nil)
+ @title, @description = title, description
+ end
+
+ attr_reader :title, :description
+
+ cache_markdown_field :title, pipeline: :single_line
+ cache_markdown_field :description
+
+ def cache_key
+ "cache-key"
+ end
+ end
+ end
+
+ let(:cache_version) { Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16 }
+ let(:thing) { klass.new(title: "`Hello`", description: "`World`") }
+ let(:expected_cache_key) { "markdown_cache:cache-key" }
+
+ it 'defines the html attributes' do
+ expect(thing).to respond_to(:title_html, :description_html, :cached_markdown_version)
+ end
+
+ it 'loads the markdown from the cache only once' do
+ expect(thing).to receive(:load_cached_markdown).once.and_call_original
+
+ thing.title_html
+ thing.description_html
+ end
+
+ it 'correctly loads the markdown if it was stored in redis' do
+ Gitlab::Redis::Cache.with do |r|
+ r.mapped_hmset(expected_cache_key,
+ title_html: 'hello',
+ description_html: 'world',
+ cached_markdown_version: cache_version)
+ end
+
+ expect(thing.title_html).to eq('hello')
+ expect(thing.description_html).to eq('world')
+ expect(thing.cached_markdown_version).to eq(cache_version)
+ end
+
+ describe "#refresh_markdown_cache!" do
+ it "stores the value in redis" do
+ expected_results = { "title_html" => "`Hello`",
+ "description_html" => "<p data-sourcepos=\"1:1-1:7\" dir=\"auto\"><code>World</code></p>",
+ "cached_markdown_version" => cache_version.to_s }
+
+ thing.refresh_markdown_cache!
+
+ results = Gitlab::Redis::Cache.with do |r|
+ r.mapped_hmget(expected_cache_key,
+ "title_html", "description_html", "cached_markdown_version")
+ end
+
+ expect(results).to eq(expected_results)
+ end
+
+ it "assigns the values" do
+ thing.refresh_markdown_cache!
+
+ expect(thing.title_html).to eq('`Hello`')
+ expect(thing.description_html).to eq("<p data-sourcepos=\"1:1-1:7\" dir=\"auto\"><code>World</code></p>")
+ expect(thing.cached_markdown_version).to eq(cache_version)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown_cache/redis/store_spec.rb b/spec/lib/gitlab/markdown_cache/redis/store_spec.rb
new file mode 100644
index 00000000000..95c68e7d491
--- /dev/null
+++ b/spec/lib/gitlab/markdown_cache/redis/store_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::MarkdownCache::Redis::Store, :clean_gitlab_redis_cache do
+ let(:storable_class) do
+ Class.new do
+ cattr_reader :cached_markdown_fields do
+ Gitlab::MarkdownCache::FieldData.new.tap do |field_data|
+ field_data[:field_1] = {}
+ field_data[:field_2] = {}
+ end
+ end
+
+ attr_accessor :field_1, :field_2, :field_1_html, :field_2_html, :cached_markdown_version
+
+ def cache_key
+ "cache-key"
+ end
+ end
+ end
+ let(:storable) { storable_class.new }
+ let(:cache_key) { "markdown_cache:#{storable.cache_key}" }
+
+ subject(:store) { described_class.new(storable) }
+
+ def read_values
+ Gitlab::Redis::Cache.with do |r|
+ r.mapped_hmget(cache_key,
+ :field_1_html, :field_2_html, :cached_markdown_version)
+ end
+ end
+
+ def store_values(values)
+ Gitlab::Redis::Cache.with do |r|
+ r.mapped_hmset(cache_key,
+ values)
+ end
+ end
+
+ describe '#save' do
+ it 'stores updates to html fields and version' do
+ values_to_store = { field_1_html: "hello", field_2_html: "world", cached_markdown_version: 1 }
+
+ store.save(values_to_store)
+
+ expect(read_values)
+ .to eq({ field_1_html: "hello", field_2_html: "world", cached_markdown_version: "1" })
+ end
+ end
+
+ describe '#read' do
+ it 'reads the html fields and version from redis if they were stored' do
+ stored_values = { field_1_html: "hello", field_2_html: "world", cached_markdown_version: 1 }
+
+ store_values(stored_values)
+
+ expect(store.read.symbolize_keys).to eq(stored_values)
+ end
+
+ it 'is mared loaded after reading' do
+ expect(store).not_to be_loaded
+
+ store.read
+
+ expect(store).to be_loaded
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
index c471c30a194..f4a6e1fc7d9 100644
--- a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
@@ -61,6 +61,33 @@ describe Gitlab::Metrics::Samplers::PumaSampler do
end
end
+ context 'with empty worker stats' do
+ let(:puma_stats) do
+ <<~EOS
+ {
+ "workers": 2,
+ "phase": 2,
+ "booted_workers": 2,
+ "old_workers": 0,
+ "worker_status": [{
+ "pid": 32534,
+ "index": 0,
+ "phase": 1,
+ "booted": true,
+ "last_checkin": "2019-05-15T07:57:55Z",
+ "last_status": {}
+ }]
+ }
+ EOS
+ end
+
+ it 'does not log worker stats' do
+ expect(subject).not_to receive(:set_worker_metrics)
+
+ subject.sample
+ end
+ end
+
context 'in single mode' do
let(:puma_stats) do
<<~EOS
diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
index 7972ff253fe..aaf8c9fa2a0 100644
--- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
@@ -10,17 +10,20 @@ describe Gitlab::Metrics::Samplers::RubySampler do
describe '#sample' do
it 'samples various statistics' do
- expect(Gitlab::Metrics::System).to receive(:memory_usage)
+ expect(Gitlab::Metrics::System).to receive(:cpu_time)
expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
+ expect(Gitlab::Metrics::System).to receive(:memory_usage)
+ expect(Gitlab::Metrics::System).to receive(:process_start_time)
+ expect(Gitlab::Metrics::System).to receive(:max_open_file_descriptors)
expect(sampler).to receive(:sample_gc)
sampler.sample
end
- it 'adds a metric containing the memory usage' do
+ it 'adds a metric containing the process resident memory bytes' do
expect(Gitlab::Metrics::System).to receive(:memory_usage).and_return(9000)
- expect(sampler.metrics[:memory_usage]).to receive(:set).with({}, 9000)
+ expect(sampler.metrics[:process_resident_memory_bytes]).to receive(:set).with({}, 9000)
sampler.sample
end
@@ -34,6 +37,27 @@ describe Gitlab::Metrics::Samplers::RubySampler do
sampler.sample
end
+ it 'adds a metric containing the process total cpu time' do
+ expect(Gitlab::Metrics::System).to receive(:cpu_time).and_return(0.51)
+ expect(sampler.metrics[:process_cpu_seconds_total]).to receive(:set).with({}, 0.51)
+
+ sampler.sample
+ end
+
+ it 'adds a metric containing the process start time' do
+ expect(Gitlab::Metrics::System).to receive(:process_start_time).and_return(12345)
+ expect(sampler.metrics[:process_start_time_seconds]).to receive(:set).with({}, 12345)
+
+ sampler.sample
+ end
+
+ it 'adds a metric containing the process max file descriptors' do
+ expect(Gitlab::Metrics::System).to receive(:max_open_file_descriptors).and_return(1024)
+ expect(sampler.metrics[:process_max_fds]).to receive(:set).with({}, 1024)
+
+ sampler.sample
+ end
+
it 'clears any GC profiles' do
expect(GC::Profiler).to receive(:clear)
diff --git a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
index 4b03f3c2532..090e456644f 100644
--- a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
@@ -39,8 +39,8 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
it 'updates metrics type unix and with addr' do
labels = { socket_type: 'unix', socket_address: socket_address }
- expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
- expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
+ expect(subject.metrics[:unicorn_active_connections]).to receive(:set).with(labels, 'active')
+ expect(subject.metrics[:unicorn_queued_connections]).to receive(:set).with(labels, 'queued')
subject.sample
end
@@ -50,7 +50,6 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
context 'unicorn listens on tcp sockets' do
let(:tcp_socket_address) { '0.0.0.0:8080' }
let(:tcp_sockets) { [tcp_socket_address] }
-
before do
allow(unicorn).to receive(:listener_names).and_return(tcp_sockets)
end
@@ -71,13 +70,29 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
it 'updates metrics type unix and with addr' do
labels = { socket_type: 'tcp', socket_address: tcp_socket_address }
- expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
- expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
+ expect(subject.metrics[:unicorn_active_connections]).to receive(:set).with(labels, 'active')
+ expect(subject.metrics[:unicorn_queued_connections]).to receive(:set).with(labels, 'queued')
subject.sample
end
end
end
+
+ context 'additional metrics' do
+ let(:unicorn_workers) { 2 }
+
+ before do
+ allow(unicorn).to receive(:listener_names).and_return([""])
+ allow(::Gitlab::Metrics::System).to receive(:cpu_time).and_return(3.14)
+ allow(subject).to receive(:unicorn_workers_count).and_return(unicorn_workers)
+ end
+
+ it "sets additional metrics" do
+ expect(subject.metrics[:unicorn_workers]).to receive(:set).with({}, unicorn_workers)
+
+ subject.sample
+ end
+ end
end
describe '#start' do
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index 14afcdf5daa..b0603d96eb2 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -13,6 +13,18 @@ describe Gitlab::Metrics::System do
expect(described_class.file_descriptor_count).to be > 0
end
end
+
+ describe '.max_open_file_descriptors' do
+ it 'returns the max allowed open file descriptors' do
+ expect(described_class.max_open_file_descriptors).to be > 0
+ end
+ end
+
+ describe '.process_start_time' do
+ it 'returns the process start time' do
+ expect(described_class.process_start_time).to be > 0
+ end
+ end
else
describe '.memory_usage' do
it 'returns 0.0' do
@@ -25,6 +37,18 @@ describe Gitlab::Metrics::System do
expect(described_class.file_descriptor_count).to eq(0)
end
end
+
+ describe '.max_open_file_descriptors' do
+ it 'returns 0' do
+ expect(described_class.max_open_file_descriptors).to eq(0)
+ end
+ end
+
+ describe 'process_start_time' do
+ it 'returns 0' do
+ expect(described_class.process_start_time).to eq(0)
+ end
+ end
end
describe '.cpu_time' do
diff --git a/spec/lib/gitlab/omniauth_initializer_spec.rb b/spec/lib/gitlab/omniauth_initializer_spec.rb
index f9c0daf1ef1..32296caf819 100644
--- a/spec/lib/gitlab/omniauth_initializer_spec.rb
+++ b/spec/lib/gitlab/omniauth_initializer_spec.rb
@@ -83,5 +83,13 @@ describe Gitlab::OmniauthInitializer do
subject.execute([cas3_config])
end
+
+ it 'configures name for openid_connect' do
+ openid_connect_config = { 'name' => 'openid_connect', 'args' => {} }
+
+ expect(devise_config).to receive(:omniauth).with(:openid_connect, name: 'openid_connect')
+
+ subject.execute([openid_connect_config])
+ end
end
end
diff --git a/spec/lib/gitlab/phabricator_import/base_worker_spec.rb b/spec/lib/gitlab/phabricator_import/base_worker_spec.rb
new file mode 100644
index 00000000000..d46d908a3e3
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/base_worker_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::BaseWorker do
+ let(:subclass) do
+ # Creating an anonymous class for a worker is complicated, as we generate the
+ # queue name from the class name.
+ Gitlab::PhabricatorImport::ImportTasksWorker
+ end
+
+ describe '.schedule' do
+ let(:arguments) { %w[project_id the_next_page] }
+
+ it 'schedules the job' do
+ expect(subclass).to receive(:perform_async).with(*arguments)
+
+ subclass.schedule(*arguments)
+ end
+
+ it 'counts the scheduled job', :clean_gitlab_redis_shared_state do
+ state = Gitlab::PhabricatorImport::WorkerState.new('project_id')
+
+ allow(subclass).to receive(:remove_job) # otherwise the job is removed before we saw it
+
+ expect { subclass.schedule(*arguments) }.to change { state.running_count }.by(1)
+ end
+ end
+
+ describe '#perform' do
+ let(:project) { create(:project, :import_started, import_url: "https://a.phab.instance") }
+ let(:worker) { subclass.new }
+ let(:state) { Gitlab::PhabricatorImport::WorkerState.new(project.id) }
+
+ before do
+ allow(worker).to receive(:import)
+ end
+
+ it 'does not break for a non-existing project' do
+ expect { worker.perform('not a thing') }.not_to raise_error
+ end
+
+ it 'does not do anything when the import is not in progress' do
+ project = create(:project, :import_failed)
+
+ expect(worker).not_to receive(:import)
+
+ worker.perform(project.id)
+ end
+
+ it 'calls import for the project' do
+ expect(worker).to receive(:import).with(project, 'other_arg')
+
+ worker.perform(project.id, 'other_arg')
+ end
+
+ it 'marks the project as imported if there was only one job running' do
+ worker.perform(project.id)
+
+ expect(project.import_state.reload).to be_finished
+ end
+
+ it 'does not mark the job as finished when there are more scheduled jobs' do
+ 2.times { state.add_job }
+
+ worker.perform(project.id)
+
+ expect(project.import_state.reload).to be_in_progress
+ end
+
+ it 'decrements the job counter' do
+ expect { worker.perform(project.id) }.to change { state.running_count }.by(-1)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/cache/map_spec.rb b/spec/lib/gitlab/phabricator_import/cache/map_spec.rb
new file mode 100644
index 00000000000..52c7a02219f
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/cache/map_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::Cache::Map, :clean_gitlab_redis_cache do
+ set(:project) { create(:project) }
+ let(:redis) { Gitlab::Redis::Cache }
+ subject(:map) { described_class.new(project) }
+
+ describe '#get_gitlab_model' do
+ it 'returns nil if there was nothing cached for the phabricator id' do
+ expect(map.get_gitlab_model('does not exist')).to be_nil
+ end
+
+ it 'returns the object if it was set in redis' do
+ issue = create(:issue, project: project)
+ set_in_redis('exists', issue)
+
+ expect(map.get_gitlab_model('exists')).to eq(issue)
+ end
+
+ it 'extends the TTL for the cache key' do
+ set_in_redis('extend', create(:issue, project: project)) do |redis|
+ redis.expire(cache_key('extend'), 10.seconds.to_i)
+ end
+
+ map.get_gitlab_model('extend')
+
+ ttl = redis.with { |redis| redis.ttl(cache_key('extend')) }
+
+ expect(ttl).to be > 10.seconds
+ end
+ end
+
+ describe '#set_gitlab_model' do
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ it 'sets the class and id in redis with a ttl' do
+ issue = create(:issue, project: project)
+
+ map.set_gitlab_model(issue, 'it is set')
+
+ set_data, ttl = redis.with do |redis|
+ redis.pipelined do |p|
+ p.mapped_hmget(cache_key('it is set'), :classname, :database_id)
+ p.ttl(cache_key('it is set'))
+ end
+ end
+
+ expect(set_data).to eq({ classname: 'Issue', database_id: issue.id.to_s })
+ expect(ttl).to be_within(1.second).of(StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
+ end
+ end
+
+ def set_in_redis(key, object)
+ redis.with do |redis|
+ redis.mapped_hmset(cache_key(key),
+ { classname: object.class, database_id: object.id })
+ yield(redis) if block_given?
+ end
+ end
+
+ def cache_key(phabricator_id)
+ subject.__send__(:cache_key_for_phabricator_id, phabricator_id)
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/conduit/client_spec.rb b/spec/lib/gitlab/phabricator_import/conduit/client_spec.rb
new file mode 100644
index 00000000000..542b3cd060f
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/conduit/client_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::Conduit::Client do
+ let(:client) do
+ described_class.new('https://see-ya-later.phabricator', 'api-token')
+ end
+
+ describe '#get' do
+ it 'performs and parses a request' do
+ params = { some: 'extra', values: %w[are passed] }
+ stub_valid_request(params)
+
+ response = client.get('test', params: params)
+
+ expect(response).to be_a(Gitlab::PhabricatorImport::Conduit::Response)
+ expect(response).to be_success
+ end
+
+ it 'wraps request errors in an `ApiError`' do
+ stub_timeout
+
+ expect { client.get('test') }.to raise_error(Gitlab::PhabricatorImport::Conduit::ApiError)
+ end
+
+ it 'raises response error' do
+ stub_error_response
+
+ expect { client.get('test') }
+ .to raise_error(Gitlab::PhabricatorImport::Conduit::ResponseError, /has the wrong length/)
+ end
+ end
+
+ def stub_valid_request(params = {})
+ WebMock.stub_request(
+ :get, 'https://see-ya-later.phabricator/api/test'
+ ).with(
+ body: CGI.unescape(params.reverse_merge('api.token' => 'api-token').to_query)
+ ).and_return(
+ status: 200,
+ body: fixture_file('phabricator_responses/maniphest.search.json')
+ )
+ end
+
+ def stub_timeout
+ WebMock.stub_request(
+ :get, 'https://see-ya-later.phabricator/api/test'
+ ).to_timeout
+ end
+
+ def stub_error_response
+ WebMock.stub_request(
+ :get, 'https://see-ya-later.phabricator/api/test'
+ ).and_return(
+ status: 200,
+ body: fixture_file('phabricator_responses/auth_failed.json')
+ )
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/conduit/maniphest_spec.rb b/spec/lib/gitlab/phabricator_import/conduit/maniphest_spec.rb
new file mode 100644
index 00000000000..0d7714649b9
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/conduit/maniphest_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::Conduit::Maniphest do
+ let(:maniphest) do
+ described_class.new(phabricator_url: 'https://see-ya-later.phabricator', api_token: 'api-token')
+ end
+
+ describe '#tasks' do
+ let(:fake_client) { double('Phabricator client') }
+
+ before do
+ allow(maniphest).to receive(:client).and_return(fake_client)
+ end
+
+ it 'calls the api with the correct params' do
+ expected_params = {
+ after: '123',
+ attachments: {
+ projects: 1, subscribers: 1, columns: 1
+ }
+ }
+
+ expect(fake_client).to receive(:get).with('maniphest.search',
+ params: expected_params)
+
+ maniphest.tasks(after: '123')
+ end
+
+ it 'returns a parsed response' do
+ response = Gitlab::PhabricatorImport::Conduit::Response
+ .new(fixture_file('phabricator_responses/maniphest.search.json'))
+
+ allow(fake_client).to receive(:get).and_return(response)
+
+ expect(maniphest.tasks).to be_a(Gitlab::PhabricatorImport::Conduit::TasksResponse)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb b/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb
new file mode 100644
index 00000000000..a8596968f14
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::Conduit::Response do
+ let(:response) { described_class.new(JSON.parse(fixture_file('phabricator_responses/maniphest.search.json')))}
+ let(:error_response) { described_class.new(JSON.parse(fixture_file('phabricator_responses/auth_failed.json'))) }
+
+ describe '.parse!' do
+ it 'raises a ResponseError if the http response was not successfull' do
+ fake_response = double(:http_response, success?: false, status: 401)
+
+ expect { described_class.parse!(fake_response) }
+ .to raise_error(Gitlab::PhabricatorImport::Conduit::ResponseError, /responded with 401/)
+ end
+
+ it 'raises a ResponseError if the response contained a Phabricator error' do
+ fake_response = double(:http_response,
+ success?: true,
+ status: 200,
+ body: fixture_file('phabricator_responses/auth_failed.json'))
+
+ expect { described_class.parse!(fake_response) }
+ .to raise_error(Gitlab::PhabricatorImport::Conduit::ResponseError, /ERR-INVALID-AUTH: API token/)
+ end
+
+ it 'raises a ResponseError if JSON parsing failed' do
+ fake_response = double(:http_response,
+ success?: true,
+ status: 200,
+ body: 'This is no JSON')
+
+ expect { described_class.parse!(fake_response) }
+ .to raise_error(Gitlab::PhabricatorImport::Conduit::ResponseError, /unexpected token at/)
+ end
+
+ it 'returns a parsed response for valid input' do
+ fake_response = double(:http_response,
+ success?: true,
+ status: 200,
+ body: fixture_file('phabricator_responses/maniphest.search.json'))
+
+ expect(described_class.parse!(fake_response)).to be_a(described_class)
+ end
+ end
+
+ describe '#success?' do
+ it { expect(response).to be_success }
+ it { expect(error_response).not_to be_success }
+ end
+
+ describe '#error_code' do
+ it { expect(error_response.error_code).to eq('ERR-INVALID-AUTH') }
+ it { expect(response.error_code).to be_nil }
+ end
+
+ describe '#error_info' do
+ it 'returns the correct error info' do
+ expected_message = 'API token "api-token" has the wrong length. API tokens should be 32 characters long.'
+
+ expect(error_response.error_info).to eq(expected_message)
+ end
+
+ it { expect(response.error_info).to be_nil }
+ end
+
+ describe '#data' do
+ it { expect(error_response.data).to be_nil }
+ it { expect(response.data).to be_an(Array) }
+ end
+
+ describe '#pagination' do
+ it { expect(error_response.pagination).to be_nil }
+
+ it 'builds the pagination correctly' do
+ expect(response.pagination).to be_a(Gitlab::PhabricatorImport::Conduit::Pagination)
+ expect(response.pagination.next_page).to eq('284')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/conduit/tasks_response_spec.rb b/spec/lib/gitlab/phabricator_import/conduit/tasks_response_spec.rb
new file mode 100644
index 00000000000..4b4c2a6276e
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/conduit/tasks_response_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::Conduit::TasksResponse do
+ let(:conduit_response) do
+ Gitlab::PhabricatorImport::Conduit::Response
+ .new(JSON.parse(fixture_file('phabricator_responses/maniphest.search.json')))
+ end
+
+ subject(:response) { described_class.new(conduit_response) }
+
+ describe '#pagination' do
+ it 'delegates to the conduit reponse' do
+ expect(response.pagination).to eq(conduit_response.pagination)
+ end
+ end
+
+ describe '#tasks' do
+ it 'builds the correct tasks representation' do
+ tasks = response.tasks
+
+ titles = tasks.map(&:issue_attributes).map { |attrs| attrs[:title] }
+
+ expect(titles).to contain_exactly('Things are slow', 'Things are broken')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/import_tasks_worker_spec.rb b/spec/lib/gitlab/phabricator_import/import_tasks_worker_spec.rb
new file mode 100644
index 00000000000..1e38ef8aaa5
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/import_tasks_worker_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::ImportTasksWorker do
+ describe '#perform' do
+ it 'calls the correct importer' do
+ project = create(:project, :import_started, import_url: "https://the.phab.ulr")
+ fake_importer = instance_double(Gitlab::PhabricatorImport::Issues::Importer)
+
+ expect(Gitlab::PhabricatorImport::Issues::Importer).to receive(:new).with(project).and_return(fake_importer)
+ expect(fake_importer).to receive(:execute)
+
+ described_class.new.perform(project.id)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/importer_spec.rb b/spec/lib/gitlab/phabricator_import/importer_spec.rb
new file mode 100644
index 00000000000..bf14010a187
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/importer_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::Importer do
+ it { expect(described_class).to be_async }
+
+ it "acts like it's importing repositories" do
+ expect(described_class).to be_imports_repository
+ end
+
+ describe '#execute' do
+ let(:project) { create(:project, :import_scheduled) }
+ subject(:importer) { described_class.new(project) }
+
+ it 'sets a custom jid that will be kept up to date' do
+ expect { importer.execute }.to change { project.import_state.reload.jid }
+ end
+
+ it 'starts importing tasks' do
+ expect(Gitlab::PhabricatorImport::ImportTasksWorker).to receive(:schedule).with(project.id)
+
+ importer.execute
+ end
+
+ it 'marks the import as failed when something goes wrong' do
+ allow(importer).to receive(:schedule_first_tasks_page).and_raise('Stuff is broken')
+
+ importer.execute
+
+ expect(project.import_state).to be_failed
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/issues/importer_spec.rb b/spec/lib/gitlab/phabricator_import/issues/importer_spec.rb
new file mode 100644
index 00000000000..2412cf76f79
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/issues/importer_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::Issues::Importer do
+ set(:project) { create(:project) }
+
+ let(:response) do
+ Gitlab::PhabricatorImport::Conduit::TasksResponse.new(
+ Gitlab::PhabricatorImport::Conduit::Response
+ .new(JSON.parse(fixture_file('phabricator_responses/maniphest.search.json')))
+ )
+ end
+
+ subject(:importer) { described_class.new(project, nil) }
+
+ before do
+ client = instance_double(Gitlab::PhabricatorImport::Conduit::Maniphest)
+
+ allow(client).to receive(:tasks).and_return(response)
+ allow(importer).to receive(:client).and_return(client)
+ end
+
+ describe '#execute' do
+ it 'imports each task in the response' do
+ response.tasks.each do |task|
+ task_importer = instance_double(Gitlab::PhabricatorImport::Issues::TaskImporter)
+
+ expect(task_importer).to receive(:execute)
+ expect(Gitlab::PhabricatorImport::Issues::TaskImporter)
+ .to receive(:new).with(project, task)
+ .and_return(task_importer)
+ end
+
+ importer.execute
+ end
+
+ it 'schedules the next batch if there is one' do
+ expect(Gitlab::PhabricatorImport::ImportTasksWorker)
+ .to receive(:schedule).with(project.id, response.pagination.next_page)
+
+ importer.execute
+ end
+
+ it 'does not reschedule when there is no next page' do
+ allow(response.pagination).to receive(:has_next_page?).and_return(false)
+
+ expect(Gitlab::PhabricatorImport::ImportTasksWorker)
+ .not_to receive(:schedule)
+
+ importer.execute
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/issues/task_importer_spec.rb b/spec/lib/gitlab/phabricator_import/issues/task_importer_spec.rb
new file mode 100644
index 00000000000..1625604e754
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/issues/task_importer_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::Issues::TaskImporter do
+ set(:project) { create(:project) }
+ let(:task) do
+ Gitlab::PhabricatorImport::Representation::Task.new(
+ {
+ 'phid' => 'the-phid',
+ 'fields' => {
+ 'name' => 'Title',
+ 'description' => {
+ 'raw' => '# This is markdown\n it can contain more text.'
+ },
+ 'dateCreated' => '1518688921',
+ 'dateClosed' => '1518789995'
+ }
+ }
+ )
+ end
+
+ describe '#execute' do
+ it 'creates the issue with the expected attributes' do
+ issue = described_class.new(project, task).execute
+
+ expect(issue.project).to eq(project)
+ expect(issue).to be_persisted
+ expect(issue.author).to eq(User.ghost)
+ expect(issue.title).to eq('Title')
+ expect(issue.description).to eq('# This is markdown\n it can contain more text.')
+ expect(issue).to be_closed
+ expect(issue.created_at).to eq(Time.at(1518688921))
+ expect(issue.closed_at).to eq(Time.at(1518789995))
+ end
+
+ it 'does not recreate the issue when called multiple times' do
+ expect { described_class.new(project, task).execute }
+ .to change { project.issues.reload.size }.from(0).to(1)
+ expect { described_class.new(project, task).execute }
+ .not_to change { project.issues.reload.size }
+ end
+
+ it 'does not trigger a save when the object did not change' do
+ existing_issue = create(:issue,
+ task.issue_attributes.merge(author: User.ghost))
+ importer = described_class.new(project, task)
+ allow(importer).to receive(:issue).and_return(existing_issue)
+
+ expect(existing_issue).not_to receive(:save!)
+
+ importer.execute
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/project_creator_spec.rb b/spec/lib/gitlab/phabricator_import/project_creator_spec.rb
new file mode 100644
index 00000000000..e9455b866ac
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/project_creator_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::ProjectCreator do
+ let(:user) { create(:user) }
+ let(:params) do
+ { path: 'new-phab-import',
+ phabricator_server_url: 'http://phab.example.com',
+ api_token: 'the-token' }
+ end
+ subject(:creator) { described_class.new(user, params) }
+
+ describe '#execute' do
+ it 'creates a project correctly and schedule an import' do
+ expect_next_instance_of(Gitlab::PhabricatorImport::Importer) do |importer|
+ expect(importer).to receive(:execute)
+ end
+
+ project = creator.execute
+
+ expect(project).to be_persisted
+ expect(project).to be_import
+ expect(project.import_type).to eq('phabricator')
+ expect(project.import_data.credentials).to match(a_hash_including(api_token: 'the-token'))
+ expect(project.import_data.data).to match(a_hash_including('phabricator_url' => 'http://phab.example.com'))
+ expect(project.import_url).to eq(Project::UNKNOWN_IMPORT_URL)
+ expect(project.namespace).to eq(user.namespace)
+ end
+
+ context 'when import params are missing' do
+ let(:params) do
+ { path: 'new-phab-import',
+ phabricator_server_url: 'http://phab.example.com',
+ api_token: '' }
+ end
+
+ it 'returns nil' do
+ expect(creator.execute).to be_nil
+ end
+ end
+
+ context 'when import params are invalid' do
+ let(:params) do
+ { path: 'new-phab-import',
+ namespace_id: '-1',
+ phabricator_server_url: 'http://phab.example.com',
+ api_token: 'the-token' }
+ end
+
+ it 'returns an unpersisted project' do
+ project = creator.execute
+
+ expect(project).not_to be_persisted
+ expect(project).not_to be_valid
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/representation/task_spec.rb b/spec/lib/gitlab/phabricator_import/representation/task_spec.rb
new file mode 100644
index 00000000000..dfbd8c546eb
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/representation/task_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::Representation::Task do
+ subject(:task) do
+ described_class.new(
+ {
+ 'phid' => 'the-phid',
+ 'fields' => {
+ 'name' => 'Title'.ljust(257, '.'), # A string padded to 257 chars
+ 'description' => {
+ 'raw' => '# This is markdown\n it can contain more text.'
+ },
+ 'dateCreated' => '1518688921',
+ 'dateClosed' => '1518789995'
+ }
+ }
+ )
+ end
+
+ describe '#issue_attributes' do
+ it 'contains the expected values' do
+ expected_attributes = {
+ title: 'Title'.ljust(255, '.'),
+ description: '# This is markdown\n it can contain more text.',
+ state: :closed,
+ created_at: Time.at(1518688921),
+ closed_at: Time.at(1518789995)
+ }
+
+ expect(task.issue_attributes).to eq(expected_attributes)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/worker_state_spec.rb b/spec/lib/gitlab/phabricator_import/worker_state_spec.rb
new file mode 100644
index 00000000000..a44947445c9
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/worker_state_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::WorkerState, :clean_gitlab_redis_shared_state do
+ subject(:state) { described_class.new('weird-project-id') }
+ let(:key) { 'phabricator-import/jobs/project-weird-project-id/job-count' }
+
+ describe '#add_job' do
+ it 'increments the counter for jobs' do
+ set_value(3)
+
+ expect { state.add_job }.to change { get_value }.from('3').to('4')
+ end
+ end
+
+ describe '#remove_job' do
+ it 'decrements the counter for jobs' do
+ set_value(3)
+
+ expect { state.remove_job }.to change { get_value }.from('3').to('2')
+ end
+ end
+
+ describe '#running_count' do
+ it 'reads the value' do
+ set_value(9)
+
+ expect(state.running_count).to eq(9)
+ end
+
+ it 'returns 0 when nothing was set' do
+ expect(state.running_count).to eq(0)
+ end
+ end
+
+ def set_value(value)
+ redis.with { |r| r.set(key, value) }
+ end
+
+ def get_value
+ redis.with { |r| r.get(key) }
+ end
+
+ def redis
+ Gitlab::Redis::SharedState
+ end
+end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 312aa3be490..3d27156b356 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -256,4 +256,28 @@ describe Gitlab::SearchResults do
expect(results.objects('merge_requests')).not_to include merge_request
end
+
+ context 'milestones' do
+ it 'returns correct set of milestones' do
+ private_project_1 = create(:project, :private)
+ private_project_2 = create(:project, :private)
+ internal_project = create(:project, :internal)
+ public_project_1 = create(:project, :public)
+ public_project_2 = create(:project, :public, :issues_disabled, :merge_requests_disabled)
+ private_project_1.add_developer(user)
+ # milestones that should not be visible
+ create(:milestone, project: private_project_2, title: 'Private project without access milestone')
+ create(:milestone, project: public_project_2, title: 'Public project with milestones disabled milestone')
+ # milestones that should be visible
+ milestone_1 = create(:milestone, project: private_project_1, title: 'Private project with access milestone', state: 'closed')
+ milestone_2 = create(:milestone, project: internal_project, title: 'Internal project milestone')
+ milestone_3 = create(:milestone, project: public_project_1, title: 'Public project with milestones enabled milestone')
+ # Global search scope takes user authorized projects, internal projects and public projects.
+ limit_projects = ProjectsFinder.new(current_user: user).execute
+
+ milestones = described_class.new(user, limit_projects, 'milestone').objects('milestones')
+
+ expect(milestones).to match_array([milestone_1, milestone_2, milestone_3])
+ end
+ end
end
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index e2f09de2808..bce2e754176 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -612,16 +612,6 @@ describe Gitlab::Shell do
FileUtils.rm_rf(created_path)
end
- it 'creates a repository' do
- expect(gitlab_shell.create_repository(repository_storage, repo_name, repo_name)).to be_truthy
-
- expect(File.stat(created_path).mode & 0o777).to eq(0o770)
-
- hooks_path = File.join(created_path, 'hooks')
- expect(File.lstat(hooks_path)).to be_symlink
- expect(File.realpath(hooks_path)).to eq(gitlab_shell_hooks_path)
- end
-
it 'returns false when the command fails' do
FileUtils.mkdir_p(File.dirname(created_path))
# This file will block the creation of the repo's .git directory. That
diff --git a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
index fe46c67a920..5f0a7e925ca 100644
--- a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
+++ b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
@@ -15,6 +15,13 @@ describe Gitlab::Template::GitlabCiYmlTemplate do
expect(all).to include('Docker')
expect(all).to include('Ruby')
end
+
+ it 'ensure that the template name is used exactly once' do
+ all = subject.all.group_by(&:name)
+ duplicates = all.select { |_, templates| templates.length > 1 }
+
+ expect(duplicates).to be_empty
+ end
end
describe '.find' do
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index 445a56ab0d8..253366e0789 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -2,6 +2,87 @@
require 'spec_helper'
describe Gitlab::UrlBlocker do
+ describe '#validate!' do
+ context 'when URI is nil' do
+ let(:import_url) { nil }
+
+ it 'returns no URI and hostname' do
+ uri, hostname = described_class.validate!(import_url)
+
+ expect(uri).to be(nil)
+ expect(hostname).to be(nil)
+ end
+ end
+
+ context 'when URI is internal' do
+ let(:import_url) { 'http://localhost' }
+
+ it 'returns URI and no hostname' do
+ uri, hostname = described_class.validate!(import_url)
+
+ expect(uri).to eq(Addressable::URI.parse('http://[::1]'))
+ expect(hostname).to eq('localhost')
+ end
+ end
+
+ context 'when the URL hostname is a domain' do
+ let(:import_url) { 'https://example.org' }
+
+ it 'returns URI and hostname' do
+ uri, hostname = described_class.validate!(import_url)
+
+ expect(uri).to eq(Addressable::URI.parse('https://93.184.216.34'))
+ expect(hostname).to eq('example.org')
+ end
+ end
+
+ context 'when the URL hostname is an IP address' do
+ let(:import_url) { 'https://93.184.216.34' }
+
+ it 'returns URI and no hostname' do
+ uri, hostname = described_class.validate!(import_url)
+
+ expect(uri).to eq(Addressable::URI.parse('https://93.184.216.34'))
+ expect(hostname).to be(nil)
+ end
+ end
+
+ context 'disabled DNS rebinding protection' do
+ context 'when URI is internal' do
+ let(:import_url) { 'http://localhost' }
+
+ it 'returns URI and no hostname' do
+ uri, hostname = described_class.validate!(import_url, dns_rebind_protection: false)
+
+ expect(uri).to eq(Addressable::URI.parse('http://localhost'))
+ expect(hostname).to be(nil)
+ end
+ end
+
+ context 'when the URL hostname is a domain' do
+ let(:import_url) { 'https://example.org' }
+
+ it 'returns URI and no hostname' do
+ uri, hostname = described_class.validate!(import_url, dns_rebind_protection: false)
+
+ expect(uri).to eq(Addressable::URI.parse('https://example.org'))
+ expect(hostname).to eq(nil)
+ end
+ end
+
+ context 'when the URL hostname is an IP address' do
+ let(:import_url) { 'https://93.184.216.34' }
+
+ it 'returns URI and no hostname' do
+ uri, hostname = described_class.validate!(import_url, dns_rebind_protection: false)
+
+ expect(uri).to eq(Addressable::URI.parse('https://93.184.216.34'))
+ expect(hostname).to be(nil)
+ end
+ end
+ end
+ end
+
describe '#blocked_url?' do
let(:ports) { Project::VALID_IMPORT_PORTS }
@@ -208,7 +289,7 @@ describe Gitlab::UrlBlocker do
end
def stub_domain_resolv(domain, ip)
- address = double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false)
+ address = double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false, ipv4?: false)
allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([address])
allow(address).to receive(:ipv6_v4mapped?).and_return(false)
end
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index 9f495a5d50b..bbcb92608d8 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -32,7 +32,7 @@ describe Gitlab::UrlBuilder do
url = described_class.build(milestone)
- expect(url).to eq "#{Settings.gitlab['url']}/#{milestone.project.full_path}/milestones/#{milestone.iid}"
+ expect(url).to eq "#{Settings.gitlab['url']}/#{milestone.project.full_path}/-/milestones/#{milestone.iid}"
end
end
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index 5861e6955a6..7242255d535 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -115,6 +115,40 @@ describe Gitlab::UrlSanitizer do
end
end
+ describe '#user' do
+ context 'credentials in hash' do
+ it 'overrides URL-provided user' do
+ sanitizer = described_class.new('http://a:b@example.com', credentials: { user: 'c', password: 'd' })
+
+ expect(sanitizer.user).to eq('c')
+ end
+ end
+
+ context 'credentials in URL' do
+ where(:url, :user) do
+ 'http://foo:bar@example.com' | 'foo'
+ 'http://foo:bar:baz@example.com' | 'foo'
+ 'http://:bar@example.com' | nil
+ 'http://foo:@example.com' | 'foo'
+ 'http://foo@example.com' | 'foo'
+ 'http://:@example.com' | nil
+ 'http://@example.com' | nil
+ 'http://example.com' | nil
+
+ # Other invalid URLs
+ nil | nil
+ '' | nil
+ 'no' | nil
+ end
+
+ with_them do
+ subject { described_class.new(url).user }
+
+ it { is_expected.to eq(user) }
+ end
+ end
+ end
+
describe '#full_url' do
context 'credentials in hash' do
where(:credentials, :userinfo) do
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index 767b5779a79..e075904b0cc 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -109,4 +109,34 @@ describe Gitlab do
expect(described_class.ee?).to eq(false)
end
end
+
+ describe '.http_proxy_env?' do
+ it 'returns true when lower case https' do
+ stub_env('https_proxy', 'https://my.proxy')
+
+ expect(described_class.http_proxy_env?).to eq(true)
+ end
+
+ it 'returns true when upper case https' do
+ stub_env('HTTPS_PROXY', 'https://my.proxy')
+
+ expect(described_class.http_proxy_env?).to eq(true)
+ end
+
+ it 'returns true when lower case http' do
+ stub_env('http_proxy', 'http://my.proxy')
+
+ expect(described_class.http_proxy_env?).to eq(true)
+ end
+
+ it 'returns true when upper case http' do
+ stub_env('HTTP_PROXY', 'http://my.proxy')
+
+ expect(described_class.http_proxy_env?).to eq(true)
+ end
+
+ it 'returns false when not set' do
+ expect(described_class.http_proxy_env?).to eq(false)
+ end
+ end
end
diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb
index 77fea5b2d24..346455067a7 100644
--- a/spec/lib/mattermost/session_spec.rb
+++ b/spec/lib/mattermost/session_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe Mattermost::Session, type: :request do
include ExclusiveLeaseHelpers
+ include StubRequests
let(:user) { create(:user) }
@@ -24,7 +25,7 @@ describe Mattermost::Session, type: :request do
let(:location) { 'http://location.tld' }
let(:cookie_header) {'MMOAUTH=taskik8az7rq8k6rkpuas7htia; Path=/;'}
let!(:stub) do
- WebMock.stub_request(:get, "#{mattermost_url}/oauth/gitlab/login")
+ stub_full_request("#{mattermost_url}/oauth/gitlab/login")
.to_return(headers: { 'location' => location, 'Set-Cookie' => cookie_header }, status: 302)
end
@@ -63,7 +64,7 @@ describe Mattermost::Session, type: :request do
end
before do
- WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete")
+ stub_full_request("#{mattermost_url}/signup/gitlab/complete")
.with(query: hash_including({ 'state' => state }))
.to_return do |request|
post "/oauth/token",
@@ -80,7 +81,7 @@ describe Mattermost::Session, type: :request do
end
end
- WebMock.stub_request(:post, "#{mattermost_url}/api/v4/users/logout")
+ stub_full_request("#{mattermost_url}/api/v4/users/logout", method: :post)
.to_return(headers: { Authorization: 'token thisworksnow' }, status: 200)
end
diff --git a/spec/mailers/emails/pages_domains_spec.rb b/spec/mailers/emails/pages_domains_spec.rb
index 050af587061..2f594dbf9d1 100644
--- a/spec/mailers/emails/pages_domains_spec.rb
+++ b/spec/mailers/emails/pages_domains_spec.rb
@@ -5,11 +5,13 @@ describe Emails::PagesDomains do
include EmailSpec::Matchers
include_context 'gitlab email notification'
- set(:project) { create(:project) }
set(:domain) { create(:pages_domain, project: project) }
- set(:user) { project.owner }
+ set(:user) { project.creator }
shared_examples 'a pages domain email' do
+ let(:test_recipient) { user }
+
+ it_behaves_like 'an email sent to a user'
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like 'a user cannot unsubscribe through footer link'
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 8f348b1b053..cbbb22ad78c 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -45,6 +45,10 @@ describe Notify do
context 'for a project' do
shared_examples 'an assignee email' do
+ let(:test_recipient) { assignee }
+
+ it_behaves_like 'an email sent to a user'
+
it 'is sent to the assignee as the author' do
sender = subject.header[:from].addrs.first
@@ -618,8 +622,10 @@ describe Notify do
end
describe 'project was moved' do
+ let(:test_recipient) { user }
subject { described_class.project_was_moved_email(project.id, user.id, "gitlab/gitlab") }
+ it_behaves_like 'an email sent to a user'
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
@@ -773,7 +779,7 @@ describe Notify do
invitee
end
- subject { described_class.member_invite_declined_email('project', project.id, project_member.invite_email, maintainer.id) }
+ subject { described_class.member_invite_declined_email('Project', project.id, project_member.invite_email, maintainer.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
@@ -1083,8 +1089,6 @@ describe Notify do
end
context 'for a group' do
- set(:group) { create(:group) }
-
describe 'group access requested' do
let(:group) { create(:group, :public, :access_requestable) }
let(:group_member) do
diff --git a/spec/migrations/change_packages_size_defaults_in_project_statistics_spec.rb b/spec/migrations/change_packages_size_defaults_in_project_statistics_spec.rb
new file mode 100644
index 00000000000..93e7e9304b1
--- /dev/null
+++ b/spec/migrations/change_packages_size_defaults_in_project_statistics_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20190516155724_change_packages_size_defaults_in_project_statistics.rb')
+
+describe ChangePackagesSizeDefaultsInProjectStatistics, :migration do
+ let(:project_statistics) { table(:project_statistics) }
+ let(:projects) { table(:projects) }
+
+ it 'removes null packages_size' do
+ stats_to_migrate = 10
+
+ stats_to_migrate.times do |i|
+ p = projects.create!(name: "project #{i}", namespace_id: 1)
+ project_statistics.create!(project_id: p.id, namespace_id: p.namespace_id)
+ end
+
+ expect { migrate! }
+ .to change { ProjectStatistics.where(packages_size: nil).count }
+ .from(stats_to_migrate)
+ .to(0)
+ end
+
+ it 'defaults packages_size to 0' do
+ project = projects.create!(name: 'a new project', namespace_id: 2)
+ stat = project_statistics.create!(project_id: project.id, namespace_id: project.namespace_id)
+
+ expect(stat.packages_size).to be_nil
+
+ migrate!
+
+ stat.reload
+ expect(stat.packages_size).to eq(0)
+ end
+end
diff --git a/spec/migrations/enqueue_reset_merge_status_spec.rb b/spec/migrations/enqueue_reset_merge_status_spec.rb
new file mode 100644
index 00000000000..0d5e33bfd46
--- /dev/null
+++ b/spec/migrations/enqueue_reset_merge_status_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190528180441_enqueue_reset_merge_status.rb')
+
+describe EnqueueResetMergeStatus, :migration, :sidekiq do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') }
+ let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') }
+ let(:merge_requests) { table(:merge_requests) }
+
+ def create_merge_request(id, extra_params = {})
+ params = {
+ id: id,
+ target_project_id: project.id,
+ target_branch: 'master',
+ source_project_id: project.id,
+ source_branch: 'mr name',
+ title: "mr name#{id}"
+ }.merge(extra_params)
+
+ merge_requests.create!(params)
+ end
+
+ it 'correctly schedules background migrations' do
+ create_merge_request(1, state: 'opened', merge_status: 'can_be_merged')
+ create_merge_request(2, state: 'opened', merge_status: 'can_be_merged')
+ create_merge_request(3, state: 'opened', merge_status: 'can_be_merged')
+ create_merge_request(4, state: 'merged', merge_status: 'can_be_merged')
+ create_merge_request(5, state: 'opened', merge_status: 'unchecked')
+
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(5.minutes, 1, 2)
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(10.minutes, 3, 3)
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/enqueue_verify_pages_domain_workers_spec.rb b/spec/migrations/enqueue_verify_pages_domain_workers_spec.rb
index afcaefa0591..abf39317188 100644
--- a/spec/migrations/enqueue_verify_pages_domain_workers_spec.rb
+++ b/spec/migrations/enqueue_verify_pages_domain_workers_spec.rb
@@ -8,9 +8,13 @@ describe EnqueueVerifyPagesDomainWorkers, :sidekiq, :migration do
end
end
+ let(:domains_table) { table(:pages_domains) }
+
describe '#up' do
it 'enqueues a verification worker for every domain' do
- domains = 1.upto(3).map { |i| PagesDomain.create!(domain: "my#{i}.domain.com") }
+ domains = Array.new(3) do |i|
+ domains_table.create!(domain: "my#{i}.domain.com", verification_code: "123#{i}")
+ end
expect { migrate! }.to change(PagesDomainVerificationWorker.jobs, :size).by(3)
diff --git a/spec/migrations/generate_lets_encrypt_private_key_spec.rb b/spec/migrations/generate_lets_encrypt_private_key_spec.rb
index f47cc0c36ef..773bf5222f0 100644
--- a/spec/migrations/generate_lets_encrypt_private_key_spec.rb
+++ b/spec/migrations/generate_lets_encrypt_private_key_spec.rb
@@ -3,17 +3,9 @@ require Rails.root.join('db', 'migrate', '20190524062810_generate_lets_encrypt_p
describe GenerateLetsEncryptPrivateKey, :migration do
describe '#up' do
- let(:applications_settings) { table(:applications_settings) }
-
- it 'generates RSA private key and saves it in application settings' do
- application_setting = described_class::ApplicationSetting.create!
-
- described_class.new.up
- application_setting.reload
-
- expect(application_setting.lets_encrypt_private_key).to be_present
+ it 'does not fail' do
expect do
- OpenSSL::PKey::RSA.new(application_setting.lets_encrypt_private_key)
+ described_class.new.up
end.not_to raise_error
end
end
diff --git a/spec/migrations/generate_missing_routes_spec.rb b/spec/migrations/generate_missing_routes_spec.rb
index 32515d353b0..30ad135d4df 100644
--- a/spec/migrations/generate_missing_routes_spec.rb
+++ b/spec/migrations/generate_missing_routes_spec.rb
@@ -8,7 +8,7 @@ describe GenerateMissingRoutes, :migration do
let(:routes) { table(:routes) }
it 'creates routes for projects without a route' do
- namespace = namespaces.create!(name: 'GitLab', path: 'gitlab')
+ namespace = namespaces.create!(name: 'GitLab', path: 'gitlab', type: 'Group')
routes.create!(
path: 'gitlab',
diff --git a/spec/migrations/migrate_old_artifacts_spec.rb b/spec/migrations/migrate_old_artifacts_spec.rb
index 79e21514506..bc826d91471 100644
--- a/spec/migrations/migrate_old_artifacts_spec.rb
+++ b/spec/migrations/migrate_old_artifacts_spec.rb
@@ -45,10 +45,6 @@ describe MigrateOldArtifacts, :migration, schema: 20170918072948 do
expect(build_with_legacy_artifacts.artifacts?).to be_falsey
end
- it "legacy artifacts are set" do
- expect(build_with_legacy_artifacts.legacy_artifacts_file_identifier).not_to be_nil
- end
-
describe '#min_id' do
subject { migration.send(:min_id) }
diff --git a/spec/migrations/remove_orphaned_label_links_spec.rb b/spec/migrations/remove_orphaned_label_links_spec.rb
index 13b8919343e..e8c44c141c3 100644
--- a/spec/migrations/remove_orphaned_label_links_spec.rb
+++ b/spec/migrations/remove_orphaned_label_links_spec.rb
@@ -10,6 +10,12 @@ describe RemoveOrphanedLabelLinks, :migration do
let(:project) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
let(:label) { create_label }
+ before do
+ # This migration was created before we introduced ProjectCiCdSetting#default_git_depth
+ allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth).and_return(nil)
+ allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth=).and_return(0)
+ end
+
context 'add foreign key on label_id' do
let!(:label_link_with_label) { create_label_link(label_id: label.id) }
let!(:label_link_without_label) { create_label_link(label_id: nil) }
diff --git a/spec/migrations/schedule_fill_valid_time_for_pages_domain_certificates_spec.rb b/spec/migrations/schedule_fill_valid_time_for_pages_domain_certificates_spec.rb
new file mode 100644
index 00000000000..54f3e264df0
--- /dev/null
+++ b/spec/migrations/schedule_fill_valid_time_for_pages_domain_certificates_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190524073827_schedule_fill_valid_time_for_pages_domain_certificates.rb')
+
+describe ScheduleFillValidTimeForPagesDomainCertificates, :migration, :sidekiq do
+ let(:migration_class) { described_class::MIGRATION }
+ let(:migration_name) { migration_class.to_s.demodulize }
+
+ let(:domains_table) { table(:pages_domains) }
+
+ let(:certificate) do
+ File.read('spec/fixtures/passphrase_x509_certificate.crt')
+ end
+
+ before do
+ domains_table.create!(domain: "domain1.example.com", verification_code: "123")
+ domains_table.create!(domain: "domain2.example.com", verification_code: "123", certificate: '')
+ domains_table.create!(domain: "domain3.example.com", verification_code: "123", certificate: certificate)
+ domains_table.create!(domain: "domain4.example.com", verification_code: "123", certificate: certificate)
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ first_id = domains_table.find_by_domain("domain3.example.com").id
+ last_id = domains_table.find_by_domain("domain4.example.com").id
+
+ expect(migration_name).to be_scheduled_delayed_migration(5.minutes, first_id, last_id)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ end
+ end
+ end
+
+ it 'sets certificate valid_not_before/not_after' do
+ perform_enqueued_jobs do
+ migrate!
+
+ domain = domains_table.find_by_domain("domain3.example.com")
+ expect(domain.certificate_valid_not_before)
+ .to eq(Time.parse("2018-03-23 14:02:08 UTC"))
+ expect(domain.certificate_valid_not_after)
+ .to eq(Time.parse("2019-03-23 14:02:08 UTC"))
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 32eef9e0e01..d98db024f73 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -30,12 +30,6 @@ describe Ci::Build do
it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) }
it { is_expected.to include_module(Ci::PipelineDelegator) }
- it { is_expected.to be_a(ArtifactMigratable) }
-
- it_behaves_like 'UpdateProjectStatistics' do
- subject { FactoryBot.build(:ci_build, pipeline: pipeline, artifacts_size: 23) }
- end
-
describe 'associations' do
it 'has a bidirectional relationship with projects' do
expect(described_class.reflect_on_association(:project).has_inverse?).to eq(:builds)
@@ -116,24 +110,6 @@ describe Ci::Build do
end
end
- context 'when job has a legacy archive' do
- let!(:job) { create(:ci_build, :legacy_artifacts) }
-
- it 'returns the job' do
- is_expected.to include(job)
- end
-
- context 'when ci_enable_legacy_artifacts feature flag is disabled' do
- before do
- stub_feature_flags(ci_enable_legacy_artifacts: false)
- end
-
- it 'does not return the job' do
- is_expected.not_to include(job)
- end
- end
- end
-
context 'when job has a job artifact archive' do
let!(:job) { create(:ci_build, :artifacts) }
@@ -464,51 +440,11 @@ describe Ci::Build do
end
end
end
-
- context 'when legacy artifacts are used' do
- let(:build) { create(:ci_build, :legacy_artifacts) }
-
- subject { build.artifacts? }
-
- context 'is expired' do
- let(:build) { create(:ci_build, :legacy_artifacts, :expired) }
-
- it { is_expected.to be_falsy }
- end
-
- context 'artifacts archive does not exist' do
- let(:build) { create(:ci_build) }
-
- it { is_expected.to be_falsy }
- end
-
- context 'artifacts archive exists' do
- let(:build) { create(:ci_build, :legacy_artifacts) }
-
- it { is_expected.to be_truthy }
-
- context 'when ci_enable_legacy_artifacts feature flag is disabled' do
- before do
- stub_feature_flags(ci_enable_legacy_artifacts: false)
- end
-
- it { is_expected.to be_falsy }
- end
- end
- end
end
describe '#browsable_artifacts?' do
subject { build.browsable_artifacts? }
- context 'artifacts metadata does not exist' do
- before do
- build.update(legacy_artifacts_metadata: nil)
- end
-
- it { is_expected.to be_falsy }
- end
-
context 'artifacts metadata does exists' do
let(:build) { create(:ci_build, :artifacts) }
@@ -764,12 +700,6 @@ describe Ci::Build do
it { is_expected.to be_truthy }
end
-
- context 'when build does not have job artifacts' do
- let(:build) { create(:ci_build, :legacy_artifacts) }
-
- it { is_expected.to be_falsy }
- end
end
describe '#has_old_trace?' do
@@ -1096,11 +1026,11 @@ describe Ci::Build do
describe 'erasable build' do
shared_examples 'erasable' do
it 'removes artifact file' do
- expect(build.artifacts_file.exists?).to be_falsy
+ expect(build.artifacts_file.present?).to be_falsy
end
it 'removes artifact metadata file' do
- expect(build.artifacts_metadata.exists?).to be_falsy
+ expect(build.artifacts_metadata.present?).to be_falsy
end
it 'removes all job_artifacts' do
@@ -1192,7 +1122,7 @@ describe Ci::Build do
let!(:build) { create(:ci_build, :success, :artifacts) }
before do
- build.remove_artifacts_metadata!
+ build.erase_erasable_artifacts!
end
describe '#erase' do
@@ -1203,76 +1133,6 @@ describe Ci::Build do
end
end
end
-
- context 'old artifacts' do
- context 'build is erasable' do
- context 'new artifacts' do
- let!(:build) { create(:ci_build, :trace_artifact, :success, :legacy_artifacts) }
-
- describe '#erase' do
- before do
- build.erase(erased_by: erased_by)
- end
-
- context 'erased by user' do
- let!(:erased_by) { create(:user, username: 'eraser') }
-
- include_examples 'erasable'
-
- it 'records user who erased a build' do
- expect(build.erased_by).to eq erased_by
- end
- end
-
- context 'erased by system' do
- let(:erased_by) { nil }
-
- include_examples 'erasable'
-
- it 'does not set user who erased a build' do
- expect(build.erased_by).to be_nil
- end
- end
- end
-
- describe '#erasable?' do
- subject { build.erasable? }
- it { is_expected.to be_truthy }
- end
-
- describe '#erased?' do
- let!(:build) { create(:ci_build, :trace_artifact, :success, :legacy_artifacts) }
- subject { build.erased? }
-
- context 'job has not been erased' do
- it { is_expected.to be_falsey }
- end
-
- context 'job has been erased' do
- before do
- build.erase
- end
-
- it { is_expected.to be_truthy }
- end
- end
-
- context 'metadata and build trace are not available' do
- let!(:build) { create(:ci_build, :success, :legacy_artifacts) }
-
- before do
- build.remove_artifacts_metadata!
- end
-
- describe '#erase' do
- it 'does not raise error' do
- expect { build.erase }.not_to raise_error
- end
- end
- end
- end
- end
- end
end
describe '#erase_erasable_artifacts!' do
@@ -1434,7 +1294,7 @@ describe Ci::Build do
build.cancel!
end
- it { is_expected.not_to be_retryable }
+ it { is_expected.to be_retryable }
end
end
@@ -1964,7 +1824,7 @@ describe Ci::Build do
context 'when build has been canceled' do
subject { build_stubbed(:ci_build, :manual, status: :canceled) }
- it { is_expected.not_to be_playable }
+ it { is_expected.to be_playable }
end
context 'when build is successful' do
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index e6d682c24d9..1ba66565e03 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -5,10 +5,6 @@ require 'spec_helper'
describe Ci::JobArtifact do
let(:artifact) { create(:ci_job_artifact, :archive) }
- it_behaves_like 'UpdateProjectStatistics' do
- subject { build(:ci_job_artifact, :archive, size: 106365) }
- end
-
describe "Associations" do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:job) }
@@ -23,6 +19,10 @@ describe Ci::JobArtifact do
it_behaves_like 'having unique enum values'
+ it_behaves_like 'UpdateProjectStatistics' do
+ subject { build(:ci_job_artifact, :archive, size: 106365) }
+ end
+
describe '.with_reports' do
let!(:artifact) { create(:ci_job_artifact, :archive) }
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index 42d4769a921..227870eb27f 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -48,32 +48,116 @@ describe Ci::PipelineSchedule do
end
end
+ describe '.runnable_schedules' do
+ subject { described_class.runnable_schedules }
+
+ let!(:pipeline_schedule) do
+ Timecop.freeze(1.day.ago) do
+ create(:ci_pipeline_schedule, :hourly)
+ end
+ end
+
+ it 'returns the runnable schedule' do
+ is_expected.to eq([pipeline_schedule])
+ end
+
+ context 'when there are no runnable schedules' do
+ let!(:pipeline_schedule) { }
+
+ it 'returns an empty array' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '.preloaded' do
+ subject { described_class.preloaded }
+
+ before do
+ create_list(:ci_pipeline_schedule, 3)
+ end
+
+ it 'preloads the associations' do
+ subject
+
+ query = ActiveRecord::QueryRecorder.new { subject.each(&:project) }
+
+ expect(query.count).to eq(2)
+ end
+ end
+
describe '#set_next_run_at' do
- let!(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly) }
+ let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly) }
+ let(:ideal_next_run_at) { pipeline_schedule.send(:ideal_next_run_at) }
+
+ let(:expected_next_run_at) do
+ Gitlab::Ci::CronParser.new(Settings.cron_jobs['pipeline_schedule_worker']['cron'], Time.zone.name)
+ .next_time_from(ideal_next_run_at)
+ end
+
+ let(:cron_worker_next_run_at) do
+ Gitlab::Ci::CronParser.new(Settings.cron_jobs['pipeline_schedule_worker']['cron'], Time.zone.name)
+ .next_time_from(Time.zone.now)
+ end
context 'when creates new pipeline schedule' do
- let(:expected_next_run_at) do
- Gitlab::Ci::CronParser.new(pipeline_schedule.cron, pipeline_schedule.cron_timezone)
- .next_time_from(Time.now)
+ it 'updates next_run_at automatically' do
+ expect(pipeline_schedule.next_run_at).to eq(expected_next_run_at)
end
+ end
- it 'updates next_run_at automatically' do
- expect(described_class.last.next_run_at).to eq(expected_next_run_at)
+ context 'when PipelineScheduleWorker runs at a specific interval' do
+ before do
+ allow(Settings).to receive(:cron_jobs) do
+ {
+ 'pipeline_schedule_worker' => {
+ 'cron' => '0 1 2 3 *'
+ }
+ }
+ end
+ end
+
+ it "updates next_run_at to the sidekiq worker's execution time" do
+ expect(pipeline_schedule.next_run_at.min).to eq(0)
+ expect(pipeline_schedule.next_run_at.hour).to eq(1)
+ expect(pipeline_schedule.next_run_at.day).to eq(2)
+ expect(pipeline_schedule.next_run_at.month).to eq(3)
end
end
- context 'when updates cron of exsisted pipeline schedule' do
- let(:new_cron) { '0 0 1 1 *' }
+ context 'when pipeline schedule runs every minute' do
+ let(:pipeline_schedule) { create(:ci_pipeline_schedule, :every_minute) }
- let(:expected_next_run_at) do
- Gitlab::Ci::CronParser.new(new_cron, pipeline_schedule.cron_timezone)
- .next_time_from(Time.now)
+ it "updates next_run_at to the sidekiq worker's execution time", :quarantine do
+ expect(pipeline_schedule.next_run_at).to eq(cron_worker_next_run_at)
+ end
+ end
+
+ context 'when there are two different pipeline schedules in different time zones' do
+ let(:pipeline_schedule_1) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'Eastern Time (US & Canada)') }
+ let(:pipeline_schedule_2) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') }
+
+ it 'sets different next_run_at' do
+ expect(pipeline_schedule_1.next_run_at).not_to eq(pipeline_schedule_2.next_run_at)
+ end
+ end
+
+ context 'when there are two different pipeline schedules in the same time zones' do
+ let(:pipeline_schedule_1) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') }
+ let(:pipeline_schedule_2) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') }
+
+ it 'sets the sames next_run_at' do
+ expect(pipeline_schedule_1.next_run_at).to eq(pipeline_schedule_2.next_run_at)
end
+ end
+
+ context 'when updates cron of exsisted pipeline schedule' do
+ let(:new_cron) { '0 0 1 1 *' }
it 'updates next_run_at automatically' do
pipeline_schedule.update!(cron: new_cron)
- expect(described_class.last.next_run_at).to eq(expected_next_run_at)
+ expect(pipeline_schedule.next_run_at).to eq(expected_next_run_at)
end
end
end
@@ -83,10 +167,11 @@ describe Ci::PipelineSchedule do
context 'when reschedules after 10 days from now' do
let(:future_time) { 10.days.from_now }
+ let(:ideal_next_run_at) { pipeline_schedule.send(:ideal_next_run_at) }
let(:expected_next_run_at) do
- Gitlab::Ci::CronParser.new(pipeline_schedule.cron, pipeline_schedule.cron_timezone)
- .next_time_from(future_time)
+ Gitlab::Ci::CronParser.new(Settings.cron_jobs['pipeline_schedule_worker']['cron'], Time.zone.name)
+ .next_time_from(ideal_next_run_at)
end
it 'points to proper next_run_at' do
@@ -99,38 +184,6 @@ describe Ci::PipelineSchedule do
end
end
- describe '#real_next_run' do
- subject do
- described_class.last.real_next_run(worker_cron: worker_cron,
- worker_time_zone: worker_time_zone)
- end
-
- context 'when GitLab time_zone is UTC' do
- before do
- allow(Time).to receive(:zone)
- .and_return(ActiveSupport::TimeZone[worker_time_zone])
- end
-
- let(:worker_time_zone) { 'UTC' }
-
- context 'when cron_timezone is Eastern Time (US & Canada)' do
- before do
- create(:ci_pipeline_schedule, :nightly,
- cron_timezone: 'Eastern Time (US & Canada)')
- end
-
- let(:worker_cron) { '0 1 2 3 *' }
-
- it 'returns the next time worker executes' do
- expect(subject.min).to eq(0)
- expect(subject.hour).to eq(1)
- expect(subject.day).to eq(2)
- expect(subject.month).to eq(3)
- end
- end
- end
- end
-
describe '#job_variables' do
let!(:pipeline_schedule) { create(:ci_pipeline_schedule) }
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index a0319b3eb0a..a8701f0efa4 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1381,6 +1381,40 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe 'auto merge' do
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, :running, project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+
+ before do
+ merge_request.update_head_pipeline
+ end
+
+ %w[succeed! drop! cancel! skip!].each do |action|
+ context "when the pipeline recieved #{action} event" do
+ it 'performs AutoMergeProcessWorker' do
+ expect(AutoMergeProcessWorker).to receive(:perform_async).with(merge_request.id)
+
+ pipeline.public_send(action)
+ end
+ end
+ end
+
+ context 'when auto merge is not enabled in the merge request' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'performs AutoMergeProcessWorker' do
+ expect(AutoMergeProcessWorker).not_to receive(:perform_async)
+
+ pipeline.succeed!
+ end
+ end
+ end
+
def create_build(name, *traits, queued_at: current, started_from: 0, **opts)
create(:ci_build, *traits,
name: name,
diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb
index fc9ebed863e..43fa1010b2b 100644
--- a/spec/models/clusters/applications/jupyter_spec.rb
+++ b/spec/models/clusters/applications/jupyter_spec.rb
@@ -96,6 +96,8 @@ describe Clusters::Applications::Jupyter do
expect(values).to match(/clientId: '?#{application.oauth_application.uid}/)
expect(values).to match(/callbackUrl: '?#{application.callback_url}/)
expect(values).to include("gitlabProjectIdWhitelist:\n - #{application.cluster.project.id}")
+ expect(values).to include("c.GitLabOAuthenticator.scope = ['api read_repository write_repository']")
+ expect(values).to match(/GITLAB_HOST: '?#{Gitlab.config.gitlab.host}/)
end
context 'when cluster belongs to a project' do
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
index d5974f47190..b38cf96de7e 100644
--- a/spec/models/clusters/applications/knative_spec.rb
+++ b/spec/models/clusters/applications/knative_spec.rb
@@ -3,9 +3,6 @@
require 'rails_helper'
describe Clusters::Applications::Knative do
- include KubernetesHelpers
- include ReactiveCachingHelpers
-
let(:knative) { create(:clusters_applications_knative) }
include_examples 'cluster application core specs', :clusters_applications_knative
@@ -146,77 +143,4 @@ describe Clusters::Applications::Knative do
describe 'validations' do
it { is_expected.to validate_presence_of(:hostname) }
end
-
- describe '#service_pod_details' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:service) { cluster.platform_kubernetes }
- let(:knative) { create(:clusters_applications_knative, cluster: cluster) }
-
- let(:namespace) do
- create(:cluster_kubernetes_namespace,
- cluster: cluster,
- cluster_project: cluster.cluster_project,
- project: cluster.cluster_project.project)
- end
-
- before do
- stub_kubeclient_discover(service.api_url)
- stub_kubeclient_knative_services
- stub_kubeclient_service_pods
- stub_reactive_cache(knative,
- {
- services: kube_response(kube_knative_services_body),
- pods: kube_response(kube_knative_pods_body(cluster.cluster_project.project.name, namespace.namespace))
- })
- synchronous_reactive_cache(knative)
- end
-
- it 'is able k8s core for pod details' do
- expect(knative.service_pod_details(namespace.namespace, cluster.cluster_project.project.name)).not_to be_nil
- end
- end
-
- describe '#services' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:service) { cluster.platform_kubernetes }
- let(:knative) { create(:clusters_applications_knative, cluster: cluster) }
-
- let(:namespace) do
- create(:cluster_kubernetes_namespace,
- cluster: cluster,
- cluster_project: cluster.cluster_project,
- project: cluster.cluster_project.project)
- end
-
- subject { knative.services }
-
- before do
- stub_kubeclient_discover(service.api_url)
- stub_kubeclient_knative_services
- stub_kubeclient_service_pods
- end
-
- it 'has an unintialized cache' do
- is_expected.to be_nil
- end
-
- context 'when using synchronous reactive cache' do
- before do
- stub_reactive_cache(knative,
- {
- services: kube_response(kube_knative_services_body),
- pods: kube_response(kube_knative_pods_body(cluster.cluster_project.project.name, namespace.namespace))
- })
- synchronous_reactive_cache(knative)
- end
-
- it 'has cached services' do
- is_expected.not_to be_nil
- end
-
- it 'matches our namespace' do
- expect(knative.services_for(ns: namespace)).not_to be_nil
- end
- end
- end
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 4739e62289a..f206bb41f45 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -38,6 +38,11 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
it { is_expected.to respond_to :project }
+ it do
+ expect(subject.knative_services_finder(subject.project))
+ .to be_instance_of(Clusters::KnativeServicesFinder)
+ end
+
describe '.enabled' do
subject { described_class.enabled }
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 78637ff10c6..0e5fb2b5153 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -2,383 +2,213 @@
require 'spec_helper'
-describe CacheMarkdownField do
- # The minimum necessary ActiveModel to test this concern
- class ThingWithMarkdownFields
- include ActiveModel::Model
- include ActiveModel::Dirty
-
- include ActiveModel::Serialization
-
- class_attribute :attribute_names
- self.attribute_names = []
-
- def attributes
- attribute_names.each_with_object({}) do |name, hsh|
- hsh[name.to_s] = send(name)
- end
+describe CacheMarkdownField, :clean_gitlab_redis_cache do
+ let(:ar_class) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'issues'
+ include CacheMarkdownField
+ cache_markdown_field :title, pipeline: :single_line
+ cache_markdown_field :description
end
+ end
- extend ActiveModel::Callbacks
- define_model_callbacks :create, :update
-
- include CacheMarkdownField
- cache_markdown_field :foo
- cache_markdown_field :baz, pipeline: :single_line
- cache_markdown_field :zoo, whitelisted: true
+ let(:other_class) do
+ Class.new do
+ include CacheMarkdownField
- def self.add_attr(name)
- self.attribute_names += [name]
- define_attribute_methods(name)
- attr_reader(name)
- define_method("#{name}=") do |value|
- write_attribute(name, value)
+ def initialize(args = {})
+ @title, @description, @cached_markdown_version = args[:title], args[:description], args[:cached_markdown_version]
+ @title_html, @description_html = args[:title_html], args[:description_html]
+ @author, @project = args[:author], args[:project]
end
- end
- add_attr :cached_markdown_version
+ attr_accessor :title, :description, :cached_markdown_version
- [:foo, :foo_html, :bar, :baz, :baz_html, :zoo, :zoo_html].each do |name|
- add_attr(name)
- end
-
- def initialize(*)
- super
-
- # Pretend new is load
- clear_changes_information
- end
-
- def read_attribute(name)
- instance_variable_get("@#{name}")
- end
-
- def write_attribute(name, value)
- send("#{name}_will_change!") unless value == read_attribute(name)
- instance_variable_set("@#{name}", value)
- end
+ cache_markdown_field :title, pipeline: :single_line
+ cache_markdown_field :description
- def save
- run_callbacks :update do
- changes_applied
+ def cache_key
+ "cache-key"
end
end
-
- def has_attribute?(attr_name)
- attribute_names.include?(attr_name)
- end
- end
-
- def thing_subclass(new_attr)
- Class.new(ThingWithMarkdownFields) { add_attr(new_attr) }
end
let(:markdown) { '`Foo`' }
- let(:html) { '<p dir="auto"><code>Foo</code></p>' }
+ let(:html) { '<p data-sourcepos="1:1-1:5" dir="auto"><code>Foo</code></p>' }
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: cache_version) }
- let(:cache_version) { CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16 }
-
- before do
- stub_commonmark_sourcepos_disabled
- end
+ let(:updated_html) { '<p data-sourcepos="1:1-1:5" dir="auto"><code>Bar</code></p>' }
- describe '.attributes' do
- it 'excludes cache attributes that is blacklisted by default' do
- expect(thing.attributes.keys.sort).to eq(%w[bar baz cached_markdown_version foo zoo zoo_html])
- end
- end
-
- context 'an unchanged markdown field' do
- before do
- thing.foo = thing.foo
- thing.save
- end
+ let(:cache_version) { Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16 }
- 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(cache_version) }
+ def thing_subclass(klass, extra_attribute)
+ Class.new(klass) { attr_accessor(extra_attribute) }
end
- context 'a changed markdown field' do
- let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version - 1) }
+ shared_examples 'a class with cached markdown fields' do
+ describe '#cached_html_up_to_date?' do
+ let(:thing) { klass.new(title: markdown, title_html: html, cached_markdown_version: cache_version) }
- before do
- thing.foo = updated_markdown
- thing.save
- end
+ subject { thing.cached_html_up_to_date?(:title) }
- it { expect(thing.foo_html).to eq(updated_html) }
- it { expect(thing.cached_markdown_version).to eq(cache_version) }
- end
+ it 'returns false when the version is absent' do
+ thing.cached_markdown_version = nil
- context 'when a markdown field is set repeatedly to an empty string' do
- it do
- expect(thing).to receive(:refresh_markdown_cache).once
- thing.foo = ''
- thing.save
- thing.foo = ''
- thing.save
- end
- end
-
- context 'when a markdown field is set repeatedly to a string which renders as empty html' do
- it do
- expect(thing).to receive(:refresh_markdown_cache).once
- thing.foo = '[//]: # (This is also a comment.)'
- thing.save
- thing.foo = '[//]: # (This is also a comment.)'
- thing.save
- end
- end
-
- context 'when a markdown field and html field are both changed' do
- it do
- expect(thing).not_to receive(:refresh_markdown_cache)
- thing.foo = '_look over there!_'
- thing.foo_html = '<em>look over there!</em>'
- thing.save
- end
- end
-
- context 'a non-markdown field changed' do
- let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version - 1) }
-
- 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
-
- context 'version is out of date' do
- let(:thing) { ThingWithMarkdownFields.new(foo: updated_markdown, foo_html: html, cached_markdown_version: nil) }
-
- before do
- thing.save
- end
-
- it { expect(thing.foo_html).to eq(updated_html) }
- it { expect(thing.cached_markdown_version).to eq(cache_version) }
- end
-
- describe '#cached_html_up_to_date?' do
- let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
-
- subject { thing.cached_html_up_to_date?(:foo) }
-
- it 'returns false when the version is absent' do
- thing.cached_markdown_version = nil
-
- is_expected.to be_falsy
- end
-
- it 'returns false when the cached version is too old' do
- thing.cached_markdown_version = cache_version - 1
-
- is_expected.to be_falsy
- end
-
- it 'returns false when the cached version is in future' do
- thing.cached_markdown_version = cache_version + 1
-
- is_expected.to be_falsy
- end
-
- it 'returns false when the local version was bumped' do
- allow(Gitlab::CurrentSettings.current_application_settings).to receive(:local_markdown_version).and_return(2)
- thing.cached_markdown_version = cache_version
-
- is_expected.to be_falsy
- end
+ is_expected.to be_falsy
+ end
- it 'returns true when the local version is default' do
- thing.cached_markdown_version = cache_version
+ it 'returns false when the version is too early' do
+ thing.cached_markdown_version -= 1
- is_expected.to be_truthy
- end
+ is_expected.to be_falsy
+ end
- it 'returns true when the cached version is just right' do
- allow(Gitlab::CurrentSettings.current_application_settings).to receive(:local_markdown_version).and_return(2)
- thing.cached_markdown_version = cache_version + 2
+ it 'returns false when the version is too late' do
+ thing.cached_markdown_version += 1
- 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 false when the local version was bumped' do
+ allow(Gitlab::CurrentSettings.current_application_settings).to receive(:local_markdown_version).and_return(2)
+ thing.cached_markdown_version = cache_version
- is_expected.to be_falsy
- end
+ is_expected.to be_falsy
+ end
- it 'returns true if markdown has not been changed but html has' do
- thing.foo_html = updated_html
+ it 'returns true when the local version is default' do
+ thing.cached_markdown_version = cache_version
- is_expected.to be_truthy
- end
+ 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
+ it 'returns true when the cached version is just right' do
+ allow(Gitlab::CurrentSettings.current_application_settings).to receive(:local_markdown_version).and_return(2)
+ thing.cached_markdown_version = cache_version + 2
- is_expected.to be_truthy
+ is_expected.to be_truthy
+ end
end
- it 'returns false if the markdown field is set but the html is not' do
- thing.foo_html = nil
+ describe '#latest_cached_markdown_version' do
+ let(:thing) { klass.new }
+ subject { thing.latest_cached_markdown_version }
- is_expected.to be_falsy
+ it 'returns default version' do
+ thing.cached_markdown_version = nil
+ is_expected.to eq(cache_version)
+ end
end
- end
-
- describe '#latest_cached_markdown_version' do
- subject { thing.latest_cached_markdown_version }
- it 'returns default version' do
- thing.cached_markdown_version = nil
- is_expected.to eq(cache_version)
- end
- end
+ describe '#refresh_markdown_cache' do
+ let(:thing) { klass.new(description: markdown, description_html: html, cached_markdown_version: cache_version) }
- describe '#refresh_markdown_cache' do
- before do
- thing.foo = updated_markdown
- end
+ before do
+ thing.description = updated_markdown
+ end
- it 'fills all html fields' do
- thing.refresh_markdown_cache
+ it 'fills all html fields' do
+ thing.refresh_markdown_cache
- 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
+ expect(thing.description_html).to eq(updated_html)
+ end
- it 'does not save the result' do
- expect(thing).not_to receive(:update_columns)
+ it 'does not save the result' do
+ expect(thing).not_to receive(:save_markdown)
- thing.refresh_markdown_cache
- end
+ thing.refresh_markdown_cache
+ end
- it 'updates the markdown cache version' do
- thing.cached_markdown_version = nil
- thing.refresh_markdown_cache
+ it 'updates the markdown cache version' do
+ thing.cached_markdown_version = nil
+ thing.refresh_markdown_cache
- expect(thing.cached_markdown_version).to eq(cache_version)
+ expect(thing.cached_markdown_version).to eq(cache_version)
+ end
end
- end
-
- describe '#refresh_markdown_cache!' do
- let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
- before do
- thing.foo = updated_markdown
- end
+ describe '#refresh_markdown_cache!' do
+ let(:thing) { klass.new(description: markdown, description_html: html, cached_markdown_version: cache_version) }
- it 'fills all html fields' do
- thing.refresh_markdown_cache!
+ before do
+ thing.description = 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.description_html).to eq(updated_html)
+ end
- thing.refresh_markdown_cache!
- end
+ it 'saves the changes' do
+ expect(thing)
+ .to receive(:save_markdown)
+ .with("description_html" => updated_html, "title_html" => "", "cached_markdown_version" => cache_version)
- 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" => "",
- "zoo_html" => "",
- "cached_markdown_version" => cache_version
- )
-
- thing.refresh_markdown_cache!
+ thing.refresh_markdown_cache!
+ end
end
- end
- describe '#banzai_render_context' do
- subject(:context) { thing.banzai_render_context(:foo) }
+ describe '#banzai_render_context' do
+ let(:thing) { klass.new(title: markdown, title_html: html, cached_markdown_version: cache_version) }
+ subject(:context) { thing.banzai_render_context(:title) }
- it 'sets project to nil if the object lacks a project' do
- is_expected.to have_key(:project)
- expect(context[:project]).to be_nil
- end
+ it 'sets project to nil if the object lacks a project' do
+ is_expected.to have_key(:project)
+ expect(context[:project]).to be_nil
+ end
- it 'excludes author if the object lacks an author' do
- is_expected.not_to have_key(:author)
- end
+ it 'excludes author if the object lacks an author' do
+ is_expected.not_to have_key(:author)
+ end
- it 'raises if the context for an unrecognised field is requested' do
- expect { thing.banzai_render_context(:not_found) }.to raise_error(ArgumentError)
- end
+ it 'raises if the context for an unrecognised field is requested' do
+ expect { thing.banzai_render_context(:not_found) }.to raise_error(ArgumentError)
+ end
- it 'includes the pipeline' do
- baz = thing.banzai_render_context(:baz)
+ it 'includes the pipeline' do
+ title_context = thing.banzai_render_context(:title)
- expect(baz[:pipeline]).to eq(:single_line)
- end
+ expect(title_context[:pipeline]).to eq(:single_line)
+ end
- it 'returns copies of the context template' do
- template = thing.cached_markdown_fields[:baz]
- copy = thing.banzai_render_context(:baz)
+ it 'returns copies of the context template' do
+ template = thing.cached_markdown_fields[:description]
+ copy = thing.banzai_render_context(:description)
- expect(copy).not_to be(template)
- end
+ expect(copy).not_to be(template)
+ end
- context 'with a project' do
- let(:project) { create(:project, group: create(:group)) }
- let(:thing) { thing_subclass(:project).new(foo: markdown, foo_html: html, project: project) }
+ context 'with a project' do
+ let(:project) { build(:project, group: create(:group)) }
+ let(:thing) { thing_subclass(klass, :project).new(title: markdown, title_html: html, project: project) }
- it 'sets the project in the context' do
- is_expected.to have_key(:project)
- expect(context[:project]).to eq(project)
+ it 'sets the project in the context' do
+ is_expected.to have_key(:project)
+ expect(context[:project]).to eq(project)
+ end
end
- it 'invalidates the cache when project changes' do
- thing.project = :new_project
- allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
-
- thing.save
+ context 'with an author' do
+ let(:thing) { thing_subclass(klass, :author).new(title: markdown, title_html: html, author: :author_value) }
- expect(thing.foo_html).to eq(updated_html)
- expect(thing.baz_html).to eq(updated_html)
- expect(thing.cached_markdown_version).to eq(cache_version)
+ it 'sets the author in the context' do
+ is_expected.to have_key(:author)
+ expect(context[:author]).to eq(:author_value)
+ end
end
end
+ end
- context 'with an author' do
- let(:thing) { thing_subclass(:author).new(foo: markdown, foo_html: html, author: :author_value) }
-
- it 'sets the author in the context' do
- is_expected.to have_key(:author)
- expect(context[:author]).to eq(:author_value)
- end
+ context 'for Active record classes' do
+ let(:klass) { ar_class }
- it 'invalidates the cache when author changes' do
- thing.author = :new_author
- allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
+ it_behaves_like 'a class with cached markdown fields'
+ end
- thing.save
+ context 'for other classes' do
+ let(:klass) { other_class }
- expect(thing.foo_html).to eq(updated_html)
- expect(thing.baz_html).to eq(updated_html)
- expect(thing.cached_markdown_version).to eq(cache_version)
- end
- end
+ it_behaves_like 'a class with cached markdown fields'
end
end
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 22d4b2cc517..7e9a8306612 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -194,58 +194,6 @@ describe Milestone, 'Milestoneish' do
end
end
- describe '#closed_items_count' do
- it 'does not count confidential issues for non project members' do
- expect(milestone.closed_items_count(non_member)).to eq 2
- end
-
- it 'does not count confidential issues for project members with guest role' do
- expect(milestone.closed_items_count(guest)).to eq 2
- end
-
- it 'counts confidential issues for author' do
- expect(milestone.closed_items_count(author)).to eq 4
- end
-
- it 'counts confidential issues for assignee' do
- expect(milestone.closed_items_count(assignee)).to eq 4
- end
-
- it 'counts confidential issues for project members' do
- expect(milestone.closed_items_count(member)).to eq 6
- end
-
- it 'counts all issues for admin' do
- expect(milestone.closed_items_count(admin)).to eq 6
- end
- end
-
- describe '#total_items_count' do
- it 'does not count confidential issues for non project members' do
- expect(milestone.total_items_count(non_member)).to eq 4
- end
-
- it 'does not count confidential issues for project members with guest role' do
- expect(milestone.total_items_count(guest)).to eq 4
- end
-
- it 'counts confidential issues for author' do
- expect(milestone.total_items_count(author)).to eq 7
- end
-
- it 'counts confidential issues for assignee' do
- expect(milestone.total_items_count(assignee)).to eq 7
- end
-
- it 'counts confidential issues for project members' do
- expect(milestone.total_items_count(member)).to eq 10
- end
-
- it 'counts all issues for admin' do
- expect(milestone.total_items_count(admin)).to eq 10
- end
- end
-
describe '#complete?' do
it 'returns false when has items opened' do
expect(milestone.complete?(non_member)).to eq false
@@ -260,28 +208,42 @@ describe Milestone, 'Milestoneish' do
end
describe '#percent_complete' do
+ context 'division by zero' do
+ let(:new_milestone) { build_stubbed(:milestone) }
+
+ it { expect(new_milestone.percent_complete(admin)).to eq(0) }
+ end
+ end
+
+ describe '#count_issues_by_state' do
it 'does not count confidential issues for non project members' do
- expect(milestone.percent_complete(non_member)).to eq 50
+ expect(milestone.closed_issues_count(non_member)).to eq 2
+ expect(milestone.total_issues_count(non_member)).to eq 3
end
it 'does not count confidential issues for project members with guest role' do
- expect(milestone.percent_complete(guest)).to eq 50
+ expect(milestone.closed_issues_count(guest)).to eq 2
+ expect(milestone.total_issues_count(guest)).to eq 3
end
it 'counts confidential issues for author' do
- expect(milestone.percent_complete(author)).to eq 57
+ expect(milestone.closed_issues_count(author)).to eq 4
+ expect(milestone.total_issues_count(author)).to eq 6
end
it 'counts confidential issues for assignee' do
- expect(milestone.percent_complete(assignee)).to eq 57
+ expect(milestone.closed_issues_count(assignee)).to eq 4
+ expect(milestone.total_issues_count(assignee)).to eq 6
end
it 'counts confidential issues for project members' do
- expect(milestone.percent_complete(member)).to eq 60
+ expect(milestone.closed_issues_count(member)).to eq 6
+ expect(milestone.total_issues_count(member)).to eq 9
end
it 'counts confidential issues for admin' do
- expect(milestone.percent_complete(admin)).to eq 60
+ expect(milestone.closed_issues_count(admin)).to eq 6
+ expect(milestone.total_issues_count(admin)).to eq 9
end
end
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
index ee613b199ad..e17b98536fa 100644
--- a/spec/models/concerns/noteable_spec.rb
+++ b/spec/models/concerns/noteable_spec.rb
@@ -260,4 +260,16 @@ describe Noteable do
end
end
end
+
+ describe '.replyable_types' do
+ it 'exposes the replyable types' do
+ expect(described_class.replyable_types).to include('Issue', 'MergeRequest')
+ end
+ end
+
+ describe '.resolvable_types' do
+ it 'exposes the replyable types' do
+ expect(described_class.resolvable_types).to include('MergeRequest')
+ end
+ end
end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index fa19cb47a0d..d9e1fe4b165 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -321,6 +321,14 @@ describe DiffNote do
end
describe '#supports_suggestion?' do
+ context 'when noteable does not exist' do
+ it 'returns false' do
+ allow(subject).to receive(:noteable) { nil }
+
+ expect(subject.supports_suggestion?).to be(false)
+ end
+ end
+
context 'when noteable does not support suggestions' do
it 'returns false' do
allow(subject.noteable).to receive(:supports_suggestion?) { false }
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index cc777cbf749..a5c7e9db2a1 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -93,6 +93,21 @@ describe Issue do
end
end
+ describe '#sort' do
+ let(:project) { create(:project) }
+
+ context "by relative_position" do
+ let!(:issue) { create(:issue, project: project) }
+ let!(:issue2) { create(:issue, project: project, relative_position: 2) }
+ let!(:issue3) { create(:issue, project: project, relative_position: 1) }
+
+ it "sorts asc with nulls at the end" do
+ issues = project.issues.sort_by_attribute('relative_position')
+ expect(issues).to eq([issue3, issue2, issue])
+ end
+ end
+ end
+
describe '#card_attributes' do
it 'includes the author name' do
allow(subject).to receive(:author).and_return(double(name: 'Robert'))
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index c72b6e9033d..fc28c216b21 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -173,6 +173,42 @@ describe MergeRequest do
end
end
+ context 'for branch' do
+ before do
+ stub_feature_flags(stricter_mr_branch_name: false)
+ end
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:branch_name, :valid) do
+ 'foo' | true
+ 'foo:bar' | false
+ '+foo:bar' | false
+ 'foo bar' | false
+ '-foo' | false
+ 'HEAD' | true
+ 'refs/heads/master' | true
+ end
+
+ with_them do
+ it "validates source_branch" do
+ subject = build(:merge_request, source_branch: branch_name, target_branch: 'master')
+
+ subject.valid?
+
+ expect(subject.errors.added?(:source_branch)).to eq(!valid)
+ end
+
+ it "validates target_branch" do
+ subject = build(:merge_request, source_branch: 'master', target_branch: branch_name)
+
+ subject.valid?
+
+ expect(subject.errors.added?(:target_branch)).to eq(!valid)
+ end
+ end
+ end
+
context 'for forks' do
let(:project) { create(:project) }
let(:fork1) { fork_project(project) }
@@ -1038,19 +1074,17 @@ describe MergeRequest do
end
end
- describe "#reset_merge_when_pipeline_succeeds" do
- let(:merge_if_green) do
- create :merge_request, merge_when_pipeline_succeeds: true, merge_user: create(:user),
- merge_params: { "should_remove_source_branch" => "1", "commit_message" => "msg" }
- end
+ describe "#auto_merge_strategy" do
+ subject { merge_request.auto_merge_strategy }
+
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
- it "sets the item to false" do
- merge_if_green.reset_merge_when_pipeline_succeeds
- merge_if_green.reload
+ it { is_expected.to eq('merge_when_pipeline_succeeds') }
- expect(merge_if_green.merge_when_pipeline_succeeds).to be_falsey
- expect(merge_if_green.merge_params["should_remove_source_branch"]).to be_nil
- expect(merge_if_green.merge_params["commit_message"]).to be_nil
+ context 'when auto merge is disabled' do
+ let(:merge_request) { create(:merge_request) }
+
+ it { is_expected.to be_nil }
end
end
@@ -1962,57 +1996,6 @@ describe MergeRequest do
end
end
- describe '#check_if_can_be_merged' do
- let(:project) { create(:project, only_allow_merge_if_pipeline_succeeds: true) }
-
- shared_examples 'checking if can be merged' do
- context 'when it is not broken and has no conflicts' do
- before do
- allow(subject).to receive(:broken?) { false }
- allow(project.repository).to receive(:can_be_merged?).and_return(true)
- end
-
- it 'is marked as mergeable' do
- expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged')
- end
- end
-
- context 'when broken' do
- before do
- allow(subject).to receive(:broken?) { true }
- allow(project.repository).to receive(:can_be_merged?).and_return(false)
- end
-
- it 'becomes unmergeable' do
- expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
- end
- end
-
- context 'when it has conflicts' do
- before do
- allow(subject).to receive(:broken?) { false }
- allow(project.repository).to receive(:can_be_merged?).and_return(false)
- end
-
- it 'becomes unmergeable' do
- expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
- end
- end
- end
-
- context 'when merge_status is unchecked' do
- subject { create(:merge_request, source_project: project, merge_status: :unchecked) }
-
- it_behaves_like 'checking if can be merged'
- end
-
- context 'when merge_status is unchecked' do
- subject { create(:merge_request, source_project: project, merge_status: :cannot_be_merged_recheck) }
-
- it_behaves_like 'checking if can be merged'
- end
- end
-
describe '#mergeable?' do
let(:project) { create(:project) }
@@ -2026,7 +2009,7 @@ describe MergeRequest do
it 'return true if #mergeable_state? is true and the MR #can_be_merged? is true' do
allow(subject).to receive(:mergeable_state?) { true }
- expect(subject).to receive(:check_if_can_be_merged)
+ expect(subject).to receive(:check_mergeability)
expect(subject).to receive(:can_be_merged?) { true }
expect(subject.mergeable?).to be_truthy
@@ -2040,7 +2023,7 @@ describe MergeRequest do
it 'checks if merge request can be merged' do
allow(subject).to receive(:mergeable_ci_state?) { true }
- expect(subject).to receive(:check_if_can_be_merged)
+ expect(subject).to receive(:check_mergeability)
subject.mergeable?
end
@@ -3108,38 +3091,6 @@ describe MergeRequest do
end
end
- describe '#mergeable_to_ref?' do
- it 'returns true when merge request is mergeable' do
- subject = create(:merge_request)
-
- expect(subject.mergeable_to_ref?).to be(true)
- end
-
- it 'returns false when merge request is already merged' do
- subject = create(:merge_request, :merged)
-
- expect(subject.mergeable_to_ref?).to be(false)
- end
-
- it 'returns false when merge request is closed' do
- subject = create(:merge_request, :closed)
-
- expect(subject.mergeable_to_ref?).to be(false)
- end
-
- it 'returns false when merge request is work in progress' do
- subject = create(:merge_request, title: 'WIP: The feature')
-
- expect(subject.mergeable_to_ref?).to be(false)
- end
-
- it 'returns false when merge request has no commits' do
- subject = create(:merge_request, source_branch: 'empty-branch', target_branch: 'master')
-
- expect(subject.mergeable_to_ref?).to be(false)
- end
- end
-
describe '#merge_participants' do
it 'contains author' do
expect(subject.merge_participants).to eq([subject.author])
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index b82368318f2..3704a2d468d 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -31,12 +31,28 @@ describe Milestone do
end
describe 'start_date' do
- it 'adds an error when start_date is greated then due_date' do
+ it 'adds an error when start_date is greater then due_date' do
milestone = build(:milestone, start_date: Date.tomorrow, due_date: Date.yesterday)
expect(milestone).not_to be_valid
expect(milestone.errors[:due_date]).to include("must be greater than start date")
end
+
+ it 'adds an error when start_date is greater than 9999-12-31' do
+ milestone = build(:milestone, start_date: Date.new(10000, 1, 1))
+
+ expect(milestone).not_to be_valid
+ expect(milestone.errors[:start_date]).to include("date must not be after 9999-12-31")
+ end
+ end
+
+ describe 'due_date' do
+ it 'adds an error when due_date is greater than 9999-12-31' do
+ milestone = build(:milestone, due_date: Date.new(10000, 1, 1))
+
+ expect(milestone).not_to be_valid
+ expect(milestone.errors[:due_date]).to include("date must not be after 9999-12-31")
+ end
end
end
@@ -166,32 +182,10 @@ describe Milestone do
end
end
- describe '#percent_complete' do
- before do
- allow(milestone).to receive_messages(
- closed_items_count: 3,
- total_items_count: 4
- )
- end
-
- it { expect(milestone.percent_complete(user)).to eq(75) }
- end
-
describe '#can_be_closed?' do
it { expect(milestone.can_be_closed?).to be_truthy }
end
- describe '#total_items_count' do
- before do
- create :closed_issue, milestone: milestone, project: project
- create :merge_request, milestone: milestone, source_project: project
- end
-
- it 'returns total count of issues and merge requests assigned to milestone' do
- expect(milestone.total_items_count(user)).to eq 2
- end
- end
-
describe '#can_be_closed?' do
before do
milestone = create :milestone, project: project
@@ -381,21 +375,6 @@ describe Milestone do
expect(milestone_ids).to be_empty
end
end
-
- context 'when there is a milestone with a date after 294276 AD', :postgresql do
- before do
- past_milestone_project_1.update!(due_date: Date.new(294277, 1, 1))
- end
-
- it 'returns the next upcoming open milestone ID for each project and group' do
- expect(milestone_ids).to contain_exactly(
- current_milestone_project_1.id,
- current_milestone_project_2.id,
- current_milestone_group_1.id,
- current_milestone_group_2.id
- )
- end
- end
end
describe '#to_reference' do
@@ -519,4 +498,20 @@ describe Milestone do
end
end
end
+
+ describe '.reference_pattern' do
+ subject { described_class.reference_pattern }
+
+ it { is_expected.to match('gitlab-org/gitlab-ce%123') }
+ it { is_expected.to match('gitlab-org/gitlab-ce%"my-milestone"') }
+ end
+
+ describe '.link_reference_pattern' do
+ subject { described_class.link_reference_pattern }
+
+ it { is_expected.to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-ce/milestones/123") }
+ it { is_expected.to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-ce/-/milestones/123") }
+ it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-ce/issues/123") }
+ it { is_expected.not_to match("gitlab-org/gitlab-ce/milestones/123") }
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index bfde367c47f..d80183af33e 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -147,6 +147,7 @@ describe Namespace do
namespace: namespace,
statistics: build(:project_statistics,
repository_size: 101,
+ wiki_size: 505,
lfs_objects_size: 202,
build_artifacts_size: 303,
packages_size: 404))
@@ -157,6 +158,7 @@ describe Namespace do
namespace: namespace,
statistics: build(:project_statistics,
repository_size: 10,
+ wiki_size: 50,
lfs_objects_size: 20,
build_artifacts_size: 30,
packages_size: 40))
@@ -167,8 +169,9 @@ describe Namespace do
project2
statistics = described_class.with_statistics.find(namespace.id)
- expect(statistics.storage_size).to eq 1110
+ expect(statistics.storage_size).to eq 1665
expect(statistics.repository_size).to eq 111
+ expect(statistics.wiki_size).to eq 555
expect(statistics.lfs_objects_size).to eq 222
expect(statistics.build_artifacts_size).to eq 333
expect(statistics.packages_size).to eq 444
@@ -179,6 +182,7 @@ describe Namespace do
expect(statistics.storage_size).to eq 0
expect(statistics.repository_size).to eq 0
+ expect(statistics.wiki_size).to eq 0
expect(statistics.lfs_objects_size).to eq 0
expect(statistics.build_artifacts_size).to eq 0
expect(statistics.packages_size).to eq 0
diff --git a/spec/models/pages_domain_acme_order_spec.rb b/spec/models/pages_domain_acme_order_spec.rb
new file mode 100644
index 00000000000..4ffb4fc7389
--- /dev/null
+++ b/spec/models/pages_domain_acme_order_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PagesDomainAcmeOrder do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '.expired' do
+ let!(:not_expired_order) { create(:pages_domain_acme_order) }
+ let!(:expired_order) { create(:pages_domain_acme_order, :expired) }
+
+ it 'returns only expired orders' do
+ expect(described_class.count).to eq(2)
+ expect(described_class.expired).to eq([expired_order])
+ end
+ end
+
+ describe '.find_by_domain_and_token' do
+ let!(:domain) { create(:pages_domain, domain: 'test.com') }
+ let!(:acme_order) { create(:pages_domain_acme_order, challenge_token: 'righttoken', pages_domain: domain) }
+
+ where(:domain_name, :challenge_token, :present) do
+ 'test.com' | 'righttoken' | true
+ 'test.com' | 'wrongtoken' | false
+ 'test.org' | 'righttoken' | false
+ end
+
+ with_them do
+ subject { described_class.find_by_domain_and_token(domain_name, challenge_token).present? }
+
+ it { is_expected.to eq(present) }
+ end
+ end
+
+ subject { create(:pages_domain_acme_order) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:pages_domain) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:pages_domain) }
+ it { is_expected.to validate_presence_of(:expires_at) }
+ it { is_expected.to validate_presence_of(:url) }
+ it { is_expected.to validate_presence_of(:challenge_token) }
+ it { is_expected.to validate_presence_of(:challenge_file_content) }
+ it { is_expected.to validate_presence_of(:private_key) }
+ end
+end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index ec4d4517f82..fdc81359d34 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -81,6 +81,17 @@ describe PagesDomain do
end
end
+ describe 'when certificate is specified' do
+ let(:domain) { build(:pages_domain) }
+
+ it 'saves validity time' do
+ domain.save
+
+ expect(domain.certificate_valid_not_before).to be_like_time(Time.parse("2016-02-12 14:32:00 UTC"))
+ expect(domain.certificate_valid_not_after).to be_like_time(Time.parse("2020-04-12 14:32:00 UTC"))
+ end
+ end
+
describe 'validate certificate' do
subject { domain }
diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb
index 4aa62028169..f596cee81dc 100644
--- a/spec/models/project_ci_cd_setting_spec.rb
+++ b/spec/models/project_ci_cd_setting_spec.rb
@@ -21,4 +21,32 @@ describe ProjectCiCdSetting do
2.times { described_class.available? }
end
end
+
+ describe 'validations' do
+ it 'validates default_git_depth is between 0 and 1000 or nil' do
+ expect(subject).to validate_numericality_of(:default_git_depth)
+ .only_integer
+ .is_greater_than_or_equal_to(0)
+ .is_less_than_or_equal_to(1000)
+ .allow_nil
+ end
+ end
+
+ describe '#default_git_depth' do
+ let(:default_value) { described_class::DEFAULT_GIT_DEPTH }
+
+ it 'sets default value for new records' do
+ project = create(:project)
+
+ expect(project.ci_cd_settings.default_git_depth).to eq(default_value)
+ end
+
+ it 'does not set default value if present' do
+ project = build(:project)
+ project.build_ci_cd_settings(default_git_depth: 0)
+ project.save!
+
+ expect(project.reload.ci_cd_settings.default_git_depth).to eq(0)
+ end
+ end
end
diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/project_services/assembla_service_spec.rb
index 7742e33e901..2c86c0ec7be 100644
--- a/spec/models/project_services/assembla_service_spec.rb
+++ b/spec/models/project_services/assembla_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe AssemblaService do
+ include StubRequests
+
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -23,12 +25,12 @@ describe AssemblaService do
)
@sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
@api_url = 'https://atlas.assembla.com/spaces/project_name/github_tool?secret_key=verySecret'
- WebMock.stub_request(:post, @api_url)
+ stub_full_request(@api_url, method: :post)
end
it "calls Assembla API" do
@assembla_service.execute(@sample_data)
- expect(WebMock).to have_requested(:post, @api_url).with(
+ expect(WebMock).to have_requested(:post, stubbed_hostname(@api_url)).with(
body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/
).once
end
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index 08c510f09df..65d227a17f9 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe BambooService, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
+ include StubRequests
let(:bamboo_url) { 'http://gitlab.com/bamboo' }
@@ -257,7 +258,7 @@ describe BambooService, :use_clean_rails_memory_store_caching do
end
def stub_bamboo_request(url, status, body)
- WebMock.stub_request(:get, url).to_return(
+ stub_full_request(url).to_return(
status: status,
headers: { 'Content-Type' => 'application/json' },
body: body
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index 091d4d8f695..ca196069055 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe BuildkiteService, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
+ include StubRequests
let(:project) { create(:project) }
@@ -110,10 +111,9 @@ describe BuildkiteService, :use_clean_rails_memory_store_caching do
body ||= %q({"status":"success"})
buildkite_full_url = 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=123'
- WebMock.stub_request(:get, buildkite_full_url).to_return(
- status: status,
- headers: { 'Content-Type' => 'application/json' },
- body: body
- )
+ stub_full_request(buildkite_full_url)
+ .to_return(status: status,
+ headers: { 'Content-Type' => 'application/json' },
+ body: body)
end
end
diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/project_services/campfire_service_spec.rb
index bf4c52fc7ab..0d3dd89e93b 100644
--- a/spec/models/project_services/campfire_service_spec.rb
+++ b/spec/models/project_services/campfire_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe CampfireService do
+ include StubRequests
+
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -49,39 +51,37 @@ describe CampfireService do
it "calls Campfire API to get a list of rooms and speak in a room" do
# make sure a valid list of rooms is returned
body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms.json')
- WebMock.stub_request(:get, @rooms_url).with(basic_auth: @auth).to_return(
+
+ stub_full_request(@rooms_url).with(basic_auth: @auth).to_return(
body: body,
status: 200,
headers: @headers
)
+
# stub the speak request with the room id found in the previous request's response
speak_url = 'https://project-name.campfirenow.com/room/123/speak.json'
- WebMock.stub_request(:post, speak_url).with(basic_auth: @auth)
+ stub_full_request(speak_url, method: :post).with(basic_auth: @auth)
@campfire_service.execute(@sample_data)
- expect(WebMock).to have_requested(:get, @rooms_url).once
- expect(WebMock).to have_requested(:post, speak_url).with(
- body: /#{project.path}.*#{@sample_data[:before]}.*#{@sample_data[:after]}/
- ).once
+ expect(WebMock).to have_requested(:get, stubbed_hostname(@rooms_url)).once
+ expect(WebMock).to have_requested(:post, stubbed_hostname(speak_url))
+ .with(body: /#{project.path}.*#{@sample_data[:before]}.*#{@sample_data[:after]}/).once
end
it "calls Campfire API to get a list of rooms but shouldn't speak in a room" do
# return a list of rooms that do not contain a room named 'test-room'
body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms2.json')
- WebMock.stub_request(:get, @rooms_url).with(basic_auth: @auth).to_return(
+ stub_full_request(@rooms_url).with(basic_auth: @auth).to_return(
body: body,
status: 200,
headers: @headers
)
- # we want to make sure no request is sent to the /speak endpoint, here is a basic
- # regexp that matches this endpoint
- speak_url = 'https://verySecret:X@project-name.campfirenow.com/room/.*/speak.json'
@campfire_service.execute(@sample_data)
- expect(WebMock).to have_requested(:get, @rooms_url).once
- expect(WebMock).not_to have_requested(:post, /#{speak_url}/)
+ expect(WebMock).to have_requested(:get, 'https://8.8.8.9/rooms.json').once
+ expect(WebMock).not_to have_requested(:post, '*/room/.*/speak.json')
end
end
end
diff --git a/spec/models/project_services/pivotaltracker_service_spec.rb b/spec/models/project_services/pivotaltracker_service_spec.rb
index 773b8b7890f..dde46c82df6 100644
--- a/spec/models/project_services/pivotaltracker_service_spec.rb
+++ b/spec/models/project_services/pivotaltracker_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe PivotaltrackerService do
+ include StubRequests
+
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -53,12 +55,12 @@ describe PivotaltrackerService do
end
before do
- WebMock.stub_request(:post, url)
+ stub_full_request(url, method: :post)
end
it 'posts correct message' do
service.execute(push_data)
- expect(WebMock).to have_requested(:post, url).with(
+ expect(WebMock).to have_requested(:post, stubbed_hostname(url)).with(
body: {
'source_commit' => {
'commit_id' => '21c12ea',
@@ -85,14 +87,14 @@ describe PivotaltrackerService do
service.execute(push_data(branch: 'master'))
service.execute(push_data(branch: 'v10'))
- expect(WebMock).to have_requested(:post, url).twice
+ expect(WebMock).to have_requested(:post, stubbed_hostname(url)).twice
end
it 'does not post message if branch is not in the list' do
service.execute(push_data(branch: 'mas'))
service.execute(push_data(branch: 'v11'))
- expect(WebMock).not_to have_requested(:post, url)
+ expect(WebMock).not_to have_requested(:post, stubbed_hostname(url))
end
end
end
diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb
index d2a45f48705..380f02739bc 100644
--- a/spec/models/project_services/pushover_service_spec.rb
+++ b/spec/models/project_services/pushover_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe PushoverService do
+ include StubRequests
+
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -57,13 +59,13 @@ describe PushoverService do
sound: sound
)
- WebMock.stub_request(:post, api_url)
+ stub_full_request(api_url, method: :post, ip_address: '8.8.8.8')
end
it 'calls Pushover API' do
pushover.execute(sample_data)
- expect(WebMock).to have_requested(:post, api_url).once
+ expect(WebMock).to have_requested(:post, 'https://8.8.8.8/1/messages.json').once
end
end
end
diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb
index 96dccae733b..1c434b25205 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe TeamcityService, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
+ include StubRequests
let(:teamcity_url) { 'http://gitlab.com/teamcity' }
@@ -212,7 +213,7 @@ describe TeamcityService, :use_clean_rails_memory_store_caching do
body ||= %Q({"build":{"status":"#{build_status}","id":"666"}})
- WebMock.stub_request(:get, teamcity_full_url).with(basic_auth: auth).to_return(
+ stub_full_request(teamcity_full_url).with(basic_auth: auth).to_return(
status: status,
headers: { 'Content-Type' => 'application/json' },
body: body
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 08662231fdf..aad08b9d4aa 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1147,7 +1147,7 @@ describe Project do
allow(project).to receive(:avatar_in_git) { true }
end
- let(:avatar_path) { "/#{project.full_path}/avatar" }
+ let(:avatar_path) { "/#{project.full_path}/-/avatar" }
it { is_expected.to eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
end
@@ -3170,6 +3170,23 @@ describe Project do
end
end
+ describe '.ids_with_milestone_available_for' do
+ let!(:user) { create(:user) }
+
+ it 'returns project ids with milestones available for user' do
+ project_1 = create(:project, :public, :merge_requests_disabled, :issues_disabled)
+ project_2 = create(:project, :public, :merge_requests_disabled)
+ project_3 = create(:project, :public, :issues_disabled)
+ project_4 = create(:project, :public)
+ project_4.project_feature.update(issues_access_level: ProjectFeature::PRIVATE, merge_requests_access_level: ProjectFeature::PRIVATE )
+
+ project_ids = described_class.ids_with_milestone_available_for(user).pluck(:id)
+
+ expect(project_ids).to include(project_2.id, project_3.id)
+ expect(project_ids).not_to include(project_1.id, project_4.id)
+ end
+ end
+
describe '.with_feature_available_for_user' do
let(:user) { create(:user) }
let(:feature) { MergeRequest }
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index 738398a06f9..358873f9a2f 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -11,21 +11,37 @@ describe ProjectStatistics do
it { is_expected.to belong_to(:namespace) }
end
+ describe 'scopes' do
+ describe '.for_project_ids' do
+ it 'returns only requested projects' do
+ stats = create_list(:project_statistics, 3)
+ project_ids = stats[0..1].map { |s| s.project_id }
+ expected_ids = stats[0..1].map { |s| s.id }
+
+ requested_stats = described_class.for_project_ids(project_ids).pluck(:id)
+
+ expect(requested_stats).to eq(expected_ids)
+ end
+ end
+ end
+
describe 'statistics columns' do
it "support values up to 8 exabytes" do
statistics.update!(
commit_count: 8.exabytes - 1,
repository_size: 2.exabytes,
+ wiki_size: 1.exabytes,
lfs_objects_size: 2.exabytes,
- build_artifacts_size: 4.exabytes - 1
+ build_artifacts_size: 3.exabytes - 1
)
statistics.reload
expect(statistics.commit_count).to eq(8.exabytes - 1)
expect(statistics.repository_size).to eq(2.exabytes)
+ expect(statistics.wiki_size).to eq(1.exabytes)
expect(statistics.lfs_objects_size).to eq(2.exabytes)
- expect(statistics.build_artifacts_size).to eq(4.exabytes - 1)
+ expect(statistics.build_artifacts_size).to eq(3.exabytes - 1)
expect(statistics.storage_size).to eq(8.exabytes - 1)
end
end
@@ -33,6 +49,7 @@ describe ProjectStatistics do
describe '#total_repository_size' do
it "sums repository and LFS object size" do
statistics.repository_size = 2
+ statistics.wiki_size = 6
statistics.lfs_objects_size = 3
statistics.build_artifacts_size = 4
@@ -40,10 +57,17 @@ describe ProjectStatistics do
end
end
+ describe '#wiki_size' do
+ it "is initialized with not null value" do
+ expect(statistics.wiki_size).to eq 0
+ end
+ end
+
describe '#refresh!' do
before do
allow(statistics).to receive(:update_commit_count)
allow(statistics).to receive(:update_repository_size)
+ allow(statistics).to receive(:update_wiki_size)
allow(statistics).to receive(:update_lfs_objects_size)
allow(statistics).to receive(:update_storage_size)
end
@@ -56,6 +80,7 @@ describe ProjectStatistics do
it "sums all counters" do
expect(statistics).to have_received(:update_commit_count)
expect(statistics).to have_received(:update_repository_size)
+ expect(statistics).to have_received(:update_wiki_size)
expect(statistics).to have_received(:update_lfs_objects_size)
end
end
@@ -69,6 +94,45 @@ 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_wiki_size)
+ end
+ end
+
+ context 'without repositories' do
+ it 'does not crash' do
+ expect(project.repository.exists?).to be_falsey
+ expect(project.wiki.repository.exists?).to be_falsey
+
+ statistics.refresh!
+
+ expect(statistics).to have_received(:update_commit_count)
+ expect(statistics).to have_received(:update_repository_size)
+ expect(statistics).to have_received(:update_wiki_size)
+ expect(statistics.repository_size).to eq(0)
+ expect(statistics.commit_count).to eq(0)
+ expect(statistics.wiki_size).to eq(0)
+ end
+ end
+
+ context 'with deleted repositories' do
+ let(:project) { create(:project, :repository, :wiki_repo) }
+
+ before do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ FileUtils.rm_rf(project.repository.path)
+ FileUtils.rm_rf(project.wiki.repository.path)
+ end
+ end
+
+ it 'does not crash' do
+ statistics.refresh!
+
+ expect(statistics).to have_received(:update_commit_count)
+ expect(statistics).to have_received(:update_repository_size)
+ expect(statistics).to have_received(:update_wiki_size)
+ expect(statistics.repository_size).to eq(0)
+ expect(statistics.commit_count).to eq(0)
+ expect(statistics.wiki_size).to eq(0)
end
end
end
@@ -95,6 +159,17 @@ describe ProjectStatistics do
end
end
+ describe '#update_wiki_size' do
+ before do
+ allow(project.wiki.repository).to receive(:size).and_return(34)
+ statistics.update_wiki_size
+ end
+
+ it "stores the size of the wiki" do
+ expect(statistics.wiki_size).to eq 34.megabytes
+ end
+ end
+
describe '#update_lfs_objects_size' do
let!(:lfs_object1) { create(:lfs_object, size: 23.megabytes) }
let!(:lfs_object2) { create(:lfs_object, size: 34.megabytes) }
@@ -114,12 +189,13 @@ describe ProjectStatistics do
it "sums all storage counters" do
statistics.update!(
repository_size: 2,
+ wiki_size: 4,
lfs_objects_size: 3
)
statistics.reload
- expect(statistics.storage_size).to eq 5
+ expect(statistics.storage_size).to eq 9
end
end
diff --git a/spec/models/resource_label_event_spec.rb b/spec/models/resource_label_event_spec.rb
index 7eeb2fae57d..cb52f154299 100644
--- a/spec/models/resource_label_event_spec.rb
+++ b/spec/models/resource_label_event_spec.rb
@@ -82,13 +82,13 @@ RSpec.describe ResourceLabelEvent, type: :model do
end
it 'returns true if markdown is outdated' do
- subject.attributes = { cached_markdown_version: ((CacheMarkdownField::CACHE_COMMONMARK_VERSION - 1) << 16) | 0 }
+ subject.attributes = { cached_markdown_version: ((Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION - 1) << 16) | 0 }
expect(subject.outdated_markdown?).to be true
end
it 'returns false if label and reference are set' do
- subject.attributes = { reference: 'whatever', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16 }
+ subject.attributes = { reference: 'whatever', cached_markdown_version: Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16 }
expect(subject.outdated_markdown?).to be false
end
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
index bb1db9a3d51..eacf383be7d 100644
--- a/spec/presenters/blob_presenter_spec.rb
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -14,6 +14,16 @@ describe BlobPresenter, :seed_helper do
end
let(:blob) { Blob.new(git_blob) }
+ describe '.web_url' do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:blob) { Gitlab::Graphql::Representation::TreeEntry.new(repository.tree.blobs.first, repository) }
+
+ subject { described_class.new(blob) }
+
+ it { expect(subject.web_url).to eq("http://localhost/#{project.full_path}/blob/#{blob.commit_id}/#{blob.path}") }
+ end
+
describe '#highlight' do
subject { described_class.new(blob) }
diff --git a/spec/presenters/ci/build_runner_presenter_spec.rb b/spec/presenters/ci/build_runner_presenter_spec.rb
index ad6cb012d0b..9ed8e3a4e0a 100644
--- a/spec/presenters/ci/build_runner_presenter_spec.rb
+++ b/spec/presenters/ci/build_runner_presenter_spec.rb
@@ -119,40 +119,22 @@ describe Ci::BuildRunnerPresenter do
end
describe '#git_depth' do
- subject { presenter.git_depth }
-
let(:build) { create(:ci_build) }
- it 'returns the correct git depth' do
- is_expected.to eq(0)
- end
+ subject(:git_depth) { presenter.git_depth }
context 'when GIT_DEPTH variable is specified' do
before do
create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: build.pipeline)
end
- it 'returns the correct git depth' do
- is_expected.to eq(1)
+ it 'returns its value' do
+ expect(git_depth).to eq(1)
end
end
- context 'when pipeline is detached merge request pipeline' do
- let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
- let(:pipeline) { merge_request.all_pipelines.first }
- let(:build) { create(:ci_build, ref: pipeline.ref, pipeline: pipeline) }
-
- it 'returns the default git depth for pipelines for merge requests' do
- is_expected.to eq(described_class::DEFAULT_GIT_DEPTH_MERGE_REQUEST)
- end
-
- context 'when pipeline is legacy detached merge request pipeline' do
- let(:merge_request) { create(:merge_request, :with_legacy_detached_merge_request_pipeline) }
-
- it 'behaves as branch pipeline' do
- is_expected.to eq(0)
- end
- end
+ it 'defaults to git depth setting for the project' do
+ expect(git_depth).to eq(build.project.default_git_depth)
end
end
@@ -162,24 +144,24 @@ describe Ci::BuildRunnerPresenter do
let(:build) { create(:ci_build) }
it 'returns the correct refspecs' do
- is_expected.to contain_exactly('+refs/tags/*:refs/tags/*',
- '+refs/heads/*:refs/remotes/origin/*')
+ is_expected.to contain_exactly("+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
end
- context 'when GIT_DEPTH variable is specified' do
- before do
- create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: build.pipeline)
- end
+ context 'when ref is tag' do
+ let(:build) { create(:ci_build, :tag) }
it 'returns the correct refspecs' do
- is_expected.to contain_exactly("+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
+ is_expected.to contain_exactly("+refs/tags/#{build.ref}:refs/tags/#{build.ref}")
end
- context 'when ref is tag' do
- let(:build) { create(:ci_build, :tag) }
+ context 'when GIT_DEPTH is zero' do
+ before do
+ create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 0, pipeline: build.pipeline)
+ end
it 'returns the correct refspecs' do
- is_expected.to contain_exactly("+refs/tags/#{build.ref}:refs/tags/#{build.ref}")
+ is_expected.to contain_exactly('+refs/tags/*:refs/tags/*',
+ '+refs/heads/*:refs/remotes/origin/*')
end
end
end
@@ -194,12 +176,24 @@ describe Ci::BuildRunnerPresenter do
.to contain_exactly('+refs/merge-requests/1/head:refs/merge-requests/1/head')
end
+ context 'when GIT_DEPTH is zero' do
+ before do
+ create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 0, pipeline: build.pipeline)
+ end
+
+ it 'returns the correct refspecs' do
+ is_expected
+ .to contain_exactly('+refs/merge-requests/1/head:refs/merge-requests/1/head',
+ '+refs/heads/*:refs/remotes/origin/*',
+ '+refs/tags/*:refs/tags/*')
+ end
+ end
+
context 'when pipeline is legacy detached merge request pipeline' do
let(:merge_request) { create(:merge_request, :with_legacy_detached_merge_request_pipeline) }
it 'returns the correct refspecs' do
- is_expected.to contain_exactly('+refs/tags/*:refs/tags/*',
- '+refs/heads/*:refs/remotes/origin/*')
+ is_expected.to contain_exactly("+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
end
end
end
diff --git a/spec/presenters/issue_presenter_spec.rb b/spec/presenters/issue_presenter_spec.rb
new file mode 100644
index 00000000000..8e24559341b
--- /dev/null
+++ b/spec/presenters/issue_presenter_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe IssuePresenter do
+ include Gitlab::Routing.url_helpers
+
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+ let(:issue) { create(:issue, project: project) }
+ let(:presenter) { described_class.new(issue, current_user: user) }
+
+ before do
+ group.add_developer(user)
+ end
+
+ describe '#web_url' do
+ it 'returns correct path' do
+ expect(presenter.web_url).to eq "http://localhost/#{group.name}/#{project.name}/issues/#{issue.iid}"
+ end
+ end
+
+ describe '#issue_path' do
+ it 'returns correct path' do
+ expect(presenter.issue_path).to eq "/#{group.name}/#{project.name}/issues/#{issue.iid}"
+ end
+ end
+end
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index 0e1aed42cc5..6408b0bd748 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -207,25 +207,25 @@ describe MergeRequestPresenter do
end
end
- describe '#cancel_merge_when_pipeline_succeeds_path' do
+ describe '#cancel_auto_merge_path' do
subject do
described_class.new(resource, current_user: user)
- .cancel_merge_when_pipeline_succeeds_path
+ .cancel_auto_merge_path
end
context 'when can cancel mwps' do
it 'returns path' do
- allow(resource).to receive(:can_cancel_merge_when_pipeline_succeeds?)
+ allow(resource).to receive(:can_cancel_auto_merge?)
.with(user)
.and_return(true)
- is_expected.to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}/cancel_merge_when_pipeline_succeeds")
+ is_expected.to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}/cancel_auto_merge")
end
end
context 'when cannot cancel mwps' do
it 'returns nil' do
- allow(resource).to receive(:can_cancel_merge_when_pipeline_succeeds?)
+ allow(resource).to receive(:can_cancel_auto_merge?)
.with(user)
.and_return(false)
diff --git a/spec/presenters/tree_entry_presenter_spec.rb b/spec/presenters/tree_entry_presenter_spec.rb
new file mode 100644
index 00000000000..d74ee5dc28f
--- /dev/null
+++ b/spec/presenters/tree_entry_presenter_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe TreeEntryPresenter do
+ include Gitlab::Routing.url_helpers
+
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:tree) { Gitlab::Graphql::Representation::TreeEntry.new(repository.tree.trees.first, repository) }
+ let(:presenter) { described_class.new(tree) }
+
+ describe '.web_url' do
+ it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/tree/#{tree.commit_id}/#{tree.path}") }
+ end
+end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index a132b85b878..f104da6ebba 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -2,6 +2,8 @@ require 'spec_helper'
require 'mime/types'
describe API::Commits do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
@@ -317,6 +319,96 @@ describe API::Commits do
expect(response).to have_gitlab_http_status(201)
end
end
+
+ context 'when the API user is a guest' do
+ def last_commit_id(project, branch_name)
+ project.repository.find_branch(branch_name)&.dereferenced_target&.id
+ end
+
+ let(:public_project) { create(:project, :public, :repository) }
+ let!(:url) { "/projects/#{public_project.id}/repository/commits" }
+ let(:guest) { create(:user).tap { |u| public_project.add_guest(u) } }
+
+ it 'returns a 403' do
+ post api(url, guest), params: valid_c_params
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ context 'when start_project is provided' do
+ context 'when posting to a forked project the user owns' do
+ let!(:forked_project) { fork_project(public_project, guest, namespace: guest.namespace, repository: true) }
+ let!(:url) { "/projects/#{forked_project.id}/repository/commits" }
+
+ before do
+ valid_c_params[:start_branch] = "master"
+ valid_c_params[:branch] = "patch"
+ end
+
+ context 'identified by Integer (id)' do
+ before do
+ valid_c_params[:start_project] = public_project.id
+ end
+
+ it 'adds a new commit to forked_project and returns a 201' do
+ expect { post api(url, guest), params: valid_c_params }
+ .to change { last_commit_id(forked_project, valid_c_params[:branch]) }
+ .and not_change { last_commit_id(public_project, valid_c_params[:start_branch]) }
+
+ expect(response).to have_gitlab_http_status(201)
+ end
+ end
+
+ context 'identified by String (full_path)' do
+ before do
+ valid_c_params[:start_project] = public_project.full_path
+ end
+
+ it 'adds a new commit to forked_project and returns a 201' do
+ expect { post api(url, guest), params: valid_c_params }
+ .to change { last_commit_id(forked_project, valid_c_params[:branch]) }
+ .and not_change { last_commit_id(public_project, valid_c_params[:start_branch]) }
+
+ expect(response).to have_gitlab_http_status(201)
+ end
+ end
+ end
+
+ context 'when the target project is not part of the fork network of start_project' do
+ let(:unrelated_project) { create(:project, :public, :repository, creator: guest) }
+ let!(:url) { "/projects/#{unrelated_project.id}/repository/commits" }
+
+ before do
+ valid_c_params[:start_branch] = "master"
+ valid_c_params[:branch] = "patch"
+ valid_c_params[:start_project] = public_project.id
+ end
+
+ it 'returns a 403' do
+ post api(url, guest), params: valid_c_params
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+
+ context 'when posting to a forked project the user does not have write access' do
+ let!(:forked_project) { fork_project(public_project, user, namespace: user.namespace, repository: true) }
+ let!(:url) { "/projects/#{forked_project.id}/repository/commits" }
+
+ before do
+ valid_c_params[:start_branch] = "master"
+ valid_c_params[:branch] = "patch"
+ valid_c_params[:start_project] = public_project.id
+ end
+
+ it 'returns a 403' do
+ post api(url, guest), params: valid_c_params
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
end
describe 'delete' do
diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb
index a724c5c3f1c..28676bb02f4 100644
--- a/spec/requests/api/graphql/gitlab_schema_spec.rb
+++ b/spec/requests/api/graphql/gitlab_schema_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'GitlabSchema configurations' do
include GraphqlHelpers
- let(:project) { create(:project) }
+ set(:project) { create(:project) }
shared_examples 'imposing query limits' do
describe '#max_complexity' do
@@ -52,13 +52,22 @@ describe 'GitlabSchema configurations' do
end
context 'multiplexed queries' do
+ let(:current_user) { nil }
+
subject do
queries = [
- { query: graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id name description)) },
- { query: graphql_query_for('echo', { 'text' => "$test" }, []), variables: { "test" => "Hello world" } }
+ { query: graphql_query_for('project', { 'fullPath' => '$fullPath' }, %w(id name description)) },
+ { query: graphql_query_for('echo', { 'text' => "$test" }, []), variables: { "test" => "Hello world" } },
+ { query: graphql_query_for('project', { 'fullPath' => project.full_path }, "userPermissions { createIssue }") }
]
- post_multiplex(queries)
+ post_multiplex(queries, current_user: current_user)
+ end
+
+ it 'does not authenticate all queries' do
+ subject
+
+ expect(json_response.last['data']['project']).to be_nil
end
it_behaves_like 'imposing query limits' do
@@ -69,18 +78,28 @@ describe 'GitlabSchema configurations' do
subject
# Expect a response for each query, even though it will be empty
- expect(json_response.size).to eq(2)
+ expect(json_response.size).to eq(3)
json_response.each do |single_query_response|
expect(single_query_response).not_to have_key('data')
end
# Expect errors for each query
- expect(graphql_errors.size).to eq(2)
+ expect(graphql_errors.size).to eq(3)
graphql_errors.each do |single_query_errors|
expect(single_query_errors.first['message']).to include('which exceeds max complexity of 4')
end
end
end
+
+ context 'authentication' do
+ let(:current_user) { project.owner }
+
+ it 'authenticates all queries' do
+ subject
+
+ expect(json_response.last['data']['project']['userPermissions']['createIssue']).to be(true)
+ end
+ end
end
context 'when IntrospectionQuery' do
@@ -92,4 +111,40 @@ describe 'GitlabSchema configurations' do
expect(graphql_errors).to be_nil
end
end
+
+ context 'logging' do
+ let(:query) { File.read(Rails.root.join('spec/fixtures/api/graphql/introspection.graphql')) }
+
+ it 'logs the query complexity and depth' do
+ analyzer_memo = {
+ query_string: query,
+ variables: {}.to_s,
+ complexity: 181,
+ depth: 0,
+ duration: 7
+ }
+
+ expect_any_instance_of(Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer).to receive(:duration).and_return(7)
+ expect(Gitlab::GraphqlLogger).to receive(:info).with(analyzer_memo)
+
+ post_graphql(query, current_user: nil)
+ end
+
+ it 'logs using `format_message`' do
+ expect_any_instance_of(Gitlab::GraphqlLogger).to receive(:format_message)
+
+ post_graphql(query, current_user: nil)
+ end
+ end
+
+ context "global id's" do
+ it 'uses GlobalID to expose ids' do
+ post_graphql(graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id)),
+ current_user: project.owner)
+
+ parsed_id = GlobalID.parse(graphql_data['project']['id'])
+
+ expect(parsed_id).to eq(project.to_global_id)
+ end
+ end
end
diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb
index 8ff95cc9af2..e0f1e4dbe9e 100644
--- a/spec/requests/api/graphql/group_query_spec.rb
+++ b/spec/requests/api/graphql/group_query_spec.rb
@@ -56,7 +56,7 @@ describe 'getting group information' do
post_graphql(group_query(group1), current_user: user1)
expect(response).to have_gitlab_http_status(200)
- expect(graphql_data['group']['id']).to eq(group1.id.to_s)
+ expect(graphql_data['group']['id']).to eq(group1.to_global_id.to_s)
expect(graphql_data['group']['name']).to eq(group1.name)
expect(graphql_data['group']['path']).to eq(group1.path)
expect(graphql_data['group']['description']).to eq(group1.description)
@@ -86,17 +86,18 @@ describe 'getting group information' do
end
it 'avoids N+1 queries' do
- post_graphql(group_query(group1), current_user: admin)
-
control_count = ActiveRecord::QueryRecorder.new do
post_graphql(group_query(group1), current_user: admin)
end.count
- create(:project, namespace: group1)
+ queries = [{ query: group_query(group1) },
+ { query: group_query(group2) }]
expect do
- post_graphql(group_query(group1), current_user: admin)
+ post_multiplex(queries, current_user: admin)
end.not_to exceed_query_limit(control_count)
+
+ expect(graphql_errors).to contain_exactly(nil, nil)
end
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb
index 8f427d71a32..d75f0df9fd3 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb
@@ -11,7 +11,7 @@ describe 'Setting WIP status of a merge request' do
let(:mutation) do
variables = {
project_path: project.full_path,
- iid: merge_request.iid
+ iid: merge_request.iid.to_s
}
graphql_mutation(:merge_request_set_wip, variables.merge(input))
end
diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb
new file mode 100644
index 00000000000..de1cd9586b6
--- /dev/null
+++ b/spec/requests/api/graphql/namespace/projects_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'getting projects', :nested_groups do
+ include GraphqlHelpers
+
+ let(:group) { create(:group) }
+ let!(:project) { create(:project, namespace: subject) }
+ let(:nested_group) { create(:group, parent: group) }
+ let!(:nested_project) { create(:project, group: nested_group) }
+ let!(:public_project) { create(:project, :public, namespace: subject) }
+ let(:user) { create(:user) }
+ let(:include_subgroups) { true }
+
+ subject { group }
+
+ let(:query) do
+ graphql_query_for(
+ 'namespace',
+ { 'fullPath' => subject.full_path },
+ <<~QUERY
+ projects(includeSubgroups: #{include_subgroups}) {
+ edges {
+ node {
+ #{all_graphql_fields_for('Project')}
+ }
+ }
+ }
+ QUERY
+ )
+ end
+
+ before do
+ group.add_owner(user)
+ end
+
+ shared_examples 'a graphql namespace' do
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: user)
+ end
+ end
+
+ it "includes the packages size if the user can read the statistics" do
+ post_graphql(query, current_user: user)
+
+ count = if include_subgroups
+ subject.all_projects.count
+ else
+ subject.projects.count
+ end
+
+ expect(graphql_data['namespace']['projects']['edges'].size).to eq(count)
+ end
+
+ context 'with no user' do
+ it 'finds only public projects' do
+ post_graphql(query, current_user: nil)
+
+ expect(graphql_data['namespace']['projects']['edges'].size).to eq(1)
+ project = graphql_data['namespace']['projects']['edges'][0]['node']
+ expect(project['id']).to eq(public_project.to_global_id.to_s)
+ end
+ end
+ end
+
+ it_behaves_like 'a graphql namespace'
+
+ context 'when the namespace is a user' do
+ subject { user.namespace }
+ let(:include_subgroups) { false }
+
+ it_behaves_like 'a graphql namespace'
+ end
+
+ context 'when not including subgroups' do
+ let(:include_subgroups) { false }
+
+ it_behaves_like 'a graphql namespace'
+ end
+end
diff --git a/spec/requests/api/graphql/project/project_statistics_spec.rb b/spec/requests/api/graphql/project/project_statistics_spec.rb
new file mode 100644
index 00000000000..8683fa1f390
--- /dev/null
+++ b/spec/requests/api/graphql/project/project_statistics_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'rendering namespace statistics' do
+ include GraphqlHelpers
+
+ let(:project) { create(:project) }
+ let!(:project_statistics) { create(:project_statistics, project: project, packages_size: 5.megabytes) }
+ let(:user) { create(:user) }
+
+ let(:query) do
+ graphql_query_for('project',
+ { 'fullPath' => project.full_path },
+ "statistics { #{all_graphql_fields_for('ProjectStatistics')} }")
+ end
+
+ before do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: user)
+ end
+ end
+
+ it "includes the packages size if the user can read the statistics" do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data['project']['statistics']['packagesSize']).to eq(5.megabytes)
+ end
+
+ context 'when the project is public' do
+ let(:project) { create(:project, :public) }
+
+ it 'includes the statistics regardless of the user' do
+ post_graphql(query, current_user: nil)
+
+ expect(graphql_data['project']['statistics']).to be_present
+ end
+ end
+end
diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb
index cca87c16f27..656d6f8b50b 100644
--- a/spec/requests/api/graphql_spec.rb
+++ b/spec/requests/api/graphql_spec.rb
@@ -16,6 +16,54 @@ describe 'GraphQL' do
end
end
+ context 'logging' do
+ shared_examples 'logging a graphql query' do
+ let(:expected_params) do
+ { query_string: query, variables: variables.to_s, duration: anything, depth: 1, complexity: 1 }
+ end
+
+ it 'logs a query with the expected params' do
+ expect(Gitlab::GraphqlLogger).to receive(:info).with(expected_params).once
+
+ post_graphql(query, variables: variables)
+ end
+
+ it 'does not instantiate any query analyzers' do # they are static and re-used
+ expect(GraphQL::Analysis::QueryComplexity).not_to receive(:new)
+ expect(GraphQL::Analysis::QueryDepth).not_to receive(:new)
+
+ 2.times { post_graphql(query, variables: variables) }
+ end
+ end
+
+ context 'with no variables' do
+ let(:variables) { {} }
+
+ it_behaves_like 'logging a graphql query'
+ end
+
+ context 'with variables' do
+ let(:variables) do
+ { "foo" => "bar" }
+ end
+
+ it_behaves_like 'logging a graphql query'
+ end
+
+ context 'when there is an error in the logger' do
+ before do
+ allow_any_instance_of(Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer).to receive(:process_variables).and_raise(StandardError.new("oh noes!"))
+ end
+
+ it 'logs the exception in Sentry and continues with the request' do
+ expect(Gitlab::Sentry).to receive(:track_exception).at_least(1).times
+ expect(Gitlab::GraphqlLogger).to receive(:info)
+
+ post_graphql(query, variables: {})
+ end
+ end
+ end
+
context 'invalid variables' do
it 'returns an error' do
post_graphql(query, variables: "This is not JSON")
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 7176bc23e34..c41408fba65 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -90,8 +90,9 @@ describe API::Groups do
it "includes statistics if requested" do
attributes = {
- storage_size: 702,
+ storage_size: 1158,
repository_size: 123,
+ wiki_size: 456,
lfs_objects_size: 234,
build_artifacts_size: 345
}.stringify_keys
diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb
index a07d7673345..0b0f754ab57 100644
--- a/spec/requests/api/issues/get_project_issues_spec.rb
+++ b/spec/requests/api/issues/get_project_issues_spec.rb
@@ -172,7 +172,9 @@ describe API::Issues do
end
it 'returns 404 when project does not exist' do
- get api('/projects/1000/issues', non_member)
+ max_project_id = Project.maximum(:id).to_i
+
+ get api("/projects/#{max_project_id + 1}/issues", non_member)
expect(response).to have_gitlab_http_status(404)
end
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 43462913497..89ee6f896f9 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -864,7 +864,7 @@ describe API::Jobs do
end
describe 'POST /projects/:id/jobs/:job_id/retry' do
- let(:job) { create(:ci_build, :failed, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :canceled, pipeline: pipeline) }
before do
post api("/projects/#{project.id}/jobs/#{job.id}/retry", api_user)
@@ -874,7 +874,7 @@ describe API::Jobs do
context 'user with :update_build permission' do
it 'retries non-running job' do
expect(response).to have_gitlab_http_status(201)
- expect(project.builds.first.status).to eq('failed')
+ expect(project.builds.first.status).to eq('canceled')
expect(json_response['status']).to eq('pending')
end
end
@@ -913,8 +913,8 @@ describe API::Jobs do
expect(response).to have_gitlab_http_status(201)
expect(job.job_artifacts.count).to eq(0)
expect(job.trace.exist?).to be_falsy
- expect(job.artifacts_file.exists?).to be_falsy
- expect(job.artifacts_metadata.exists?).to be_falsy
+ expect(job.artifacts_file.present?).to be_falsy
+ expect(job.artifacts_metadata.present?).to be_falsy
expect(job.has_job_artifacts?).to be_falsy
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 5c94a87529b..4cb4fcc890d 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1473,7 +1473,7 @@ describe API::MergeRequests do
end
it "enables merge when pipeline succeeds if the pipeline is active" do
- allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline)
+ allow_any_instance_of(MergeRequest).to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
allow(pipeline).to receive(:active?).and_return(true)
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), params: { merge_when_pipeline_succeeds: true }
@@ -1484,7 +1484,7 @@ describe API::MergeRequests do
end
it "enables merge when pipeline succeeds if the pipeline is active and only_allow_merge_if_pipeline_succeeds is true" do
- allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline)
+ allow_any_instance_of(MergeRequest).to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
allow(pipeline).to receive(:active?).and_return(true)
project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true)
@@ -1546,52 +1546,65 @@ describe API::MergeRequests do
end
end
- describe "PUT /projects/:id/merge_requests/:merge_request_iid/merge_to_ref" do
- let(:pipeline) { create(:ci_pipeline_without_jobs) }
+ describe "GET /projects/:id/merge_requests/:merge_request_iid/merge_ref" do
+ before do
+ merge_request.mark_as_unchecked!
+ end
+
+ let(:merge_request_iid) { merge_request.iid }
+
let(:url) do
- "/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge_to_ref"
+ "/projects/#{project.id}/merge_requests/#{merge_request_iid}/merge_ref"
end
it 'returns the generated ID from the merge service in case of success' do
- put api(url, user), params: { merge_commit_message: 'Custom message' }
-
- commit = project.commit(json_response['commit_id'])
+ get api(url, user)
expect(response).to have_gitlab_http_status(200)
- expect(json_response['commit_id']).to be_present
- expect(commit.message).to eq('Custom message')
+ expect(json_response['commit_id']).to eq(merge_request.merge_ref_head.sha)
end
it "returns 400 if branch can't be merged" do
- merge_request.update!(state: 'merged')
+ merge_request.update!(merge_status: 'cannot_be_merged')
- put api(url, user)
+ get api(url, user)
expect(response).to have_gitlab_http_status(400)
- expect(json_response['message'])
- .to eq("Merge request is not mergeable to #{merge_request.merge_ref_path}")
+ expect(json_response['message']).to eq('Merge request is not mergeable')
end
- it 'returns 403 if user has no permissions to merge to the ref' do
- user2 = create(:user)
- project.add_reporter(user2)
+ context 'when user has no access to the MR' do
+ let(:project) { create(:project, :private) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- put api(url, user2)
+ it 'returns 404' do
+ project.add_guest(user)
- expect(response).to have_gitlab_http_status(403)
- expect(json_response['message']).to eq('403 Forbidden')
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response['message']).to eq('404 Not found')
+ end
end
- it 'returns 404 for an invalid merge request IID' do
- put api("/projects/#{project.id}/merge_requests/12345/merge_to_ref", user)
+ context 'when invalid merge request IID' do
+ let(:merge_request_iid) { '12345' }
- expect(response).to have_gitlab_http_status(404)
+ it 'returns 404' do
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
end
- it "returns 404 if the merge request id is used instead of iid" do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+ context 'when merge request ID is used instead IID' do
+ let(:merge_request_iid) { merge_request.id }
- expect(response).to have_gitlab_http_status(404)
+ it 'returns 404' do
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
end
end
@@ -1950,7 +1963,7 @@ describe API::MergeRequests do
describe 'POST :id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds' do
before do
- ::MergeRequests::MergeWhenPipelineSucceedsService.new(merge_request.target_project, user).execute(merge_request)
+ ::AutoMergeService.new(merge_request.target_project, user).execute(merge_request, AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
end
it 'removes the merge_when_pipeline_succeeds status' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 16d306f39cd..799e84e83c1 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1025,7 +1025,54 @@ describe API::Projects do
end
end
- context 'when authenticated' do
+ context 'when authenticated as an admin' do
+ it 'returns a project by id' do
+ project
+ project_member
+ group = create(:group)
+ link = create(:project_group_link, project: project, group: group)
+
+ get api("/projects/#{project.id}", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['id']).to eq(project.id)
+ expect(json_response['description']).to eq(project.description)
+ expect(json_response['default_branch']).to eq(project.default_branch)
+ expect(json_response['tag_list']).to be_an Array
+ expect(json_response['archived']).to be_falsey
+ expect(json_response['visibility']).to be_present
+ expect(json_response['ssh_url_to_repo']).to be_present
+ expect(json_response['http_url_to_repo']).to be_present
+ expect(json_response['web_url']).to be_present
+ expect(json_response['owner']).to be_a Hash
+ expect(json_response['name']).to eq(project.name)
+ expect(json_response['path']).to be_present
+ expect(json_response['issues_enabled']).to be_present
+ expect(json_response['merge_requests_enabled']).to be_present
+ expect(json_response['wiki_enabled']).to be_present
+ expect(json_response['jobs_enabled']).to be_present
+ expect(json_response['snippets_enabled']).to be_present
+ expect(json_response['container_registry_enabled']).to be_present
+ expect(json_response['created_at']).to be_present
+ expect(json_response['last_activity_at']).to be_present
+ expect(json_response['shared_runners_enabled']).to be_present
+ expect(json_response['creator_id']).to be_present
+ expect(json_response['namespace']).to be_present
+ expect(json_response['avatar_url']).to be_nil
+ expect(json_response['star_count']).to be_present
+ expect(json_response['forks_count']).to be_present
+ expect(json_response['public_jobs']).to be_present
+ expect(json_response['shared_with_groups']).to be_an Array
+ expect(json_response['shared_with_groups'].length).to eq(1)
+ expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
+ expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
+ expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
+ expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds)
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
+ end
+ end
+
+ context 'when authenticated as a regular user' do
before do
project
project_member
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 4006e697a41..038c958b5cc 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -444,8 +444,8 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
'sha' => job.sha,
'before_sha' => job.before_sha,
'ref_type' => 'branch',
- 'refspecs' => %w[+refs/heads/*:refs/remotes/origin/* +refs/tags/*:refs/tags/*],
- 'depth' => 0 }
+ 'refspecs' => ["+refs/heads/#{job.ref}:refs/remotes/origin/#{job.ref}"],
+ 'depth' => project.default_git_depth }
end
let(:expected_steps) do
@@ -531,7 +531,11 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
- context 'when GIT_DEPTH is not specified' do
+ context 'when GIT_DEPTH is not specified and there is no default git depth for the project' do
+ before do
+ project.update!(default_git_depth: nil)
+ end
+
it 'specifies refspecs' do
request_job
@@ -587,7 +591,11 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
- context 'when GIT_DEPTH is not specified' do
+ context 'when GIT_DEPTH is not specified and there is no default git depth for the project' do
+ before do
+ project.update!(default_git_depth: nil)
+ end
+
it 'specifies refspecs' do
request_job
@@ -1632,8 +1640,8 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
let!(:metadata) { file_upload2 }
let!(:metadata_sha256) { Digest::SHA256.file(metadata.path).hexdigest }
- let(:stored_artifacts_file) { job.reload.artifacts_file.file }
- let(:stored_metadata_file) { job.reload.artifacts_metadata.file }
+ let(:stored_artifacts_file) { job.reload.artifacts_file }
+ let(:stored_metadata_file) { job.reload.artifacts_metadata }
let(:stored_artifacts_size) { job.reload.artifacts_size }
let(:stored_artifacts_sha256) { job.reload.job_artifacts_archive.file_sha256 }
let(:stored_metadata_sha256) { job.reload.job_artifacts_metadata.file_sha256 }
@@ -1654,9 +1662,9 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
it 'stores artifacts and artifacts metadata' do
expect(response).to have_gitlab_http_status(201)
- expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
- expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
- expect(stored_artifacts_size).to eq(72821)
+ expect(stored_artifacts_file.filename).to eq(artifacts.original_filename)
+ expect(stored_metadata_file.filename).to eq(metadata.original_filename)
+ expect(stored_artifacts_size).to eq(artifacts.size)
expect(stored_artifacts_sha256).to eq(artifacts_sha256)
expect(stored_metadata_sha256).to eq(metadata_sha256)
end
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 7d61ec9c4d8..3e0b478abb3 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -70,11 +70,30 @@ describe API::Search do
context 'for milestones scope' do
before do
create(:milestone, project: project, title: 'awesome milestone')
+ end
+
+ context 'when user can read project milestones' do
+ before do
+ get api('/search', user), params: { scope: 'milestones', search: 'awesome' }
+ end
- get api('/search', user), params: { scope: 'milestones', search: 'awesome' }
+ it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
end
- it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
+ context 'when user cannot read project milestones' do
+ before do
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+ project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'returns empty array' do
+ get api('/search', user), params: { scope: 'milestones', search: 'awesome' }
+
+ milestones = JSON.parse(response.body)
+
+ expect(milestones).to be_empty
+ end
+ end
end
context 'for users scope' do
@@ -318,11 +337,30 @@ describe API::Search do
context 'for milestones scope' do
before do
create(:milestone, project: project, title: 'awesome milestone')
+ end
+
+ context 'when user can read milestones' do
+ before do
+ get api("/projects/#{project.id}/search", user), params: { scope: 'milestones', search: 'awesome' }
+ end
- get api("/projects/#{project.id}/search", user), params: { scope: 'milestones', search: 'awesome' }
+ it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
end
- it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
+ context 'when user cannot read project milestones' do
+ before do
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+ project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'returns empty array' do
+ get api("/projects/#{project.id}/search", user), params: { scope: 'milestones', search: 'awesome' }
+
+ milestones = JSON.parse(response.body)
+
+ expect(milestones).to be_empty
+ end
+ end
end
context 'for users scope' do
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 527ab1cfb66..8a60980fe80 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -13,6 +13,7 @@ describe API::Settings, 'Settings' do
expect(json_response['default_projects_limit']).to eq(42)
expect(json_response['password_authentication_enabled_for_web']).to be_truthy
expect(json_response['repository_storages']).to eq(['default'])
+ expect(json_response['password_authentication_enabled']).to be_truthy
expect(json_response['plantuml_enabled']).to be_falsey
expect(json_response['plantuml_url']).to be_nil
expect(json_response['default_project_visibility']).to be_a String
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index b6e8d74c2e9..0e2f3face71 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -1,12 +1,14 @@
require 'spec_helper'
describe API::SystemHooks do
+ include StubRequests
+
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let!(:hook) { create(:system_hook, url: "http://example.com") }
before do
- stub_request(:post, hook.url)
+ stub_full_request(hook.url, method: :post)
end
describe "GET /hooks" do
@@ -68,6 +70,8 @@ describe API::SystemHooks do
end
it 'sets default values for events' do
+ stub_full_request('http://mep.mep', method: :post)
+
post api('/hooks', admin), params: { url: 'http://mep.mep' }
expect(response).to have_gitlab_http_status(201)
@@ -78,6 +82,8 @@ describe API::SystemHooks do
end
it 'sets explicit values for events' do
+ stub_full_request('http://mep.mep', method: :post)
+
post api('/hooks', admin),
params: {
url: 'http://mep.mep',
diff --git a/spec/requests/api/task_completion_status_spec.rb b/spec/requests/api/task_completion_status_spec.rb
new file mode 100644
index 00000000000..ee2531197b1
--- /dev/null
+++ b/spec/requests/api/task_completion_status_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'task completion status response' do
+ set(:user) { create(:user) }
+ set(:project) do
+ create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ end
+
+ shared_examples 'taskable completion status provider' do |path|
+ samples = [
+ {
+ description: '',
+ expected_count: 0,
+ expected_completed_count: 0
+ },
+ {
+ description: 'Lorem ipsum',
+ expected_count: 0,
+ expected_completed_count: 0
+ },
+ {
+ description: %{- [ ] task 1
+ - [x] task 2 },
+ expected_count: 2,
+ expected_completed_count: 1
+ },
+ {
+ description: %{- [ ] task 1
+ - [ ] task 2 },
+ expected_count: 2,
+ expected_completed_count: 0
+ },
+ {
+ description: %{- [x] task 1
+ - [x] task 2 },
+ expected_count: 2,
+ expected_completed_count: 2
+ },
+ {
+ description: %{- [ ] task 1},
+ expected_count: 1,
+ expected_completed_count: 0
+ },
+ {
+ description: %{- [x] task 1},
+ expected_count: 1,
+ expected_completed_count: 1
+ }
+ ]
+ samples.each do |sample_data|
+ context "with a description of #{sample_data[:description].inspect}" do
+ before do
+ taskable.update!(description: sample_data[:description])
+
+ get api("#{path}?iids[]=#{taskable.iid}", user)
+ end
+
+ it { expect(response).to have_gitlab_http_status(200) }
+
+ it 'returns the expected results' do
+ expect(json_response).to be_an Array
+ expect(json_response).not_to be_empty
+
+ task_completion_status = json_response.first['task_completion_status']
+ expect(task_completion_status['count']).to eq(sample_data[:expected_count])
+ expect(task_completion_status['completed_count']).to eq(sample_data[:expected_completed_count])
+ end
+ end
+ end
+ end
+
+ context 'task list completion status for issues' do
+ it_behaves_like 'taskable completion status provider', '/issues' do
+ let(:taskable) { create(:issue, project: project, author: user) }
+ end
+ end
+
+ context 'task list completion status for merge_requests' do
+ it_behaves_like 'taskable completion status provider', '/merge_requests' do
+ let(:taskable) { create(:merge_request, source_project: project, target_project: project, author: user) }
+ end
+ end
+end
diff --git a/spec/routing/import_routing_spec.rb b/spec/routing/import_routing_spec.rb
index 106f92082e4..3fdede7914d 100644
--- a/spec/routing/import_routing_spec.rb
+++ b/spec/routing/import_routing_spec.rb
@@ -174,3 +174,15 @@ describe Import::GitlabProjectsController, 'routing' do
expect(get('/import/gitlab_project/new')).to route_to('import/gitlab_projects#new')
end
end
+
+# new_import_phabricator GET /import/phabricator/new(.:format) import/phabricator#new
+# import_phabricator POST /import/phabricator(.:format) import/phabricator#create
+describe Import::PhabricatorController, 'routing' do
+ it 'to #create' do
+ expect(post("/import/phabricator")).to route_to("import/phabricator#create")
+ end
+
+ it 'to #new' do
+ expect(get("/import/phabricator/new")).to route_to("import/phabricator#new")
+ end
+end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 6f40e88d26f..83775b1040e 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -470,18 +470,23 @@ describe 'project routing' do
it_behaves_like 'RESTful project resources' do
let(:controller) { 'milestones' }
let(:actions) { [:index, :create, :new, :edit, :show, :update] }
+ let(:controller_path) { '/-/milestones' }
end
it 'to #promote' do
- expect(post('/gitlab/gitlabhq/milestones/1/promote')).to route_to('projects/milestones#promote', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "1")
+ expect(post('/gitlab/gitlabhq/-/milestones/1/promote')).to route_to('projects/milestones#promote', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "1")
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/milestones", "/gitlab/gitlabhq/-/milestones"
end
# project_labels GET /:project_id/labels(.:format) labels#index
describe Projects::LabelsController, 'routing' do
it 'to #index' do
- expect(get('/gitlab/gitlabhq/labels')).to route_to('projects/labels#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ expect(get('/gitlab/gitlabhq/-/labels')).to route_to('projects/labels#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/labels", "/gitlab/gitlabhq/-/labels"
end
# sort_project_issues POST /:project_id/issues/sort(.:format) issues#sort
@@ -623,20 +628,24 @@ describe 'project routing' do
describe Projects::ForksController, 'routing' do
it 'to #new' do
- expect(get('/gitlab/gitlabhq/forks/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ expect(get('/gitlab/gitlabhq/-/forks/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
it 'to #create' do
- expect(post('/gitlab/gitlabhq/forks')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ expect(post('/gitlab/gitlabhq/-/forks')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/forks", "/gitlab/gitlabhq/-/forks"
end
# project_avatar DELETE /project/avatar(.:format) projects/avatars#destroy
describe Projects::AvatarsController, 'routing' do
it 'to #destroy' do
- expect(delete('/gitlab/gitlabhq/avatar')).to route_to(
+ expect(delete('/gitlab/gitlabhq/-/avatar')).to route_to(
'projects/avatars#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/avatar", "/gitlab/gitlabhq/-/avatar"
end
describe Projects::PagesDomainsController, 'routing' do
diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb
index b89898f26f7..a27c22191f4 100644
--- a/spec/serializers/merge_request_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_widget_entity_spec.rb
@@ -297,4 +297,50 @@ describe MergeRequestWidgetEntity do
end
end
end
+
+ describe 'auto merge' do
+ context 'when auto merge is enabled' do
+ let(:resource) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ it 'returns auto merge related information' do
+ expect(subject[:auto_merge_enabled]).to be_truthy
+ expect(subject[:auto_merge_strategy]).to eq('merge_when_pipeline_succeeds')
+ end
+ end
+
+ context 'when auto merge is not enabled' do
+ let(:resource) { create(:merge_request) }
+
+ it 'returns auto merge related information' do
+ expect(subject[:auto_merge_enabled]).to be_falsy
+ expect(subject[:auto_merge_strategy]).to be_nil
+ end
+ end
+
+ context 'when head pipeline is running' do
+ before do
+ create(:ci_pipeline, :running, project: project,
+ ref: resource.source_branch,
+ sha: resource.diff_head_sha)
+ resource.update_head_pipeline
+ end
+
+ it 'returns available auto merge strategies' do
+ expect(subject[:available_auto_merge_strategies]).to eq(%w[merge_when_pipeline_succeeds])
+ end
+ end
+
+ context 'when head pipeline is finished' do
+ before do
+ create(:ci_pipeline, :success, project: project,
+ ref: resource.source_branch,
+ sha: resource.diff_head_sha)
+ resource.update_head_pipeline
+ end
+
+ it 'returns available auto merge strategies' do
+ expect(subject[:available_auto_merge_strategies]).to be_empty
+ end
+ end
+ end
end
diff --git a/spec/services/auto_merge/base_service_spec.rb b/spec/services/auto_merge/base_service_spec.rb
new file mode 100644
index 00000000000..197fa16961d
--- /dev/null
+++ b/spec/services/auto_merge/base_service_spec.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AutoMerge::BaseService do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(project, user, params) }
+ let(:merge_request) { create(:merge_request) }
+ let(:params) { {} }
+
+ describe '#execute' do
+ subject { service.execute(merge_request) }
+
+ it 'sets properies to the merge request' do
+ subject
+
+ merge_request.reload
+ expect(merge_request).to be_auto_merge_enabled
+ expect(merge_request.merge_user).to eq(user)
+ expect(merge_request.auto_merge_strategy).to eq('base')
+ end
+
+ it 'yields block' do
+ expect { |b| service.execute(merge_request, &b) }.to yield_control.once
+ end
+
+ it 'returns activated strategy name' do
+ is_expected.to eq(:base)
+ end
+
+ context 'when merge parameters are given' do
+ let(:params) do
+ {
+ 'commit_message' => "Merge branch 'patch-12' into 'master'",
+ 'sha' => "200fcc9c260f7219eaf0daba87d818f0922c5b18",
+ 'should_remove_source_branch' => false,
+ 'squash' => false,
+ 'squash_commit_message' => "Update README.md"
+ }
+ end
+
+ it 'sets merge parameters' do
+ subject
+
+ merge_request.reload
+ expect(merge_request.merge_params['commit_message']).to eq("Merge branch 'patch-12' into 'master'")
+ expect(merge_request.merge_params['sha']).to eq('200fcc9c260f7219eaf0daba87d818f0922c5b18')
+ expect(merge_request.merge_params['should_remove_source_branch']).to eq(false)
+ expect(merge_request.merge_params['squash']).to eq(false)
+ expect(merge_request.merge_params['squash_commit_message']).to eq('Update README.md')
+ end
+ end
+
+ context 'when strategy is merge when pipeline succeeds' do
+ let(:service) { AutoMerge::MergeWhenPipelineSucceedsService.new(project, user) }
+
+ it 'sets the auto merge strategy' do
+ subject
+
+ merge_request.reload
+ expect(merge_request.auto_merge_strategy).to eq(AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
+ end
+
+ it 'returns activated strategy name' do
+ is_expected.to eq(AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS.to_sym)
+ end
+ end
+
+ context 'when failed to save' do
+ before do
+ allow(merge_request).to receive(:save) { false }
+ end
+
+ it 'does not yield block' do
+ expect { |b| service.execute(merge_request, &b) }.not_to yield_control
+ end
+
+ it 'returns failed' do
+ is_expected.to eq(:failed)
+ end
+ end
+ end
+
+ describe '#cancel' do
+ subject { service.cancel(merge_request) }
+
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ it 'removes properies from the merge request' do
+ subject
+
+ merge_request.reload
+ expect(merge_request).not_to be_auto_merge_enabled
+ expect(merge_request.merge_user).to be_nil
+ expect(merge_request.auto_merge_strategy).to be_nil
+ end
+
+ it 'yields block' do
+ expect { |b| service.cancel(merge_request, &b) }.to yield_control.once
+ end
+
+ it 'returns success status' do
+ expect(subject[:status]).to eq(:success)
+ end
+
+ context 'when merge params are set' do
+ before do
+ merge_request.update!(merge_params:
+ {
+ 'should_remove_source_branch' => false,
+ 'commit_message' => "Merge branch 'patch-12' into 'master'",
+ 'squash_commit_message' => "Update README.md",
+ 'auto_merge_strategy' => 'merge_when_pipeline_succeeds'
+ })
+ end
+
+ it 'removes merge parameters' do
+ subject
+
+ merge_request.reload
+ expect(merge_request.merge_params['should_remove_source_branch']).to be_nil
+ expect(merge_request.merge_params['commit_message']).to be_nil
+ expect(merge_request.merge_params['squash_commit_message']).to be_nil
+ expect(merge_request.merge_params['auto_merge_strategy']).to be_nil
+ end
+ end
+
+ context 'when failed to save' do
+ before do
+ allow(merge_request).to receive(:save) { false }
+ end
+
+ it 'does not yield block' do
+ expect { |b| service.execute(merge_request, &b) }.not_to yield_control
+ end
+
+ it 'returns error status' do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to eq("Can't cancel the automatic merge")
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
index 96f61f3f103..a20bf8e17e4 100644
--- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe MergeRequests::MergeWhenPipelineSucceedsService do
+describe AutoMerge::MergeWhenPipelineSucceedsService do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -21,6 +21,27 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
described_class.new(project, user, commit_message: 'Awesome message')
end
+ describe "#available_for?" do
+ subject { service.available_for?(mr_merge_if_green_enabled) }
+
+ let(:pipeline_status) { :running }
+
+ before do
+ create(:ci_pipeline, pipeline_status, ref: mr_merge_if_green_enabled.source_branch,
+ sha: mr_merge_if_green_enabled.diff_head_sha,
+ project: mr_merge_if_green_enabled.source_project)
+ mr_merge_if_green_enabled.update_head_pipeline
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when the head piipeline succeeded' do
+ let(:pipeline_status) { :success }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
describe "#execute" do
let(:merge_request) do
create(:merge_request, target_project: project, source_project: project,
@@ -30,8 +51,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
context 'first time enabling' do
before do
allow(merge_request)
- .to receive(:head_pipeline)
- .and_return(pipeline)
+ .to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
service.execute(merge_request)
end
@@ -39,7 +59,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
it 'sets the params, merge_user, and flag' do
expect(merge_request).to be_valid
expect(merge_request.merge_when_pipeline_succeeds).to be_truthy
- expect(merge_request.merge_params).to eq commit_message: 'Awesome message'
+ expect(merge_request.merge_params).to include commit_message: 'Awesome message'
expect(merge_request.merge_user).to be user
end
@@ -54,8 +74,8 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) }
before do
- allow(mr_merge_if_green_enabled).to receive(:head_pipeline)
- .and_return(pipeline)
+ allow(mr_merge_if_green_enabled)
+ .to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
allow(mr_merge_if_green_enabled).to receive(:mergeable?)
.and_return(true)
@@ -72,7 +92,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
end
end
- describe "#trigger" do
+ describe "#process" do
let(:merge_request_ref) { mr_merge_if_green_enabled.source_branch }
let(:merge_request_head) do
project.commit(mr_merge_if_green_enabled.source_branch).id
@@ -86,8 +106,11 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
end
it "merges all merge requests with merge when the pipeline succeeds enabled" do
+ allow(mr_merge_if_green_enabled)
+ .to receive_messages(head_pipeline: triggering_pipeline, actual_head_pipeline: triggering_pipeline)
+
expect(MergeWorker).to receive(:perform_async)
- service.trigger(triggering_pipeline)
+ service.process(mr_merge_if_green_enabled)
end
end
@@ -99,7 +122,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
it 'does not merge request' do
expect(MergeWorker).not_to receive(:perform_async)
- service.trigger(old_pipeline)
+ service.process(mr_merge_if_green_enabled)
end
end
@@ -111,7 +134,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
it 'does not merge request' do
expect(MergeWorker).not_to receive(:perform_async)
- service.trigger(unrelated_pipeline)
+ service.process(mr_merge_if_green_enabled)
end
end
@@ -125,8 +148,11 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
end
it 'merges the associated merge request' do
+ allow(mr_merge_if_green_enabled)
+ .to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
+
expect(MergeWorker).to receive(:perform_async)
- service.trigger(pipeline)
+ service.process(mr_merge_if_green_enabled)
end
end
end
diff --git a/spec/services/auto_merge_service_spec.rb b/spec/services/auto_merge_service_spec.rb
new file mode 100644
index 00000000000..d0eefed3150
--- /dev/null
+++ b/spec/services/auto_merge_service_spec.rb
@@ -0,0 +1,140 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AutoMergeService do
+ set(:project) { create(:project) }
+ set(:user) { create(:user) }
+ let(:service) { described_class.new(project, user) }
+
+ describe '.all_strategies' do
+ subject { described_class.all_strategies }
+
+ it 'returns all strategies' do
+ is_expected.to eq(AutoMergeService::STRATEGIES)
+ end
+ end
+
+ describe '#available_strategies' do
+ subject { service.available_strategies(merge_request) }
+
+ let(:merge_request) { create(:merge_request) }
+ let(:pipeline_status) { :running }
+
+ before do
+ create(:ci_pipeline, pipeline_status, ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ project: merge_request.source_project)
+
+ merge_request.update_head_pipeline
+ end
+
+ it 'returns available strategies' do
+ is_expected.to include('merge_when_pipeline_succeeds')
+ end
+
+ context 'when the head piipeline succeeded' do
+ let(:pipeline_status) { :success }
+
+ it 'returns available strategies' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '.get_service_class' do
+ subject { described_class.get_service_class(strategy) }
+
+ let(:strategy) { AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS }
+
+ it 'returns service instance' do
+ is_expected.to eq(AutoMerge::MergeWhenPipelineSucceedsService)
+ end
+
+ context 'when strategy is not present' do
+ let(:strategy) { }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#execute' do
+ subject { service.execute(merge_request, strategy) }
+
+ let(:merge_request) { create(:merge_request) }
+ let(:pipeline_status) { :running }
+ let(:strategy) { AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS }
+
+ before do
+ create(:ci_pipeline, pipeline_status, ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ project: merge_request.source_project)
+
+ merge_request.update_head_pipeline
+ end
+
+ it 'delegates to a relevant service instance' do
+ expect_next_instance_of(AutoMerge::MergeWhenPipelineSucceedsService) do |service|
+ expect(service).to receive(:execute).with(merge_request)
+ end
+
+ subject
+ end
+
+ context 'when the head piipeline succeeded' do
+ let(:pipeline_status) { :success }
+
+ it 'returns failed' do
+ is_expected.to eq(:failed)
+ end
+ end
+ end
+
+ describe '#process' do
+ subject { service.process(merge_request) }
+
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ it 'delegates to a relevant service instance' do
+ expect_next_instance_of(AutoMerge::MergeWhenPipelineSucceedsService) do |service|
+ expect(service).to receive(:process).with(merge_request)
+ end
+
+ subject
+ end
+
+ context 'when auto merge is not enabled' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#cancel' do
+ subject { service.cancel(merge_request) }
+
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ it 'delegates to a relevant service instance' do
+ expect_next_instance_of(AutoMerge::MergeWhenPipelineSucceedsService) do |service|
+ expect(service).to receive(:cancel).with(merge_request)
+ end
+
+ subject
+ end
+
+ context 'when auto merge is not enabled' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'returns error' do
+ expect(subject[:message]).to eq("Can't cancel the automatic merge")
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:http_status]).to eq(406)
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 9a3ac75e418..867692d4d64 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -973,7 +973,7 @@ describe Ci::CreatePipelineService do
let(:merge_request) do
create(:merge_request,
source_project: project,
- source_branch: ref_name,
+ source_branch: Gitlab::Git.ref_name(ref_name),
target_project: project,
target_branch: 'master')
end
@@ -1004,7 +1004,7 @@ describe Ci::CreatePipelineService do
let(:merge_request) do
create(:merge_request,
source_project: project,
- source_branch: ref_name,
+ source_branch: Gitlab::Git.ref_name(ref_name),
target_project: project,
target_branch: 'master')
end
@@ -1033,7 +1033,7 @@ describe Ci::CreatePipelineService do
let(:merge_request) do
create(:merge_request,
source_project: project,
- source_branch: ref_name,
+ source_branch: Gitlab::Git.ref_name(ref_name),
target_project: project,
target_branch: 'master')
end
diff --git a/spec/services/ci/pipeline_schedule_service_spec.rb b/spec/services/ci/pipeline_schedule_service_spec.rb
new file mode 100644
index 00000000000..f2ac53cb25a
--- /dev/null
+++ b/spec/services/ci/pipeline_schedule_service_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::PipelineScheduleService do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(project, user) }
+
+ describe '#execute' do
+ subject { service.execute(schedule) }
+
+ let(:schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
+
+ it 'schedules next run' do
+ expect(schedule).to receive(:schedule_next_run!)
+
+ subject
+ end
+
+ it 'runs RunPipelineScheduleWorker' do
+ expect(RunPipelineScheduleWorker)
+ .to receive(:perform_async).with(schedule.id, schedule.owner.id)
+
+ subject
+ end
+ end
+end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index df2376384ca..e9a26400723 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -23,7 +23,7 @@ describe Ci::RetryBuildService do
REJECT_ACCESSORS =
%i[id status user token token_encrypted coverage trace runner
- artifacts_expire_at artifacts_file artifacts_metadata artifacts_size
+ artifacts_expire_at
created_at updated_at started_at finished_at queued_at erased_by
erased_at auto_canceled_by job_artifacts job_artifacts_archive
job_artifacts_metadata job_artifacts_trace job_artifacts_junit
@@ -38,7 +38,8 @@ describe Ci::RetryBuildService do
runner_id tag_taggings taggings tags trigger_request_id
user_id auto_canceled_by_id retried failure_reason
sourced_pipelines artifacts_file_store artifacts_metadata_store
- metadata runner_session trace_chunks].freeze
+ metadata runner_session trace_chunks
+ artifacts_file artifacts_metadata artifacts_size].freeze
shared_examples 'build duplication' do
let(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index ffa612cf315..29b7e0f17e2 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -52,6 +52,14 @@ describe MergeRequests::CloseService do
it 'marks todos as done' do
expect(todo.reload).to be_done
end
+
+ context 'when auto merge is enabled' do
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ it 'cancels the auto merge' do
+ expect(@merge_request).not_to be_auto_merge_enabled
+ end
+ end
end
it 'updates metrics' do
diff --git a/spec/services/merge_requests/create_pipeline_service_spec.rb b/spec/services/merge_requests/create_pipeline_service_spec.rb
new file mode 100644
index 00000000000..9479439bde8
--- /dev/null
+++ b/spec/services/merge_requests/create_pipeline_service_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MergeRequests::CreatePipelineService do
+ set(:project) { create(:project, :repository) }
+ set(:user) { create(:user) }
+ let(:service) { described_class.new(project, user, params) }
+ let(:params) { {} }
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
+ subject { service.execute(merge_request) }
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(config))
+ end
+
+ let(:config) do
+ { rspec: { script: 'echo', only: ['merge_requests'] } }
+ end
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_branch: 'feature',
+ source_project: project,
+ target_branch: 'master',
+ target_project: project)
+ end
+
+ it 'creates a detached merge request pipeline' do
+ expect { subject }.to change { Ci::Pipeline.count }.by(1)
+
+ expect(subject).to be_persisted
+ expect(subject).to be_detached_merge_request_pipeline
+ end
+
+ context 'when service is called multiple times' do
+ it 'creates a pipeline once' do
+ expect do
+ service.execute(merge_request)
+ service.execute(merge_request)
+ end.to change { Ci::Pipeline.count }.by(1)
+ end
+
+ context 'when allow_duplicate option is true' do
+ let(:params) { { allow_duplicate: true } }
+
+ it 'creates pipelines multiple times' do
+ expect do
+ service.execute(merge_request)
+ service.execute(merge_request)
+ end.to change { Ci::Pipeline.count }.by(2)
+ end
+ end
+ end
+
+ context 'when .gitlab-ci.yml does not have only: [merge_requests] keyword' do
+ let(:config) do
+ { rspec: { script: 'echo' } }
+ end
+
+ it 'does not create a pipeline' do
+ expect { subject }.not_to change { Ci::Pipeline.count }
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb
index 0ac23050caf..5d492e4b013 100644
--- a/spec/services/merge_requests/merge_to_ref_service_spec.rb
+++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb
@@ -32,10 +32,8 @@ describe MergeRequests::MergeToRefService do
expect(result[:status]).to eq(:success)
expect(result[:commit_id]).to be_present
- expect(result[:source_id]).to eq(merge_request.source_branch_sha)
- expect(result[:target_id]).to eq(merge_request.target_branch_sha)
expect(repository.ref_exists?(target_ref)).to be(true)
- expect(ref_head.id).to eq(result[:commit_id])
+ expect(ref_head.sha).to eq(result[:commit_id])
end
end
@@ -72,10 +70,6 @@ describe MergeRequests::MergeToRefService do
let(:merge_request) { create(:merge_request, :simple) }
let(:project) { merge_request.project }
- before do
- project.add_maintainer(user)
- end
-
describe '#execute' do
let(:service) do
described_class.new(project, user, commit_message: 'Awesome message',
@@ -92,6 +86,12 @@ describe MergeRequests::MergeToRefService do
it_behaves_like 'successfully evaluates pre-condition checks'
context 'commit history comparison with regular MergeService' do
+ before do
+ # The merge service needs an authorized user while merge-to-ref
+ # doesn't.
+ project.add_maintainer(user)
+ end
+
let(:merge_ref_service) do
described_class.new(project, user, {})
end
@@ -136,9 +136,9 @@ describe MergeRequests::MergeToRefService do
let(:merge_method) { :merge }
it 'returns error' do
- allow(merge_request).to receive(:mergeable_to_ref?) { false }
+ allow(project).to receive_message_chain(:repository, :merge_to_ref) { nil }
- error_message = "Merge request is not mergeable to #{merge_request.merge_ref_path}"
+ error_message = 'Conflicts detected during merge'
result = service.execute(merge_request)
@@ -170,28 +170,5 @@ describe MergeRequests::MergeToRefService do
it { expect(todo).not_to be_done }
end
-
- context 'when merge request is WIP state' do
- it 'fails to merge' do
- merge_request = create(:merge_request, title: 'WIP: The feature')
-
- result = service.execute(merge_request)
-
- expect(result[:status]).to eq(:error)
- expect(result[:message]).to eq("Merge request is not mergeable to #{merge_request.merge_ref_path}")
- end
- end
-
- it 'returns error when user has no authorization to admin the merge request' do
- unauthorized_user = create(:user)
- project.add_reporter(unauthorized_user)
-
- service = described_class.new(project, unauthorized_user)
-
- result = service.execute(merge_request)
-
- expect(result[:status]).to eq(:error)
- expect(result[:message]).to eq('You are not allowed to merge to this ref')
- end
end
end
diff --git a/spec/services/merge_requests/mergeability_check_service_spec.rb b/spec/services/merge_requests/mergeability_check_service_spec.rb
new file mode 100644
index 00000000000..aa0485467ed
--- /dev/null
+++ b/spec/services/merge_requests/mergeability_check_service_spec.rb
@@ -0,0 +1,187 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MergeRequests::MergeabilityCheckService do
+ shared_examples_for 'unmergeable merge request' do
+ it 'updates or keeps merge status as cannot_be_merged' do
+ subject
+
+ expect(merge_request.merge_status).to eq('cannot_be_merged')
+ end
+
+ it 'does not change the merge ref HEAD' do
+ expect { subject }.not_to change(merge_request, :merge_ref_head)
+ end
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_error
+ end
+ end
+
+ shared_examples_for 'mergeable merge request' do
+ it 'updates or keeps merge status as can_be_merged' do
+ subject
+
+ expect(merge_request.merge_status).to eq('can_be_merged')
+ end
+
+ it 'updates the merge ref' do
+ expect { subject }.to change(merge_request, :merge_ref_head).from(nil)
+ end
+
+ it 'returns ServiceResponse.success' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_success
+ end
+
+ it 'ServiceResponse has merge_ref_head payload' do
+ result = subject
+
+ expect(result.payload.keys).to contain_exactly(:merge_ref_head)
+ expect(result.payload[:merge_ref_head].keys)
+ .to contain_exactly(:commit_id, :target_id, :source_id)
+ end
+ end
+
+ describe '#execute' do
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, merge_status: :unchecked, source_project: project, target_project: project) }
+ let(:repo) { project.repository }
+
+ subject { described_class.new(merge_request).execute }
+
+ before do
+ project.add_developer(merge_request.author)
+ end
+
+ it_behaves_like 'mergeable merge request'
+
+ context 'when multiple calls to the service' do
+ it 'returns success' do
+ subject
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.success?).to be(true)
+ end
+
+ it 'second call does not change the merge-ref' do
+ expect { subject }.to change(merge_request, :merge_ref_head).from(nil)
+ expect { subject }.not_to change(merge_request, :merge_ref_head)
+ end
+ end
+
+ context 'when broken' do
+ before do
+ allow(merge_request).to receive(:broken?) { true }
+ allow(project.repository).to receive(:can_be_merged?) { false }
+ end
+
+ it_behaves_like 'unmergeable merge request'
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq('Merge request is not mergeable')
+ end
+ end
+
+ context 'when it has conflicts' do
+ before do
+ allow(merge_request).to receive(:broken?) { false }
+ allow(project.repository).to receive(:can_be_merged?) { false }
+ end
+
+ it_behaves_like 'unmergeable merge request'
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq('Merge request is not mergeable')
+ end
+ end
+
+ context 'when MR cannot be merged and has no merge ref' do
+ before do
+ merge_request.mark_as_unmergeable!
+ end
+
+ it_behaves_like 'unmergeable merge request'
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq('Merge request is not mergeable')
+ end
+ end
+
+ context 'when MR cannot be merged and has outdated merge ref' do
+ before do
+ MergeRequests::MergeToRefService.new(project, merge_request.author).execute(merge_request)
+ merge_request.mark_as_unmergeable!
+ end
+
+ it_behaves_like 'unmergeable merge request'
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq('Merge request is not mergeable')
+ end
+ end
+
+ context 'when merge request is not given' do
+ subject { described_class.new(nil).execute }
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.message).to eq('Invalid argument')
+ end
+ end
+
+ context 'when read only DB' do
+ it 'returns ServiceResponse.error' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.message).to eq('Unsupported operation')
+ end
+ end
+
+ context 'when MR is mergeable but merge-ref does not exists' do
+ before do
+ merge_request.mark_as_mergeable!
+ end
+
+ it 'keeps merge status as can_be_merged' do
+ expect { subject }.not_to change(merge_request, :merge_status).from('can_be_merged')
+ end
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq('Merge ref was not found')
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb
index f7a39bb42d5..54b9c6dae38 100644
--- a/spec/services/merge_requests/push_options_handler_service_spec.rb
+++ b/spec/services/merge_requests/push_options_handler_service_spec.rb
@@ -76,10 +76,11 @@ describe MergeRequests::PushOptionsHandlerService do
shared_examples_for 'a service that can set the merge request to merge when pipeline succeeds' do
subject(:last_mr) { MergeRequest.last }
- it 'sets merge_when_pipeline_succeeds' do
+ it 'sets auto_merge_enabled' do
service.execute
- expect(last_mr.merge_when_pipeline_succeeds).to eq(true)
+ expect(last_mr.auto_merge_enabled).to eq(true)
+ expect(last_mr.auto_merge_strategy).to eq(AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
end
it 'sets merge_user to the user' do
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 7258428589f..6ba67c7165c 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -23,7 +23,8 @@ describe MergeRequests::RefreshService do
source_branch: 'master',
target_branch: 'feature',
target_project: @project,
- merge_when_pipeline_succeeds: true,
+ auto_merge_enabled: true,
+ auto_merge_strategy: AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS,
merge_user: @user)
@another_merge_request = create(:merge_request,
@@ -31,7 +32,8 @@ describe MergeRequests::RefreshService do
source_branch: 'master',
target_branch: 'test',
target_project: @project,
- merge_when_pipeline_succeeds: true,
+ auto_merge_enabled: true,
+ auto_merge_strategy: AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS,
merge_user: @user)
@fork_merge_request = create(:merge_request,
@@ -83,7 +85,7 @@ describe MergeRequests::RefreshService do
expect(@merge_request.notes).not_to be_empty
expect(@merge_request).to be_open
- expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey
+ expect(@merge_request.auto_merge_enabled).to be_falsey
expect(@merge_request.diff_head_sha).to eq(@newrev)
expect(@fork_merge_request).to be_open
expect(@fork_merge_request.notes).to be_empty
@@ -292,7 +294,7 @@ describe MergeRequests::RefreshService do
expect(@merge_request.notes).not_to be_empty
expect(@merge_request).to be_open
- expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey
+ expect(@merge_request.auto_merge_enabled).to be_falsey
expect(@merge_request.diff_head_sha).to eq(@newrev)
expect(@fork_merge_request).to be_open
expect(@fork_merge_request.notes).to be_empty
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index ba4c9ce60f3..fbfcd95e204 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -217,8 +217,9 @@ describe MergeRequests::UpdateService, :mailer do
head_pipeline_of: merge_request
)
- expect(MergeRequests::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user)
+ expect(AutoMerge::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user, {})
.and_return(service_mock)
+ allow(service_mock).to receive(:available_for?) { true }
expect(service_mock).to receive(:execute).with(merge_request)
end
diff --git a/spec/services/pages_domains/create_acme_order_service_spec.rb b/spec/services/pages_domains/create_acme_order_service_spec.rb
new file mode 100644
index 00000000000..d59aa9b979e
--- /dev/null
+++ b/spec/services/pages_domains/create_acme_order_service_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PagesDomains::CreateAcmeOrderService do
+ include LetsEncryptHelpers
+
+ let(:pages_domain) { create(:pages_domain) }
+
+ let(:challenge) { ::Gitlab::LetsEncrypt::Challenge.new(acme_challenge_double) }
+
+ let(:order_double) do
+ Gitlab::LetsEncrypt::Order.new(acme_order_double).tap do |order|
+ allow(order).to receive(:new_challenge).and_return(challenge)
+ end
+ end
+
+ let(:lets_encrypt_client) do
+ instance_double('Gitlab::LetsEncrypt::Client').tap do |client|
+ allow(client).to receive(:new_order).with(pages_domain.domain)
+ .and_return(order_double)
+ end
+ end
+
+ let(:service) { described_class.new(pages_domain) }
+
+ before do
+ allow(::Gitlab::LetsEncrypt::Client).to receive(:new).and_return(lets_encrypt_client)
+ end
+
+ it 'saves order to database before requesting validation' do
+ allow(pages_domain.acme_orders).to receive(:create!).and_call_original
+ allow(challenge).to receive(:request_validation).and_call_original
+
+ service.execute
+
+ expect(pages_domain.acme_orders).to have_received(:create!).ordered
+ expect(challenge).to have_received(:request_validation).ordered
+ end
+
+ it 'generates and saves private key' do
+ service.execute
+
+ saved_order = PagesDomainAcmeOrder.last
+ expect { OpenSSL::PKey::RSA.new(saved_order.private_key) }.not_to raise_error
+ end
+
+ it 'properly saves order attributes' do
+ service.execute
+
+ saved_order = PagesDomainAcmeOrder.last
+ expect(saved_order.url).to eq(order_double.url)
+ expect(saved_order.expires_at).to be_like_time(order_double.expires)
+ end
+
+ it 'properly saves challenge attributes' do
+ service.execute
+
+ saved_order = PagesDomainAcmeOrder.last
+ expect(saved_order.challenge_token).to eq(challenge.token)
+ expect(saved_order.challenge_file_content).to eq(challenge.file_content)
+ end
+end
diff --git a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
new file mode 100644
index 00000000000..6d7be27939c
--- /dev/null
+++ b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
@@ -0,0 +1,146 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PagesDomains::ObtainLetsEncryptCertificateService do
+ include LetsEncryptHelpers
+
+ let(:pages_domain) { create(:pages_domain, :without_certificate, :without_key) }
+ let(:service) { described_class.new(pages_domain) }
+
+ before do
+ stub_lets_encrypt_settings
+ end
+
+ def expect_to_create_acme_challenge
+ expect(::PagesDomains::CreateAcmeOrderService).to receive(:new).with(pages_domain)
+ .and_wrap_original do |m, *args|
+ create_service = m.call(*args)
+
+ expect(create_service).to receive(:execute)
+
+ create_service
+ end
+ end
+
+ def stub_lets_encrypt_order(url, status)
+ order = ::Gitlab::LetsEncrypt::Order.new(acme_order_double(status: status))
+
+ allow_any_instance_of(::Gitlab::LetsEncrypt::Client).to(
+ receive(:load_order).with(url).and_return(order)
+ )
+
+ order
+ end
+
+ context 'when there is no acme order' do
+ it 'creates acme order' do
+ expect_to_create_acme_challenge
+
+ service.execute
+ end
+ end
+
+ context 'when there is expired acme order' do
+ let!(:existing_order) do
+ create(:pages_domain_acme_order, :expired, pages_domain: pages_domain)
+ end
+
+ it 'removes acme order and creates new one' do
+ expect_to_create_acme_challenge
+
+ service.execute
+
+ expect(PagesDomainAcmeOrder.find_by_id(existing_order.id)).to be_nil
+ end
+ end
+
+ %w(pending processing).each do |status|
+ context "there is an order in '#{status}' status" do
+ let(:existing_order) do
+ create(:pages_domain_acme_order, pages_domain: pages_domain)
+ end
+
+ before do
+ stub_lets_encrypt_order(existing_order.url, status)
+ end
+
+ it 'does not raise errors' do
+ expect do
+ service.execute
+ end.not_to raise_error
+ end
+ end
+ end
+
+ context 'when order is ready' do
+ let(:existing_order) do
+ create(:pages_domain_acme_order, pages_domain: pages_domain)
+ end
+
+ let!(:api_order) do
+ stub_lets_encrypt_order(existing_order.url, 'ready')
+ end
+
+ it 'request certificate' do
+ expect(api_order).to receive(:request_certificate).and_call_original
+
+ service.execute
+ end
+ end
+
+ context 'when order is valid' do
+ let(:existing_order) do
+ create(:pages_domain_acme_order, pages_domain: pages_domain)
+ end
+
+ let!(:api_order) do
+ stub_lets_encrypt_order(existing_order.url, 'valid')
+ end
+
+ let(:certificate) do
+ key = OpenSSL::PKey.read(existing_order.private_key)
+
+ subject = "/C=BE/O=Test/OU=Test/CN=#{pages_domain.domain}"
+
+ cert = OpenSSL::X509::Certificate.new
+ cert.subject = cert.issuer = OpenSSL::X509::Name.parse(subject)
+ cert.not_before = Time.now
+ cert.not_after = 1.year.from_now
+ cert.public_key = key.public_key
+ cert.serial = 0x0
+ cert.version = 2
+
+ ef = OpenSSL::X509::ExtensionFactory.new
+ ef.subject_certificate = cert
+ ef.issuer_certificate = cert
+ cert.extensions = [
+ ef.create_extension("basicConstraints", "CA:TRUE", true),
+ ef.create_extension("subjectKeyIdentifier", "hash")
+ ]
+ cert.add_extension ef.create_extension("authorityKeyIdentifier",
+ "keyid:always,issuer:always")
+
+ cert.sign key, OpenSSL::Digest::SHA1.new
+
+ cert.to_pem
+ end
+
+ before do
+ expect(api_order).to receive(:certificate) { certificate }
+ end
+
+ it 'saves private_key and certificate for domain' do
+ service.execute
+
+ expect(pages_domain.key).to be_present
+ expect(pages_domain.certificate).to eq(certificate)
+ end
+
+ it 'removes order from database' do
+ service.execute
+
+ expect(PagesDomainAcmeOrder.find_by_id(existing_order.id)).to be_nil
+ end
+ end
+end
diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb
index f7261cd7125..d25e9958831 100644
--- a/spec/services/preview_markdown_service_spec.rb
+++ b/spec/services/preview_markdown_service_spec.rb
@@ -56,7 +56,9 @@ describe PreviewMarkdownService do
expect(Gitlab::Diff::SuggestionsParser)
.to receive(:parse)
- .with(text, position: position, project: merge_request.project)
+ .with(text, position: position,
+ project: merge_request.project,
+ supports_suggestion: true)
.and_call_original
result = service.execute
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index ec3f1782e8f..3211a6e1310 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -145,6 +145,30 @@ describe Projects::ForkService do
end
end
+ context "CI/CD settings" do
+ let(:to_project) { fork_project(@from_project, @to_user) }
+
+ context "when origin has git depth specified" do
+ before do
+ @from_project.update(default_git_depth: 42)
+ end
+
+ it "inherits default_git_depth from the origin project" do
+ expect(to_project.default_git_depth).to eq(42)
+ end
+ end
+
+ context "when origin does not define git depth" do
+ before do
+ @from_project.update!(default_git_depth: nil)
+ end
+
+ it "the fork has git depth set to 0" do
+ expect(to_project.default_git_depth).to eq(0)
+ end
+ end
+ end
+
context "when project has restricted visibility level" do
context "and only one visibility level is restricted" do
before do
diff --git a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
index f4470b50753..75d534c59bf 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
@@ -2,6 +2,8 @@
require 'spec_helper'
describe Projects::LfsPointers::LfsDownloadService do
+ include StubRequests
+
let(:project) { create(:project) }
let(:lfs_content) { SecureRandom.random_bytes(10) }
let(:oid) { Digest::SHA256.hexdigest(lfs_content) }
@@ -62,7 +64,7 @@ describe Projects::LfsPointers::LfsDownloadService do
describe '#execute' do
context 'when file download succeeds' do
before do
- WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
+ stub_full_request(download_link).to_return(body: lfs_content)
end
it_behaves_like 'lfs object is created'
@@ -104,7 +106,7 @@ describe Projects::LfsPointers::LfsDownloadService do
let(:size) { 1 }
before do
- WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
+ stub_full_request(download_link).to_return(body: lfs_content)
end
it_behaves_like 'no lfs object is created'
@@ -118,7 +120,7 @@ describe Projects::LfsPointers::LfsDownloadService do
context 'when downloaded lfs file has a different oid' do
before do
- WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
+ stub_full_request(download_link).to_return(body: lfs_content)
allow_any_instance_of(Digest::SHA256).to receive(:hexdigest).and_return('foobar')
end
@@ -136,7 +138,7 @@ describe Projects::LfsPointers::LfsDownloadService do
let(:lfs_object) { LfsDownloadObject.new(oid: oid, size: size, link: download_link_with_credentials) }
before do
- WebMock.stub_request(:get, download_link).with(headers: { 'Authorization' => 'Basic dXNlcjpwYXNzd29yZA==' }).to_return(body: lfs_content)
+ stub_full_request(download_link).with(headers: { 'Authorization' => 'Basic dXNlcjpwYXNzd29yZA==' }).to_return(body: lfs_content)
end
it 'the request adds authorization headers' do
@@ -149,7 +151,7 @@ describe Projects::LfsPointers::LfsDownloadService do
let(:local_request_setting) { true }
before do
- WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
+ stub_full_request(download_link, ip_address: '192.168.2.120').to_return(body: lfs_content)
end
it_behaves_like 'lfs object is created'
@@ -173,7 +175,8 @@ describe Projects::LfsPointers::LfsDownloadService do
with_them do
before do
- WebMock.stub_request(:get, download_link).to_return(status: 301, headers: { 'Location' => redirect_link })
+ stub_full_request(download_link, ip_address: '192.168.2.120')
+ .to_return(status: 301, headers: { 'Location' => redirect_link })
end
it_behaves_like 'no lfs object is created'
@@ -184,8 +187,8 @@ describe Projects::LfsPointers::LfsDownloadService do
let(:redirect_link) { "http://example.com/"}
before do
- WebMock.stub_request(:get, download_link).to_return(status: 301, headers: { 'Location' => redirect_link })
- WebMock.stub_request(:get, redirect_link).to_return(body: lfs_content)
+ stub_full_request(download_link).to_return(status: 301, headers: { 'Location' => redirect_link })
+ stub_full_request(redirect_link).to_return(body: lfs_content)
end
it_behaves_like 'lfs object is created'
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index 7c91f0bbe6e..b597717c347 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -27,59 +27,6 @@ describe Projects::UpdatePagesService do
it { is_expected.not_to match(Gitlab::PathRegex.namespace_format_regex) }
end
- context 'legacy artifacts' do
- before do
- build.update(legacy_artifacts_file: file)
- build.update(legacy_artifacts_metadata: metadata)
- end
-
- describe 'pages artifacts' do
- it "doesn't delete artifacts after deploying" do
- expect(execute).to eq(:success)
-
- expect(build.reload.artifacts?).to eq(true)
- end
- end
-
- it 'succeeds' do
- expect(project.pages_deployed?).to be_falsey
- expect(execute).to eq(:success)
- expect(project.pages_deployed?).to be_truthy
-
- # Check that all expected files are extracted
- %w[index.html zero .hidden/file].each do |filename|
- expect(File.exist?(File.join(project.public_pages_path, filename))).to be_truthy
- end
- end
-
- it 'limits pages size' do
- stub_application_setting(max_pages_size: 1)
- expect(execute).not_to eq(:success)
- end
-
- it 'removes pages after destroy' do
- expect(PagesWorker).to receive(:perform_in)
- expect(project.pages_deployed?).to be_falsey
- expect(execute).to eq(:success)
- expect(project.pages_deployed?).to be_truthy
- project.destroy
- expect(project.pages_deployed?).to be_falsey
- end
-
- it 'fails if sha on branch is not latest' do
- build.update(ref: 'feature')
-
- expect(execute).not_to eq(:success)
- end
-
- it 'fails for empty file fails' do
- build.update(legacy_artifacts_file: empty_file)
-
- expect { execute }
- .to raise_error(Projects::UpdatePagesService::FailedToExtractError)
- end
- end
-
context 'for new artifacts' do
context "for a valid job" do
before do
@@ -207,7 +154,7 @@ describe Projects::UpdatePagesService do
end
it 'fails for invalid archive' do
- build.update(legacy_artifacts_file: invalid_file)
+ create(:ci_job_artifact, :archive, file: invalid_file, job: build)
expect(execute).not_to eq(:success)
end
@@ -218,8 +165,8 @@ describe Projects::UpdatePagesService do
file = fixture_file_upload('spec/fixtures/pages.zip')
metafile = fixture_file_upload('spec/fixtures/pages.zip.meta')
- build.update(legacy_artifacts_file: file)
- build.update(legacy_artifacts_metadata: metafile)
+ create(:ci_job_artifact, :archive, file: file, job: build)
+ create(:ci_job_artifact, :metadata, file: metafile, job: build)
allow(build).to receive(:artifacts_metadata_entry)
.and_return(metadata)
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 5ad30b58511..1dcfb739eb6 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -45,6 +45,7 @@ describe Projects::UpdateService do
it 'updates the project to private' do
expect(TodosDestroyer::ProjectPrivateWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id)
+ expect(TodosDestroyer::ConfidentialIssueWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, nil, project.id)
result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE)
diff --git a/spec/services/projects/update_statistics_service_spec.rb b/spec/services/projects/update_statistics_service_spec.rb
index 5000ea58e5f..8534853fbc7 100644
--- a/spec/services/projects/update_statistics_service_spec.rb
+++ b/spec/services/projects/update_statistics_service_spec.rb
@@ -17,19 +17,9 @@ describe Projects::UpdateStatisticsService do
end
end
- context 'with an existing project without a repository' do
+ context 'with an existing project' do
let(:project) { create(:project) }
- it 'does nothing' do
- expect_any_instance_of(ProjectStatistics).not_to receive(:refresh!)
-
- service.execute
- end
- end
-
- context 'with an existing project with a repository' do
- let(:project) { create(:project, :repository) }
-
it 'refreshes the project statistics' do
expect_any_instance_of(ProjectStatistics).to receive(:refresh!)
.with(only: statistics.map(&:to_sym))
diff --git a/spec/services/service_response_spec.rb b/spec/services/service_response_spec.rb
index 30bd4d6820b..e790d272e61 100644
--- a/spec/services/service_response_spec.rb
+++ b/spec/services/service_response_spec.rb
@@ -16,6 +16,13 @@ describe ServiceResponse do
expect(response).to be_success
expect(response.message).to eq('Good orange')
end
+
+ it 'creates a successful response with payload' do
+ response = described_class.success(payload: { good: 'orange' })
+
+ expect(response).to be_success
+ expect(response.payload).to eq(good: 'orange')
+ end
end
describe '.error' do
@@ -33,6 +40,15 @@ describe ServiceResponse do
expect(response.message).to eq('Bad apple')
expect(response.http_status).to eq(400)
end
+
+ it 'creates a failed response with payload' do
+ response = described_class.error(message: 'Bad apple',
+ payload: { bad: 'apple' })
+
+ expect(response).to be_error
+ expect(response.message).to eq('Bad apple')
+ expect(response.payload).to eq(bad: 'apple')
+ end
end
describe '#success?' do
diff --git a/spec/services/submit_usage_ping_service_spec.rb b/spec/services/submit_usage_ping_service_spec.rb
index 78df9bf96bf..653f17a4324 100644
--- a/spec/services/submit_usage_ping_service_spec.rb
+++ b/spec/services/submit_usage_ping_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe SubmitUsagePingService do
+ include StubRequests
+
context 'when usage ping is disabled' do
before do
stub_application_setting(usage_ping_enabled: false)
@@ -99,7 +101,7 @@ describe SubmitUsagePingService do
end
def stub_response(body)
- stub_request(:post, 'https://version.gitlab.com/usage_data')
+ stub_full_request('https://version.gitlab.com/usage_data', method: :post)
.to_return(
headers: { 'Content-Type' => 'application/json' },
body: body.to_json
diff --git a/spec/services/suggestions/create_service_spec.rb b/spec/services/suggestions/create_service_spec.rb
index ccd44e615a8..d95f9e3349b 100644
--- a/spec/services/suggestions/create_service_spec.rb
+++ b/spec/services/suggestions/create_service_spec.rb
@@ -96,7 +96,7 @@ describe Suggestions::CreateService do
it 'creates no suggestion when diff file is not found' do
expect_next_instance_of(DiffNote) do |diff_note|
- expect(diff_note).to receive(:latest_diff_file).twice { nil }
+ expect(diff_note).to receive(:latest_diff_file).once { nil }
end
expect { subject.execute }.not_to change(Suggestion, :count)
diff --git a/spec/services/todos/destroy/confidential_issue_service_spec.rb b/spec/services/todos/destroy/confidential_issue_service_spec.rb
index 78b6744b426..9f7e656f7d3 100644
--- a/spec/services/todos/destroy/confidential_issue_service_spec.rb
+++ b/spec/services/todos/destroy/confidential_issue_service_spec.rb
@@ -9,36 +9,60 @@ describe Todos::Destroy::ConfidentialIssueService do
let(:assignee) { create(:user) }
let(:guest) { create(:user) }
let(:project_member) { create(:user) }
- let(:issue) { create(:issue, project: project, author: author, assignees: [assignee]) }
-
- let!(:todo_issue_non_member) { create(:todo, user: user, target: issue, project: project) }
- let!(:todo_issue_member) { create(:todo, user: project_member, target: issue, project: project) }
- let!(:todo_issue_author) { create(:todo, user: author, target: issue, project: project) }
- let!(:todo_issue_asignee) { create(:todo, user: assignee, target: issue, project: project) }
- let!(:todo_issue_guest) { create(:todo, user: guest, target: issue, project: project) }
- let!(:todo_another_non_member) { create(:todo, user: user, project: project) }
+ let(:issue_1) { create(:issue, :confidential, project: project, author: author, assignees: [assignee]) }
describe '#execute' do
before do
project.add_developer(project_member)
project.add_guest(guest)
+
+ # todos not to be deleted
+ create(:todo, user: project_member, target: issue_1, project: project)
+ create(:todo, user: author, target: issue_1, project: project)
+ create(:todo, user: assignee, target: issue_1, project: project)
+ create(:todo, user: user, project: project)
+ # Todos to be deleted
+ create(:todo, user: guest, target: issue_1, project: project)
+ create(:todo, user: user, target: issue_1, project: project)
end
- subject { described_class.new(issue.id).execute }
+ subject { described_class.new(issue_id: issue_1.id).execute }
- context 'when provided issue is confidential' do
- before do
- issue.update!(confidential: true)
+ context 'when issue_id parameter is present' do
+ context 'when provided issue is confidential' do
+ it 'removes issue todos for users who can not access the confidential issue' do
+ expect { subject }.to change { Todo.count }.from(6).to(4)
+ end
end
- it 'removes issue todos for users who can not access the confidential issue' do
- expect { subject }.to change { Todo.count }.from(6).to(4)
+ context 'when provided issue is not confidential' do
+ it 'does not remove any todos' do
+ issue_1.update(confidential: false)
+
+ expect { subject }.not_to change { Todo.count }
+ end
end
end
- context 'when provided issue is not confidential' do
- it 'does not remove any todos' do
- expect { subject }.not_to change { Todo.count }
+ context 'when project_id parameter is present' do
+ subject { described_class.new(issue_id: nil, project_id: project.id).execute }
+
+ it 'removes issues todos for users that cannot access confidential issues' do
+ issue_2 = create(:issue, :confidential, project: project)
+ issue_3 = create(:issue, :confidential, project: project, author: author, assignees: [assignee])
+ issue_4 = create(:issue, project: project)
+ # Todos not to be deleted
+ create(:todo, user: guest, target: issue_1, project: project)
+ create(:todo, user: assignee, target: issue_1, project: project)
+ create(:todo, user: project_member, target: issue_2, project: project)
+ create(:todo, user: author, target: issue_3, project: project)
+ create(:todo, user: user, target: issue_4, project: project)
+ create(:todo, user: user, project: project)
+ # Todos to be deleted
+ create(:todo, user: user, target: issue_1, project: project)
+ create(:todo, user: guest, target: issue_2, project: project)
+
+ expect { subject }.to change { Todo.count }.from(14).to(10)
end
end
end
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 75ba2479b63..37bafc0c002 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe WebHookService do
+ include StubRequests
+
let(:project) { create(:project) }
let(:project_hook) { create(:project_hook) }
let(:headers) do
@@ -67,11 +69,11 @@ describe WebHookService do
let(:project_hook) { create(:project_hook, url: 'https://demo:demo@example.org/') }
it 'uses the credentials' do
- WebMock.stub_request(:post, url)
+ stub_full_request(url, method: :post)
service_instance.execute
- expect(WebMock).to have_requested(:post, url).with(
+ expect(WebMock).to have_requested(:post, stubbed_hostname(url)).with(
headers: headers.merge('Authorization' => 'Basic ZGVtbzpkZW1v')
).once
end
@@ -82,11 +84,11 @@ describe WebHookService do
let(:project_hook) { create(:project_hook, url: 'https://demo@example.org/') }
it 'uses the credentials anyways' do
- WebMock.stub_request(:post, url)
+ stub_full_request(url, method: :post)
service_instance.execute
- expect(WebMock).to have_requested(:post, url).with(
+ expect(WebMock).to have_requested(:post, stubbed_hostname(url)).with(
headers: headers.merge('Authorization' => 'Basic ZGVtbzo=')
).once
end
diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb
index 89dfbf931d2..5d5a0a7b5d2 100644
--- a/spec/support/features/reportable_note_shared_examples.rb
+++ b/spec/support/features/reportable_note_shared_examples.rb
@@ -20,7 +20,7 @@ shared_examples 'reportable note' do |type|
dropdown = comment.find(more_actions_selector)
open_dropdown(dropdown)
- expect(dropdown).to have_link('Report abuse to GitLab', href: abuse_report_path)
+ expect(dropdown).to have_link('Report abuse to admin', href: abuse_report_path)
if type == 'issue' || type == 'merge_request'
expect(dropdown).to have_button('Delete comment')
@@ -33,7 +33,7 @@ shared_examples 'reportable note' do |type|
dropdown = comment.find(more_actions_selector)
open_dropdown(dropdown)
- dropdown.click_link('Report abuse to GitLab')
+ dropdown.click_link('Report abuse to admin')
expect(find('#user_name')['value']).to match(note.author.username)
expect(find('#abuse_report_message')['value']).to match(noteable_note_url(note))
diff --git a/spec/support/features/variable_list_shared_examples.rb b/spec/support/features/variable_list_shared_examples.rb
index 92a19dd22a2..01531864c1f 100644
--- a/spec/support/features/variable_list_shared_examples.rb
+++ b/spec/support/features/variable_list_shared_examples.rb
@@ -45,12 +45,12 @@ shared_examples 'variable list' do
end
end
- it 'defaults to masked' do
+ it 'defaults to unmasked' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('key')
find('.js-ci-variable-input-value').set('key_value')
- expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
end
click_button('Save variables')
@@ -62,7 +62,7 @@ shared_examples 'variable list' do
page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
expect(find('.js-ci-variable-input-key').value).to eq('key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value')
- expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
end
end
@@ -234,12 +234,14 @@ shared_examples 'variable list' do
end
it 'edits variable to be unmasked' do
- page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
- expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
+ page.within('.js-ci-variable-list-section .js-row:last-child') do
+ find('.js-ci-variable-input-key').set('unmasked_key')
+ find('.js-ci-variable-input-value').set('unmasked_value')
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
find('.ci-variable-masked-item .js-project-feature-toggle').click
- expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
end
click_button('Save variables')
@@ -248,12 +250,6 @@ shared_examples 'variable list' do
visit page_path
page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
- expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
- end
- end
-
- it 'edits variable to be masked' do
- page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
find('.ci-variable-masked-item .js-project-feature-toggle').click
@@ -268,6 +264,14 @@ shared_examples 'variable list' do
page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
+ end
+ end
+
+ it 'edits variable to be masked' do
+ page.within('.js-ci-variable-list-section .js-row:last-child') do
+ find('.js-ci-variable-input-key').set('masked_key')
+ find('.js-ci-variable-input-value').set('masked_value')
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
find('.ci-variable-masked-item .js-project-feature-toggle').click
@@ -348,10 +352,11 @@ shared_examples 'variable list' do
end
end
- it 'shows validation error box about empty values' do
+ it 'shows validation error box about masking empty values' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('empty_value')
find('.js-ci-variable-input-value').set('')
+ find('.ci-variable-masked-item .js-project-feature-toggle').click
end
click_button('Save variables')
@@ -367,6 +372,7 @@ shared_examples 'variable list' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('unmaskable_value')
find('.js-ci-variable-input-value').set('???')
+ find('.ci-variable-masked-item .js-project-feature-toggle').click
end
click_button('Save variables')
diff --git a/spec/support/helpers/git_helpers.rb b/spec/support/helpers/git_helpers.rb
index 99a7c39852e..99c5871ba54 100644
--- a/spec/support/helpers/git_helpers.rb
+++ b/spec/support/helpers/git_helpers.rb
@@ -6,12 +6,4 @@ module GitHelpers
Rugged::Repository.new(path)
end
-
- def project_hook_exists?(project)
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- project_path = project.repository.raw_repository.path
-
- File.exist?(File.join(project_path, 'hooks', 'post-receive'))
- end
- end
end
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index 78b7ae9c00c..011c4df0fe5 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -17,17 +17,38 @@ module KubernetesHelpers
kube_response(kube_deployments_body)
end
- def stub_kubeclient_discover(api_url)
+ def stub_kubeclient_discover_base(api_url)
WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
- WebMock.stub_request(:get, api_url + '/apis/extensions/v1beta1').to_return(kube_response(kube_v1beta1_discovery_body))
- WebMock.stub_request(:get, api_url + '/apis/rbac.authorization.k8s.io/v1').to_return(kube_response(kube_v1_rbac_authorization_discovery_body))
- WebMock.stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1').to_return(kube_response(kube_v1alpha1_serving_knative_discovery_body))
+ WebMock
+ .stub_request(:get, api_url + '/apis/extensions/v1beta1')
+ .to_return(kube_response(kube_v1beta1_discovery_body))
+ WebMock
+ .stub_request(:get, api_url + '/apis/rbac.authorization.k8s.io/v1')
+ .to_return(kube_response(kube_v1_rbac_authorization_discovery_body))
+ end
+
+ def stub_kubeclient_discover(api_url)
+ stub_kubeclient_discover_base(api_url)
+
+ WebMock
+ .stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1')
+ .to_return(kube_response(kube_v1alpha1_serving_knative_discovery_body))
+ end
+
+ def stub_kubeclient_discover_knative_not_found(api_url)
+ stub_kubeclient_discover_base(api_url)
+
+ WebMock
+ .stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1')
+ .to_return(status: [404, "Resource Not Found"])
end
- def stub_kubeclient_service_pods(status: nil)
+ def stub_kubeclient_service_pods(response = nil, options = {})
stub_kubeclient_discover(service.api_url)
- pods_url = service.api_url + "/api/v1/pods"
- response = { status: status } if status
+
+ namespace_path = options[:namespace].present? ? "namespaces/#{options[:namespace]}/" : ""
+
+ pods_url = service.api_url + "/api/v1/#{namespace_path}pods"
WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
end
@@ -56,15 +77,18 @@ module KubernetesHelpers
WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response)
end
- def stub_kubeclient_knative_services(**options)
+ def stub_kubeclient_knative_services(options = {})
+ namespace_path = options[:namespace].present? ? "namespaces/#{options[:namespace]}/" : ""
+
options[:name] ||= "kubetest"
- options[:namespace] ||= "default"
options[:domain] ||= "example.com"
+ options[:response] ||= kube_response(kube_knative_services_body(options))
stub_kubeclient_discover(service.api_url)
- knative_url = service.api_url + "/apis/serving.knative.dev/v1alpha1/services"
- WebMock.stub_request(:get, knative_url).to_return(kube_response(kube_knative_services_body(options)))
+ knative_url = service.api_url + "/apis/serving.knative.dev/v1alpha1/#{namespace_path}services"
+
+ WebMock.stub_request(:get, knative_url).to_return(options[:response])
end
def stub_kubeclient_get_secret(api_url, **options)
diff --git a/spec/support/helpers/lets_encrypt_helpers.rb b/spec/support/helpers/lets_encrypt_helpers.rb
index 7f0886b451c..2857416ad95 100644
--- a/spec/support/helpers/lets_encrypt_helpers.rb
+++ b/spec/support/helpers/lets_encrypt_helpers.rb
@@ -1,6 +1,26 @@
# frozen_string_literal: true
module LetsEncryptHelpers
+ ACME_ORDER_METHODS = {
+ url: 'https://example.com/',
+ status: 'valid',
+ expires: 2.days.from_now
+ }.freeze
+
+ ACME_CHALLENGE_METHODS = {
+ status: 'pending',
+ token: 'tokenvalue',
+ file_content: 'hereisfilecontent',
+ request_validation: true
+ }.freeze
+
+ def stub_lets_encrypt_settings
+ stub_application_setting(
+ lets_encrypt_notification_email: 'myemail@test.example.com',
+ lets_encrypt_terms_of_service_accepted: true
+ )
+ end
+
def stub_lets_encrypt_client
client = instance_double('Acme::Client')
@@ -16,4 +36,24 @@ module LetsEncryptHelpers
client
end
+
+ def acme_challenge_double
+ challenge = instance_double('Acme::Client::Resources::Challenges::HTTP01')
+ allow(challenge).to receive_messages(ACME_CHALLENGE_METHODS)
+ challenge
+ end
+
+ def acme_authorization_double
+ authorization = instance_double('Acme::Client::Resources::Authorization')
+ allow(authorization).to receive(:http).and_return(acme_challenge_double)
+ authorization
+ end
+
+ def acme_order_double(attributes = {})
+ acme_order = instance_double('Acme::Client::Resources::Order')
+ allow(acme_order).to receive_messages(ACME_ORDER_METHODS.merge(attributes))
+ allow(acme_order).to receive(:authorizations).and_return([acme_authorization_double])
+ allow(acme_order).to receive(:finalize)
+ acme_order
+ end
end
diff --git a/spec/support/helpers/stub_requests.rb b/spec/support/helpers/stub_requests.rb
new file mode 100644
index 00000000000..5cad35282c0
--- /dev/null
+++ b/spec/support/helpers/stub_requests.rb
@@ -0,0 +1,40 @@
+module StubRequests
+ IP_ADDRESS_STUB = '8.8.8.9'.freeze
+
+ # Fully stubs a request using WebMock class. This class also
+ # stubs the IP address the URL is translated to (DNS lookup).
+ #
+ # It expects the final request to go to the `ip_address` instead the given url.
+ # That's primarily a DNS rebind attack prevention of Gitlab::HTTP
+ # (see: Gitlab::UrlBlocker).
+ #
+ def stub_full_request(url, ip_address: IP_ADDRESS_STUB, port: 80, method: :get)
+ stub_dns(url, ip_address: ip_address, port: port)
+
+ url = stubbed_hostname(url, hostname: ip_address)
+ WebMock.stub_request(method, url)
+ end
+
+ def stub_dns(url, ip_address:, port: 80)
+ url = parse_url(url)
+ socket = Socket.sockaddr_in(port, ip_address)
+ addr = Addrinfo.new(socket)
+
+ # See Gitlab::UrlBlocker
+ allow(Addrinfo).to receive(:getaddrinfo)
+ .with(url.hostname, url.port, nil, :STREAM)
+ .and_return([addr])
+ end
+
+ def stubbed_hostname(url, hostname: IP_ADDRESS_STUB)
+ url = parse_url(url)
+ url.hostname = hostname
+ url.to_s
+ end
+
+ private
+
+ def parse_url(url)
+ url.is_a?(URI) ? url : URI(url)
+ end
+end
diff --git a/spec/support/matchers/eq_pem.rb b/spec/support/matchers/eq_pem.rb
new file mode 100644
index 00000000000..158281e4a19
--- /dev/null
+++ b/spec/support/matchers/eq_pem.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :eq_pem do |expected_pem_string|
+ match do |actual|
+ actual.to_pem == expected_pem_string
+ end
+
+ description do
+ "contain pem #{expected_pem_string}"
+ end
+end
diff --git a/spec/support/shared_context/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index 54d9f5b15f2..54d9f5b15f2 100644
--- a/spec/support/shared_context/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
diff --git a/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb b/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb
index 982e0317f7f..b7080c68270 100644
--- a/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb
+++ b/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb
@@ -9,87 +9,87 @@
# - `filepath`: path of the file (contains filename)
# - `subject`: the request to be made to the controller. Example:
# subject { get :show, namespace_id: project.namespace, project_id: project }
-shared_examples 'repository lfs file load' do
- context 'when file is stored in lfs' do
- let(:lfs_oid) { '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' }
- let(:lfs_size) { '1575078' }
- let!(:lfs_object) { create(:lfs_object, oid: lfs_oid, size: lfs_size) }
+shared_examples 'a controller that can serve LFS files' do
+ let(:lfs_oid) { '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' }
+ let(:lfs_size) { '1575078' }
+ let!(:lfs_object) { create(:lfs_object, oid: lfs_oid, size: lfs_size) }
+
+ context 'when lfs is enabled' do
+ before do
+ allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true)
+ end
- context 'when lfs is enabled' do
+ context 'when project has access' do
before do
- allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true)
+ project.lfs_objects << lfs_object
+ allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true)
+ allow(controller).to receive(:send_file) { controller.head :ok }
end
- context 'when project has access' do
- before do
- project.lfs_objects << lfs_object
- allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true)
- allow(controller).to receive(:send_file) { controller.head :ok }
- end
+ it 'serves the file' do
+ lfs_uploader = LfsObjectUploader.new(lfs_object)
- it 'serves the file' do
- # Notice the filename= is omitted from the disposition; this is because
- # Rails 5 will append this header in send_file
- expect(controller).to receive(:send_file)
- .with(
- "#{LfsObjectUploader.root}/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897",
- filename: filename,
- disposition: %Q(attachment; filename*=UTF-8''#{filename}))
+ # Notice the filename= is omitted from the disposition; this is because
+ # Rails 5 will append this header in send_file
+ expect(controller).to receive(:send_file)
+ .with(
+ File.join(lfs_uploader.root, lfs_uploader.store_dir, lfs_uploader.filename),
+ filename: filename,
+ disposition: %Q(attachment; filename*=UTF-8''#{filename}))
- subject
+ subject
- expect(response).to have_gitlab_http_status(200)
- end
+ expect(response).to have_gitlab_http_status(200)
+ end
- context 'and lfs uses object storage' do
- let(:lfs_object) { create(:lfs_object, :with_file, oid: lfs_oid, size: lfs_size) }
+ context 'and lfs uses object storage' do
+ let(:lfs_object) { create(:lfs_object, :with_file, oid: lfs_oid, size: lfs_size) }
- before do
- stub_lfs_object_storage
- lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
- end
+ before do
+ stub_lfs_object_storage
+ lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+ end
- it 'responds with redirect to file' do
- subject
+ it 'responds with redirect to file' do
+ subject
- expect(response).to have_gitlab_http_status(302)
- expect(response.location).to include(lfs_object.reload.file.path)
- end
+ expect(response).to have_gitlab_http_status(302)
+ expect(response.location).to include(lfs_object.reload.file.path)
+ end
- it 'sets content disposition' do
- subject
+ it 'sets content disposition' do
+ subject
- file_uri = URI.parse(response.location)
- params = CGI.parse(file_uri.query)
+ file_uri = URI.parse(response.location)
+ params = CGI.parse(file_uri.query)
- expect(params["response-content-disposition"].first).to eq(%q(attachment; filename="lfs_object.iso"; filename*=UTF-8''lfs_object.iso))
- end
+ expect(params["response-content-disposition"].first).to eq(%Q(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
end
end
+ end
- context 'when project does not have access' do
- it 'does not serve the file' do
- subject
+ context 'when project does not have access' do
+ it 'does not serve the file' do
+ subject
- expect(response).to have_gitlab_http_status(404)
- end
+ expect(response).to have_gitlab_http_status(404)
end
end
+ end
- context 'when lfs is not enabled' do
- before do
- allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(false)
- end
+ context 'when lfs is not enabled' do
+ before do
+ allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(false)
+ end
- it 'delivers ASCII file' do
- subject
+ it 'delivers ASCII file' do
+ subject
- expect(response).to have_gitlab_http_status(200)
- expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(response.header['Content-Disposition'])
- .to eq('inline')
- expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
- end
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(response.header['Content-Disposition'])
+ .to eq('inline')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
end
end
end
diff --git a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
index 7a04e940ee5..1b09c3dd636 100644
--- a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
+++ b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
@@ -3,16 +3,16 @@
require 'spec_helper'
shared_examples_for 'UpdateProjectStatistics' do
- let(:project) { subject.project }
- let(:stat) { described_class.statistic_name }
- let(:attribute) { described_class.statistic_attribute }
+ let(:project) { subject.project }
+ let(:project_statistics_name) { described_class.project_statistics_name }
+ let(:statistic_attribute) { described_class.statistic_attribute }
def reload_stat
- project.statistics.reload.send(stat).to_i
+ project.statistics.reload.send(project_statistics_name).to_i
end
def read_attribute
- subject.read_attribute(attribute).to_i
+ subject.read_attribute(statistic_attribute).to_i
end
it { is_expected.to be_new_record }
@@ -39,7 +39,8 @@ shared_examples_for 'UpdateProjectStatistics' do
.to receive(:increment_statistic)
.and_call_original
- subject.write_attribute(attribute, read_attribute + delta)
+ subject.write_attribute(statistic_attribute, read_attribute + delta)
+
expect { subject.save! }
.to change { reload_stat }
.by(delta)
diff --git a/spec/support/shared_examples/notify_shared_examples.rb b/spec/support/shared_examples/notify_shared_examples.rb
index 4fff1c4e228..897c9106d77 100644
--- a/spec/support/shared_examples/notify_shared_examples.rb
+++ b/spec/support/shared_examples/notify_shared_examples.rb
@@ -1,5 +1,7 @@
shared_context 'gitlab email notification' do
- set(:project) { create(:project, :repository, name: 'a-known-name') }
+ set(:group) { create(:group) }
+ set(:subgroup) { create(:group, parent: group) }
+ set(:project) { create(:project, :repository, name: 'a-known-name', group: group) }
set(:recipient) { create(:user, email: 'recipient@example.com') }
let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name }
@@ -39,6 +41,47 @@ shared_examples 'an email sent from GitLab' do
end
end
+shared_examples 'an email sent to a user' do
+ let(:group_notification_email) { 'user+group@example.com' }
+
+ it 'is sent to user\'s global notification email address' do
+ expect(subject).to deliver_to(test_recipient.notification_email)
+ end
+
+ context 'that is part of a project\'s group' do
+ it 'is sent to user\'s group notification email address when set' do
+ create(:notification_setting, user: test_recipient, source: project.group, notification_email: group_notification_email)
+ expect(subject).to deliver_to(group_notification_email)
+ end
+
+ it 'is sent to user\'s global notification email address when no group email set' do
+ create(:notification_setting, user: test_recipient, source: project.group, notification_email: '')
+ expect(subject).to deliver_to(test_recipient.notification_email)
+ end
+ end
+
+ context 'when project is in a sub-group', :nested_groups do
+ before do
+ project.update!(group: subgroup)
+ end
+
+ it 'is sent to user\'s subgroup notification email address when set' do
+ # Set top-level group notification email address to make sure it doesn't get selected
+ create(:notification_setting, user: test_recipient, source: group, notification_email: group_notification_email)
+
+ subgroup_notification_email = 'user+subgroup@example.com'
+ create(:notification_setting, user: test_recipient, source: subgroup, notification_email: subgroup_notification_email)
+
+ expect(subject).to deliver_to(subgroup_notification_email)
+ end
+
+ it 'is sent to user\'s group notification email address when set and subgroup email address not set' do
+ create(:notification_setting, user: test_recipient, source: subgroup, notification_email: '')
+ expect(subject).to deliver_to(test_recipient.notification_email)
+ end
+ end
+end
+
shared_examples 'an email that contains a header with author username' do
it 'has X-GitLab-Author header containing author\'s username' do
is_expected.to have_header 'X-GitLab-Author', user.username
diff --git a/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb b/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
index 8544fb62b5a..be69c10d7c8 100644
--- a/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
+++ b/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
@@ -13,61 +13,6 @@ describe 'gitlab:artifacts namespace rake task' do
subject { run_rake_task('gitlab:artifacts:migrate') }
- context 'legacy artifacts' do
- describe 'migrate' do
- let!(:build) { create(:ci_build, :legacy_artifacts, artifacts_file_store: store, artifacts_metadata_store: store) }
-
- context 'when local storage is used' do
- let(:store) { ObjectStorage::Store::LOCAL }
-
- context 'and job does not have file store defined' do
- let(:object_storage_enabled) { true }
- let(:store) { nil }
-
- it "migrates file to remote storage" do
- subject
-
- expect(build.reload.artifacts_file_store).to eq(ObjectStorage::Store::REMOTE)
- expect(build.reload.artifacts_metadata_store).to eq(ObjectStorage::Store::REMOTE)
- end
- end
-
- context 'and remote storage is defined' do
- let(:object_storage_enabled) { true }
-
- it "migrates file to remote storage" do
- subject
-
- expect(build.reload.artifacts_file_store).to eq(ObjectStorage::Store::REMOTE)
- expect(build.reload.artifacts_metadata_store).to eq(ObjectStorage::Store::REMOTE)
- end
- end
-
- context 'and remote storage is not defined' do
- it "fails to migrate to remote storage" do
- subject
-
- expect(build.reload.artifacts_file_store).to eq(ObjectStorage::Store::LOCAL)
- expect(build.reload.artifacts_metadata_store).to eq(ObjectStorage::Store::LOCAL)
- end
- end
- end
-
- context 'when remote storage is used' do
- let(:object_storage_enabled) { true }
-
- let(:store) { ObjectStorage::Store::REMOTE }
-
- it "file stays on remote storage" do
- subject
-
- expect(build.reload.artifacts_file_store).to eq(ObjectStorage::Store::REMOTE)
- expect(build.reload.artifacts_metadata_store).to eq(ObjectStorage::Store::REMOTE)
- end
- end
- end
- end
-
context 'job artifacts' do
let!(:artifact) { create(:ci_job_artifact, :archive, file_store: store) }
diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb
index a9d14070177..c3e912b02c5 100644
--- a/spec/tasks/gitlab/shell_rake_spec.rb
+++ b/spec/tasks/gitlab/shell_rake_spec.rb
@@ -7,14 +7,8 @@ describe 'gitlab:shell rake tasks' do
stub_warn_user_is_not_gitlab
end
- after do
- TestEnv.sabotage_gitlab_shell_hooks
- end
-
describe 'install task' do
- it 'invokes create_hooks task' do
- expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke)
-
+ it 'installs and compiles gitlab-shell' do
storages = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
Gitlab.config.repositories.storages.values.map(&:legacy_disk_path)
end
@@ -24,14 +18,4 @@ describe 'gitlab:shell rake tasks' do
run_rake_task('gitlab:shell:install')
end
end
-
- describe 'create_hooks task' do
- it 'calls gitlab-shell bin/create_hooks' do
- expect_any_instance_of(Object).to receive(:system)
- .with("#{Gitlab.config.gitlab_shell.path}/bin/create-hooks",
- *Gitlab::TaskHelpers.repository_storage_paths_args)
-
- run_rake_task('gitlab:shell:create_hooks')
- end
- end
end
diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb
index 555a58e9aa1..4188e7caccb 100644
--- a/spec/tasks/tokens_spec.rb
+++ b/spec/tasks/tokens_spec.rb
@@ -8,13 +8,13 @@ describe 'tokens rake tasks' do
end
describe 'reset_all_email task' do
- it 'invokes create_hooks task' do
+ it 'changes the incoming email token' do
expect { run_rake_task('tokens:reset_all_email') }.to change { user.reload.incoming_email_token }
end
end
describe 'reset_all_feed task' do
- it 'invokes create_hooks task' do
+ it 'changes the feed token for the user' do
expect { run_rake_task('tokens:reset_all_feed') }.to change { user.reload.feed_token }
end
end
diff --git a/spec/uploaders/legacy_artifact_uploader_spec.rb b/spec/uploaders/legacy_artifact_uploader_spec.rb
deleted file mode 100644
index 0589563b502..00000000000
--- a/spec/uploaders/legacy_artifact_uploader_spec.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-require 'rails_helper'
-
-describe LegacyArtifactUploader do
- let(:store) { described_class::Store::LOCAL }
- let(:job) { create(:ci_build, artifacts_file_store: store) }
- let(:uploader) { described_class.new(job, :legacy_artifacts_file) }
- let(:local_path) { described_class.root }
-
- subject { uploader }
-
- # TODO: move to Workhorse::UploadPath
- describe '.workhorse_upload_path' do
- subject { described_class.workhorse_upload_path }
-
- it { is_expected.to start_with(local_path) }
- it { is_expected.to end_with('tmp/uploads') }
- end
-
- it_behaves_like "builds correct paths",
- store_dir: %r[\d{4}_\d{1,2}/\d+/\d+\z],
- cache_dir: %r[artifacts/tmp/cache],
- work_dir: %r[artifacts/tmp/work]
-
- context 'object store is remote' do
- before do
- stub_artifacts_object_storage
- end
-
- include_context 'with storage', described_class::Store::REMOTE
-
- it_behaves_like "builds correct paths",
- store_dir: %r[\d{4}_\d{1,2}/\d+/\d+\z]
- end
-
- describe '#filename' do
- # we need to use uploader, as this makes to use mounter
- # which initialises uploader.file object
- let(:uploader) { job.artifacts_file }
-
- subject { uploader.filename }
-
- it { is_expected.to be_nil }
- end
-
- context 'file is stored in valid path' do
- let(:file) do
- fixture_file_upload('spec/fixtures/ci_build_artifacts.zip', 'application/zip')
- end
-
- before do
- uploader.store!(file)
- end
-
- subject { uploader.file.path }
-
- it { is_expected.to start_with("#{uploader.root}") }
- it { is_expected.to include("/#{job.created_at.utc.strftime('%Y_%m')}/") }
- it { is_expected.to include("/#{job.project_id}/") }
- it { is_expected.to end_with("ci_build_artifacts.zip") }
- end
-end
diff --git a/spec/uploaders/workers/object_storage/background_move_worker_spec.rb b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
index 95813d15e52..cc8970d2ba0 100644
--- a/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
+++ b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
@@ -48,40 +48,6 @@ describe ObjectStorage::BackgroundMoveWorker do
end
end
- context 'for legacy artifacts' do
- let(:build) { create(:ci_build, :legacy_artifacts) }
- let(:uploader_class) { LegacyArtifactUploader }
- let(:subject_class) { Ci::Build }
- let(:file_field) { :artifacts_file }
- let(:subject_id) { build.id }
-
- context 'when local storage is used' do
- let(:store) { local }
-
- context 'and remote storage is defined' do
- before do
- stub_artifacts_object_storage(background_upload: true)
- end
-
- it "migrates file to remote storage" do
- perform
-
- expect(build.reload.artifacts_file_store).to eq(remote)
- end
-
- context 'for artifacts_metadata' do
- let(:file_field) { :artifacts_metadata }
-
- it 'migrates metadata to remote storage' do
- perform
-
- expect(build.reload.artifacts_metadata_store).to eq(remote)
- end
- end
- end
- end
- end
-
context 'for job artifacts' do
let(:artifact) { create(:ci_job_artifact, :archive) }
let(:uploader_class) { JobArtifactUploader }
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 1086546c10d..457dd2e940f 100644
--- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -27,7 +27,7 @@ describe 'projects/commit/_commit_box.html.haml' do
render
- expect(rendered).to have_text("Pipeline ##{third_pipeline.id} failed")
+ expect(rendered).to have_text("Pipeline ##{third_pipeline.id} (##{third_pipeline.iid}) failed")
end
end
@@ -40,7 +40,7 @@ describe 'projects/commit/_commit_box.html.haml' do
it 'shows correct pipeline description' do
render
- expect(rendered).to have_text "Pipeline ##{pipeline.id} " \
+ expect(rendered).to have_text "Pipeline ##{pipeline.id} (##{pipeline.iid}) " \
'waiting for manual action'
end
end
diff --git a/spec/views/projects/jobs/_build.html.haml_spec.rb b/spec/views/projects/jobs/_build.html.haml_spec.rb
index 1d58891036e..97b25a6976f 100644
--- a/spec/views/projects/jobs/_build.html.haml_spec.rb
+++ b/spec/views/projects/jobs/_build.html.haml_spec.rb
@@ -4,7 +4,7 @@ describe 'projects/ci/jobs/_build' do
include Devise::Test::ControllerHelpers
let(:project) { create(:project, :repository) }
- let(:pipeline) { create(:ci_empty_pipeline, id: 1337, project: project, sha: project.commit.id) }
+ let(:pipeline) { create(:ci_empty_pipeline, id: 1337, iid: 57, project: project, sha: project.commit.id) }
let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', stage_idx: 1, name: 'rspec 0:2', status: :pending) }
before do
@@ -15,14 +15,14 @@ describe 'projects/ci/jobs/_build' do
it 'won\'t include a column with a link to its pipeline by default' do
render partial: 'projects/ci/builds/build', locals: { build: build }
- expect(rendered).not_to have_link('#1337')
- expect(rendered).not_to have_text('#1337 by API')
+ expect(rendered).not_to have_link('#1337 (#57)')
+ expect(rendered).not_to have_text('#1337 (#57) by API')
end
it 'can include a column with a link to its pipeline' do
render partial: 'projects/ci/builds/build', locals: { build: build, pipeline_link: true }
- expect(rendered).to have_link('#1337')
- expect(rendered).to have_text('#1337 by API')
+ expect(rendered).to have_link('#1337 (#57)')
+ expect(rendered).to have_text('#1337 (#57) by API')
end
end
diff --git a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
index 8a9ab02eaca..ae47f364296 100644
--- a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
+++ b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
@@ -12,10 +12,10 @@ describe 'projects/notes/_more_actions_dropdown' do
assign(:project, project)
end
- it 'shows Report abuse to GitLab button if not editable and not current users comment' do
+ it 'shows Report abuse to admin button if not editable and not current users comment' do
render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: false, note: note
- expect(rendered).to have_link('Report abuse to GitLab')
+ expect(rendered).to have_link('Report abuse to admin')
end
it 'does not show the More actions button if not editable and current users comment' do
@@ -24,10 +24,10 @@ describe 'projects/notes/_more_actions_dropdown' do
expect(rendered).not_to have_selector('.dropdown.more-actions')
end
- it 'shows Report abuse to GitLab and Delete buttons if editable and not current users comment' do
+ it 'shows Report abuse to admin and Delete buttons if editable and not current users comment' do
render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: true, note: note
- expect(rendered).to have_link('Report abuse to GitLab')
+ expect(rendered).to have_link('Report abuse to admin')
expect(rendered).to have_link('Delete comment')
end
diff --git a/spec/workers/auto_merge_process_worker_spec.rb b/spec/workers/auto_merge_process_worker_spec.rb
new file mode 100644
index 00000000000..616727ce5ca
--- /dev/null
+++ b/spec/workers/auto_merge_process_worker_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AutoMergeProcessWorker do
+ describe '#perform' do
+ subject { described_class.new.perform(merge_request&.id) }
+
+ context 'when merge request is found' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'executes AutoMergeService' do
+ expect_next_instance_of(AutoMergeService) do |auto_merge|
+ expect(auto_merge).to receive(:process)
+ end
+
+ subject
+ end
+ end
+
+ context 'when merge request is not found' do
+ let(:merge_request) { nil }
+
+ it 'does not execute AutoMergeService' do
+ expect(AutoMergeService).not_to receive(:new)
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/workers/expire_build_instance_artifacts_worker_spec.rb b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
index bdb5a3801d9..39f676f1057 100644
--- a/spec/workers/expire_build_instance_artifacts_worker_spec.rb
+++ b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
@@ -21,7 +21,7 @@ describe ExpireBuildInstanceArtifactsWorker do
end
it 'does remove files' do
- expect(build.reload.artifacts_file.exists?).to be_falsey
+ expect(build.reload.artifacts_file.present?).to be_falsey
end
it 'does remove the job artifact record' do
@@ -40,7 +40,7 @@ describe ExpireBuildInstanceArtifactsWorker do
end
it 'does not remove files' do
- expect(build.reload.artifacts_file.exists?).to be_truthy
+ expect(build.reload.artifacts_file.present?).to be_truthy
end
it 'does not remove the job artifact record' do
@@ -56,7 +56,7 @@ describe ExpireBuildInstanceArtifactsWorker do
end
it 'does not remove files' do
- expect(build.reload.artifacts_file.exists?).to be_truthy
+ expect(build.reload.artifacts_file.present?).to be_truthy
end
it 'does not remove the job artifact record' do
diff --git a/spec/workers/pages_domain_verification_cron_worker_spec.rb b/spec/workers/pages_domain_verification_cron_worker_spec.rb
index 186824a444f..3fb86adee11 100644
--- a/spec/workers/pages_domain_verification_cron_worker_spec.rb
+++ b/spec/workers/pages_domain_verification_cron_worker_spec.rb
@@ -10,6 +10,13 @@ describe PagesDomainVerificationCronWorker do
let!(:reverify) { create(:pages_domain, :reverify) }
let!(:disabled) { create(:pages_domain, :disabled) }
+ it 'does nothing if the database is read-only' do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+ expect(PagesDomainVerificationWorker).not_to receive(:perform_async).with(reverify.id)
+
+ worker.perform
+ end
+
it 'enqueues a PagesDomainVerificationWorker for domains needing verification' do
[reverify, disabled].each do |domain|
expect(PagesDomainVerificationWorker).to receive(:perform_async).with(domain.id)
diff --git a/spec/workers/pages_domain_verification_worker_spec.rb b/spec/workers/pages_domain_verification_worker_spec.rb
index 2f23dcb487f..f51ac1f4323 100644
--- a/spec/workers/pages_domain_verification_worker_spec.rb
+++ b/spec/workers/pages_domain_verification_worker_spec.rb
@@ -8,6 +8,13 @@ describe PagesDomainVerificationWorker do
let(:domain) { create(:pages_domain) }
describe '#perform' do
+ it 'does nothing if the database is read-only' do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+ expect(PagesDomain).not_to receive(:find_by).with(id: domain.id)
+
+ worker.perform(domain.id)
+ end
+
it 'does nothing for a non-existent domain' do
domain.destroy
diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb
index 8c604b13297..9326db34209 100644
--- a/spec/workers/pipeline_schedule_worker_spec.rb
+++ b/spec/workers/pipeline_schedule_worker_spec.rb
@@ -41,16 +41,6 @@ describe PipelineScheduleWorker do
it_behaves_like 'successful scheduling'
- context 'when exclusive lease has already been taken by the other instance' do
- before do
- stub_exclusive_lease_taken(described_class::EXCLUSIVE_LOCK_KEY, timeout: described_class::LOCK_TIMEOUT)
- end
-
- it 'raises an error and does not start creating pipelines' do
- expect { subject }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
- end
- end
-
context 'when the latest commit contains [ci skip]' do
before do
allow_any_instance_of(Ci::Pipeline)
@@ -77,47 +67,19 @@ describe PipelineScheduleWorker do
stub_ci_pipeline_yaml_file(YAML.dump(rspec: { variables: 'rspec' } ))
end
- it 'creates a failed pipeline with the reason' do
- expect { subject }.to change { project.ci_pipelines.count }.by(1)
- expect(Ci::Pipeline.last).to be_config_error
- expect(Ci::Pipeline.last.yaml_errors).not_to be_nil
+ it 'does not creates a new pipeline' do
+ expect { subject }.not_to change { project.ci_pipelines.count }
end
end
end
context 'when the schedule is not runnable by the user' do
- before do
- expect(Gitlab::Sentry)
- .to receive(:track_exception)
- .with(Ci::CreatePipelineService::CreateError,
- issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
- extra: { schedule_id: pipeline_schedule.id } ).once
- end
-
it 'does not deactivate the schedule' do
subject
expect(pipeline_schedule.reload.active).to be_truthy
end
- it 'increments Prometheus counter' do
- expect(Gitlab::Metrics)
- .to receive(:counter)
- .with(:pipeline_schedule_creation_failed_total, "Counter of failed attempts of pipeline schedule creation")
- .and_call_original
-
- subject
- end
-
- it 'logging a pipeline error' do
- expect(Rails.logger)
- .to receive(:error)
- .with(a_string_matching("Insufficient permissions to create a new pipeline"))
- .and_call_original
-
- subject
- end
-
it 'does not create a pipeline' do
expect { subject }.not_to change { project.ci_pipelines.count }
end
@@ -131,21 +93,6 @@ describe PipelineScheduleWorker do
before do
stub_ci_pipeline_yaml_file(nil)
project.add_maintainer(user)
-
- expect(Gitlab::Sentry)
- .to receive(:track_exception)
- .with(Ci::CreatePipelineService::CreateError,
- issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
- extra: { schedule_id: pipeline_schedule.id } ).once
- end
-
- it 'logging a pipeline error' do
- expect(Rails.logger)
- .to receive(:error)
- .with(a_string_matching("Missing .gitlab-ci.yml file"))
- .and_call_original
-
- subject
end
it 'does not create a pipeline' do
diff --git a/spec/workers/pipeline_success_worker_spec.rb b/spec/workers/pipeline_success_worker_spec.rb
deleted file mode 100644
index 4cbe384b47a..00000000000
--- a/spec/workers/pipeline_success_worker_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe PipelineSuccessWorker do
- describe '#perform' do
- context 'when pipeline exists' do
- let(:pipeline) { create(:ci_pipeline, status: 'success') }
-
- it 'performs "merge when pipeline succeeds"' do
- expect_any_instance_of(
- MergeRequests::MergeWhenPipelineSucceedsService
- ).to receive(:trigger)
-
- described_class.new.perform(pipeline.id)
- end
- end
-
- context 'when pipeline does not exist' do
- it 'does not raise exception' do
- expect { described_class.new.perform(123) }
- .not_to raise_error
- end
- end
- end
-end
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index 3c40269adc7..51afb076da1 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -25,10 +25,11 @@ describe ProjectCacheWorker do
end
context 'with an existing project without a repository' do
- it 'does nothing' do
+ it 'updates statistics but does not refresh the method cashes' do
allow_any_instance_of(Repository).to receive(:exists?).and_return(false)
- expect(worker).not_to receive(:update_statistics)
+ expect(worker).to receive(:update_statistics)
+ expect_any_instance_of(Repository).not_to receive(:refresh_method_caches)
worker.perform(project.id)
end
diff --git a/spec/workers/run_pipeline_schedule_worker_spec.rb b/spec/workers/run_pipeline_schedule_worker_spec.rb
index 690af22f4dc..7414470f8e7 100644
--- a/spec/workers/run_pipeline_schedule_worker_spec.rb
+++ b/spec/workers/run_pipeline_schedule_worker_spec.rb
@@ -32,7 +32,37 @@ describe RunPipelineScheduleWorker do
it 'calls the Service' do
expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service)
- expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule)
+ expect(create_pipeline_service).to receive(:execute!).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule)
+
+ worker.perform(pipeline_schedule.id, user.id)
+ end
+ end
+
+ context 'when database statement timeout happens' do
+ before do
+ allow(Ci::CreatePipelineService).to receive(:new) { raise ActiveRecord::StatementInvalid }
+
+ expect(Gitlab::Sentry)
+ .to receive(:track_exception)
+ .with(ActiveRecord::StatementInvalid,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
+ extra: { schedule_id: pipeline_schedule.id } ).once
+ end
+
+ it 'increments Prometheus counter' do
+ expect(Gitlab::Metrics)
+ .to receive(:counter)
+ .with(:pipeline_schedule_creation_failed_total, "Counter of failed attempts of pipeline schedule creation")
+ .and_call_original
+
+ worker.perform(pipeline_schedule.id, user.id)
+ end
+
+ it 'logging a pipeline error' do
+ expect(Rails.logger)
+ .to receive(:error)
+ .with(a_string_matching('ActiveRecord::StatementInvalid'))
+ .and_call_original
worker.perform(pipeline_schedule.id, user.id)
end
diff --git a/spec/workers/todos_destroyer/confidential_issue_worker_spec.rb b/spec/workers/todos_destroyer/confidential_issue_worker_spec.rb
index 18876b71615..0907e2768ba 100644
--- a/spec/workers/todos_destroyer/confidential_issue_worker_spec.rb
+++ b/spec/workers/todos_destroyer/confidential_issue_worker_spec.rb
@@ -3,12 +3,19 @@
require 'spec_helper'
describe TodosDestroyer::ConfidentialIssueWorker do
- it "calls the Todos::Destroy::ConfidentialIssueService with the params it was given" do
- service = double
+ let(:service) { double }
- expect(::Todos::Destroy::ConfidentialIssueService).to receive(:new).with(100).and_return(service)
+ it "calls the Todos::Destroy::ConfidentialIssueService with issue_id parameter" do
+ expect(::Todos::Destroy::ConfidentialIssueService).to receive(:new).with(issue_id: 100, project_id: nil).and_return(service)
expect(service).to receive(:execute)
described_class.new.perform(100)
end
+
+ it "calls the Todos::Destroy::ConfidentialIssueService with project_id parameter" do
+ expect(::Todos::Destroy::ConfidentialIssueService).to receive(:new).with(issue_id: nil, project_id: 100).and_return(service)
+ expect(service).to receive(:execute)
+
+ described_class.new.perform(nil, 100)
+ end
end
diff --git a/public/visual-review-toolbar.js b/vendor/assets/javascripts/visual_review_toolbar.js
index 6a0fdb29cc2..12a3a4c9672 100644
--- a/public/visual-review-toolbar.js
+++ b/vendor/assets/javascripts/visual_review_toolbar.js
@@ -2,260 +2,11 @@
/////////////////// STYLES ////////////////////
///////////////////////////////////////////////
+// this style must be applied inline
const buttonClearStyles = `
-webkit-appearance: none;
`;
-const buttonBaseStyles = `
- cursor: pointer;
- transition: background-color 100ms linear, border-color 100ms linear, color 100ms linear, box-shadow 100ms linear;
-`;
-
-const buttonSuccessActiveStyles = `
- background-color: #168f48;
- border-color: #12753a;
- color: #fff;
-`;
-
-const buttonSuccessHoverStyles = `
- color: #fff;
- background-color: #137e3f;
- border-color: #127339;
-`;
-
-const buttonSuccessStyles = `
- ${buttonBaseStyles}
- background-color: #1aaa55;
- border: 1px solid #168f48;
- color: #fff;
-`;
-
-const buttonSecondaryStyles = `
- ${buttonBaseStyles}
- background: none #fff;
- margin: 0 .5rem;
- border: 1px solid #e3e3e3;
-`;
-
-const buttonSecondaryActiveStyles = `
- color: #2e2e2e;
- background-color: #e1e1e1;
- border-color: #dadada;
-`;
-
-const buttonSecondaryHoverStyles = `
- background-color: #f0f0f0;
- border-color: #e3e3e3;
- color: #2e2e2e;
-`;
-
-const buttonWideStyles = `
- width: 100%;
-`;
-
-const buttonWrapperStyles = `
- margin-top: 1rem;
- display: flex;
- align-items: baseline;
- justify-content: flex-end;
-`;
-
-const collapseStyles = `
- ${buttonBaseStyles}
- width: 2.4rem;
- height: 2.2rem;
- margin-left: 1rem;
- padding: .5rem;
-`;
-
-const collapseClosedStyles = `
- ${collapseStyles}
- align-self: center;
-`;
-
-const collapseOpenStyles = `
- ${collapseStyles}
-`;
-
-const checkboxLabelStyles = `
- padding: 0 .2rem;
-`;
-
-const checkboxWrapperStyles = `
- display: flex;
- align-items: baseline;
-`;
-
-const formStyles = `
- display: flex;
- flex-direction: column;
- width: 100%
-`;
-
-const labelStyles = `
- font-weight: 600;
- display: inline-block;
- width: 100%;
-`;
-
-const linkStyles = `
- color: #1b69b6;
- text-decoration: none;
- background-color: transparent;
- background-image: none;
-`;
-
-const messageStyles = `
- padding: .25rem 0;
- margin: 0;
- line-height: 1.2rem;
-`;
-
-const metadataNoteStyles = `
- font-size: .7rem;
- line-height: 1rem;
- color: #666;
- margin-bottom: 0;
-`;
-
-const inputStyles = `
- width: 100%;
- border: 1px solid #dfdfdf;
- border-radius: 4px;
- padding: .1rem .2rem;
- min-height: 2rem;
- max-width: 17rem;
-`;
-
-const svgInnerStyles = `
- pointer-events: none;
-`;
-
-const wrapperClosedStyles = `
- max-width: 3.4rem;
- max-height: 3.4rem;
-`;
-
-const wrapperOpenStyles = `
- max-width: 22rem;
- max-height: 22rem;
-`;
-
-const wrapperStyles = `
- max-width: 22rem;
- max-height: 22rem;
- overflow: scroll;
- position: fixed;
- bottom: 1rem;
- right: 1rem;
- display: flex;
- flex-direction: row-reverse;
- padding: 1rem;
- background-color: #fff;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
- 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
- 'Noto Color Emoji';
- font-size: .8rem;
- font-weight: 400;
- color: #2e2e2e;
-`;
-
-const gitlabStyles = `
- #gitlab-collapse > * {
- ${svgInnerStyles}
- }
-
- #gitlab-form-wrapper {
- ${formStyles}
- }
-
- #gitlab-review-container {
- ${wrapperStyles}
- }
-
- .gitlab-open-wrapper {
- ${wrapperOpenStyles}
- }
-
- .gitlab-closed-wrapper {
- ${wrapperClosedStyles}
- }
-
- .gitlab-button-secondary {
- ${buttonSecondaryStyles}
- }
-
- .gitlab-button-secondary:hover {
- ${buttonSecondaryHoverStyles}
- }
-
- .gitlab-button-secondary:active {
- ${buttonSecondaryActiveStyles}
- }
-
- .gitlab-button-success:hover {
- ${buttonSuccessHoverStyles}
- }
-
- .gitlab-button-success:active {
- ${buttonSuccessActiveStyles}
- }
-
- .gitlab-button-success {
- ${buttonSuccessStyles}
- }
-
- .gitlab-button-wide {
- ${buttonWideStyles}
- }
-
- .gitlab-button-wrapper {
- ${buttonWrapperStyles}
- }
-
- .gitlab-collapse-closed {
- ${collapseClosedStyles}
- }
-
- .gitlab-collapse-open {
- ${collapseOpenStyles}
- }
-
- .gitlab-checkbox-label {
- ${checkboxLabelStyles}
- }
-
- .gitlab-checkbox-wrapper {
- ${checkboxWrapperStyles}
- }
-
- .gitlab-label {
- ${labelStyles}
- }
-
- .gitlab-link {
- ${linkStyles}
- }
-
- .gitlab-message {
- ${messageStyles}
- }
-
- .gitlab-metadata-note {
- ${metadataNoteStyles}
- }
-
- .gitlab-input {
- ${inputStyles}
- }
-`;
-
-function addStylesheet() {
- const styleEl = document.createElement('style');
- styleEl.insertAdjacentHTML('beforeend', gitlabStyles);
- document.head.appendChild(styleEl);
-}
-
///////////////////////////////////////////////
/////////////////// STATE ////////////////////
///////////////////////////////////////////////
@@ -275,25 +26,24 @@ const comment = `
<p class='gitlab-metadata-note'>Additional metadata will be included: browser, OS, current page, user agent, and viewport dimensions.</p>
</div>
<div class='gitlab-button-wrapper''>
- <button class='gitlab-button-secondary' style='${buttonClearStyles}' type='button' id='gitlab-logout-button'> Logout </button>
- <button class='gitlab-button-success' style='${buttonClearStyles}' type='button' id='gitlab-comment-button'> Send feedback </button>
+ <button class='gitlab-button gitlab-button-secondary' style='${buttonClearStyles}' type='button' id='gitlab-logout-button'> Logout </button>
+ <button class='gitlab-button gitlab-button-success' style='${buttonClearStyles}' type='button' id='gitlab-comment-button'> Send feedback </button>
</div>
`;
const commentIcon = `
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/comment</title><path d="M4 11.132l1.446-.964A1 1 0 0 1 6 10h5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v6.132zM6.303 12l-2.748 1.832A1 1 0 0 1 2 13V5a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3H6.303z" id="gitlab-comment-icon"/></svg>
-`
+`;
const compressIcon = `
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/compress</title><path d="M5.27 12.182l-1.562 1.561a1 1 0 0 1-1.414 0h-.001a1 1 0 0 1 0-1.415l1.56-1.56L2.44 9.353a.5.5 0 0 1 .353-.854H7.09a.5.5 0 0 1 .5.5v4.294a.5.5 0 0 1-.853.353l-1.467-1.465zm6.911-6.914l1.464 1.464a.5.5 0 0 1-.353.854H8.999a.5.5 0 0 1-.5-.5V2.793a.5.5 0 0 1 .854-.354l1.414 1.415 1.56-1.561a1 1 0 1 1 1.415 1.414l-1.561 1.56z" id="gitlab-compress-icon"/></svg>
`;
const collapseButton = `
- <button id='gitlab-collapse' style='${buttonClearStyles}' class='gitlab-collapse-open gitlab-button-secondary'>${compressIcon}</button>
+ <button id='gitlab-collapse' style='${buttonClearStyles}' class='gitlab-button gitlab-button-secondary gitlab-collapse gitlab-collapse-open'>${compressIcon}</button>
`;
-
-const form = (content) => `
+const form = content => `
<div id='gitlab-form-wrapper'>
${content}
</div>
@@ -310,7 +60,7 @@ const login = `
<label for="remember_token" class='gitlab-checkbox-label'>Remember me</label>
</div>
<div class='gitlab-button-wrapper'>
- <button class='gitlab-button-wide gitlab-button-success' style='${buttonClearStyles}' type='button' id='gitlab-login'> Submit </button>
+ <button class='gitlab-button-wide gitlab-button gitlab-button-success' style='${buttonClearStyles}' type='button' id='gitlab-login'> Submit </button>
</div>
`;
@@ -319,25 +69,25 @@ const login = `
///////////////////////////////////////////////
// from https://developer.mozilla.org/en-US/docs/Web/API/Window/navigator
-function getBrowserId (sUsrAg) {
- var aKeys = ["MSIE", "Edge", "Firefox", "Safari", "Chrome", "Opera"],
- nIdx = aKeys.length - 1;
+function getBrowserId(sUsrAg) {
+ var aKeys = ['MSIE', 'Edge', 'Firefox', 'Safari', 'Chrome', 'Opera'],
+ nIdx = aKeys.length - 1;
for (nIdx; nIdx > -1 && sUsrAg.indexOf(aKeys[nIdx]) === -1; nIdx--);
return aKeys[nIdx];
}
-function addCommentForm () {
+function addCommentForm() {
const formWrapper = document.getElementById('gitlab-form-wrapper');
formWrapper.innerHTML = comment;
}
-function addLoginForm () {
+function addLoginForm() {
const formWrapper = document.getElementById('gitlab-form-wrapper');
formWrapper.innerHTML = login;
}
-function authorizeUser () {
+function authorizeUser() {
// Clear any old errors
clearNote('gitlab-token');
@@ -357,13 +107,12 @@ function authorizeUser () {
return;
}
-function authSuccess (token) {
+function authSuccess(token) {
data.token = token;
addCommentForm();
}
-
-function clearNote (inputId) {
+function clearNote(inputId) {
const note = document.getElementById('gitlab-validation-note');
note.innerText = '';
note.style.color = '';
@@ -374,7 +123,7 @@ function clearNote (inputId) {
}
}
-function confirmAndClear (mergeRequestId) {
+function confirmAndClear(mergeRequestId) {
const commentButton = document.getElementById('gitlab-comment-button');
const note = document.getElementById('gitlab-validation-note');
@@ -384,7 +133,7 @@ function confirmAndClear (mergeRequestId) {
setTimeout(resetCommentButton, 1000);
}
-function getInitialState () {
+function getInitialState() {
const { localStorage } = window;
try {
@@ -396,22 +145,21 @@ function getInitialState () {
}
return login;
-
} catch (err) {
return login;
}
}
-function getProjectDetails () {
- const { innerWidth,
- innerHeight,
- location: { href },
- navigator: {
- platform, userAgent
- } } = window;
+function getProjectDetails() {
+ const {
+ innerWidth,
+ innerHeight,
+ location: { href },
+ navigator: { platform, userAgent },
+ } = window;
const browser = getBrowserId(userAgent);
- const scriptEl = document.getElementById('review-app-toolbar-script')
+ const scriptEl = document.getElementById('review-app-toolbar-script');
const { projectId, mergeRequestId, mrUrl } = scriptEl.dataset;
return {
@@ -427,7 +175,7 @@ function getProjectDetails () {
};
}
-function logoutUser () {
+function logoutUser() {
const { localStorage } = window;
// All the browsers we support have localStorage, so let's silently fail
@@ -441,7 +189,7 @@ function logoutUser () {
addLoginForm();
}
-function postComment ({
+function postComment({
href,
platform,
browser,
@@ -478,32 +226,34 @@ function postComment ({
const url = `
${mrUrl}/api/v4/projects/${projectId}/merge_requests/${mergeRequestId}/discussions`;
-
const body = `${commentText} ${detailText}`;
fetch(url, {
- method: 'POST',
- headers: {
+ method: 'POST',
+ headers: {
'PRIVATE-TOKEN': data.token,
'Content-Type': 'application/json',
},
body: JSON.stringify({ body }),
})
- .then((response) => {
- if (response.ok) {
- confirmAndClear(mergeRequestId);
- return;
- }
-
- throw new Error(`${response.status}: ${response.statusText}`)
- })
- .catch((err) => {
- postError(`The feedback was not sent successfully. Please try again. Error: ${err.message}`, 'gitlab-comment');
- resetCommentBox();
- });
+ .then(response => {
+ if (response.ok) {
+ confirmAndClear(mergeRequestId);
+ return;
+ }
+
+ throw new Error(`${response.status}: ${response.statusText}`);
+ })
+ .catch(err => {
+ postError(
+ `The feedback was not sent successfully. Please try again. Error: ${err.message}`,
+ 'gitlab-comment',
+ );
+ resetCommentBox();
+ });
}
-function postError (message, inputId) {
+function postError(message, inputId) {
const note = document.getElementById('gitlab-validation-note');
const field = document.getElementById(inputId);
field.style.borderColor = '#db3b21';
@@ -543,8 +293,7 @@ function setInProgressState() {
commentBox.style.pointerEvents = 'none';
}
-function storeToken (token) {
-
+function storeToken(token) {
const { localStorage } = window;
// All the browsers we support have localStorage, so let's silently fail
@@ -556,7 +305,7 @@ function storeToken (token) {
}
}
-function toggleForm () {
+function toggleForm() {
const container = document.getElementById('gitlab-review-container');
const collapseButton = document.getElementById('gitlab-collapse');
const form = document.getElementById('gitlab-form-wrapper');
@@ -578,7 +327,7 @@ function toggleForm () {
display: 'none',
backgroundColor: 'rgba(255, 255, 255, 0)',
},
- }
+ };
const nextState = collapseButton.classList.contains('gitlab-collapse-open') ? CLOSED : OPEN;
@@ -593,9 +342,9 @@ function toggleForm () {
///////////////// INJECTION //////////////////
///////////////////////////////////////////////
-function noop() {};
+function noop() {}
-const eventLookup = ({target: { id }}) => {
+const eventLookup = ({ target: { id } }) => {
switch (id) {
case 'gitlab-collapse':
return toggleForm;
@@ -620,9 +369,9 @@ window.addEventListener('load', () => {
container.insertAdjacentHTML('beforeend', form(content));
document.body.insertBefore(container, document.body.firstChild);
- addStylesheet();
- document.getElementById('gitlab-review-container').addEventListener('click', (event) => {
+ document.getElementById('gitlab-review-container').addEventListener('click', event => {
eventLookup(event)();
});
+
});
diff --git a/vendor/jupyter/values.yaml b/vendor/jupyter/values.yaml
index a5e13fdc104..0fbf36b39cc 100644
--- a/vendor/jupyter/values.yaml
+++ b/vendor/jupyter/values.yaml
@@ -4,9 +4,9 @@ rbac:
hub:
extraEnv:
JUPYTER_ENABLE_LAB: 1
- SINGLEUSER_IMAGE: 'registry.gitlab.com/gitlab-org/jupyterhub-user-image:latest'
extraConfig: |
c.KubeSpawner.cmd = ['jupyter-labhub']
+ c.GitLabOAuthenticator.scope = ['api read_repository write_repository']
async def add_auth_env(spawner):
'''
@@ -23,6 +23,8 @@ hub:
spawner.environment['GITLAB_ACCESS_TOKEN'] = auth_state['access_token']
spawner.environment['GITLAB_USER_LOGIN'] = auth_state['gitlab_user']['username']
spawner.environment['GITLAB_USER_ID'] = str(auth_state['gitlab_user']['id'])
+ spawner.environment['GITLAB_USER_EMAIL'] = auth_state['gitlab_user']['email']
+ spawner.environment['GITLAB_USER_NAME'] = auth_state['gitlab_user']['name']
c.KubeSpawner.pre_spawn_hook = add_auth_env
@@ -33,10 +35,22 @@ auth:
singleuser:
defaultUrl: "/lab"
+ image:
+ name: registry.gitlab.com/gitlab-org/jupyterhub-user-image
+ tag: latest
lifecycleHooks:
postStart:
exec:
- command: ["sh", "-c", "git clone https://gitlab.com/gitlab-org/nurtch-demo.git DevOps-Runbook-Demo || true"]
+ command:
+ - "sh"
+ - "-c"
+ - >
+ git clone https://gitlab.com/gitlab-org/nurtch-demo.git DevOps-Runbook-Demo || true;
+ echo "https://${GITLAB_USER_LOGIN}:${GITLAB_ACCESS_TOKEN}@${GITLAB_HOST}" > ~/.git-credentials;
+ git config --global credential.helper store;
+ git config --global user.email "${GITLAB_USER_EMAIL}";
+ git config --global user.name "${GITLAB_USER_NAME}";
+ jupyter serverextension enable --py jupyterlab_git
ingress:
enabled: true
diff --git a/yarn.lock b/yarn.lock
index 7c119e2c9dd..b6d31ea08e6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -680,10 +680,10 @@
dependencies:
bootstrap "^4.1.3"
-"@gitlab/eslint-config@^1.5.0":
- version "1.5.0"
- resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-1.5.0.tgz#0c8c3ae74f276eb6671bd7c60f331bc0f2d2e5cf"
- integrity sha512-KgJgoIZNpGauFpCV1iCptesYN7I8abtYRBLU9xcH0oocC/xp3JmbLfsZ+lEtrk8pl99Q2mKiAuaPpzxjXr6hBw==
+"@gitlab/eslint-config@^1.6.0":
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-1.6.0.tgz#1fd247d6ab477d53d4c330e05f007e3afa303689"
+ integrity sha512-EZffCwsRZmRWPP6N3wp20EJDVGYLG1v43/W7fF/gYQpUjcRclC8ks/jEv8UppasSDlanDmkh1bLWoE9CelSyyw==
dependencies:
babel-eslint "^10.0.1"
eslint-config-airbnb-base "^13.1.0"
@@ -698,10 +698,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.63.0.tgz#9dd544026d203e4ce6efed72b05db68f710c4d49"
integrity sha512-YztrReFTg31B7v5wtUC5j15KHNcMebtW+kACytEU42XomMaIwk4USIbygqWlq0VRHA2VHJrHApfJHIjxiCCQcA==
-"@gitlab/ui@^3.10.0":
- version "3.10.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.10.0.tgz#0f16772b7fe8052dabd37aba2ae436255b9e0f0a"
- integrity sha512-po6fh2T8esa2Nach73AYLdoTg8N0YrRa5GkJk5GoxVrHYoAUD8T1Rn3pXXXKSsQdQcYjIZJ6uvY8sL+qg+Yjww==
+"@gitlab/ui@^3.11.0":
+ version "3.11.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.11.0.tgz#7bba82c893f47abbfe7995281dc0ce95290dcc4e"
+ integrity sha512-55Qxyj2wZILznZJUTUxY1SUuw028IgmP6ZyLd5XF3xk91HWSyq5/zrlr/qRTFGL1cABhxoBLScmXsnOc2CIO0w==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.2.1"
@@ -712,7 +712,7 @@
js-beautify "^1.8.8"
lodash "^4.17.11"
url-search-params-polyfill "^5.0.0"
- vue "^2.5.21"
+ vue "^2.6.10"
vue-loader "^15.4.2"
"@gitlab/vue-toasted@^1.2.1":
@@ -1398,6 +1398,24 @@ apollo-client@^2.5.1:
tslib "^1.9.3"
zen-observable "^0.8.0"
+apollo-link-batch-http@^1.2.11:
+ version "1.2.11"
+ resolved "https://registry.yarnpkg.com/apollo-link-batch-http/-/apollo-link-batch-http-1.2.11.tgz#ae42dbcc02820658e1e267d05bf2aae7ac208088"
+ integrity sha512-f+KEdbP51I3AeEaBDW2lKS3eaPK/1IXaTM9F2moj02s1hgC/TzeUORRuUeOExW8ggXveW1Jzp6aYMJ2SQkZJyA==
+ dependencies:
+ apollo-link "^1.2.11"
+ apollo-link-batch "^1.1.12"
+ apollo-link-http-common "^0.2.13"
+ tslib "^1.9.3"
+
+apollo-link-batch@^1.1.12:
+ version "1.1.12"
+ resolved "https://registry.yarnpkg.com/apollo-link-batch/-/apollo-link-batch-1.1.12.tgz#64eb231082f182b0395ef7ab903600627f6c7fe8"
+ integrity sha512-6NqLiB9tEGxRiyhtnX/7CPHkmFG0IXfEP7pC5kirhjV+4KxqBaWvJnJGKpGp7Owgdph7KJlV+9+niOKEkcwreg==
+ dependencies:
+ apollo-link "^1.2.11"
+ tslib "^1.9.3"
+
apollo-link-dedup@^1.0.0:
version "1.0.10"
resolved "https://registry.yarnpkg.com/apollo-link-dedup/-/apollo-link-dedup-1.0.10.tgz#7b94589fe7f969777efd18a129043c78430800ae"
@@ -1405,7 +1423,7 @@ apollo-link-dedup@^1.0.0:
dependencies:
apollo-link "^1.2.3"
-apollo-link-http-common@^0.2.8:
+apollo-link-http-common@^0.2.13, apollo-link-http-common@^0.2.8:
version "0.2.13"
resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.13.tgz#c688f6baaffdc7b269b2db7ae89dae7c58b5b350"
integrity sha512-Uyg1ECQpTTA691Fwx5e6Rc/6CPSu4TB4pQRTGIpwZ4l5JDOQ+812Wvi/e3IInmzOZpwx5YrrOfXrtN8BrsDXoA==
@@ -2798,11 +2816,16 @@ core-js@3.0.1:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.0.1.tgz#1343182634298f7f38622f95e73f54e48ddf4738"
integrity sha512-sco40rF+2KlE0ROMvydjkrVMMG1vYilP2ALoRXcYR4obqbYIuV3Bg+51GEDW+HF8n7NRA+iaA4qD0nD9lo9mew==
-core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1:
+core-js@^2.2.0, core-js@^2.4.0:
version "2.5.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
integrity sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==
+core-js@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.1.3.tgz#95700bca5f248f5f78c0ec63e784eca663ec4138"
+ integrity sha512-PWZ+ZfuaKf178BIAg+CRsljwjIMRV8MY00CbZczkR6Zk5LfkSkjGoaab3+bqRQWVITNZxQB7TFYz+CFcyuamvA==
+
core-js@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.3.0.tgz#fab83fbb0b2d8dc85fa636c4b9d34c75420c6d65"
@@ -11353,7 +11376,7 @@ vue-virtual-scroll-list@^1.3.1:
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.3.1.tgz#efcb83d3a3dcc69cd886fa4de1130a65493e8f76"
integrity sha512-PMTxiK9/P1LtgoWWw4n1QnmDDkYqIdWWCNdt1L4JD9g6rwDgnsGsSV10bAnd5n7DQLHGWHjRex+zAbjXWT8t0g==
-vue@^2.5.21, vue@^2.6.10:
+vue@^2.6.10:
version "2.6.10"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"
integrity sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==