summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml2
-rw-r--r--.rubocop_todo.yml3
-rw-r--r--CHANGELOG.md38
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/images/auth_buttons/salesforce_64.pngbin0 -> 8774 bytes
-rw-r--r--app/assets/javascripts/boards/index.js8
-rw-r--r--app/assets/javascripts/boards/mount_multiple_boards_switcher.js2
-rw-r--r--app/assets/javascripts/boards/services/board_service.js76
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js111
-rw-r--r--app/assets/javascripts/branches/divergence_graph.js4
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/dropdown.vue58
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/project_form_group.vue136
-rw-r--r--app/assets/javascripts/confidential_merge_request/index.js30
-rw-r--r--app/assets/javascripts/confidential_merge_request/state.js5
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js47
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js8
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussion_reply.vue55
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue1
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue8
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue27
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue17
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_comment_row.vue40
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue85
-rw-r--r--app/assets/javascripts/diffs/store/actions.js34
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js65
-rw-r--r--app/assets/javascripts/diffs/store/utils.js45
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue11
-rw-r--r--app/assets/javascripts/group.js4
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue3
-rw-r--r--app/assets/javascripts/ide/components/external_link.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue21
-rw-r--r--app/assets/javascripts/ide/components/repo_file_status_icon.vue5
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue5
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js11
-rw-r--r--app/assets/javascripts/main.js6
-rw-r--r--app/assets/javascripts/main_ee.js1
-rw-r--r--app/assets/javascripts/manual_ordering.js2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue65
-rw-r--r--app/assets/javascripts/monitoring/stores/utils.js17
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue43
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_actions.vue26
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue6
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue51
-rw-r--r--app/assets/javascripts/notes/components/discussion_reply_placeholder.vue8
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue16
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue36
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue4
-rw-r--r--app/assets/javascripts/notes/components/note_signed_out_widget.vue19
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue29
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue18
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue3
-rw-r--r--app/assets/javascripts/notes/mixins/resolvable.js6
-rw-r--r--app/assets/javascripts/notes/stores/actions.js12
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js4
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue15
-rw-r--r--app/assets/javascripts/performance_bar/components/simple_metric.vue33
-rw-r--r--app/assets/javascripts/projects/project_new.js4
-rw-r--r--app/assets/javascripts/releases/components/release_block.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue10
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue29
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue19
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue19
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss4
-rw-r--r--app/assets/stylesheets/pages/diff.scss16
-rw-r--r--app/assets/stylesheets/pages/notes.scss29
-rw-r--r--app/controllers/concerns/issuable_collections.rb2
-rw-r--r--app/controllers/concerns/issuable_collections_action.rb4
-rw-r--r--app/controllers/concerns/notes_actions.rb14
-rw-r--r--app/controllers/dashboard/projects_controller.rb2
-rw-r--r--app/controllers/dashboard/todos_controller.rb4
-rw-r--r--app/controllers/projects/application_controller.rb5
-rw-r--r--app/controllers/projects/branches_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/controllers/projects/settings/repository_controller.rb2
-rw-r--r--app/controllers/projects/templates_controller.rb17
-rw-r--r--app/controllers/projects_controller.rb4
-rw-r--r--app/controllers/snippets/notes_controller.rb8
-rw-r--r--app/controllers/snippets_controller.rb2
-rw-r--r--app/controllers/uploads_controller.rb20
-rw-r--r--app/finders/runner_jobs_finder.rb19
-rw-r--r--app/graphql/gitlab_schema.rb1
-rw-r--r--app/graphql/types/base_field.rb19
-rw-r--r--app/graphql/types/ci/detailed_status_type.rb3
-rw-r--r--app/graphql/types/issue_state_enum.rb3
-rw-r--r--app/graphql/types/label_type.rb2
-rw-r--r--app/graphql/types/merge_request_state_enum.rb3
-rw-r--r--app/graphql/types/merge_request_type.rb2
-rw-r--r--app/graphql/types/metadata_type.rb2
-rw-r--r--app/graphql/types/mutation_type.rb2
-rw-r--r--app/graphql/types/namespace_type.rb2
-rw-r--r--app/graphql/types/notes/diff_position_type.rb3
-rw-r--r--app/graphql/types/permission_types/merge_request.rb4
-rw-r--r--app/graphql/types/project_type.rb8
-rw-r--r--app/graphql/types/query_type.rb5
-rw-r--r--app/graphql/types/repository_type.rb6
-rw-r--r--app/graphql/types/task_completion_status.rb4
-rw-r--r--app/graphql/types/tree/blob_type.rb3
-rw-r--r--app/graphql/types/tree/submodule_type.rb3
-rw-r--r--app/graphql/types/tree/tree_entry_type.rb3
-rw-r--r--app/graphql/types/tree/tree_type.rb9
-rw-r--r--app/helpers/application_settings_helper.rb2
-rw-r--r--app/helpers/auth_helper.rb2
-rw-r--r--app/helpers/branches_helper.rb8
-rw-r--r--app/helpers/issuables_helper.rb6
-rw-r--r--app/helpers/issues_helper.rb2
-rw-r--r--app/helpers/preferences_helper.rb2
-rw-r--r--app/helpers/snippets_helper.rb10
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/models/concerns/issuable.rb6
-rw-r--r--app/models/deploy_token.rb14
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/merge_request.rb29
-rw-r--r--app/models/merge_requests_closing_issues.rb35
-rw-r--r--app/models/namespace/aggregation_schedule.rb8
-rw-r--r--app/models/project_services/kubernetes_service.rb129
-rw-r--r--app/models/release.rb10
-rw-r--r--app/models/repository.rb5
-rw-r--r--app/policies/repository_policy.rb5
-rw-r--r--app/serializers/test_suite_comparer_entity.rb29
-rw-r--r--app/services/auto_merge/base_service.rb14
-rw-r--r--app/services/auto_merge/merge_when_pipeline_succeeds_service.rb10
-rw-r--r--app/services/auto_merge_service.rb6
-rw-r--r--app/services/branches/diverging_commit_counts_service.rb24
-rw-r--r--app/services/ci/compare_reports_base_service.rb6
-rw-r--r--app/services/ci/create_pipeline_service.rb2
-rw-r--r--app/services/deploy_tokens/create_service.rb4
-rw-r--r--app/services/discussions/update_diff_position_service.rb3
-rw-r--r--app/services/merge_requests/base_service.rb4
-rw-r--r--app/services/merge_requests/close_service.rb2
-rw-r--r--app/services/merge_requests/merge_to_ref_service.rb16
-rw-r--r--app/services/merge_requests/rebase_service.rb4
-rw-r--r--app/services/merge_requests/refresh_service.rb6
-rw-r--r--app/services/merge_requests/update_service.rb2
-rw-r--r--app/services/prometheus/adapter_service.rb7
-rw-r--r--app/services/releases/concerns.rb4
-rw-r--r--app/services/releases/create_service.rb1
-rw-r--r--app/services/system_note_service.rb24
-rw-r--r--app/uploaders/file_mover.rb82
-rw-r--r--app/uploaders/file_uploader.rb2
-rw-r--r--app/uploaders/personal_file_uploader.rb2
-rw-r--r--app/validators/color_validator.rb2
-rw-r--r--app/views/admin/application_settings/_grafana.html.haml17
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml11
-rw-r--r--app/views/dashboard/todos/_todo.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml24
-rw-r--r--app/views/discussions/_discussion.html.haml4
-rw-r--r--app/views/discussions/_new_issue_for_discussion.html.haml4
-rw-r--r--app/views/layouts/fullscreen.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml5
-rw-r--r--app/views/layouts/snippets.html.haml5
-rw-r--r--app/views/profiles/preferences/show.html.haml12
-rw-r--r--app/views/projects/_flash_messages.html.haml1
-rw-r--r--app/views/projects/deploy_tokens/_form.html.haml5
-rw-r--r--app/views/projects/issues/_new_branch.html.haml13
-rw-r--r--app/views/shared/_issuable_meta_data.html.haml2
-rw-r--r--app/views/shared/issuable/_form.html.haml6
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/notes/_comment_button.html.haml4
-rw-r--r--app/workers/rebase_worker.rb2
-rw-r--r--changelogs/unreleased/12550-fullscrean.yml5
-rw-r--r--changelogs/unreleased/12553-preferences.yml5
-rw-r--r--changelogs/unreleased/32452-multiple-discussions.yml5
-rw-r--r--changelogs/unreleased/50228-deploy-tokens-custom-username.yml5
-rw-r--r--changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml5
-rw-r--r--changelogs/unreleased/53357-fix-plus-in-upload-file-names.yml5
-rw-r--r--changelogs/unreleased/54117-transactional-rebase.yml5
-rw-r--r--changelogs/unreleased/55487-enable-group-terminals-button.yml5
-rw-r--r--changelogs/unreleased/55953-renamed-discussion-to-thread.yml5
-rw-r--r--changelogs/unreleased/57793-fix-line-age.yml5
-rw-r--r--changelogs/unreleased/61005-grafanaInAdminSettingsMonitoringMenu.yml5
-rw-r--r--changelogs/unreleased/62124-new-threaded-discussion-design.yml5
-rw-r--r--changelogs/unreleased/63873-process-start-time.yml6
-rw-r--r--changelogs/unreleased/63971-remove-istanbul.yml5
-rw-r--r--changelogs/unreleased/add-salesforce-logo.yml5
-rw-r--r--changelogs/unreleased/asciidoc-enable-syntax-highlighting.yml5
-rw-r--r--changelogs/unreleased/create-merge-train-ref-ce.yml5
-rw-r--r--changelogs/unreleased/fix-sidekiq-transaction-check-race.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.51.0.yml5
-rw-r--r--changelogs/unreleased/issue-63222.yml5
-rw-r--r--changelogs/unreleased/limit-amount-of-tests-returned.yml5
-rw-r--r--changelogs/unreleased/osw-persist-tmp-snippet-uploads.yml5
-rw-r--r--changelogs/unreleased/pre-releases-38105a.yml5
-rw-r--r--changelogs/unreleased/rj-fix-manual-order.yml5
-rw-r--r--changelogs/unreleased/security-2858-fix-color-validation.yml5
-rw-r--r--changelogs/unreleased/security-59581-related-merge-requests-count.yml5
-rw-r--r--changelogs/unreleased/security-DOS_issue_comments_banzai.yml5
-rw-r--r--changelogs/unreleased/security-bvl-enforce-graphql-type-authorization.yml5
-rw-r--r--changelogs/unreleased/security-fp-prevent-billion-laughs-attack.yml5
-rw-r--r--changelogs/unreleased/security-mr-head-pipeline-leak.yml5
-rw-r--r--changelogs/unreleased/security-notes-in-private-snippets.yml5
-rw-r--r--changelogs/unreleased/security-prevent-detection-of-merge-request-template-name.yml5
-rw-r--r--changelogs/unreleased/sh-cache-flipper-checks-in-memory.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-63349.yml5
-rw-r--r--changelogs/unreleased/sh-improve-redis-peek.yml5
-rw-r--r--changelogs/unreleased/slugify.yml5
-rw-r--r--changelogs/unreleased/update-todo-in-ui.yml5
-rw-r--r--changelogs/unreleased/use-pg-9-6-11-on-ci.yml5
-rw-r--r--changelogs/unreleased/winh-jest-markdown-header.yml5
-rw-r--r--changelogs/unreleased/winh-updateResolvableDiscussionsCounts-typo.yml5
-rw-r--r--config/README.md3
-rw-r--r--config/initializers/7_prometheus_metrics.rb20
-rw-r--r--config/initializers/forbid_sidekiq_in_transactions.rb13
-rw-r--r--config/initializers/peek.rb3
-rw-r--r--config/initializers/rack_attack_logging.rb14
-rw-r--r--config/no_todos_messages.yml8
-rw-r--r--config/routes/project.rb5
-rw-r--r--config/routes/uploads.rb4
-rw-r--r--danger/database/Dangerfile32
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb12
-rw-r--r--db/migrate/20190613044655_add_username_to_deploy_tokens.rb9
-rw-r--r--db/migrate/20190617123615_add_grafana_to_settings.rb18
-rw-r--r--db/migrate/20190621022810_add_last_ci_minutes_usage_notification_level_to_namespaces.rb9
-rw-r--r--db/migrate/20190621151636_add_merge_request_rebase_jid.rb9
-rw-r--r--db/migrate/20190624123615_add_grafana_url_to_settings.rb18
-rw-r--r--db/migrate/20190628185000_add_released_at_to_releases_table.rb11
-rw-r--r--db/migrate/20190628185004_backfill_and_add_not_null_constraint_to_released_at_column_on_releases_table.rb18
-rw-r--r--db/migrate/20190703130053_remove_gitaly_feature_flags.rb48
-rw-r--r--db/post_migrate/20180202111106_remove_project_labels_group_id.rb19
-rw-r--r--db/schema.rb8
-rw-r--r--doc/README.md2
-rw-r--r--doc/administration/auth/ldap.md4
-rw-r--r--doc/administration/geo/disaster_recovery/background_verification.md10
-rw-r--r--doc/administration/geo/disaster_recovery/bring_primary_back.md28
-rw-r--r--doc/administration/geo/disaster_recovery/index.md229
-rw-r--r--doc/administration/geo/disaster_recovery/planned_failover.md41
-rw-r--r--doc/administration/geo/replication/configuration.md175
-rw-r--r--doc/administration/geo/replication/database.md600
-rw-r--r--doc/administration/geo/replication/external_database.md160
-rw-r--r--doc/administration/geo/replication/high_availability.md252
-rw-r--r--doc/administration/geo/replication/index.md8
-rw-r--r--doc/administration/geo/replication/remove_geo_node.md45
-rw-r--r--doc/administration/geo/replication/troubleshooting.md326
-rw-r--r--doc/administration/geo/replication/updating_the_geo_nodes.md331
-rw-r--r--doc/administration/gitaly/index.md8
-rw-r--r--doc/administration/high_availability/redis.md4
-rw-r--r--doc/administration/incoming_email.md2
-rw-r--r--doc/administration/index.md2
-rw-r--r--doc/administration/issue_closing_pattern.md4
-rw-r--r--doc/administration/logs.md3
-rw-r--r--doc/administration/monitoring/performance/grafana_configuration.md15
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md4
-rw-r--r--doc/api/group_boards.md289
-rw-r--r--doc/api/group_milestones.md29
-rw-r--r--doc/api/groups.md213
-rw-r--r--doc/api/issues.md157
-rw-r--r--doc/api/issues_statistics.md12
-rw-r--r--doc/api/jobs.md2
-rw-r--r--doc/api/merge_requests.md208
-rw-r--r--doc/api/milestones.md29
-rw-r--r--doc/api/namespaces.md16
-rw-r--r--doc/api/notification_settings.md52
-rw-r--r--doc/api/pipeline_schedules.md12
-rw-r--r--doc/api/pipelines.md10
-rw-r--r--doc/api/project_aliases.md16
-rw-r--r--doc/api/project_snippets.md6
-rw-r--r--doc/api/projects.md232
-rw-r--r--doc/api/protected_branches.md154
-rw-r--r--doc/api/runners.md38
-rw-r--r--doc/api/services.md2
-rw-r--r--doc/api/settings.md79
-rw-r--r--doc/api/users.md221
-rw-r--r--doc/api/vulnerabilities.md14
-rw-r--r--doc/ci/README.md1
-rw-r--r--doc/ci/docker/using_docker_images.md8
-rw-r--r--doc/ci/merge_request_pipelines/index.md147
-rw-r--r--doc/ci/merge_request_pipelines/pipelines_for_merged_results/img/merge_request_pipeline.png (renamed from doc/ci/merge_request_pipelines/img/merge_request_pipeline.png)bin10152 -> 10152 bytes
-rw-r--r--doc/ci/merge_request_pipelines/pipelines_for_merged_results/img/merge_request_pipeline_config.png (renamed from doc/ci/merge_request_pipelines/img/merge_request_pipeline_config.png)bin10889 -> 10889 bytes
-rw-r--r--doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md78
-rw-r--r--doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/img/merge_train_cancel_v12_0.png (renamed from doc/ci/merge_request_pipelines/img/merge_train_cancel.png)bin19577 -> 19577 bytes
-rw-r--r--doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/img/merge_train_config_v12_0.png (renamed from doc/ci/merge_request_pipelines/img/merge_train_config.png)bin24267 -> 24267 bytes
-rw-r--r--doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/img/merge_train_start_v12_0.png (renamed from doc/ci/merge_request_pipelines/img/merge_train_start.png)bin8365 -> 8365 bytes
-rw-r--r--doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/img/merge_train_start_when_pipeline_succeeds_v12_0.png (renamed from doc/ci/merge_request_pipelines/img/merge_train_start_when_pipeline_succeeds.png)bin10323 -> 10323 bytes
-rw-r--r--doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md88
-rw-r--r--doc/ci/variables/predefined_variables.md2
-rw-r--r--doc/ci/yaml/README.md6
-rw-r--r--doc/customization/issue_closing.md4
-rw-r--r--doc/customization/system_header_and_footer_messages.md3
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/api_graphql_styleguide.md15
-rw-r--r--doc/development/architecture.md4
-rw-r--r--doc/development/automatic_ce_ee_merge.md53
-rw-r--r--doc/development/changelog.md2
-rw-r--r--doc/development/contributing/issue_workflow.md8
-rw-r--r--doc/development/contributing/merge_request_workflow.md3
-rw-r--r--doc/development/database_debugging.md2
-rw-r--r--doc/development/ee_features.md9
-rw-r--r--doc/development/elasticsearch.md2
-rw-r--r--doc/development/fe_guide/development_process.md24
-rw-r--r--doc/development/file_storage.md6
-rw-r--r--doc/development/go_guide/index.md2
-rw-r--r--doc/development/testing_guide/end_to_end/index.md2
-rw-r--r--doc/development/testing_guide/frontend_testing.md1
-rw-r--r--doc/development/testing_guide/index.md2
-rw-r--r--doc/gitlab-basics/README.md2
-rw-r--r--doc/gitlab-basics/create-issue.md4
-rw-r--r--doc/install/installation.md2
-rw-r--r--doc/integration/elasticsearch.md2
-rw-r--r--doc/intro/README.md4
-rw-r--r--doc/public_access/public_access.md6
-rw-r--r--doc/topics/autodevops/index.md49
-rw-r--r--doc/user/admin_area/settings/account_and_limit_settings.md4
-rw-r--r--doc/user/clusters/applications.md2
-rw-r--r--doc/user/gitlab_com/index.md10
-rw-r--r--doc/user/markdown.md2
-rw-r--r--doc/user/permissions.md4
-rw-r--r--doc/user/project/canary_deployments.md3
-rw-r--r--doc/user/project/clusters/index.md2
-rw-r--r--doc/user/project/code_owners.md15
-rw-r--r--doc/user/project/container_registry.md2
-rw-r--r--doc/user/project/cycle_analytics.md2
-rw-r--r--doc/user/project/deploy_boards.md2
-rw-r--r--doc/user/project/integrations/prometheus_library/cloudwatch.md2
-rw-r--r--doc/user/project/integrations/prometheus_library/haproxy.md4
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx.md6
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx_ingress.md6
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md6
-rw-r--r--doc/user/project/issues/automatic_issue_closing.md62
-rw-r--r--doc/user/project/issues/closing_issues.md62
-rw-r--r--doc/user/project/issues/create_new_issue.md107
-rw-r--r--doc/user/project/issues/crosslinking_issues.md23
-rw-r--r--doc/user/project/issues/csv_import.md55
-rw-r--r--doc/user/project/issues/deleting_issues.md16
-rw-r--r--doc/user/project/issues/due_dates.md29
-rw-r--r--doc/user/project/issues/index.md95
-rw-r--r--doc/user/project/issues/issue_data_and_actions.md21
-rw-r--r--doc/user/project/issues/managing_issues.md225
-rw-r--r--doc/user/project/issues/moving_issues.md38
-rw-r--r--doc/user/project/issues/similar_issues.md19
-rw-r--r--doc/user/project/merge_requests/index.md2
-rw-r--r--doc/user/project/merge_requests/squash_and_merge.md5
-rw-r--r--doc/user/project/merge_requests/work_in_progress_merge_requests.md4
-rw-r--r--doc/user/project/pages/index.md2
-rw-r--r--doc/user/project/pipelines/job_artifacts.md3
-rw-r--r--doc/user/project/quick_actions.md10
-rw-r--r--doc/user/project/repository/branches/index.md2
-rw-r--r--doc/user/project/repository/index.md13
-rw-r--r--doc/user/project/repository/web_editor.md3
-rw-r--r--doc/user/project/settings/index.md6
-rw-r--r--doc/user/search/index.md6
-rw-r--r--doc/workflow/README.md2
-rw-r--r--doc/workflow/gitlab_flow.md37
-rw-r--r--doc/workflow/img/ci_mr.png (renamed from doc/workflow/ci_mr.png)bin12024 -> 12024 bytes
-rw-r--r--doc/workflow/img/close_issue_mr.png (renamed from doc/workflow/close_issue_mr.png)bin42108 -> 42108 bytes
-rw-r--r--doc/workflow/img/environment_branches.png (renamed from doc/workflow/environment_branches.png)bin12354 -> 12354 bytes
-rw-r--r--doc/workflow/img/four_stages.png (renamed from doc/workflow/four_stages.png)bin7124 -> 7124 bytes
-rw-r--r--doc/workflow/img/git_pull.png (renamed from doc/workflow/git_pull.png)bin28701 -> 28701 bytes
-rw-r--r--doc/workflow/img/gitdashflow.png (renamed from doc/workflow/gitdashflow.png)bin68177 -> 68177 bytes
-rw-r--r--doc/workflow/img/github_flow.png (renamed from doc/workflow/github_flow.png)bin6173 -> 6173 bytes
-rw-r--r--doc/workflow/img/gitlab_flow.png (renamed from doc/workflow/gitlab_flow.png)bin47430 -> 47430 bytes
-rw-r--r--doc/workflow/img/good_commit.png (renamed from doc/workflow/good_commit.png)bin8740 -> 8740 bytes
-rw-r--r--doc/workflow/img/merge_commits.png (renamed from doc/workflow/merge_commits.png)bin7564 -> 7564 bytes
-rw-r--r--doc/workflow/img/merge_request.png (renamed from doc/workflow/merge_request.png)bin47225 -> 47225 bytes
-rw-r--r--doc/workflow/img/messy_flow.png (renamed from doc/workflow/messy_flow.png)bin11663 -> 11663 bytes
-rw-r--r--doc/workflow/img/mr_inline_comments.png (renamed from doc/workflow/mr_inline_comments.png)bin52503 -> 52503 bytes
-rw-r--r--doc/workflow/img/production_branch.png (renamed from doc/workflow/production_branch.png)bin7262 -> 7262 bytes
-rw-r--r--doc/workflow/img/rebase.png (renamed from doc/workflow/rebase.png)bin28939 -> 28939 bytes
-rw-r--r--doc/workflow/img/release_branches.png (renamed from doc/workflow/release_branches.png)bin12736 -> 12736 bytes
-rw-r--r--doc/workflow/img/remove_checkbox.png (renamed from doc/workflow/remove_checkbox.png)bin6904 -> 6904 bytes
-rw-r--r--doc/workflow/notifications.md24
-rw-r--r--doc/workflow/todos.md138
-rw-r--r--lib/api/api.rb5
-rw-r--r--lib/api/entities.rb12
-rw-r--r--lib/api/issues.rb6
-rw-r--r--lib/api/merge_requests.rb5
-rw-r--r--lib/api/projects.rb2
-rw-r--r--lib/api/releases.rb2
-rw-r--r--lib/api/runners.rb2
-rw-r--r--lib/api/settings.rb2
-rw-r--r--lib/api/todos.rb2
-rw-r--r--lib/banzai/filter/relative_link_filter.rb2
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb2
-rw-r--r--lib/banzai/pipeline/ascii_doc_pipeline.rb1
-rw-r--r--lib/feature.rb23
-rw-r--r--lib/feature/gitaly.rb7
-rw-r--r--lib/gitlab/asciidoc.rb3
-rw-r--r--lib/gitlab/asciidoc/syntax_highlighter/html_pipeline_adapter.rb15
-rw-r--r--lib/gitlab/background_migration/migrate_system_uploads_to_new_folder.rb29
-rw-r--r--lib/gitlab/background_migration/move_personal_snippet_files.rb82
-rw-r--r--lib/gitlab/ci/config.rb5
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml20
-rw-r--r--lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml1
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml1
-rw-r--r--lib/gitlab/config/loader/yaml.rb34
-rw-r--r--lib/gitlab/danger/helper.rb6
-rw-r--r--lib/gitlab/diff/position.rb4
-rw-r--r--lib/gitlab/diff/position_tracer.rb192
-rw-r--r--lib/gitlab/diff/position_tracer/base_strategy.rb26
-rw-r--r--lib/gitlab/diff/position_tracer/image_strategy.rb50
-rw-r--r--lib/gitlab/diff/position_tracer/line_strategy.rb201
-rw-r--r--lib/gitlab/git/repository.rb4
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb5
-rw-r--r--lib/gitlab/graphql/authorize.rb2
-rw-r--r--lib/gitlab/graphql/authorize/authorize_field_service.rb2
-rw-r--r--lib/gitlab/graphql/calls_gitaly.rb15
-rw-r--r--lib/gitlab/graphql/calls_gitaly/instrumentation.rb40
-rw-r--r--lib/gitlab/graphql/mount_mutation.rb5
-rw-r--r--lib/gitlab/import_export/import_export.yml1
-rw-r--r--lib/gitlab/issuable_metadata.rb4
-rw-r--r--lib/gitlab/legacy_github_import/release_formatter.rb1
-rw-r--r--lib/gitlab/metrics/samplers/ruby_sampler.rb7
-rw-r--r--lib/gitlab/metrics/system.rb12
-rw-r--r--lib/gitlab/namespaced_session_store.rb15
-rw-r--r--lib/gitlab/performance_bar.rb28
-rw-r--r--lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb12
-rw-r--r--lib/gitlab/quick_actions/issuable_actions.rb4
-rw-r--r--lib/gitlab/sidekiq_status.rb6
-rw-r--r--lib/gitlab/user_extractor.rb56
-rw-r--r--lib/gitlab/utils/deep_size.rb79
-rw-r--r--lib/peek/views/redis.rb82
-rw-r--r--locale/gitlab.pot244
-rw-r--r--package.json3
-rw-r--r--qa/qa/page/project/operations/kubernetes/show.rb8
-rw-r--r--qa/qa/page/project/show.rb1
-rw-r--r--qa/qa/page/project/sub_menus/ci_cd.rb2
-rw-r--r--qa/qa/page/project/sub_menus/issues.rb2
-rw-r--r--qa/qa/page/project/sub_menus/operations.rb2
-rw-r--r--qa/qa/page/project/sub_menus/repository.rb2
-rw-r--r--qa/qa/page/project/sub_menus/settings.rb2
-rw-r--r--qa/qa/resource/kubernetes_cluster.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb43
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb24
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb21
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb3
-rw-r--r--rubocop/cop/graphql/authorize_types.rb61
-rw-r--r--rubocop/rubocop.rb1
-rwxr-xr-xscripts/review_apps/review-apps.sh128
-rw-r--r--spec/controllers/dashboard/projects_controller_spec.rb18
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb2
-rw-r--r--spec/controllers/projects/settings/repository_controller_spec.rb20
-rw-r--r--spec/controllers/projects/templates_controller_spec.rb110
-rw-r--r--spec/controllers/projects_controller_spec.rb47
-rw-r--r--spec/controllers/snippets/notes_controller_spec.rb113
-rw-r--r--spec/controllers/snippets_controller_spec.rb4
-rw-r--r--spec/controllers/uploads_controller_spec.rb177
-rw-r--r--spec/factories/releases.rb1
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb2
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb14
-rw-r--r--spec/features/discussion_comments/commit_spec.rb4
-rw-r--r--spec/features/discussion_comments/issue_spec.rb4
-rw-r--r--spec/features/discussion_comments/merge_request_spec.rb4
-rw-r--r--spec/features/discussion_comments/snippets_spec.rb4
-rw-r--r--spec/features/groups/issues_spec.rb19
-rw-r--r--spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb32
-rw-r--r--spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb24
-rw-r--r--spec/features/issues/todo_spec.rb8
-rw-r--r--spec/features/issues/user_creates_confidential_merge_request_spec.rb54
-rw-r--r--spec/features/merge_request/user_comments_on_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb174
-rw-r--r--spec/features/merge_request/user_sees_discussions_spec.rb8
-rw-r--r--spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb12
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb62
-rw-r--r--spec/features/merge_request/user_suggests_changes_on_diff_spec.rb4
-rw-r--r--spec/features/oauth_login_spec.rb12
-rw-r--r--spec/features/projects/releases/user_views_releases_spec.rb12
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb6
-rw-r--r--spec/features/snippets/user_creates_snippet_spec.rb2
-rw-r--r--spec/finders/releases_finder_spec.rb6
-rw-r--r--spec/finders/runner_jobs_finder_spec.rb22
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json4
-rw-r--r--spec/frontend/boards/services/board_service_spec.js4
-rw-r--r--spec/frontend/branches/divergence_graph_spec.js12
-rw-r--r--spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap101
-rw-r--r--spec/frontend/confidential_merge_request/components/dropdown_spec.js56
-rw-r--r--spec/frontend/confidential_merge_request/components/project_form_group_spec.js77
-rw-r--r--spec/frontend/create_merge_request_dropdown_spec.js (renamed from spec/javascripts/create_merge_request_dropdown_spec.js)9
-rw-r--r--spec/frontend/diffs/components/diff_discussion_reply_spec.js90
-rw-r--r--spec/frontend/diffs/components/diff_gutter_avatars_spec.js113
-rw-r--r--spec/frontend/diffs/mock_data/diff_discussions.js529
-rw-r--r--spec/frontend/error_tracking_settings/components/app_spec.js (renamed from spec/javascripts/error_tracking_settings/components/app_spec.js)2
-rw-r--r--spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js (renamed from spec/javascripts/error_tracking_settings/components/error_tracking_form_spec.js)0
-rw-r--r--spec/frontend/error_tracking_settings/components/project_dropdown_spec.js (renamed from spec/javascripts/error_tracking_settings/components/project_dropdown_spec.js)0
-rw-r--r--spec/frontend/error_tracking_settings/mock.js (renamed from spec/javascripts/error_tracking_settings/mock.js)2
-rw-r--r--spec/frontend/error_tracking_settings/store/actions_spec.js (renamed from spec/javascripts/error_tracking_settings/store/actions_spec.js)11
-rw-r--r--spec/frontend/error_tracking_settings/store/getters_spec.js (renamed from spec/javascripts/error_tracking_settings/store/getters_spec.js)0
-rw-r--r--spec/frontend/error_tracking_settings/store/mutation_spec.js (renamed from spec/javascripts/error_tracking_settings/store/mutation_spec.js)2
-rw-r--r--spec/frontend/error_tracking_settings/utils_spec.js (renamed from spec/javascripts/error_tracking_settings/utils_spec.js)0
-rw-r--r--spec/frontend/lib/utils/text_utility_spec.js19
-rw-r--r--spec/frontend/monitoring/__snapshots__/dashboard_state_spec.js.snap37
-rw-r--r--spec/frontend/monitoring/dashboard_state_spec.js43
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js2
-rw-r--r--spec/frontend/notes/components/discussion_reply_placeholder_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js (renamed from spec/javascripts/vue_shared/components/markdown/header_spec.js)14
-rw-r--r--spec/graphql/gitlab_schema_spec.rb4
-rw-r--r--spec/graphql/types/base_field_spec.rb59
-rw-r--r--spec/graphql/types/label_type_spec.rb2
-rw-r--r--spec/graphql/types/metadata_type_spec.rb1
-rw-r--r--spec/graphql/types/namespace_type_spec.rb2
-rw-r--r--spec/graphql/types/query_type_spec.rb4
-rw-r--r--spec/helpers/preferences_helper_spec.rb2
-rw-r--r--spec/javascripts/boards/mock_data.js5
-rw-r--r--spec/javascripts/collapsed_sidebar_todo_spec.js16
-rw-r--r--spec/javascripts/diffs/components/diff_file_header_spec.js6
-rw-r--r--spec/javascripts/diffs/components/diff_gutter_avatars_spec.js146
-rw-r--r--spec/javascripts/diffs/components/inline_diff_view_spec.js3
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js1
-rw-r--r--spec/javascripts/environments/environment_terminal_button_spec.js40
-rw-r--r--spec/javascripts/monitoring/dashboard_state_spec.js101
-rw-r--r--spec/javascripts/monitoring/store/utils_spec.js37
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js52
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js16
-rw-r--r--spec/javascripts/performance_bar/components/simple_metric_spec.js47
-rw-r--r--spec/javascripts/releases/components/release_block_spec.js12
-rw-r--r--spec/javascripts/sidebar/todo_spec.js8
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js8
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb5
-rw-r--r--spec/lib/feature_spec.rb62
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb68
-rw-r--r--spec/lib/gitlab/auth_spec.rb9
-rw-r--r--spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb21
-rw-r--r--spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb74
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb23
-rw-r--r--spec/lib/gitlab/config/loader/yaml_spec.rb72
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb21
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb13
-rw-r--r--spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb238
-rw-r--r--spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb1805
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb1918
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb7
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb4
-rw-r--r--spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb82
-rw-r--r--spec/lib/gitlab/graphql/calls_gitaly/instrumentation_spec.rb23
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/lib/gitlab/issuable_metadata_spec.rb8
-rw-r--r--spec/lib/gitlab/legacy_github_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb4
-rw-r--r--spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb16
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb12
-rw-r--r--spec/lib/gitlab/namespaced_session_store_spec.rb30
-rw-r--r--spec/lib/gitlab/performance_bar_spec.rb27
-rw-r--r--spec/lib/gitlab/user_extractor_spec.rb78
-rw-r--r--spec/lib/gitlab/utils/deep_size_spec.rb43
-rw-r--r--spec/migrations/backfill_and_add_not_null_constraint_to_released_at_column_on_releases_table_spec.rb28
-rw-r--r--spec/migrations/backfill_store_project_full_path_in_repo_spec.rb2
-rw-r--r--spec/migrations/remove_project_labels_group_id_spec.rb21
-rw-r--r--spec/models/ci/pipeline_spec.rb24
-rw-r--r--spec/models/deploy_token_spec.rb35
-rw-r--r--spec/models/deployment_spec.rb6
-rw-r--r--spec/models/merge_request_spec.rb115
-rw-r--r--spec/models/namespace/aggregation_schedule_spec.rb31
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb229
-rw-r--r--spec/models/release_spec.rb10
-rw-r--r--spec/models/repository_spec.rb5
-rw-r--r--spec/requests/api/graphql/namespace/projects_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/repository_spec.rb24
-rw-r--r--spec/requests/api/graphql_spec.rb31
-rw-r--r--spec/requests/api/issues/get_group_issues_spec.rb30
-rw-r--r--spec/requests/api/issues/get_project_issues_spec.rb58
-rw-r--r--spec/requests/api/issues/issues_spec.rb31
-rw-r--r--spec/requests/api/merge_requests_spec.rb38
-rw-r--r--spec/requests/api/projects_spec.rb2
-rw-r--r--spec/requests/api/releases_spec.rb81
-rw-r--r--spec/requests/api/runners_spec.rb44
-rw-r--r--spec/requests/rack_attack_global_spec.rb43
-rw-r--r--spec/routing/project_routing_spec.rb20
-rw-r--r--spec/routing/uploads_routing_spec.rb13
-rw-r--r--spec/rubocop/cop/graphql/authorize_types_spec.rb66
-rw-r--r--spec/serializers/test_case_entity_spec.rb2
-rw-r--r--spec/serializers/test_reports_comparer_entity_spec.rb8
-rw-r--r--spec/serializers/test_reports_comparer_serializer_spec.rb8
-rw-r--r--spec/serializers/test_suite_comparer_entity_spec.rb92
-rw-r--r--spec/services/auto_merge/base_service_spec.rb45
-rw-r--r--spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb16
-rw-r--r--spec/services/auto_merge_service_spec.rb25
-rw-r--r--spec/services/branches/diverging_commit_counts_service_spec.rb33
-rw-r--r--spec/services/deploy_tokens/create_service_spec.rb16
-rw-r--r--spec/services/merge_requests/merge_to_ref_service_spec.rb55
-rw-r--r--spec/services/merge_requests/rebase_service_spec.rb28
-rw-r--r--spec/services/system_note_service_spec.rb44
-rw-r--r--spec/support/features/discussion_comments_shared_example.rb40
-rw-r--r--spec/support/features/resolving_discussions_in_issues_shared_examples.rb2
-rw-r--r--spec/support/helpers/devise_helpers.rb12
-rw-r--r--spec/support/helpers/fake_u2f_device.rb4
-rw-r--r--spec/support/helpers/login_helpers.rb12
-rw-r--r--spec/support/helpers/position_tracer_helpers.rb93
-rw-r--r--spec/support/matchers/gitaly_matchers.rb2
-rw-r--r--spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/issues/merge_requests_count_shared_examples.rb37
-rw-r--r--spec/support/test_reports/test_reports_helper.rb34
-rw-r--r--spec/uploaders/file_mover_spec.rb132
-rw-r--r--spec/uploaders/file_uploader_spec.rb62
-rw-r--r--spec/validators/color_validator_spec.rb43
-rw-r--r--spec/workers/namespaces/schedule_aggregation_worker_spec.rb39
-rw-r--r--tmp/prometheus_multiproc_dir/puma/.gitkeep0
-rw-r--r--tmp/prometheus_multiproc_dir/sidekiq/.gitkeep0
-rw-r--r--tmp/prometheus_multiproc_dir/unicorn/.gitkeep0
-rw-r--r--yarn.lock108
608 files changed, 12615 insertions, 7013 deletions
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index 0c0985decdd..45a6a177943 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -8,7 +8,7 @@
.use-pg: &use-pg
services:
- - name: postgres:9.6
+ - name: postgres:9.6.11
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index f2ada743bf3..01c96e06547 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -1,6 +1,6 @@
.use-pg: &use-pg
services:
- - name: postgres:9.6
+ - name: postgres:9.6.11
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 61fd48fd72e..ce019de213b 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -77,7 +77,7 @@ schedule:review-build-cng:
.review-deploy-base: &review-deploy-base
<<: *review-base
allow_failure: true
- retry: 2
+ retry: 1
stage: review
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 698570efb07..41714eefa97 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -466,12 +466,10 @@ Rails/LinkToBlank:
Rails/Presence:
Exclude:
- 'app/models/ci/pipeline.rb'
- - 'app/models/clusters/platforms/kubernetes.rb'
- 'app/models/concerns/mentionable.rb'
- 'app/models/project_services/hipchat_service.rb'
- 'app/models/project_services/irker_service.rb'
- 'app/models/project_services/jira_service.rb'
- - 'app/models/project_services/kubernetes_service.rb'
- 'app/models/project_services/packagist_service.rb'
- 'app/models/wiki_page.rb'
- 'lib/gitlab/github_import/importer/releases_importer.rb'
@@ -514,7 +512,6 @@ Security/YAMLLoad:
- 'spec/config/mail_room_spec.rb'
- 'spec/initializers/secret_token_spec.rb'
- 'spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb'
- - 'spec/models/project_services/kubernetes_service_spec.rb'
# Offense count: 34
# Configuration parameters: EnforcedStyle.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8d4509e370d..0b73585722f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,23 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 12.0.3 (2019-06-27)
+
+- No changes.
+### Security (10 changes)
+
+- Persist tmp snippet uploads at users.
+- Gate MR head_pipeline behind read_pipeline ability.
+- Fix DoS vulnerability in color validation regex.
+- Expose merge requests count based on user access.
+- Fix Denial of Service for comments when rendering issues/MR comments.
+- Add missing authorizations in GraphQL.
+- Disable Rails SQL query cache when applying service templates.
+- Prevent Billion Laughs attack.
+- Correctly check permissions when creating snippet notes.
+- Prevent the detection of merge request templates by unauthorized users.
+
+
## 12.0.2 (2019-06-25)
### Fixed (7 changes, 1 of them is from the community)
@@ -555,6 +572,27 @@ entry.
- Add some frozen string to spec/**/*.rb. (gfyoung)
+## 11.10.8 (2019-06-27)
+
+- No changes.
+### Security (10 changes)
+
+- Fix Denial of Service for comments when rendering issues/MR comments.
+- Gate MR head_pipeline behind read_pipeline ability.
+- Fix DoS vulnerability in color validation regex.
+- Expose merge requests count based on user access.
+- Persist tmp snippet uploads at users.
+- Add missing authorizations in GraphQL.
+- Disable Rails SQL query cache when applying service templates.
+- Prevent Billion Laughs attack.
+- Correctly check permissions when creating snippet notes.
+- Prevent the detection of merge request templates by unauthorized users.
+
+### Performance (1 change)
+
+- Add improvements to global search of issues and merge requests. !27817
+
+
## 11.10.6 (2019-06-04)
### Fixed (7 changes, 1 of them is from the community)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 7f3a46a841e..ba0a719118c 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.49.0
+1.51.0
diff --git a/Gemfile b/Gemfile
index 1264d75eac6..b1750afec91 100644
--- a/Gemfile
+++ b/Gemfile
@@ -429,7 +429,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 1.32.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 1.36.0', require: 'gitaly'
gem 'grpc', '~> 1.19.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 5b648d43137..5099167e03f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -303,7 +303,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (1.32.0)
+ gitaly-proto (1.36.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-labkit (0.3.0)
@@ -1092,7 +1092,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 1.32.0)
+ gitaly-proto (~> 1.36.0)
github-markup (~> 1.7.0)
gitlab-labkit (~> 0.3.0)
gitlab-markup (~> 1.7.0)
diff --git a/app/assets/images/auth_buttons/salesforce_64.png b/app/assets/images/auth_buttons/salesforce_64.png
new file mode 100644
index 00000000000..c8a86a0c515
--- /dev/null
+++ b/app/assets/images/auth_buttons/salesforce_64.png
Binary files differ
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index a020765f335..23b107abefa 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import Vue from 'vue';
+import mountMultipleBoardsSwitcher from 'ee_else_ce/boards/mount_multiple_boards_switcher';
import Flash from '~/flash';
import { __ } from '~/locale';
import './models/label';
@@ -20,7 +21,7 @@ import modalMixin from './mixins/modal_mixins';
import './filters/due_date_filters';
import Board from './components/board';
import BoardSidebar from './components/board_sidebar';
-import initNewListDropdown from './components/new_list_dropdown';
+import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
import BoardAddIssuesModal from './components/modal/index.vue';
import '~/vue_shared/vue_resource_interceptor';
import {
@@ -78,13 +79,14 @@ export default () => {
},
},
created() {
- gl.boardService = new BoardService({
+ boardsStore.setEndpoints({
boardsEndpoint: this.boardsEndpoint,
recentBoardsEndpoint: this.recentBoardsEndpoint,
listsEndpoint: this.listsEndpoint,
bulkUpdatePath: this.bulkUpdatePath,
boardId: this.boardId,
});
+ gl.boardService = new BoardService();
boardsStore.rootPath = this.boardsEndpoint;
eventHub.$on('updateTokens', this.updateTokens);
@@ -278,4 +280,6 @@ export default () => {
`,
});
}
+
+ mountMultipleBoardsSwitcher();
};
diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
new file mode 100644
index 00000000000..bdb14a7f2f2
--- /dev/null
+++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
@@ -0,0 +1,2 @@
+// this will be moved from EE to CE as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/53811
+export default () => {};
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index 7d463f17ab1..580d04a3649 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -1,106 +1,66 @@
-import axios from '../../lib/utils/axios_utils';
-import { mergeUrlParams } from '../../lib/utils/url_utility';
+/* eslint-disable class-methods-use-this */
-export default class BoardService {
- constructor({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) {
- this.boardsEndpoint = boardsEndpoint;
- this.boardId = boardId;
- this.listsEndpoint = listsEndpoint;
- this.listsEndpointGenerate = `${listsEndpoint}/generate.json`;
- this.bulkUpdatePath = bulkUpdatePath;
- this.recentBoardsEndpoint = `${recentBoardsEndpoint}.json`;
- }
+import boardsStore from '~/boards/stores/boards_store';
+export default class BoardService {
generateBoardsPath(id) {
- return `${this.boardsEndpoint}${id ? `/${id}` : ''}.json`;
+ return boardsStore.generateBoardsPath(id);
}
generateIssuesPath(id) {
- return `${this.listsEndpoint}${id ? `/${id}` : ''}/issues`;
+ return boardsStore.generateIssuesPath(id);
}
static generateIssuePath(boardId, id) {
- return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${
- id ? `/${id}` : ''
- }`;
+ return boardsStore.generateIssuePath(boardId, id);
}
all() {
- return axios.get(this.listsEndpoint);
+ return boardsStore.all();
}
generateDefaultLists() {
- return axios.post(this.listsEndpointGenerate, {});
+ return boardsStore.generateDefaultLists();
}
createList(entityId, entityType) {
- const list = {
- [entityType]: entityId,
- };
-
- return axios.post(this.listsEndpoint, {
- list,
- });
+ return boardsStore.createList(entityId, entityType);
}
updateList(id, position) {
- return axios.put(`${this.listsEndpoint}/${id}`, {
- list: {
- position,
- },
- });
+ return boardsStore.updateList(id, position);
}
destroyList(id) {
- return axios.delete(`${this.listsEndpoint}/${id}`);
+ return boardsStore.destroyList(id);
}
getIssuesForList(id, filter = {}) {
- const data = { id };
- Object.keys(filter).forEach(key => {
- data[key] = filter[key];
- });
-
- return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
+ return boardsStore.getIssuesForList(id, filter);
}
moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) {
- return axios.put(BoardService.generateIssuePath(this.boardId, id), {
- from_list_id: fromListId,
- to_list_id: toListId,
- move_before_id: moveBeforeId,
- move_after_id: moveAfterId,
- });
+ return boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId);
}
newIssue(id, issue) {
- return axios.post(this.generateIssuesPath(id), {
- issue,
- });
+ return boardsStore.newIssue(id, issue);
}
getBacklog(data) {
- return axios.get(
- mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`),
- );
+ return boardsStore.getBacklog(data);
}
bulkUpdate(issueIds, extraData = {}) {
- const data = {
- update: Object.assign(extraData, {
- issuable_ids: issueIds.join(','),
- }),
- };
-
- return axios.post(this.bulkUpdatePath, data);
+ return boardsStore.bulkUpdate(issueIds, extraData);
}
static getIssueInfo(endpoint) {
- return axios.get(endpoint);
+ return boardsStore.getIssueInfo(endpoint);
}
static toggleIssueSubscription(endpoint) {
- return axios.post(endpoint);
+ return boardsStore.toggleIssueSubscription(endpoint);
}
}
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 4ba4cde6bae..b9cd4a143ef 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -8,6 +8,8 @@ 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 axios from '~/lib/utils/axios_utils';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '../eventhub';
const boardsStore = {
@@ -28,6 +30,7 @@ const boardsStore = {
},
currentPage: '',
reload: false,
+ endpoints: {},
},
detail: {
issue: {},
@@ -36,6 +39,19 @@ const boardsStore = {
issue: {},
list: {},
},
+
+ setEndpoints({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) {
+ const listsEndpointGenerate = `${listsEndpoint}/generate.json`;
+ this.state.endpoints = {
+ boardsEndpoint,
+ boardId,
+ listsEndpoint,
+ listsEndpointGenerate,
+ bulkUpdatePath,
+ recentBoardsEndpoint: `${recentBoardsEndpoint}.json`,
+ };
+ },
+
create() {
this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&');
@@ -229,6 +245,101 @@ const boardsStore = {
setTimeTrackingLimitToHours(limitToHours) {
this.timeTracking.limitToHours = parseBoolean(limitToHours);
},
+
+ generateBoardsPath(id) {
+ return `${this.state.endpoints.boardsEndpoint}${id ? `/${id}` : ''}.json`;
+ },
+
+ generateIssuesPath(id) {
+ return `${this.state.endpoints.listsEndpoint}${id ? `/${id}` : ''}/issues`;
+ },
+
+ generateIssuePath(boardId, id) {
+ return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${
+ id ? `/${id}` : ''
+ }`;
+ },
+
+ all() {
+ return axios.get(this.state.endpoints.listsEndpoint);
+ },
+
+ generateDefaultLists() {
+ return axios.post(this.state.endpoints.listsEndpointGenerate, {});
+ },
+
+ createList(entityId, entityType) {
+ const list = {
+ [entityType]: entityId,
+ };
+
+ return axios.post(this.state.endpoints.listsEndpoint, {
+ list,
+ });
+ },
+
+ updateList(id, position) {
+ return axios.put(`${this.state.endpoints.listsEndpoint}/${id}`, {
+ list: {
+ position,
+ },
+ });
+ },
+
+ destroyList(id) {
+ return axios.delete(`${this.state.endpoints.listsEndpoint}/${id}`);
+ },
+
+ getIssuesForList(id, filter = {}) {
+ const data = { id };
+ Object.keys(filter).forEach(key => {
+ data[key] = filter[key];
+ });
+
+ return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
+ },
+
+ moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) {
+ return axios.put(this.generateIssuePath(this.state.endpoints.boardId, id), {
+ from_list_id: fromListId,
+ to_list_id: toListId,
+ move_before_id: moveBeforeId,
+ move_after_id: moveAfterId,
+ });
+ },
+
+ newIssue(id, issue) {
+ return axios.post(this.generateIssuesPath(id), {
+ issue,
+ });
+ },
+
+ getBacklog(data) {
+ return axios.get(
+ mergeUrlParams(
+ data,
+ `${gon.relative_url_root}/-/boards/${this.state.endpoints.boardId}/issues.json`,
+ ),
+ );
+ },
+
+ bulkUpdate(issueIds, extraData = {}) {
+ const data = {
+ update: Object.assign(extraData, {
+ issuable_ids: issueIds.join(','),
+ }),
+ };
+
+ return axios.post(this.state.endpoints.bulkUpdatePath, data);
+ },
+
+ getIssueInfo(endpoint) {
+ return axios.get(endpoint);
+ },
+
+ toggleIssueSubscription(endpoint) {
+ return axios.post(endpoint);
+ },
};
BoardsStoreEE.initEESpecific(boardsStore);
diff --git a/app/assets/javascripts/branches/divergence_graph.js b/app/assets/javascripts/branches/divergence_graph.js
index 96bc6a5f8e8..7dbaf984acf 100644
--- a/app/assets/javascripts/branches/divergence_graph.js
+++ b/app/assets/javascripts/branches/divergence_graph.js
@@ -36,7 +36,9 @@ export default endpoint => {
}, 100);
Object.entries(data).forEach(([branchName, val]) => {
- const el = document.querySelector(`.js-branch-${branchName} .js-branch-divergence-graph`);
+ const el = document.querySelector(
+ `[data-name="${branchName}"] .js-branch-divergence-graph`,
+ );
if (!el) return;
diff --git a/app/assets/javascripts/confidential_merge_request/components/dropdown.vue b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue
new file mode 100644
index 00000000000..444640980af
--- /dev/null
+++ b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue
@@ -0,0 +1,58 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { __ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ Icon,
+ },
+ props: {
+ projects: {
+ type: Array,
+ required: true,
+ },
+ selectedProject: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ computed: {
+ dropdownText() {
+ if (Object.keys(this.selectedProject).length) {
+ return this.selectedProject.name;
+ }
+
+ return __('Select private project');
+ },
+ },
+ methods: {
+ selectProject(project) {
+ this.$emit('click', project);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown toggle-class="d-flex align-items-center w-100" class="w-100">
+ <template slot="button-content">
+ <span class="str-truncated-100 mr-2">
+ <icon name="lock" />
+ {{ dropdownText }}
+ </span>
+ <icon name="chevron-down" class="ml-auto" />
+ </template>
+ <gl-dropdown-item v-for="project in projects" :key="project.id" @click="selectProject(project)">
+ <icon
+ name="mobile-issue-close"
+ :class="{ icon: project.id !== selectedProject.id }"
+ class="js-active-project-check"
+ />
+ <span class="ml-1">{{ project.name }}</span>
+ </gl-dropdown-item>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
new file mode 100644
index 00000000000..b89729375be
--- /dev/null
+++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
@@ -0,0 +1,136 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import { __, sprintf } from '../../locale';
+import createFlash from '../../flash';
+import Api from '../../api';
+import state from '../state';
+import Dropdown from './dropdown.vue';
+
+export default {
+ components: {
+ GlLink,
+ Dropdown,
+ },
+ props: {
+ namespacePath: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ newForkPath: {
+ type: String,
+ required: true,
+ },
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ projects: [],
+ };
+ },
+ computed: {
+ selectedProject() {
+ return state.selectedProject;
+ },
+ noForkText() {
+ return sprintf(
+ __(
+ 'To protect this issues confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private.',
+ ),
+ { link_start: `<a href="${this.newForkPath}" class="help-link">`, link_end: '</a>' },
+ false,
+ );
+ },
+ },
+ mounted() {
+ this.fetchProjects();
+ this.createBtn = document.querySelector('.js-create-target');
+ this.warningText = document.querySelector('.js-exposed-info-warning');
+ },
+ methods: {
+ selectProject(project) {
+ if (project) {
+ Object.assign(state, {
+ selectedProject: project,
+ });
+
+ if (project.namespaceFullPath !== this.namespacePath) {
+ this.showWarning();
+ }
+ } else if (this.createBtn) {
+ this.createBtn.setAttribute('disabled', 'disabled');
+ }
+ },
+ normalizeProjectData(data) {
+ return data.map(p => ({
+ id: p.id,
+ name: p.name_with_namespace,
+ pathWithNamespace: p.path_with_namespace,
+ namespaceFullpath: p.namespace.full_path,
+ }));
+ },
+ fetchProjects() {
+ Api.projectForks(this.projectPath, {
+ with_merge_requests_enabled: true,
+ min_access_level: 30,
+ visibility: 'private',
+ })
+ .then(({ data }) => {
+ this.projects = this.normalizeProjectData(data);
+ this.selectProject(this.projects[0]);
+ })
+ .catch(e => {
+ createFlash(__('Error fetching forked projects. Please try again.'));
+ throw e;
+ });
+ },
+ showWarning() {
+ if (this.warningText) {
+ this.warningText.classList.remove('hidden');
+ }
+
+ if (this.createBtn) {
+ this.createBtn.classList.add('btn-warning');
+ this.createBtn.classList.remove('btn-success');
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="form-group">
+ <label>{{ __('Project') }}</label>
+ <div>
+ <dropdown
+ v-if="projects.length"
+ :projects="projects"
+ :selected-project="selectedProject"
+ @click="selectProject"
+ />
+ <p class="text-muted mt-1 mb-0">
+ <template v-if="projects.length">
+ {{
+ __(
+ 'To protect this issues confidentiality, a private fork of this project was selected.',
+ )
+ }}
+ </template>
+ <template v-else>
+ {{ __('No forks available to you.') }}<br />
+ <span v-html="noForkText"></span>
+ </template>
+ <gl-link :href="helpPagePath" class="help-link" target="_blank">
+ <span class="sr-only">{{ __('Read more') }}</span>
+ <i class="fa fa-question-circle" aria-hidden="true"></i>
+ </gl-link>
+ </p>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/confidential_merge_request/index.js b/app/assets/javascripts/confidential_merge_request/index.js
new file mode 100644
index 00000000000..9672821d30e
--- /dev/null
+++ b/app/assets/javascripts/confidential_merge_request/index.js
@@ -0,0 +1,30 @@
+import Vue from 'vue';
+import { parseBoolean } from '../lib/utils/common_utils';
+import ProjectFormGroup from './components/project_form_group.vue';
+import state from './state';
+
+export function isConfidentialIssue() {
+ return parseBoolean(document.querySelector('.js-create-mr').dataset.isConfidential);
+}
+
+export function canCreateConfidentialMergeRequest() {
+ return isConfidentialIssue() && Object.keys(state.selectedProject).length > 0;
+}
+
+export function init() {
+ const el = document.getElementById('js-forked-project');
+
+ return new Vue({
+ el,
+ render(h) {
+ return h(ProjectFormGroup, {
+ props: {
+ namespacePath: el.dataset.namespacePath,
+ projectPath: el.dataset.projectPath,
+ newForkPath: el.dataset.newForkPath,
+ helpPagePath: el.dataset.helpPagePath,
+ },
+ });
+ },
+ });
+}
diff --git a/app/assets/javascripts/confidential_merge_request/state.js b/app/assets/javascripts/confidential_merge_request/state.js
new file mode 100644
index 00000000000..95b0580f4b9
--- /dev/null
+++ b/app/assets/javascripts/confidential_merge_request/state.js
@@ -0,0 +1,5 @@
+import Vue from 'vue';
+
+export default Vue.observable({
+ selectedProject: {},
+});
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index 8f5cece0788..052168bb21c 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -5,6 +5,12 @@ import Flash from './flash';
import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
import { __, sprintf } from './locale';
+import {
+ init as initConfidentialMergeRequest,
+ isConfidentialIssue,
+ canCreateConfidentialMergeRequest,
+} from './confidential_merge_request';
+import confidentialMergeRequestState from './confidential_merge_request/state';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter);
@@ -12,6 +18,17 @@ const InputSetter = Object.assign({}, ISetter);
const CREATE_MERGE_REQUEST = 'create-mr';
const CREATE_BRANCH = 'create-branch';
+function createEndpoint(projectPath, endpoint) {
+ if (canCreateConfidentialMergeRequest()) {
+ return endpoint.replace(
+ projectPath,
+ confidentialMergeRequestState.selectedProject.pathWithNamespace,
+ );
+ }
+
+ return endpoint;
+}
+
export default class CreateMergeRequestDropdown {
constructor(wrapperEl) {
this.wrapperEl = wrapperEl;
@@ -42,6 +59,8 @@ export default class CreateMergeRequestDropdown {
this.refIsValid = true;
this.refsPath = this.wrapperEl.dataset.refsPath;
this.suggestedRef = this.refInput.value;
+ this.projectPath = this.wrapperEl.dataset.projectPath;
+ this.projectId = this.wrapperEl.dataset.projectId;
// These regexps are used to replace
// a backend generated new branch name and its source (ref)
@@ -58,6 +77,14 @@ export default class CreateMergeRequestDropdown {
};
this.init();
+
+ if (isConfidentialIssue()) {
+ this.createMergeRequestButton.setAttribute(
+ 'data-dropdown-trigger',
+ '#create-merge-request-dropdown',
+ );
+ initConfidentialMergeRequest();
+ }
}
available() {
@@ -113,7 +140,9 @@ export default class CreateMergeRequestDropdown {
this.isCreatingBranch = true;
return axios
- .post(this.createBranchPath)
+ .post(createEndpoint(this.projectPath, this.createBranchPath), {
+ confidential_issue_project_id: canCreateConfidentialMergeRequest() ? this.projectId : null,
+ })
.then(({ data }) => {
this.branchCreated = true;
window.location.href = data.url;
@@ -125,7 +154,11 @@ export default class CreateMergeRequestDropdown {
this.isCreatingMergeRequest = true;
return axios
- .post(this.createMrPath)
+ .post(this.createMrPath, {
+ target_project_id: canCreateConfidentialMergeRequest()
+ ? confidentialMergeRequestState.selectedProject.id
+ : null,
+ })
.then(({ data }) => {
this.mergeRequestCreated = true;
window.location.href = data.url;
@@ -149,6 +182,8 @@ export default class CreateMergeRequestDropdown {
}
enable() {
+ if (!canCreateConfidentialMergeRequest()) return;
+
this.createMergeRequestButton.classList.remove('disabled');
this.createMergeRequestButton.removeAttribute('disabled');
@@ -205,7 +240,7 @@ export default class CreateMergeRequestDropdown {
if (!ref) return false;
return axios
- .get(`${this.refsPath}${encodeURIComponent(ref)}`)
+ .get(`${createEndpoint(this.projectPath, this.refsPath)}${encodeURIComponent(ref)}`)
.then(({ data }) => {
const branches = data[Object.keys(data)[0]];
const tags = data[Object.keys(data)[1]];
@@ -325,6 +360,12 @@ export default class CreateMergeRequestDropdown {
let xhr = null;
event.preventDefault();
+ if (isConfidentialIssue() && !event.target.classList.contains('js-create-target')) {
+ this.droplab.hooks.forEach(hook => hook.list.toggle());
+
+ return;
+ }
+
if (this.isBusy()) {
return;
}
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
index f66e07ba31a..7817b41514d 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
@@ -32,15 +32,15 @@ const CommentAndResolveBtn = Vue.extend({
buttonText: function() {
if (this.isDiscussionResolved) {
if (this.textareaIsEmpty) {
- return __('Unresolve discussion');
+ return __('Unresolve thread');
} else {
- return __('Comment & unresolve discussion');
+ return __('Comment & unresolve thread');
}
} else {
if (this.textareaIsEmpty) {
- return __('Resolve discussion');
+ return __('Resolve thread');
} else {
- return __('Comment & resolve discussion');
+ return __('Comment & resolve thread');
}
}
},
diff --git a/app/assets/javascripts/diffs/components/diff_discussion_reply.vue b/app/assets/javascripts/diffs/components/diff_discussion_reply.vue
new file mode 100644
index 00000000000..2aa5e9b3339
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_discussion_reply.vue
@@ -0,0 +1,55 @@
+<script>
+import { mapGetters } from 'vuex';
+import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue';
+import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+
+export default {
+ name: 'DiffDiscussionReply',
+ components: {
+ NoteSignedOutWidget,
+ ReplyPlaceholder,
+ UserAvatarLink,
+ },
+ props: {
+ hasForm: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ renderReplyPlaceholder: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapGetters({
+ currentUser: 'getUserData',
+ userCanReply: 'userCanReply',
+ }),
+ },
+};
+</script>
+
+<template>
+ <div class="discussion-reply-holder d-flex clearfix">
+ <template v-if="userCanReply">
+ <slot v-if="hasForm" name="form"></slot>
+ <template v-else-if="renderReplyPlaceholder">
+ <user-avatar-link
+ :link-href="currentUser.path"
+ :img-src="currentUser.avatar_url"
+ :img-alt="currentUser.name"
+ :img-size="40"
+ class="d-none d-sm-block"
+ />
+ <reply-placeholder
+ class="qa-discussion-reply"
+ :button-text="__('Start a new discussion...')"
+ @onClick="$emit('showNewDiscussionForm')"
+ />
+ </template>
+ </template>
+ <note-signed-out-widget v-else />
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index 4c73eea4049..b0460bacff2 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -80,7 +80,6 @@ export default {
v-show="isExpanded(discussion)"
:discussion="discussion"
:render-diff-file="false"
- :always-expanded="true"
:discussions-by-diff-order="true"
:line="line"
:help-page-path="helpPagePath"
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index eb9f1465945..4b226e30699 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -151,7 +151,11 @@ export default {
stickyMonitor(this.$refs.header, contentTop() - fileHeaderHeight - 1, false);
},
methods: {
- ...mapActions('diffs', ['toggleFileDiscussions', 'toggleFullDiff']),
+ ...mapActions('diffs', [
+ 'toggleFileDiscussions',
+ 'toggleFileDiscussionWrappers',
+ 'toggleFullDiff',
+ ]),
handleToggleFile(e, checkTarget) {
if (
!checkTarget ||
@@ -165,7 +169,7 @@ export default {
this.$emit('showForkMessage');
},
handleToggleDiscussions() {
- this.toggleFileDiscussions(this.diffFile);
+ this.toggleFileDiscussionWrappers(this.diffFile);
},
handleFileNameClick(e) {
const isLinkToOtherPage =
diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
index e28909b7be3..af5550aec3b 100644
--- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -1,5 +1,4 @@
<script>
-import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize, truncate } from '~/lib/utils/text_utility';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
@@ -19,11 +18,13 @@ export default {
type: Array,
required: true,
},
+ discussionsExpanded: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
- discussionsExpanded() {
- return this.discussions.every(discussion => discussion.expanded);
- },
allDiscussions() {
return this.discussions.reduce((acc, note) => acc.concat(note.notes), []);
},
@@ -45,26 +46,14 @@ export default {
},
},
methods: {
- ...mapActions(['toggleDiscussion']),
getTooltipText(noteData) {
let { note } = noteData;
-
if (note.length > LENGTH_OF_AVATAR_TOOLTIP) {
note = truncate(note, LENGTH_OF_AVATAR_TOOLTIP);
}
return `${noteData.author.name}: ${note}`;
},
- toggleDiscussions() {
- const forceExpanded = this.discussions.some(discussion => !discussion.expanded);
-
- this.discussions.forEach(discussion => {
- this.toggleDiscussion({
- discussionId: discussion.id,
- forceExpanded,
- });
- });
- },
},
};
</script>
@@ -76,7 +65,7 @@ export default {
type="button"
:aria-label="__('Show comments')"
class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button"
- @click="toggleDiscussions"
+ @click="$emit('toggleLineDiscussions')"
>
<icon :size="12" name="collapse" />
</button>
@@ -87,7 +76,7 @@ export default {
:img-src="note.author.avatar_url"
:tooltip-text="getTooltipText(note)"
class="diff-comment-avatar js-diff-comment-avatar"
- @click.native="toggleDiscussions"
+ @click.native="$emit('toggleLineDiscussions')"
/>
<span
v-if="moreText"
@@ -97,7 +86,7 @@ export default {
data-container="body"
data-placement="top"
role="button"
- @click="toggleDiscussions"
+ @click="$emit('toggleLineDiscussions')"
>+{{ moreCount }}</span
>
</template>
diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
index 1281f9b17ef..351110f0a87 100644
--- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -105,7 +105,13 @@ export default {
},
},
methods: {
- ...mapActions('diffs', ['loadMoreLines', 'showCommentForm', 'setHighlightedRow']),
+ ...mapActions('diffs', [
+ 'loadMoreLines',
+ 'showCommentForm',
+ 'setHighlightedRow',
+ 'toggleLineDiscussions',
+ 'toggleLineDiscussionWrappers',
+ ]),
handleCommentButton() {
this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
},
@@ -184,7 +190,14 @@ export default {
@click="setHighlightedRow(lineCode)"
>
</a>
- <diff-gutter-avatars v-if="shouldShowAvatarsOnGutter" :discussions="line.discussions" />
+ <diff-gutter-avatars
+ v-if="shouldShowAvatarsOnGutter"
+ :discussions="line.discussions"
+ :discussions-expanded="line.discussionsExpanded"
+ @toggleLineDiscussions="
+ toggleLineDiscussions({ lineCode, fileHash, expanded: !line.discussionsExpanded })
+ "
+ />
</template>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
index 1faa0493e79..ca3285e9afd 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
@@ -1,11 +1,14 @@
<script>
-import diffDiscussions from './diff_discussions.vue';
-import diffLineNoteForm from './diff_line_note_form.vue';
+import { mapActions } from 'vuex';
+import DiffDiscussions from './diff_discussions.vue';
+import DiffLineNoteForm from './diff_line_note_form.vue';
+import DiffDiscussionReply from './diff_discussion_reply.vue';
export default {
components: {
- diffDiscussions,
- diffLineNoteForm,
+ DiffDiscussions,
+ DiffLineNoteForm,
+ DiffDiscussionReply,
},
props: {
line: {
@@ -32,10 +35,12 @@ export default {
if (!this.line.discussions || !this.line.discussions.length) {
return false;
}
-
- return this.line.discussions.every(discussion => discussion.expanded);
+ return this.line.discussionsExpanded;
},
},
+ methods: {
+ ...mapActions('diffs', ['showCommentForm']),
+ },
};
</script>
@@ -49,13 +54,22 @@ export default {
:discussions="line.discussions"
:help-page-path="helpPagePath"
/>
- <diff-line-note-form
- v-if="line.hasForm"
- :diff-file-hash="diffFileHash"
- :line="line"
- :note-target-line="line"
- :help-page-path="helpPagePath"
- />
+ <diff-discussion-reply
+ :has-form="line.hasForm"
+ :render-reply-placeholder="Boolean(line.discussions.length)"
+ @showNewDiscussionForm="
+ showCommentForm({ lineCode: line.line_code, fileHash: diffFileHash })
+ "
+ >
+ <template #form>
+ <diff-line-note-form
+ :diff-file-hash="diffFileHash"
+ :line="line"
+ :note-target-line="line"
+ :help-page-path="helpPagePath"
+ />
+ </template>
+ </diff-discussion-reply>
</div>
</td>
</tr>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
index d2e54edca85..c00b0e010ff 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
@@ -1,11 +1,14 @@
<script>
-import diffDiscussions from './diff_discussions.vue';
-import diffLineNoteForm from './diff_line_note_form.vue';
+import { mapActions } from 'vuex';
+import DiffDiscussions from './diff_discussions.vue';
+import DiffLineNoteForm from './diff_line_note_form.vue';
+import DiffDiscussionReply from './diff_discussion_reply.vue';
export default {
components: {
- diffDiscussions,
- diffLineNoteForm,
+ DiffDiscussions,
+ DiffLineNoteForm,
+ DiffDiscussionReply,
},
props: {
line: {
@@ -29,24 +32,30 @@ export default {
computed: {
hasExpandedDiscussionOnLeft() {
return this.line.left && this.line.left.discussions.length
- ? this.line.left.discussions.every(discussion => discussion.expanded)
+ ? this.line.left.discussionsExpanded
: false;
},
hasExpandedDiscussionOnRight() {
return this.line.right && this.line.right.discussions.length
- ? this.line.right.discussions.every(discussion => discussion.expanded)
+ ? this.line.right.discussionsExpanded
: false;
},
hasAnyExpandedDiscussion() {
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
},
shouldRenderDiscussionsOnLeft() {
- return this.line.left && this.line.left.discussions && this.hasExpandedDiscussionOnLeft;
+ return (
+ this.line.left &&
+ this.line.left.discussions &&
+ this.line.left.discussions.length &&
+ this.hasExpandedDiscussionOnLeft
+ );
},
shouldRenderDiscussionsOnRight() {
return (
this.line.right &&
this.line.right.discussions &&
+ this.line.right.discussions.length &&
this.hasExpandedDiscussionOnRight &&
this.line.right.type
);
@@ -81,6 +90,22 @@ export default {
return hasCommentFormOnLeft || hasCommentFormOnRight;
},
+ shouldRenderReplyPlaceholderOnLeft() {
+ return Boolean(
+ this.line.left && this.line.left.discussions && this.line.left.discussions.length,
+ );
+ },
+ shouldRenderReplyPlaceholderOnRight() {
+ return Boolean(
+ this.line.right && this.line.right.discussions && this.line.right.discussions.length,
+ );
+ },
+ },
+ methods: {
+ ...mapActions('diffs', ['showCommentForm']),
+ showNewDiscussionForm() {
+ this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.diffFileHash });
+ },
},
};
</script>
@@ -90,37 +115,49 @@ export default {
<td class="notes-content parallel old" colspan="2">
<div v-if="shouldRenderDiscussionsOnLeft" class="content">
<diff-discussions
- v-if="line.left.discussions.length"
:discussions="line.left.discussions"
:line="line.left"
:help-page-path="helpPagePath"
/>
</div>
- <diff-line-note-form
- v-if="showLeftSideCommentForm"
- :diff-file-hash="diffFileHash"
- :line="line.left"
- :note-target-line="line.left"
- :help-page-path="helpPagePath"
- line-position="left"
- />
+ <diff-discussion-reply
+ :has-form="showLeftSideCommentForm"
+ :render-reply-placeholder="shouldRenderReplyPlaceholderOnLeft"
+ @showNewDiscussionForm="showNewDiscussionForm"
+ >
+ <template #form>
+ <diff-line-note-form
+ :diff-file-hash="diffFileHash"
+ :line="line.left"
+ :note-target-line="line.left"
+ :help-page-path="helpPagePath"
+ line-position="left"
+ />
+ </template>
+ </diff-discussion-reply>
</td>
<td class="notes-content parallel new" colspan="2">
<div v-if="shouldRenderDiscussionsOnRight" class="content">
<diff-discussions
- v-if="line.right.discussions.length"
:discussions="line.right.discussions"
:line="line.right"
:help-page-path="helpPagePath"
/>
</div>
- <diff-line-note-form
- v-if="showRightSideCommentForm"
- :diff-file-hash="diffFileHash"
- :line="line.right"
- :note-target-line="line.right"
- line-position="right"
- />
+ <diff-discussion-reply
+ :has-form="showRightSideCommentForm"
+ :render-reply-placeholder="shouldRenderReplyPlaceholderOnRight"
+ @showNewDiscussionForm="showNewDiscussionForm"
+ >
+ <template #form>
+ <diff-line-note-form
+ :diff-file-hash="diffFileHash"
+ :line="line.right"
+ :note-target-line="line.right"
+ line-position="right"
+ />
+ </template>
+ </diff-discussion-reply>
</td>
</tr>
</template>
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 88d7b4bba63..32e0d8f42ee 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -12,6 +12,7 @@ import {
getNoteFormData,
convertExpandLines,
idleCallback,
+ allDiscussionWrappersExpanded,
} from './utils';
import * as types from './mutation_types';
import {
@@ -79,6 +80,7 @@ export const assignDiscussionsToDiff = (
discussions = rootState.notes.discussions,
) => {
const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
+ const hash = getLocationHash();
discussions
.filter(discussion => discussion.diff_discussion)
@@ -86,6 +88,7 @@ export const assignDiscussionsToDiff = (
commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
discussion,
diffPositionByLineCode,
+ hash,
});
});
@@ -99,6 +102,10 @@ export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash: file_hash, lineCode: line_code, id });
};
+export const toggleLineDiscussions = ({ commit }, options) => {
+ commit(types.TOGGLE_LINE_DISCUSSIONS, options);
+};
+
export const renderFileForDiscussionId = ({ commit, rootState, state }, discussionId) => {
const discussion = rootState.notes.discussions.find(d => d.id === discussionId);
@@ -257,6 +264,31 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
});
};
+export const toggleFileDiscussionWrappers = ({ commit }, diff) => {
+ const discussionWrappersExpanded = allDiscussionWrappersExpanded(diff);
+ let linesWithDiscussions;
+ if (diff.highlighted_diff_lines) {
+ linesWithDiscussions = diff.highlighted_diff_lines.filter(line => line.discussions.length);
+ }
+ if (diff.parallel_diff_lines) {
+ linesWithDiscussions = diff.parallel_diff_lines.filter(
+ line =>
+ (line.left && line.left.discussions.length) ||
+ (line.right && line.right.discussions.length),
+ );
+ }
+
+ if (linesWithDiscussions.length) {
+ linesWithDiscussions.forEach(line => {
+ commit(types.TOGGLE_LINE_DISCUSSIONS, {
+ fileHash: diff.file_hash,
+ lineCode: line.line_code,
+ expanded: !discussionWrappersExpanded,
+ });
+ });
+ }
+};
+
export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
const postData = getNoteFormData({
commit: state.commit,
@@ -267,7 +299,7 @@ export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
return dispatch('saveNote', postData, { root: true })
.then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
.then(discussion => dispatch('assignDiscussionsToDiff', [discussion]))
- .then(() => dispatch('updateResolvableDiscussonsCounts', null, { root: true }))
+ .then(() => dispatch('updateResolvableDiscussionsCounts', null, { root: true }))
.then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash))
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
};
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 8d6111da500..9db56331faa 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -35,3 +35,5 @@ export const ADD_CURRENT_VIEW_DIFF_FILE_LINES = 'ADD_CURRENT_VIEW_DIFF_FILE_LINE
export const TOGGLE_DIFF_FILE_RENDERING_MORE = 'TOGGLE_DIFF_FILE_RENDERING_MORE';
export const SET_SHOW_SUGGEST_POPOVER = 'SET_SHOW_SUGGEST_POPOVER';
+
+export const TOGGLE_LINE_DISCUSSIONS = 'TOGGLE_LINE_DISCUSSIONS';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 00181a63c43..a66f205bbbd 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -6,6 +6,7 @@ import {
addContextLines,
prepareDiffData,
isDiscussionApplicableToLine,
+ updateLineInFile,
} from './utils';
import * as types from './mutation_types';
@@ -109,7 +110,7 @@ export default {
}));
},
- [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode }) {
+ [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode, hash }) {
const { latestDiff } = state;
const discussionLineCode = discussion.line_code;
@@ -130,13 +131,27 @@ export default {
: [],
});
+ const setDiscussionsExpanded = line => {
+ const isLineNoteTargeted = line.discussions.some(
+ disc => disc.notes && disc.notes.find(note => hash === `note_${note.id}`),
+ );
+
+ return {
+ ...line,
+ discussionsExpanded:
+ line.discussions && line.discussions.length
+ ? line.discussions.some(disc => !disc.resolved) || isLineNoteTargeted
+ : false,
+ };
+ };
+
state.diffFiles = state.diffFiles.map(diffFile => {
if (diffFile.file_hash === fileHash) {
const file = { ...diffFile };
if (file.highlighted_diff_lines) {
file.highlighted_diff_lines = file.highlighted_diff_lines.map(line =>
- lineCheck(line) ? mapDiscussions(line) : line,
+ setDiscussionsExpanded(lineCheck(line) ? mapDiscussions(line) : line),
);
}
@@ -148,8 +163,10 @@ export default {
if (left || right) {
return {
...line,
- left: line.left ? mapDiscussions(line.left) : null,
- right: line.right ? mapDiscussions(line.right, () => !left) : null,
+ left: line.left ? setDiscussionsExpanded(mapDiscussions(line.left)) : null,
+ right: line.right
+ ? setDiscussionsExpanded(mapDiscussions(line.right, () => !left))
+ : null,
};
}
@@ -173,32 +190,11 @@ export default {
[types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) {
const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash);
if (selectedFile) {
- if (selectedFile.parallel_diff_lines) {
- const targetLine = selectedFile.parallel_diff_lines.find(
- line =>
- (line.left && line.left.line_code === lineCode) ||
- (line.right && line.right.line_code === lineCode),
- );
- if (targetLine) {
- const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right';
-
- Object.assign(targetLine[side], {
- discussions: targetLine[side].discussions.filter(discussion => discussion.notes.length),
- });
- }
- }
-
- if (selectedFile.highlighted_diff_lines) {
- const targetInlineLine = selectedFile.highlighted_diff_lines.find(
- line => line.line_code === lineCode,
- );
-
- if (targetInlineLine) {
- Object.assign(targetInlineLine, {
- discussions: targetInlineLine.discussions.filter(discussion => discussion.notes.length),
- });
- }
- }
+ updateLineInFile(selectedFile, lineCode, line =>
+ Object.assign(line, {
+ discussions: line.discussions.filter(discussion => discussion.notes.length),
+ }),
+ );
if (selectedFile.discussions && selectedFile.discussions.length) {
selectedFile.discussions = selectedFile.discussions.filter(
@@ -207,6 +203,15 @@ export default {
}
}
},
+
+ [types.TOGGLE_LINE_DISCUSSIONS](state, { fileHash, lineCode, expanded }) {
+ const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash);
+
+ updateLineInFile(selectedFile, lineCode, line =>
+ Object.assign(line, { discussionsExpanded: expanded }),
+ );
+ },
+
[types.TOGGLE_FOLDER_OPEN](state, path) {
state.treeEntries[path].opened = !state.treeEntries[path].opened;
},
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 71956255eef..1c3ed84001c 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -454,3 +454,48 @@ export const convertExpandLines = ({
};
export const idleCallback = cb => requestIdleCallback(cb);
+
+export const updateLineInFile = (selectedFile, lineCode, updateFn) => {
+ if (selectedFile.parallel_diff_lines) {
+ const targetLine = selectedFile.parallel_diff_lines.find(
+ line =>
+ (line.left && line.left.line_code === lineCode) ||
+ (line.right && line.right.line_code === lineCode),
+ );
+ if (targetLine) {
+ const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right';
+
+ updateFn(targetLine[side]);
+ }
+ }
+ if (selectedFile.highlighted_diff_lines) {
+ const targetInlineLine = selectedFile.highlighted_diff_lines.find(
+ line => line.line_code === lineCode,
+ );
+
+ if (targetInlineLine) {
+ updateFn(targetInlineLine);
+ }
+ }
+};
+
+export const allDiscussionWrappersExpanded = diff => {
+ const discussionsExpandedArray = [];
+ if (diff.parallel_diff_lines) {
+ diff.parallel_diff_lines.forEach(line => {
+ if (line.left && line.left.discussions.length) {
+ discussionsExpandedArray.push(line.left.discussionsExpanded);
+ }
+ if (line.right && line.right.discussions.length) {
+ discussionsExpandedArray.push(line.right.discussionsExpanded);
+ }
+ });
+ } else if (diff.highlighted_diff_lines) {
+ diff.parallel_diff_lines.forEach(line => {
+ if (line.discussions.length) {
+ discussionsExpandedArray.push(line.discussionsExpanded);
+ }
+ });
+ }
+ return discussionsExpandedArray.every(el => el);
+};
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index dc68443493c..813045cb5e4 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -15,7 +15,6 @@ import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { CLUSTER_TYPE } from '~/clusters/constants';
/**
* Environment Item Component
@@ -81,15 +80,6 @@ export default {
},
/**
- * Hide group cluster features which are not currently implemented.
- *
- * @returns {Boolean}
- */
- disableGroupClusterFeatures() {
- return this.model && this.model.cluster_type === CLUSTER_TYPE.GROUP;
- },
-
- /**
* Returns whether the environment can be stopped.
*
* @returns {Boolean}
@@ -581,7 +571,6 @@ export default {
<terminal-button-component
v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"
- :disabled="disableGroupClusterFeatures"
/>
<rollback-component
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js
index 903c838e266..460174caf4d 100644
--- a/app/assets/javascripts/group.js
+++ b/app/assets/javascripts/group.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import { slugifyWithHyphens } from './lib/utils/text_utility';
+import { slugify } from './lib/utils/text_utility';
export default class Group {
constructor() {
@@ -14,7 +14,7 @@ export default class Group {
}
update() {
- const slug = slugifyWithHyphens(this.groupName.val());
+ const slug = slugify(this.groupName.val());
this.groupPath.val(slug);
}
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 4be4b02ac1e..c8fbc3cb9f1 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -107,7 +107,8 @@ export default {
@click="openFileInEditor"
>
<span class="multi-file-commit-list-file-path d-flex align-items-center">
- <file-icon :file-name="file.name" class="append-right-8" />{{ file.name }}
+ <file-icon :file-name="file.name" class="append-right-8" />
+ {{ file.name }}
</span>
<div class="ml-auto d-flex align-items-center">
<div class="d-flex align-items-center ide-commit-list-changed-icon">
diff --git a/app/assets/javascripts/ide/components/external_link.vue b/app/assets/javascripts/ide/components/external_link.vue
index 954f84cea17..d1857f0176a 100644
--- a/app/assets/javascripts/ide/components/external_link.vue
+++ b/app/assets/javascripts/ide/components/external_link.vue
@@ -27,7 +27,7 @@ export default {
target="_blank"
rel="noopener noreferrer"
>
- <span class="vertical-align-middle">Open in file view</span>
+ <span class="vertical-align-middle">{{ __('Open in file view') }}</span>
<icon :size="16" name="external-link" css-classes="vertical-align-middle space-right" />
</a>
</div>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index f952b1e7b80..5fcb11a232e 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -8,6 +8,7 @@ import { activityBarViews, viewerTypes } from '../constants';
import Editor from '../lib/editor';
import ExternalLink from './external_link.vue';
import FileTemplatesBar from './file_templates/bar.vue';
+import { __ } from '~/locale';
export default {
components: {
@@ -160,7 +161,14 @@ export default {
this.createEditorInstance();
})
.catch(err => {
- flash('Error setting up editor. Please try again.', 'alert', document, null, false, true);
+ flash(
+ __('Error setting up editor. Please try again.'),
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
throw err;
});
},
@@ -247,12 +255,8 @@ export default {
role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'editor' })"
>
- <template v-if="viewer === $options.viewerTypes.edit">
- {{ __('Edit') }}
- </template>
- <template v-else>
- {{ __('Review') }}
- </template>
+ <template v-if="viewer === $options.viewerTypes.edit">{{ __('Edit') }}</template>
+ <template v-else>{{ __('Review') }}</template>
</a>
</li>
<li v-if="file.previewMode" :class="previewTabCSS">
@@ -260,9 +264,8 @@ export default {
href="javascript:void(0);"
role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'preview' })"
+ >{{ file.previewMode.previewTitle }}</a
>
- {{ file.previewMode.previewTitle }}
- </a>
</li>
</ul>
<external-link :file="file" />
diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
index a964d90b090..84a962bfc7d 100644
--- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue
+++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
@@ -1,4 +1,5 @@
<script>
+import { __, sprintf } from '~/locale';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import '~/lib/utils/datetime_utility';
@@ -18,7 +19,9 @@ export default {
},
computed: {
lockTooltip() {
- return `Locked by ${this.file.file_lock.user.name}`;
+ return sprintf(__(`Locked by %{fileLockUserName}`), {
+ fileLockUserName: this.file.file_lock.user.name,
+ });
},
},
};
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index f6aa2295844..7615cfc966e 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -1,4 +1,5 @@
<script>
+import { __, sprintf } from '~/locale';
import { mapActions } from 'vuex';
import FileIcon from '~/vue_shared/components/file_icon.vue';
@@ -27,9 +28,9 @@ export default {
computed: {
closeLabel() {
if (this.fileHasChanged) {
- return `${this.tab.name} changed`;
+ return sprintf(__(`%{tabname} changed`), { tabname: this.tab.name });
}
- return `Close ${this.tab.name}`;
+ return sprintf(__(`Close %{tabname}`, { tabname: this.tab.name }));
},
showChangedIcon() {
if (this.tab.pending) return true;
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index cc1d85fd97d..d38f59b5861 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -44,11 +44,18 @@ export const pluralize = (str, count) => str + (count > 1 || count === 0 ? 's' :
export const dasherize = str => str.replace(/[_\s]+/g, '-');
/**
- * Replaces whitespaces with hyphens and converts to lower case
+ * Replaces whitespaces with hyphens, convert to lower case and remove non-allowed special characters
* @param {String} str
* @returns {String}
*/
-export const slugifyWithHyphens = str => str.toLowerCase().replace(/\s+/g, '-');
+export const slugify = str => {
+ const slug = str
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-zA-Z0-9_.-]+/g, '-');
+
+ return slug === '-' ? '' : slug;
+};
/**
* Replaces whitespaces with underscore and converts to lower case
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 9f30a989295..9e97f345717 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -33,6 +33,8 @@ import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import { __ } from './locale';
+import 'ee_else_ce/main_ee';
+
// expose jQuery as global (TODO: remove these)
window.jQuery = jQuery;
window.$ = jQuery;
@@ -119,11 +121,15 @@ function deferredInitialisation() {
.catch(() => {});
}
+ const glTooltipDelay = localStorage.getItem('gl-tooltip-delay');
+ const delay = glTooltipDelay ? JSON.parse(glTooltipDelay) : 0;
+
// Initialize tooltips
$body.tooltip({
selector: '.has-tooltip, [data-toggle="tooltip"]',
trigger: 'hover',
boundary: 'viewport',
+ delay,
});
// Initialize popovers
diff --git a/app/assets/javascripts/main_ee.js b/app/assets/javascripts/main_ee.js
new file mode 100644
index 00000000000..84d74775163
--- /dev/null
+++ b/app/assets/javascripts/main_ee.js
@@ -0,0 +1 @@
+// This is an empty file to satisfy ee_else_ce import for the EE main entry point
diff --git a/app/assets/javascripts/manual_ordering.js b/app/assets/javascripts/manual_ordering.js
index e16ddbfef7e..012d1e70410 100644
--- a/app/assets/javascripts/manual_ordering.js
+++ b/app/assets/javascripts/manual_ordering.js
@@ -21,7 +21,7 @@ const updateIssue = (url, issueList, { move_before_id, move_after_id }) =>
const initManualOrdering = () => {
const issueList = document.querySelector('.manual-ordering');
- if (!issueList || !(gon.features && gon.features.manualSorting)) {
+ if (!issueList || !(gon.features && gon.features.manualSorting) || !(gon.current_user_id > 0)) {
return;
}
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 2cbda8ea05d..ed25a6e3684 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -124,6 +124,11 @@ export default {
required: false,
default: '',
},
+ smallEmptyState: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -386,6 +391,7 @@ export default {
:empty-loading-svg-path="emptyLoadingSvgPath"
:empty-no-data-svg-path="emptyNoDataSvgPath"
:empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
+ :compact="smallEmptyState"
/>
</div>
</template>
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index a3c6de14aa4..1bb40447a3e 100644
--- a/app/assets/javascripts/monitoring/components/empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -1,7 +1,11 @@
<script>
import { __ } from '~/locale';
+import { GlEmptyState } from '@gitlab/ui';
export default {
+ components: {
+ GlEmptyState,
+ },
props: {
documentationPath: {
type: String,
@@ -37,6 +41,11 @@ export default {
type: String,
required: true,
},
+ compact: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -58,6 +67,8 @@ export default {
If this takes a long time, ensure that data is available.`),
buttonText: __('View documentation'),
buttonPath: this.documentationPath,
+ secondaryButtonText: '',
+ secondaryButtonPath: '',
},
noData: {
svgUrl: this.emptyNoDataSvgPath,
@@ -66,13 +77,19 @@ export default {
no data to display.`),
buttonText: __('Configure Prometheus'),
buttonPath: this.settingsPath,
+ secondaryButtonText: '',
+ secondaryButtonPath: '',
},
unableToConnect: {
svgUrl: this.emptyUnableToConnectSvgPath,
title: __('Unable to connect to Prometheus server'),
- description: 'Ensure connectivity is available from the GitLab server to the ',
+ description: __(
+ 'Ensure connectivity is available from the GitLab server to the Prometheus server',
+ ),
buttonText: __('View documentation'),
buttonPath: this.documentationPath,
+ secondaryButtonText: __('Configure Prometheus'),
+ secondaryButtonPath: this.settingsPath,
},
},
};
@@ -81,45 +98,19 @@ export default {
currentState() {
return this.states[this.selectedState];
},
- showButtonDescription() {
- if (this.selectedState === 'unableToConnect') return true;
- return false;
- },
},
};
</script>
<template>
- <div class="row empty-state js-empty-state">
- <div class="col-12">
- <div class="state-svg svg-content">
- <img :src="currentState.svgUrl" />
- </div>
- </div>
-
- <div class="col-12">
- <div class="text-content">
- <h4 class="state-title text-center">{{ currentState.title }}</h4>
- <p class="state-description">
- {{ currentState.description }}
- <a v-if="showButtonDescription" :href="settingsPath">{{ __('Prometheus server') }}</a>
- </p>
-
- <div class="text-center">
- <a
- v-if="currentState.buttonPath"
- :href="currentState.buttonPath"
- class="btn btn-success"
- >{{ currentState.buttonText }}</a
- >
- <a
- v-if="currentState.secondaryButtonPath"
- :href="currentState.secondaryButtonPath"
- class="btn"
- >{{ currentState.secondaryButtonText }}</a
- >
- </div>
- </div>
- </div>
- </div>
+ <gl-empty-state
+ :title="currentState.title"
+ :description="currentState.description"
+ :primary-button-text="currentState.buttonText"
+ :primary-button-link="currentState.buttonPath"
+ :secondary-button-text="currentState.secondaryButtonText"
+ :secondary-button-link="currentState.secondaryButtonPath"
+ :svg-path="currentState.svgUrl"
+ :compact="compact"
+ />
</template>
diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js
index 84e1f1c4c20..721942f9d3b 100644
--- a/app/assets/javascripts/monitoring/stores/utils.js
+++ b/app/assets/javascripts/monitoring/stores/utils.js
@@ -36,15 +36,26 @@ function removeTimeSeriesNoData(queries) {
// { metricId: 2, ...query2Attrs }] },
// { title: 'new title', y_label: 'MB', queries: [{ metricId: 3, ...query3Attrs }]}
// ]
-function groupQueriesByChartInfo(metrics) {
+export function groupQueriesByChartInfo(metrics) {
const metricsByChart = metrics.reduce((accumulator, metric) => {
const { queries, ...chart } = metric;
- const metricId = chart.id ? chart.id.toString() : null;
const chartKey = `${chart.title}|${chart.y_label}`;
accumulator[chartKey] = accumulator[chartKey] || { ...chart, queries: [] };
- queries.forEach(queryAttrs => accumulator[chartKey].queries.push({ metricId, ...queryAttrs }));
+ queries.forEach(queryAttrs => {
+ let metricId;
+
+ if (chart.id) {
+ metricId = chart.id.toString();
+ } else if (queryAttrs.metric_id) {
+ metricId = queryAttrs.metric_id.toString();
+ } else {
+ metricId = null;
+ }
+
+ accumulator[chartKey].queries.push({ metricId, ...queryAttrs });
+ });
return accumulator;
}, {});
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 075c28e8d07..6c1738f0f1b 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -65,14 +65,12 @@ export default {
return this.getUserData.id;
},
commentButtonTitle() {
- return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion';
+ return this.noteType === constants.COMMENT ? __('Comment') : __('Start thread');
},
startDiscussionDescription() {
- let text = 'Discuss a specific suggestion or question';
- if (this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE) {
- text += ' that needs to be resolved';
- }
- return `${text}.`;
+ return this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE
+ ? __('Discuss a specific suggestion or question that needs to be resolved.')
+ : __('Discuss a specific suggestion or question.');
},
isOpen() {
return this.openState === constants.OPENED || this.openState === constants.REOPENED;
@@ -127,8 +125,8 @@ export default {
},
issuableTypeTitle() {
return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE
- ? 'merge request'
- : 'issue';
+ ? __('merge request')
+ : __('issue');
},
trackingLabel() {
return slugifyWithUnderscore(`${this.commentButtonTitle} button`);
@@ -203,7 +201,7 @@ export default {
this.discard();
} else {
Flash(
- 'Something went wrong while adding your comment. Please try again.',
+ __('Something went wrong while adding your comment. Please try again.'),
'alert',
this.$refs.commentForm,
);
@@ -219,8 +217,9 @@ export default {
.catch(() => {
this.enableButton();
this.discard(false);
- const msg = `Your comment could not be submitted!
-Please check your network connection and try again.`;
+ const msg = __(
+ 'Your comment could not be submitted! Please check your network connection and try again.',
+ );
Flash(msg, 'alert', this.$el);
this.note = noteData.data.note.note; // Restore textarea content.
this.removePlaceholderNotes();
@@ -298,7 +297,7 @@ Please check your network connection and try again.`;
const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType));
this.autosave = new Autosave($(this.$refs.textarea), [
- 'Note',
+ __('Note'),
noteableType,
this.getNoteableData.id,
]);
@@ -359,8 +358,8 @@ Please check your network connection and try again.`;
class="note-textarea js-vue-comment-form js-note-text
js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input"
data-supports-quick-actions="true"
- aria-label="Description"
- placeholder="Write a comment or drag your files here…"
+ :aria-label="__('Description')"
+ :placeholder="__('Write a comment or drag your files here…')"
@keydown.up="editCurrentUserLastNote()"
@keydown.meta.enter="handleSave()"
@keydown.ctrl.enter="handleSave()"
@@ -381,7 +380,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
data-track-event="click_button"
@click.prevent="handleSave()"
>
- {{ __(commentButtonTitle) }}
+ {{ commentButtonTitle }}
</button>
<button
:disabled="isSubmitButtonDisabled"
@@ -390,7 +389,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
class="btn btn-success note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
data-display="static"
data-toggle="dropdown"
- aria-label="Open comment type dropdown"
+ :aria-label="__('Open comment type dropdown')"
>
<i aria-hidden="true" class="fa fa-caret-down toggle-icon"> </i>
</button>
@@ -404,8 +403,14 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
>
<i aria-hidden="true" class="fa fa-check icon"> </i>
<div class="description">
- <strong>Comment</strong>
- <p>Add a general comment to this {{ noteableDisplayName }}.</p>
+ <strong>{{ __('Comment') }}</strong>
+ <p>
+ {{
+ sprintf(__('Add a general comment to this %{noteableDisplayName}.'), {
+ noteableDisplayName,
+ })
+ }}
+ </p>
</div>
</button>
</li>
@@ -418,7 +423,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
>
<i aria-hidden="true" class="fa fa-check icon"> </i>
<div class="description">
- <strong>Start discussion</strong>
+ <strong>{{ __('Start thread') }}</strong>
<p>{{ startDiscussionDescription }}</p>
</div>
</button>
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index 54c242b2fda..164e79c6294 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -100,7 +100,7 @@ export default {
class="btn-link btn-link-retry btn-no-padding js-toggle-lazy-diff-retry-button"
@click="fetchDiff"
>
- Try again
+ {{ __('Try again') }}
</button>
</td>
<td v-else class="line_content js-success-lazy-load">
diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue
index 1357a5268d6..f4570c1292c 100644
--- a/app/assets/javascripts/notes/components/discussion_actions.vue
+++ b/app/assets/javascripts/notes/components/discussion_actions.vue
@@ -39,15 +39,23 @@ export default {
</script>
<template>
- <div class="discussion-with-resolve-btn clearfix">
- <reply-placeholder class="qa-discussion-reply" @onClick="$emit('showReplyForm')" />
-
- <div class="btn-group discussion-actions" role="group">
- <resolve-discussion-button
- v-if="discussion.resolvable"
- :is-resolving="isResolving"
- :button-title="resolveButtonTitle"
- @onClick="$emit('resolve')"
+ <div class="discussion-with-resolve-btn">
+ <reply-placeholder
+ :button-text="s__('MergeRequests|Reply...')"
+ class="qa-discussion-reply"
+ @onClick="$emit('showReplyForm')"
+ />
+ <resolve-discussion-button
+ v-if="discussion.resolvable"
+ :is-resolving="isResolving"
+ :button-title="resolveButtonTitle"
+ @onClick="$emit('resolve')"
+ />
+ <div v-if="discussion.resolvable" class="btn-group discussion-actions ml-sm-2" role="group">
+ <resolve-with-issue-button v-if="resolveWithIssuePath" :url="resolveWithIssuePath" />
+ <jump-to-next-discussion-button
+ v-if="shouldShowJumpToNextDiscussion"
+ @onClick="$emit('jumpToNextDiscussion')"
/>
<resolve-with-issue-button
v-if="discussion.resolvable && resolveWithIssuePath"
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index efd84f5722c..d7ffa0abb79 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -61,7 +61,7 @@ export default {
</span>
<span class="line-resolve-text">
{{ resolvedDiscussionsCount }}/{{ resolvableDiscussionsCount }}
- {{ n__('discussion resolved', 'discussions resolved', resolvableDiscussionsCount) }}
+ {{ n__('thread resolved', 'threads resolved', resolvableDiscussionsCount) }}
</span>
</div>
<div
@@ -72,7 +72,7 @@ export default {
<a
v-gl-tooltip
:href="resolveAllDiscussionsIssuePath"
- :title="s__('Resolve all discussions in new issue')"
+ :title="s__('Resolve all threads in new issue')"
class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"
>
<icon name="issue-new" />
@@ -81,7 +81,7 @@ export default {
<div v-if="isLoggedIn && !allResolved" class="btn-group btn-group-sm" role="group">
<button
v-gl-tooltip
- title="Jump to first unresolved discussion"
+ title="Jump to first unresolved thread"
class="btn btn-default discussion-next-btn"
@click="jumpToFirstUnresolvedDiscussion"
>
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index 30971ad5227..2ff0fee62f3 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -1,11 +1,11 @@
<script>
-import { mapGetters } from 'vuex';
+import { mapGetters, mapActions } from 'vuex';
import { SYSTEM_NOTE } from '../constants';
import { __ } from '~/locale';
-import NoteableNote from './noteable_note.vue';
-import PlaceholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
-import PlaceholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
+import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
+import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
import SystemNote from '~/vue_shared/components/notes/system_note.vue';
+import NoteableNote from './noteable_note.vue';
import ToggleRepliesWidget from './toggle_replies_widget.vue';
import NoteEditedText from './note_edited_text.vue';
@@ -72,6 +72,7 @@ export default {
},
},
methods: {
+ ...mapActions(['toggleDiscussion']),
componentName(note) {
if (note.isPlaceholderNote) {
if (note.placeholderType === SYSTEM_NOTE) {
@@ -101,7 +102,7 @@ export default {
<component
:is="componentName(firstNote)"
:note="componentData(firstNote)"
- :line="line"
+ :line="line || diffLine"
:commit="commit"
:help-page-path="helpPagePath"
:show-reply-button="userCanReply"
@@ -118,23 +119,29 @@ export default {
/>
<slot slot="avatar-badge" name="avatar-badge"></slot>
</component>
- <toggle-replies-widget
- v-if="hasReplies"
- :collapsed="!isExpanded"
- :replies="replies"
- @toggle="$emit('toggleDiscussion')"
- />
- <template v-if="isExpanded">
- <component
- :is="componentName(note)"
- v-for="note in replies"
- :key="note.id"
- :note="componentData(note)"
- :help-page-path="helpPagePath"
- :line="line"
- @handleDeleteNote="$emit('deleteNote')"
+ <div
+ :class="discussion.diff_discussion ? 'discussion-collapsible bordered-box clearfix' : ''"
+ >
+ <toggle-replies-widget
+ v-if="hasReplies"
+ :collapsed="!isExpanded"
+ :replies="replies"
+ :class="{ 'discussion-toggle-replies': discussion.diff_discussion }"
+ @toggle="toggleDiscussion({ discussionId: discussion.id })"
/>
- </template>
+ <template v-if="isExpanded">
+ <component
+ :is="componentName(note)"
+ v-for="note in replies"
+ :key="note.id"
+ :note="componentData(note)"
+ :help-page-path="helpPagePath"
+ :line="line"
+ @handleDeleteNote="$emit('deleteNote')"
+ />
+ </template>
+ <slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
+ </div>
</template>
<template v-else>
<component
@@ -148,8 +155,8 @@ export default {
>
<slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
</component>
+ <slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
</template>
</ul>
- <slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue b/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue
index ea590905e3c..0204169214b 100644
--- a/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue
+++ b/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue
@@ -1,6 +1,12 @@
<script>
export default {
name: 'ReplyPlaceholder',
+ props: {
+ buttonText: {
+ type: String,
+ required: true,
+ },
+ },
};
</script>
@@ -12,6 +18,6 @@ export default {
:title="s__('MergeRequests|Add a reply')"
@click="$emit('onClick')"
>
- {{ s__('MergeRequests|Reply...') }}
+ {{ buttonText }}
</button>
</template>
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
index e413398696a..f03e6fd73d7 100644
--- a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
@@ -25,7 +25,7 @@ export default {
<gl-button
v-gl-tooltip
:href="url"
- :title="s__('MergeRequests|Resolve this discussion in a new issue')"
+ :title="s__('MergeRequests|Resolve this thread in a new issue')"
class="new-issue-for-discussion discussion-create-issue-btn"
>
<icon name="issue-new" />
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index 941b6d5cab3..d4a57d5d58d 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -4,6 +4,7 @@ import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
import { glEmojiTag } from '../../emoji';
+import { __, sprintf } from '~/locale';
export default {
components: {
@@ -108,23 +109,26 @@ export default {
// Add myself to the beginning of the list so title will start with You.
if (hasReactionByCurrentUser) {
- namesToShow.unshift('You');
+ namesToShow.unshift(__('You'));
}
let title = '';
// We have 10+ awarded user, join them with comma and add `and x more`.
if (remainingAwardList.length) {
- title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`;
+ title = sprintf(__(`%{listToShow}, and %{awardsListLength} more.`), {
+ listToShow: namesToShow.join(', '),
+ awardsListLength: remainingAwardList.length,
+ });
} else if (namesToShow.length > 1) {
// Join all names with comma but not the last one, it will be added with and text.
title = namesToShow.slice(0, namesToShow.length - 1).join(', ');
// If we have more than 2 users we need an extra comma before and text.
title += namesToShow.length > 2 ? ',' : '';
- title += ` and ${namesToShow.slice(-1)}`; // Append and text
+ title += sprintf(__(` and %{sliced}`), { sliced: namesToShow.slice(-1) }); // Append and text
} else {
// We have only 2 users so join them with and.
- title = namesToShow.join(' and ');
+ title = namesToShow.join(__(' and '));
}
return title;
@@ -155,7 +159,7 @@ export default {
awardName: parsedName,
};
- this.toggleAwardRequest(data).catch(() => Flash('Something went wrong on our end.'));
+ this.toggleAwardRequest(data).catch(() => Flash(__('Something went wrong on our end.')));
},
},
};
@@ -184,7 +188,7 @@ export default {
:class="{ 'js-user-authored': isAuthoredByMe }"
class="award-control btn js-add-award"
title="Add reaction"
- aria-label="Add reaction"
+ :aria-label="__('Add reaction')"
data-boundary="viewport"
type="button"
>
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 042ed196933..3823861c0b9 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -1,14 +1,14 @@
<script>
import { mergeUrlParams } from '~/lib/utils/url_utility';
import { mapGetters, mapActions } from 'vuex';
+import noteFormMixin from 'ee_else_ce/notes/mixins/note_form';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable';
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
import { getDraft, updateDraft } from '~/lib/utils/autosave';
-import noteFormMixin from 'ee_else_ce/notes/mixins/note_form';
export default {
name: 'NoteForm',
@@ -174,6 +174,18 @@ export default {
(this.line && this.line.can_receive_suggestion)
);
},
+ changedCommentText() {
+ return sprintf(
+ __(
+ 'This comment has changed since you started editing, please review the %{startTag}updated comment%{endTag} to ensure information is not lost.',
+ ),
+ {
+ startTag: `<a href="${this.noteHash}" target="_blank" rel="noopener noreferrer">`,
+ endTag: '</a>',
+ },
+ false,
+ );
+ },
},
watch: {
noteBody() {
@@ -228,11 +240,11 @@ export default {
<template>
<div ref="editNoteForm" class="note-edit-form current-note-edit-form js-discussion-note-form">
- <div v-if="conflictWhileEditing" class="js-conflict-edit-warning alert alert-danger">
- This comment has changed since you started editing, please review the
- <a :href="noteHash" target="_blank" rel="noopener noreferrer">updated comment</a> to ensure
- information is not lost.
- </div>
+ <div
+ v-if="conflictWhileEditing"
+ class="js-conflict-edit-warning alert alert-danger"
+ v-html="changedCommentText"
+ ></div>
<div class="flash-container timeline-content"></div>
<form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form">
<issue-warning
@@ -264,8 +276,8 @@ export default {
name="note[note]"
class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
dir="auto"
- aria-label="Description"
- placeholder="Write a comment or drag your files here…"
+ :aria-label="__('Description')"
+ :placeholder="__('Write a comment or drag your files here…')"
@keydown.meta.enter="handleKeySubmit()"
@keydown.ctrl.enter="handleKeySubmit()"
@keydown.exact.up="editMyLastNote()"
@@ -283,11 +295,11 @@ export default {
type="checkbox"
class="qa-unresolve-review-discussion"
/>
- {{ __('Unresolve discussion') }}
+ {{ __('Unresolve thread') }}
</template>
<template v-else>
<input v-model="isResolving" type="checkbox" class="qa-resolve-review-discussion" />
- {{ __('Resolve discussion') }}
+ {{ __('Resolve thread') }}
</template>
</label>
</p>
@@ -339,7 +351,7 @@ export default {
type="button"
@click="cancelHandler()"
>
- Cancel
+ {{ __('Cancel') }}
</button>
</template>
</div>
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index fbf82fab9e9..3158e086f6c 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -70,7 +70,7 @@ export default {
@click="handleToggle"
>
<i :class="toggleChevronClass" class="fa" aria-hidden="true"></i>
- {{ __('Toggle discussion') }}
+ {{ __('Toggle thread') }}
</button>
</div>
<a
@@ -103,7 +103,7 @@ export default {
</template>
<i
class="fa fa-spinner fa-spin editing-spinner"
- aria-label="Comment is being updated"
+ :aria-label="__('Comment is being updated')"
aria-hidden="true"
></i>
</span>
diff --git a/app/assets/javascripts/notes/components/note_signed_out_widget.vue b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
index e3eb92956b1..ccfe84ab098 100644
--- a/app/assets/javascripts/notes/components/note_signed_out_widget.vue
+++ b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
@@ -1,5 +1,6 @@
<script>
import { mapGetters } from 'vuex';
+import { __, sprintf } from '~/locale';
export default {
computed: {
@@ -10,12 +11,24 @@ export default {
signInLink() {
return this.getNotesDataByProp('newSessionPath');
},
+ signedOutText() {
+ return sprintf(
+ __(
+ 'Please %{startTagRegister}register%{endRegisterTag} or %{startTagSignIn}sign in%{endSignInTag} to reply',
+ ),
+ {
+ startTagRegister: `<a href="${this.registerLink}">`,
+ startTagSignIn: `<a href="${this.signInLink}">`,
+ endRegisterTag: '</a>',
+ endSignInTag: '</a>',
+ },
+ false,
+ );
+ },
},
};
</script>
<template>
- <div class="disabled-comment text-center">
- Please <a :href="registerLink">register</a> or <a :href="signInLink">sign in</a> to reply
- </div>
+ <div class="disabled-comment text-center" v-html="signedOutText"></div>
</template>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index b8eaff32cce..3fbd0a9f715 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -132,7 +132,7 @@ export default {
return this.discussion.diff_discussion && this.renderDiffFile;
},
shouldGroupReplies() {
- return !this.shouldRenderDiffs && !this.discussion.diff_discussion;
+ return !this.shouldRenderDiffs;
},
wrapperComponent() {
return this.shouldRenderDiffs ? diffWithNote : 'div';
@@ -174,22 +174,20 @@ export default {
active: isActive,
} = this.discussion;
- let text = s__('MergeRequests|started a discussion');
+ let text = s__('MergeRequests|started a thread');
if (isForCommit) {
- text = s__(
- 'MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}',
- );
+ text = s__('MergeRequests|started a thread on commit %{linkStart}%{commitId}%{linkEnd}');
} else if (isDiffDiscussion && commitId) {
text = isActive
- ? s__('MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}')
+ ? s__('MergeRequests|started a thread on commit %{linkStart}%{commitId}%{linkEnd}')
: s__(
- 'MergeRequests|started a discussion on an outdated change in commit %{linkStart}%{commitId}%{linkEnd}',
+ 'MergeRequests|started a thread on an outdated change in commit %{linkStart}%{commitId}%{linkEnd}',
);
} else if (isDiffDiscussion) {
text = isActive
- ? s__('MergeRequests|started a discussion on %{linkStart}the diff%{linkEnd}')
+ ? s__('MergeRequests|started a thread on %{linkStart}the diff%{linkEnd}')
: s__(
- 'MergeRequests|started a discussion on %{linkStart}an old version of the diff%{linkEnd}',
+ 'MergeRequests|started a thread on %{linkStart}an old version of the diff%{linkEnd}',
);
}
@@ -250,6 +248,11 @@ export default {
clearDraft(this.autosaveKey);
},
saveReply(noteText, form, callback) {
+ if (!noteText) {
+ this.cancelReplyForm();
+ callback();
+ return;
+ }
const postData = {
in_reply_to_discussion_id: this.discussion.reply_id,
target_type: this.getNoteableData.targetType,
@@ -280,8 +283,9 @@ export default {
this.removePlaceholderNotes();
this.isReplying = true;
this.$nextTick(() => {
- const msg = `Your comment could not be submitted!
-Please check your network connection and try again.`;
+ const msg = __(
+ 'Your comment could not be submitted! Please check your network connection and try again.',
+ );
Flash(msg, 'alert', this.$el);
this.$refs.noteForm.note = noteText;
callback(err);
@@ -363,7 +367,6 @@ Please check your network connection and try again.`;
:line="line"
:should-group-replies="shouldGroupReplies"
@startReplying="showReplyForm"
- @toggleDiscussion="toggleDiscussionHandler"
@deleteNote="deleteNoteHandler"
>
<slot slot="avatar-badge" name="avatar-badge"></slot>
@@ -376,7 +379,7 @@ Please check your network connection and try again.`;
<div
v-else-if="showReplies"
:class="{ 'is-replying': isReplying }"
- class="discussion-reply-holder"
+ class="discussion-reply-holder clearfix"
>
<user-avatar-link
v-if="!isReplying && userCanReply"
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index aa80e25a3e0..2f201839d45 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -5,7 +5,7 @@ import { escape } from 'underscore';
import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import draftMixin from 'ee_else_ce/notes/mixins/draft';
-import { s__, sprintf } from '../../locale';
+import { __, s__, sprintf } from '../../locale';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue';
@@ -128,9 +128,13 @@ export default {
this.$emit('handleEdit');
},
deleteHandler() {
- const typeOfComment = this.note.isDraft ? 'pending comment' : 'comment';
- // eslint-disable-next-line no-alert
- if (window.confirm(`Are you sure you want to delete this ${typeOfComment}?`)) {
+ const typeOfComment = this.note.isDraft ? __('pending comment') : __('comment');
+ if (
+ // eslint-disable-next-line no-alert
+ window.confirm(
+ sprintf(__('Are you sure you want to delete this %{typeOfComment}?'), { typeOfComment }),
+ )
+ ) {
this.isDeleting = true;
this.$emit('handleDeleteNote', this.note);
@@ -141,7 +145,7 @@ export default {
this.isDeleting = false;
})
.catch(() => {
- Flash('Something went wrong while deleting your note. Please try again.');
+ Flash(__('Something went wrong while deleting your note. Please try again.'));
this.isDeleting = false;
});
}
@@ -185,7 +189,7 @@ export default {
this.isRequesting = false;
this.isEditing = true;
this.$nextTick(() => {
- const msg = 'Something went wrong while editing your comment. Please try again.';
+ const msg = __('Something went wrong while editing your comment. Please try again.');
Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText);
callback();
@@ -195,7 +199,7 @@ export default {
formCancelHandler(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
// eslint-disable-next-line no-alert
- if (!window.confirm('Are you sure you want to cancel editing this comment?')) return;
+ if (!window.confirm(__('Are you sure you want to cancel editing this comment?'))) return;
}
this.$refs.noteBody.resetAutoSave();
if (this.oldContent) {
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 4d00e957973..a0695f9e191 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,4 +1,5 @@
<script>
+import { __ } from '~/locale';
import { mapGetters, mapActions } from 'vuex';
import { getLocationHash } from '../../lib/utils/url_utility';
import Flash from '../../flash';
@@ -170,7 +171,7 @@ export default {
.catch(() => {
this.setLoadingState(false);
this.setNotesFetchedState(true);
- Flash('Something went wrong while fetching comments. Please try again.');
+ Flash(__('Something went wrong while fetching comments. Please try again.'));
});
},
initPolling() {
diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js
index 2329727bca2..16b7598ee09 100644
--- a/app/assets/javascripts/notes/mixins/resolvable.js
+++ b/app/assets/javascripts/notes/mixins/resolvable.js
@@ -20,13 +20,13 @@ export default {
resolveButtonTitle() {
if (this.updatedNoteBody) {
if (this.discussionResolved) {
- return __('Comment & unresolve discussion');
+ return __('Comment & unresolve thread');
}
- return __('Comment & resolve discussion');
+ return __('Comment & resolve thread');
}
- return this.discussionResolved ? __('Unresolve discussion') : __('Resolve discussion');
+ return this.discussionResolved ? __('Unresolve thread') : __('Resolve thread');
},
},
methods: {
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 63658d49a05..9054b4779aa 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -51,7 +51,7 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter }) =>
.then(res => res.json())
.then(discussions => {
commit(types.SET_INITIAL_DISCUSSIONS, discussions);
- dispatch('updateResolvableDiscussonsCounts');
+ dispatch('updateResolvableDiscussionsCounts');
});
export const updateDiscussion = ({ commit, state }, discussion) => {
@@ -67,7 +67,7 @@ export const deleteNote = ({ commit, dispatch, state }, note) =>
commit(types.DELETE_NOTE, note);
dispatch('updateMergeRequestWidget');
- dispatch('updateResolvableDiscussonsCounts');
+ dispatch('updateResolvableDiscussionsCounts');
if (isInMRPage()) {
dispatch('diffs/removeDiscussionsFromDiff', discussion);
@@ -117,7 +117,7 @@ export const replyToDiscussion = ({ commit, state, getters, dispatch }, { endpoi
dispatch('updateMergeRequestWidget');
dispatch('startTaskList');
- dispatch('updateResolvableDiscussonsCounts');
+ dispatch('updateResolvableDiscussionsCounts');
} else {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
}
@@ -135,7 +135,7 @@ export const createNewNote = ({ commit, dispatch }, { endpoint, data }) =>
dispatch('updateMergeRequestWidget');
dispatch('startTaskList');
- dispatch('updateResolvableDiscussonsCounts');
+ dispatch('updateResolvableDiscussionsCounts');
}
return res;
});
@@ -168,7 +168,7 @@ export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved,
commit(mutationType, res);
- dispatch('updateResolvableDiscussonsCounts');
+ dispatch('updateResolvableDiscussionsCounts');
dispatch('updateMergeRequestWidget');
});
@@ -442,7 +442,7 @@ export const startTaskList = ({ dispatch }) =>
}),
);
-export const updateResolvableDiscussonsCounts = ({ commit }) =>
+export const updateResolvableDiscussionsCounts = ({ commit }) =>
commit(types.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS);
export const submitSuggestion = (
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index 1b56b97f751..d51d411f3c6 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -82,7 +82,7 @@ export default class Todos {
})
.catch(() => {
this.updateRowState(target, true);
- return flash(__('Error updating todo status.'));
+ return flash(__('Error updating status of to-do item.'));
});
}
@@ -124,7 +124,7 @@ export default class Todos {
this.updateAllState(target, data);
this.updateBadges(data);
})
- .catch(() => flash(__('Error updating status for all todos.')));
+ .catch(() => flash(__('Error updating status for all to-do items.')));
}
updateAllState(target, data) {
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index 185003c306e..015c1527500 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -4,14 +4,12 @@ import { glEmojiTag } from '~/emoji';
import detailedMetric from './detailed_metric.vue';
import requestSelector from './request_selector.vue';
-import simpleMetric from './simple_metric.vue';
import { s__ } from '~/locale';
export default {
components: {
detailedMetric,
requestSelector,
- simpleMetric,
},
props: {
store: {
@@ -43,8 +41,13 @@ export default {
details: 'details',
keys: ['feature', 'request'],
},
+ {
+ metric: 'redis',
+ header: 'Redis calls',
+ details: 'details',
+ keys: ['cmd'],
+ },
],
- simpleMetrics: ['redis'],
data() {
return { currentRequestId: '' };
},
@@ -124,12 +127,6 @@ export default {
</button>
<a v-else :href="profileUrl">{{ s__('PerformanceBar|profile') }}</a>
</div>
- <simple-metric
- v-for="metric in $options.simpleMetrics"
- :key="metric"
- :current-request="currentRequest"
- :metric="metric"
- />
<div id="peek-view-gc" class="view">
<span v-if="currentRequest.details" class="bold">
<span title="Invoke Time">{{ currentRequest.details.gc.gc_time }}</span
diff --git a/app/assets/javascripts/performance_bar/components/simple_metric.vue b/app/assets/javascripts/performance_bar/components/simple_metric.vue
deleted file mode 100644
index 358a57d5bc5..00000000000
--- a/app/assets/javascripts/performance_bar/components/simple_metric.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-<script>
-export default {
- props: {
- currentRequest: {
- type: Object,
- required: true,
- },
- metric: {
- type: String,
- required: true,
- },
- },
- computed: {
- duration() {
- return (
- this.currentRequest.details[this.metric] &&
- this.currentRequest.details[this.metric].duration
- );
- },
- calls() {
- return (
- this.currentRequest.details[this.metric] && this.currentRequest.details[this.metric].calls
- );
- },
- },
-};
-</script>
-<template>
- <div :id="`peek-view-${metric}`" class="view">
- <span v-if="currentRequest.details" class="bold"> {{ duration }} / {{ calls }} </span>
- {{ metric }}
- </div>
-</template>
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index ea82ff4e340..9066844f687 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils';
-import { slugifyWithHyphens } from '../lib/utils/text_utility';
+import { slugify } from '../lib/utils/text_utility';
import { s__ } from '~/locale';
let hasUserDefinedProjectPath = false;
@@ -34,7 +34,7 @@ const deriveProjectPathFromUrl = $projectImportUrl => {
};
const onProjectNameChange = ($projectNameInput, $projectPathInput) => {
- const slug = slugifyWithHyphens($projectNameInput.val());
+ const slug = slugify($projectNameInput.val());
$projectPathInput.val(slug);
};
diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue
index c82b65cd97b..0031ba04d78 100644
--- a/app/assets/javascripts/releases/components/release_block.vue
+++ b/app/assets/javascripts/releases/components/release_block.vue
@@ -28,7 +28,7 @@ export default {
computed: {
releasedTimeAgo() {
return sprintf(__('released %{time}'), {
- time: this.timeFormated(this.release.created_at),
+ time: this.timeFormated(this.release.released_at),
});
},
userImageAltDescription() {
@@ -56,8 +56,8 @@ export default {
<div class="card-body">
<h2 class="card-title mt-0">
{{ release.name }}
- <gl-badge v-if="release.pre_release" variant="warning" class="align-middle">{{
- __('Pre-release')
+ <gl-badge v-if="release.upcoming_release" variant="warning" class="align-middle">{{
+ __('Upcoming Release')
}}</gl-badge>
</h2>
@@ -74,7 +74,7 @@ export default {
<div class="append-right-4">
&bull;
- <span v-gl-tooltip.bottom :title="tooltipTitle(release.created_at)">
+ <span v-gl-tooltip.bottom :title="tooltipTitle(release.released_at)">
{{ releasedTimeAgo }}
</span>
</div>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
index 0ad2b3a73a2..fa6b6bfaef1 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
@@ -1,4 +1,6 @@
<script>
+import { n__ } from '~/locale';
+
export default {
name: 'AssigneeTitle',
props: {
@@ -24,7 +26,7 @@ export default {
computed: {
assigneeTitle() {
const assignees = this.numberOfAssignees;
- return assignees > 1 ? `${assignees} Assignees` : 'Assignee';
+ return n__('Assignee', `%d Assignees`, assignees);
},
},
};
@@ -32,18 +34,18 @@ export default {
<template>
<div class="title hide-collapsed">
{{ assigneeTitle }}
- <i v-if="loading" aria-hidden="true" class="fa fa-spinner fa-spin block-loading"> </i>
+ <i v-if="loading" aria-hidden="true" class="fa fa-spinner fa-spin block-loading"></i>
<a v-if="editable" class="js-sidebar-dropdown-toggle edit-link float-right" href="#">
{{ __('Edit') }}
</a>
<a
v-if="showToggle"
- aria-label="Toggle sidebar"
+ :aria-label="__('Toggle sidebar')"
class="gutter-toggle float-right js-sidebar-toggle"
href="#"
role="button"
>
- <i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"> </i>
+ <i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i>
</a>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index 0074d7099dc..805c21d0965 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -1,5 +1,5 @@
<script>
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
@@ -62,7 +62,8 @@ export default {
return this.numberOfHiddenAssignees > 0;
},
hiddenAssigneesLabel() {
- return `+ ${this.numberOfHiddenAssignees} more`;
+ const { numberOfHiddenAssignees } = this;
+ return sprintf(__('+ %{numberOfHiddenAssignees} more'), { numberOfHiddenAssignees });
},
collapsedTooltipTitle() {
const maxRender = Math.min(this.defaultRenderCount, this.users.length);
@@ -103,12 +104,15 @@ export default {
// Everyone can merge
return null;
} else if (cannotMergeCount === assigneesCount && assigneesCount > 1) {
- return 'No one can merge';
+ return __('No one can merge');
} else if (assigneesCount === 1) {
- return 'Cannot merge';
+ return __('Cannot merge');
}
- return `${canMergeCount}/${assigneesCount} can merge`;
+ return sprintf(__('%{canMergeCount}/%{assigneesCount} can merge'), {
+ canMergeCount,
+ assigneesCount,
+ });
},
},
methods: {
@@ -128,7 +132,7 @@ export default {
return `${this.rootPath}${user.username}`;
},
assigneeAlt(user) {
- return `${user.name}'s avatar`;
+ return sprintf(__("%{userName}'s avatar"), { userName: user.name });
},
assigneeUsername(user) {
return `@${user.username}`;
@@ -153,7 +157,7 @@ export default {
data-placement="left"
data-boundary="viewport"
>
- <i v-if="hasNoUsers" aria-label="None" class="fa fa-user"> </i>
+ <i v-if="hasNoUsers" :aria-label="__('None')" class="fa fa-user"> </i>
<button
v-for="(user, index) in users"
v-if="shouldRenderCollapsedAssignee(index)"
@@ -185,9 +189,12 @@ export default {
</span>
<template v-if="hasNoUsers">
<span class="assign-yourself no-value qa-assign-yourself">
- None
+ {{ __('None') }}
<template v-if="editable">
- - <button type="button" class="btn-link" @click="assignSelf">assign yourself</button>
+ -
+ <button type="button" class="btn-link" @click="assignSelf">
+ {{ __('assign yourself') }}
+ </button>
</template>
</span>
</template>
@@ -232,9 +239,7 @@ export default {
<template v-if="showLess">
{{ hiddenAssigneesLabel }}
</template>
- <template v-else>
- - show less
- </template>
+ <template v-else>{{ __('- show less') }}</template>
</button>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index cfa7029b388..70dc3d2cdfa 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -4,6 +4,7 @@ import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
import AssigneeTitle from './assignee_title.vue';
import Assignees from './assignees.vue';
+import { __ } from '~/locale';
export default {
name: 'SidebarAssignees',
@@ -74,7 +75,7 @@ export default {
.then(setLoadingFalse.bind(this))
.catch(() => {
setLoadingFalse();
- return new Flash('Error occurred when saving assignees');
+ return new Flash(__('Error occurred when saving assignees'));
});
},
},
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
index 4b9bb5c7b0e..5d0e39e8195 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
@@ -1,6 +1,7 @@
<script>
import $ from 'jquery';
import eventHub from '../../event_hub';
+import { __ } from '~/locale';
export default {
props: {
@@ -15,7 +16,7 @@ export default {
},
computed: {
toggleButtonText() {
- return this.isConfidential ? 'Turn Off' : 'Turn On';
+ return this.isConfidential ? __('Turn Off') : __('Turn On');
},
updateConfidentialBool() {
return !this.isConfidential;
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
index 657ac837baf..24d5b14ded9 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -79,7 +79,7 @@ export default {
} else if (this.showSpentOnlyState) {
return `${this.timeSpent} / --`;
} else if (this.showNoTimeTrackingState) {
- return 'None';
+ return __('None');
}
return '';
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
index bc263bc36e4..06aca547183 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
@@ -2,6 +2,7 @@
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import tooltip from '../../../vue_shared/directives/tooltip';
import { GlProgressBar } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
export default {
name: 'TimeTrackingComparisonPane',
@@ -43,8 +44,14 @@ export default {
return stringifyTime(this.parsedTimeRemaining);
},
timeRemainingTooltip() {
- const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:';
- return `${prefix} ${this.timeRemainingHumanReadable}`;
+ const { timeRemainingHumanReadable, timeRemainingMinutes } = this;
+ return timeRemainingMinutes < 0
+ ? sprintf(s__('TimeTracking|Over by %{timeRemainingHumanReadable}'), {
+ timeRemainingHumanReadable,
+ })
+ : sprintf(s__('TimeTracking|Time remaining: %{timeRemainingHumanReadable}'), {
+ timeRemainingHumanReadable,
+ });
},
/* Diff values for comparison meter */
timeRemainingMinutes() {
@@ -74,12 +81,12 @@ export default {
<gl-progress-bar :value="timeRemainingPercent" :variant="progressBarVariant" />
<div class="compare-display-container">
<div class="compare-display float-left">
- <span class="compare-label"> {{ s__('TimeTracking|Spent') }} </span>
- <span class="compare-value spent"> {{ timeSpentHumanReadable }} </span>
+ <span class="compare-label">{{ s__('TimeTracking|Spent') }}</span>
+ <span class="compare-value spent">{{ timeSpentHumanReadable }}</span>
</div>
<div class="compare-display estimated float-right">
- <span class="compare-label"> {{ s__('TimeTrackingEstimated|Est') }} </span>
- <span class="compare-value"> {{ timeEstimateHumanReadable }} </span>
+ <span class="compare-label">{{ s__('TimeTrackingEstimated|Est') }}</span>
+ <span class="compare-value">{{ timeEstimateHumanReadable }}</span>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
index 7c7356e2afa..c2f30310e2e 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
@@ -1,4 +1,6 @@
<script>
+import { sprintf, s__ } from '~/locale';
+
export default {
name: 'TimeTrackingSpentOnlyPane',
props: {
@@ -7,11 +9,22 @@ export default {
required: true,
},
},
+ computed: {
+ timeSpent() {
+ return sprintf(
+ s__('TimeTracking|%{startTag}Spent: %{endTag}%{timeSpentHumanReadable}'),
+ {
+ startTag: '<span class="bold">',
+ endTag: '</span>',
+ timeSpentHumanReadable: this.timeSpentHumanReadable,
+ },
+ false,
+ );
+ },
+ },
};
</script>
<template>
- <div class="time-tracking-spend-only-pane">
- <span class="bold">Spent:</span> {{ timeSpentHumanReadable }}
- </div>
+ <div class="time-tracking-spend-only-pane" v-html="timeSpent"></div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
index 57125c78cf6..e6f2fe2b5fc 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -5,8 +5,8 @@ import { GlLoadingIcon } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
-const MARK_TEXT = __('Mark todo as done');
-const TODO_TEXT = __('Add todo');
+const MARK_TEXT = __('Mark as done');
+const TODO_TEXT = __('Add a To Do');
export default {
directives: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
index a9fb40a4949..d4a5fdb4b97 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
@@ -20,7 +20,7 @@ export default {
<status-icon :show-disabled-button="true" status="warning" />
<div class="media-body space-children">
<span class="bold">
- {{ s__('mrWidget|There are unresolved discussions. Please resolve these discussions') }}
+ {{ s__('mrWidget|There are unresolved threads. Please resolve these threads') }}
</span>
<a
v-if="mr.createIssueToResolveDiscussionsPath"
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 baed26a157c..af02b8969ee 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -39,7 +39,7 @@ export default {
</script>
<template>
- <timeline-entry-item class="note being-posted fade-in-half">
+ <timeline-entry-item class="note note-wrapper being-posted fade-in-half">
<div class="timeline-icon">
<user-avatar-link
:link-href="getUserData.path"
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index cd951f67293..a12029d2419 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -287,8 +287,8 @@
list-style: none;
padding: 0 1px;
- a,
- button,
+ a:not(.help-link),
+ button:not(.btn),
.menu-item {
@include dropdown-link;
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index d2d35d91e0b..623c44e062f 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -1093,6 +1093,17 @@ table.code {
line-height: 0;
}
+.discussion-collapsible {
+ margin: 0 $gl-padding $gl-padding 71px;
+}
+
+.parallel {
+ .discussion-collapsible {
+ margin: $gl-padding;
+ margin-top: 0;
+ }
+}
+
@media (max-width: map-get($grid-breakpoints, md)-1) {
.diffs .files {
@include fixed-width-container;
@@ -1110,6 +1121,11 @@ table.code {
padding-right: 0;
}
}
+
+ .discussion-collapsible {
+ margin: $gl-padding;
+ margin-top: 0;
+ }
}
.image-diff-overlay,
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index e880b941d67..7bd1a4138e4 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -134,6 +134,16 @@ $note-form-margin-left: 72px;
}
}
+ .discussion-toggle-replies {
+ border-top: 0;
+ border-radius: 4px 4px 0 0;
+
+ &.collapsed {
+ border: 0;
+ border-radius: 4px;
+ }
+ }
+
.note-created-ago,
.note-updated-at {
white-space: normal;
@@ -462,6 +472,14 @@ $note-form-margin-left: 72px;
position: relative;
}
+ .notes-content .discussion-notes.diff-discussions {
+ border-bottom: 1px solid $border-color;
+
+ &:nth-last-child(1) {
+ border-bottom: 0;
+ }
+ }
+
.notes_holder {
font-family: $regular-font;
@@ -517,6 +535,17 @@ $note-form-margin-left: 72px;
.discussion-reply-holder {
border-radius: 0 0 $border-radius-default $border-radius-default;
position: relative;
+
+ .discussion-form {
+ width: 100%;
+ background-color: $gray-light;
+ padding: 0;
+ }
+
+ .disabled-comment {
+ padding: $gl-vert-padding 0;
+ width: 100%;
+ }
}
}
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 88a0690938a..21b3949e361 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -42,7 +42,7 @@ module IssuableCollections
@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)
+ @issuable_meta_data = issuable_meta_data(@issuables, collection_type, current_user)
@total_pages = issuable_page_count
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
diff --git a/app/controllers/concerns/issuable_collections_action.rb b/app/controllers/concerns/issuable_collections_action.rb
index 18ed4027eac..4ad287c4a13 100644
--- a/app/controllers/concerns/issuable_collections_action.rb
+++ b/app/controllers/concerns/issuable_collections_action.rb
@@ -11,7 +11,7 @@ module IssuableCollectionsAction
.non_archived
.page(params[:page])
- @issuable_meta_data = issuable_meta_data(@issues, collection_type)
+ @issuable_meta_data = issuable_meta_data(@issues, collection_type, current_user)
respond_to do |format|
format.html
@@ -22,7 +22,7 @@ module IssuableCollectionsAction
def merge_requests
@merge_requests = issuables_collection.page(params[:page])
- @issuable_meta_data = issuable_meta_data(@merge_requests, collection_type)
+ @issuable_meta_data = issuable_meta_data(@merge_requests, collection_type, current_user)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index f96d1821095..0098c4cdf4c 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -203,17 +203,17 @@ module NotesActions
# These params are also sent by the client but we need to set these based on
# target_type and target_id because we're checking permissions based on that
- create_params[:noteable_type] = params[:target_type].classify
+ create_params[:noteable_type] = noteable.class.name
- case params[:target_type]
- when 'commit'
- create_params[:commit_id] = params[:target_id]
- when 'merge_request'
- create_params[:noteable_id] = params[:target_id]
+ case noteable
+ when Commit
+ create_params[:commit_id] = noteable.id
+ when MergeRequest
+ create_params[:noteable_id] = noteable.id
# Notes on MergeRequest can have an extra `commit_id` context
create_params[:commit_id] = params.dig(:note, :commit_id)
else
- create_params[:noteable_id] = params[:target_id]
+ create_params[:noteable_id] = noteable.id
end
end
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index d43f5393ecc..daeb8fda417 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -7,8 +7,8 @@ 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
+ before_action :projects, only: [:index]
skip_cross_project_access_check :index, :starred
def index
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 27980466a42..8f6fcb362d2 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -22,7 +22,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
format.html do
redirect_to dashboard_todos_path,
status: 302,
- notice: _('Todo was successfully marked as done.')
+ notice: _('To-do item successfully marked as done.')
end
format.js { head :ok }
format.json { render json: todos_counts }
@@ -33,7 +33,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
updated_ids = TodoService.new.mark_todos_as_done(@todos, current_user)
respond_to do |format|
- format.html { redirect_to dashboard_todos_path, status: 302, notice: _('All todos were marked as done.') }
+ format.html { redirect_to dashboard_todos_path, status: 302, notice: _('Everything on your to-do list is marked as done.') }
format.js { head :ok }
format.json { render json: todos_counts.merge(updated_ids: updated_ids) }
end
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 90396c15375..b1f285f76d7 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -12,6 +12,11 @@ class Projects::ApplicationController < ApplicationController
helper_method :repository, :can_collaborate_with_project?, :user_access
+ rescue_from Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError do |exception|
+ log_exception(exception)
+ render_404
+ end
+
private
def project
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index d77f64a84f5..141a7dfb923 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -169,7 +169,7 @@ class Projects::BranchesController < Projects::ApplicationController
end
def confidential_issue_project
- return unless Feature.enabled?(:create_confidential_merge_request, @project)
+ return unless helpers.create_confidential_merge_request_enabled?
return if params[:confidential_issue_project_id].blank?
confidential_issue_project = Project.find(params[:confidential_issue_project_id])
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index e275b417784..b866f574f67 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -172,7 +172,7 @@ class Projects::IssuesController < Projects::ApplicationController
def create_merge_request
create_params = params.slice(:branch_name, :ref).merge(issue_iid: issue.iid)
- create_params[:target_project_id] = params[:target_project_id] if Feature.enabled?(:create_confidential_merge_request, @project)
+ create_params[:target_project_id] = params[:target_project_id] if helpers.create_confidential_merge_request_enabled?
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, create_params).execute
if result[:status] == :success
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 7ee8e0ea8f8..2aa2508be16 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -201,7 +201,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def rebase
- RebaseWorker.perform_async(@merge_request.id, current_user.id)
+ @merge_request.rebase_async(current_user.id)
head :ok
end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index ac3004d069f..bc2ce15286f 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -99,7 +99,7 @@ module Projects
end
def deploy_token_params
- params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry)
+ params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry, :username)
end
end
end
diff --git a/app/controllers/projects/templates_controller.rb b/app/controllers/projects/templates_controller.rb
index 7ceea4e5b96..f987033a26c 100644
--- a/app/controllers/projects/templates_controller.rb
+++ b/app/controllers/projects/templates_controller.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
class Projects::TemplatesController < Projects::ApplicationController
- before_action :authenticate_user!, :get_template_class
+ before_action :authenticate_user!
+ before_action :authorize_can_read_issuable!
+ before_action :get_template_class
def show
template = @template_type.find(params[:key], project)
@@ -13,9 +15,20 @@ class Projects::TemplatesController < Projects::ApplicationController
private
+ # User must have:
+ # - `read_merge_request` to see merge request templates, or
+ # - `read_issue` to see issue templates
+ #
+ # Note params[:template_type] has a route constraint to limit it to
+ # `merge_request` or `issue`
+ def authorize_can_read_issuable!
+ action = [:read_, params[:template_type]].join
+
+ authorize_action!(action)
+ end
+
def get_template_class
template_types = { issue: Gitlab::Template::IssueTemplate, merge_request: Gitlab::Template::MergeRequestTemplate }.with_indifferent_access
@template_type = template_types[params[:template_type]]
- render json: [], status: :not_found unless @template_type
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 12db493978b..feefc7f8137 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -182,7 +182,7 @@ class ProjectsController < Projects::ApplicationController
end
def housekeeping
- ::Projects::HousekeepingService.new(@project).execute
+ ::Projects::HousekeepingService.new(@project, :gc).execute
redirect_to(
project_path(@project),
@@ -298,7 +298,7 @@ class ProjectsController < Projects::ApplicationController
elsif @project.feature_available?(:issues, current_user)
@issues = issuables_collection.page(params[:page])
@collection_type = 'Issue'
- @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
+ @issuable_meta_data = issuable_meta_data(@issues, @collection_type, current_user)
end
render :show
diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb
index eee14b0faf4..612897f27e6 100644
--- a/app/controllers/snippets/notes_controller.rb
+++ b/app/controllers/snippets/notes_controller.rb
@@ -5,8 +5,8 @@ class Snippets::NotesController < ApplicationController
include ToggleAwardEmoji
skip_before_action :authenticate_user!, only: [:index]
- before_action :snippet
- before_action :authorize_read_snippet!, only: [:show, :index, :create]
+ before_action :authorize_read_snippet!, only: [:show, :index]
+ before_action :authorize_create_note!, only: [:create]
private
@@ -33,4 +33,8 @@ class Snippets::NotesController < ApplicationController
def authorize_read_snippet!
return render_404 unless can?(current_user, :read_personal_snippet, snippet)
end
+
+ def authorize_create_note!
+ access_denied! unless can?(current_user, :create_note, noteable)
+ end
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 8ea5450b4e8..fad036b8df8 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -137,7 +137,7 @@ class SnippetsController < ApplicationController
def move_temporary_files
params[:files].each do |file|
- FileMover.new(file, @snippet).execute
+ FileMover.new(file, from_model: current_user, to_model: @snippet).execute
end
end
end
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 5d28635232b..94bd18f70d4 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -41,7 +41,11 @@ class UploadsController < ApplicationController
when Note
can?(current_user, :read_project, model.project)
when User
- true
+ # We validate the current user has enough (writing)
+ # access to itself when a secret is given.
+ # For instance, user avatars are readable by anyone,
+ # while temporary, user snippet uploads are not.
+ !secret? || can?(current_user, :update_user, model)
when Appearance
true
else
@@ -56,9 +60,13 @@ class UploadsController < ApplicationController
def authorize_create_access!
return unless model
- # for now we support only personal snippets comments. Only personal_snippet
- # is allowed as a model to #create through routing.
- authorized = can?(current_user, :create_note, model)
+ authorized =
+ case model
+ when User
+ can?(current_user, :update_user, model)
+ else
+ can?(current_user, :create_note, model)
+ end
render_unauthorized unless authorized
end
@@ -75,6 +83,10 @@ class UploadsController < ApplicationController
User === model || Appearance === model
end
+ def secret?
+ params[:secret].present?
+ end
+
def upload_model_class
MODEL_CLASSES[params[:model]] || raise(UnknownUploadModelError)
end
diff --git a/app/finders/runner_jobs_finder.rb b/app/finders/runner_jobs_finder.rb
index 4fca4ec94f3..ef90817416a 100644
--- a/app/finders/runner_jobs_finder.rb
+++ b/app/finders/runner_jobs_finder.rb
@@ -3,6 +3,8 @@
class RunnerJobsFinder
attr_reader :runner, :params
+ ALLOWED_INDEXED_COLUMNS = %w[id].freeze
+
def initialize(runner, params = {})
@runner = runner
@params = params
@@ -11,7 +13,7 @@ class RunnerJobsFinder
def execute
items = @runner.builds
items = by_status(items)
- items
+ sort_items(items)
end
private
@@ -23,4 +25,19 @@ class RunnerJobsFinder
items.where(status: params[:status])
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def sort_items(items)
+ return items unless ALLOWED_INDEXED_COLUMNS.include?(params[:order_by])
+
+ order_by = params[:order_by]
+ sort = if /\A(ASC|DESC)\z/i.match?(params[:sort])
+ params[:sort]
+ else
+ :desc
+ end
+
+ items.order(order_by => sort)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 5615909c4ec..152ebb930e2 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -13,6 +13,7 @@ class GitlabSchema < GraphQL::Schema
use BatchLoader::GraphQL
use Gitlab::Graphql::Authorize
use Gitlab::Graphql::Present
+ use Gitlab::Graphql::CallsGitaly
use Gitlab::Graphql::Connections
use Gitlab::Graphql::GenericTracing
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index dd0d9105df6..efeee4a7a4d 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -7,18 +7,34 @@ module Types
DEFAULT_COMPLEXITY = 1
def initialize(*args, **kwargs, &block)
+ @calls_gitaly = !!kwargs.delete(:calls_gitaly)
+ @constant_complexity = !!kwargs[:complexity]
kwargs[:complexity] ||= field_complexity(kwargs[:resolver_class])
super(*args, **kwargs, &block)
end
+ def base_complexity
+ complexity = DEFAULT_COMPLEXITY
+ complexity += 1 if calls_gitaly?
+ complexity
+ end
+
+ def calls_gitaly?
+ @calls_gitaly
+ end
+
+ def constant_complexity?
+ @constant_complexity
+ end
+
private
def field_complexity(resolver_class)
if resolver_class
field_resolver_complexity
else
- DEFAULT_COMPLEXITY
+ base_complexity
end
end
@@ -31,6 +47,7 @@ module Types
proc do |ctx, args, child_complexity|
# Resolvers may add extra complexity depending on used arguments
complexity = child_complexity + self.resolver&.try(:resolver_complexity, args, child_complexity: child_complexity).to_i
+ complexity += 1 if calls_gitaly?
field_defn = to_graphql
diff --git a/app/graphql/types/ci/detailed_status_type.rb b/app/graphql/types/ci/detailed_status_type.rb
index 2987354b556..5f7d7a934ce 100644
--- a/app/graphql/types/ci/detailed_status_type.rb
+++ b/app/graphql/types/ci/detailed_status_type.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module Types
module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ # This is presented through `PipelineType` that has its own authorization
class DetailedStatusType < BaseObject
graphql_name 'DetailedStatus'
@@ -13,5 +15,6 @@ module Types
field :text, GraphQL::STRING_TYPE, null: false
field :tooltip, GraphQL::STRING_TYPE, null: false, method: :status_tooltip
end
+ # rubocop: enable Graphql/AuthorizeTypes
end
end
diff --git a/app/graphql/types/issue_state_enum.rb b/app/graphql/types/issue_state_enum.rb
index 6521407fc9d..70c34fbe491 100644
--- a/app/graphql/types/issue_state_enum.rb
+++ b/app/graphql/types/issue_state_enum.rb
@@ -1,8 +1,11 @@
# frozen_string_literal: true
module Types
+ # rubocop: disable Graphql/AuthorizeTypes
+ # This is a BaseEnum through IssuableEnum, so it does not need authorization
class IssueStateEnum < IssuableStateEnum
graphql_name 'IssueState'
description 'State of a GitLab issue'
end
+ # rubocop: enable Graphql/AuthorizeTypes
end
diff --git a/app/graphql/types/label_type.rb b/app/graphql/types/label_type.rb
index 50eb1b89c61..3aeda2e7953 100644
--- a/app/graphql/types/label_type.rb
+++ b/app/graphql/types/label_type.rb
@@ -4,6 +4,8 @@ module Types
class LabelType < BaseObject
graphql_name 'Label'
+ authorize :read_label
+
field :description, GraphQL::STRING_TYPE, null: true
markdown_field :description_html, null: true
field :title, GraphQL::STRING_TYPE, null: false
diff --git a/app/graphql/types/merge_request_state_enum.rb b/app/graphql/types/merge_request_state_enum.rb
index 92f52726ab3..37c890a3c8d 100644
--- a/app/graphql/types/merge_request_state_enum.rb
+++ b/app/graphql/types/merge_request_state_enum.rb
@@ -1,10 +1,13 @@
# frozen_string_literal: true
module Types
+ # rubocop: disable Graphql/AuthorizeTypes
+ # This is a BaseEnum through IssuableEnum, so it does not need authorization
class MergeRequestStateEnum < IssuableStateEnum
graphql_name 'MergeRequestState'
description 'State of a GitLab merge request'
value 'merged'
end
+ # rubocop: enable Graphql/AuthorizeTypes
end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 577ccd48ef8..6734d4761c2 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -43,7 +43,7 @@ module Types
field :allow_collaboration, GraphQL::BOOLEAN_TYPE, null: true
field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false
field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true
- field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false
+ field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false, calls_gitaly: true
field :merge_commit_message, GraphQL::STRING_TYPE, method: :default_merge_commit_message, null: true, deprecation_reason: "Renamed to defaultMergeCommitMessage"
field :default_merge_commit_message, GraphQL::STRING_TYPE, null: true
field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false
diff --git a/app/graphql/types/metadata_type.rb b/app/graphql/types/metadata_type.rb
index 2d8bad0614b..7d7813a7652 100644
--- a/app/graphql/types/metadata_type.rb
+++ b/app/graphql/types/metadata_type.rb
@@ -4,6 +4,8 @@ module Types
class MetadataType < ::Types::BaseObject
graphql_name 'Metadata'
+ authorize :read_instance_metadata
+
field :version, GraphQL::STRING_TYPE, null: false
field :revision, GraphQL::STRING_TYPE, null: false
end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 6ef1d816b7c..bc5fb709522 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -9,6 +9,6 @@ module Types
mount_mutation Mutations::AwardEmojis::Add
mount_mutation Mutations::AwardEmojis::Remove
mount_mutation Mutations::AwardEmojis::Toggle
- mount_mutation Mutations::MergeRequests::SetWip
+ mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true
end
end
diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb
index 62feccaa660..f105e9e6e28 100644
--- a/app/graphql/types/namespace_type.rb
+++ b/app/graphql/types/namespace_type.rb
@@ -4,6 +4,8 @@ module Types
class NamespaceType < BaseObject
graphql_name 'Namespace'
+ authorize :read_namespace
+
field :id, GraphQL::ID_TYPE, null: false
field :name, GraphQL::STRING_TYPE, null: false
diff --git a/app/graphql/types/notes/diff_position_type.rb b/app/graphql/types/notes/diff_position_type.rb
index 104ccb79bbb..ebc24451715 100644
--- a/app/graphql/types/notes/diff_position_type.rb
+++ b/app/graphql/types/notes/diff_position_type.rb
@@ -2,6 +2,8 @@
module Types
module Notes
+ # rubocop: disable Graphql/AuthorizeTypes
+ # This is presented through `NoteType` that has its own authorization
class DiffPositionType < BaseObject
graphql_name 'DiffPosition'
@@ -42,5 +44,6 @@ module Types
description: "The total height of the image",
resolve: -> (position, _args, _ctx) { position.height if position.on_image? }
end
+ # rubocop: enable Graphql/AuthorizeTypes
end
end
diff --git a/app/graphql/types/permission_types/merge_request.rb b/app/graphql/types/permission_types/merge_request.rb
index 13995d3ea8f..d877fc177d2 100644
--- a/app/graphql/types/permission_types/merge_request.rb
+++ b/app/graphql/types/permission_types/merge_request.rb
@@ -10,8 +10,8 @@ module Types
abilities :read_merge_request, :admin_merge_request,
:update_merge_request, :create_note
- permission_field :push_to_source_branch, method: :can_push_to_source_branch?
- permission_field :remove_source_branch, method: :can_remove_source_branch?
+ permission_field :push_to_source_branch, method: :can_push_to_source_branch?, calls_gitaly: true
+ permission_field :remove_source_branch, method: :can_remove_source_branch?, calls_gitaly: true
permission_field :cherry_pick_on_current_merge_request, method: :can_cherry_pick_on_current_merge_request?
permission_field :revert_on_current_merge_request, method: :can_revert_on_current_merge_request?
end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index ac957eafafc..13be71c26ee 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -26,7 +26,7 @@ module Types
field :web_url, GraphQL::STRING_TYPE, null: true
field :star_count, GraphQL::INT_TYPE, null: false
- field :forks_count, GraphQL::INT_TYPE, null: false
+ field :forks_count, GraphQL::INT_TYPE, null: false, calls_gitaly: true # 4 times
field :created_at, Types::TimeType, null: true
field :last_activity_at, Types::TimeType, null: true
@@ -40,7 +40,7 @@ module Types
field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :merge_requests_ff_only_enabled, GraphQL::BOOLEAN_TYPE, null: true
- field :avatar_url, GraphQL::STRING_TYPE, null: true, resolve: -> (project, args, ctx) do
+ field :avatar_url, GraphQL::STRING_TYPE, null: true, calls_gitaly: true, resolve: -> (project, args, ctx) do
project.avatar_url(only_path: false)
end
@@ -67,14 +67,14 @@ module Types
field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true
field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true
- field :namespace, Types::NamespaceType, null: false
+ field :namespace, Types::NamespaceType, null: true
field :group, Types::GroupType, null: true
field :statistics, Types::ProjectStatisticsType,
null: true,
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchProjectStatisticsLoader.new(obj.id).find }
- field :repository, Types::RepositoryType, null: false
+ field :repository, Types::RepositoryType, null: true
field :merge_requests,
Types::MergeRequestType.connection_type,
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 536bdb077ad..53d36b43576 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -22,10 +22,7 @@ module Types
field :metadata, Types::MetadataType,
null: true,
resolver: Resolvers::MetadataResolver,
- description: 'Metadata about GitLab' do |*args|
-
- authorize :read_instance_metadata
- end
+ description: 'Metadata about GitLab'
field :echo, GraphQL::STRING_TYPE, null: false, function: Functions::Echo.new
end
diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb
index 5987467e1ea..b024eca61fc 100644
--- a/app/graphql/types/repository_type.rb
+++ b/app/graphql/types/repository_type.rb
@@ -6,9 +6,9 @@ module Types
authorize :download_code
- field :root_ref, GraphQL::STRING_TYPE, null: true
- field :empty, GraphQL::BOOLEAN_TYPE, null: false, method: :empty?
+ field :root_ref, GraphQL::STRING_TYPE, null: true, calls_gitaly: true
+ field :empty, GraphQL::BOOLEAN_TYPE, null: false, method: :empty?, calls_gitaly: true
field :exists, GraphQL::BOOLEAN_TYPE, null: false, method: :exists?
- field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver
+ field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true
end
end
diff --git a/app/graphql/types/task_completion_status.rb b/app/graphql/types/task_completion_status.rb
index c289802509d..ac128481ac4 100644
--- a/app/graphql/types/task_completion_status.rb
+++ b/app/graphql/types/task_completion_status.rb
@@ -1,6 +1,9 @@
# frozen_string_literal: true
module Types
+ # rubocop: disable Graphql/AuthorizeTypes
+ # This is used in `IssueType` and `MergeRequestType` both of which have their
+ # own authorization
class TaskCompletionStatus < BaseObject
graphql_name 'TaskCompletionStatus'
description 'Completion status of tasks'
@@ -8,4 +11,5 @@ module Types
field :count, GraphQL::INT_TYPE, null: false
field :completed_count, GraphQL::INT_TYPE, null: false
end
+ # rubocop: enable Graphql/AuthorizeTypes
end
diff --git a/app/graphql/types/tree/blob_type.rb b/app/graphql/types/tree/blob_type.rb
index 760781f3612..9497e378dc0 100644
--- a/app/graphql/types/tree/blob_type.rb
+++ b/app/graphql/types/tree/blob_type.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module Types
module Tree
+ # rubocop: disable Graphql/AuthorizeTypes
+ # This is presented through `Repository` that has its own authorization
class BlobType < BaseObject
implements Types::Tree::EntryType
@@ -12,6 +14,7 @@ module Types
field :lfs_oid, GraphQL::STRING_TYPE, null: true, resolve: -> (blob, args, ctx) do
Gitlab::Graphql::Loaders::BatchLfsOidLoader.new(blob.repository, blob.id).find
end
+ # rubocop: enable Graphql/AuthorizeTypes
end
end
end
diff --git a/app/graphql/types/tree/submodule_type.rb b/app/graphql/types/tree/submodule_type.rb
index cea76dbfd2a..8cb1e04f5ba 100644
--- a/app/graphql/types/tree/submodule_type.rb
+++ b/app/graphql/types/tree/submodule_type.rb
@@ -1,10 +1,13 @@
# frozen_string_literal: true
module Types
module Tree
+ # rubocop: disable Graphql/AuthorizeTypes
+ # This is presented through `Repository` that has its own authorization
class SubmoduleType < BaseObject
implements Types::Tree::EntryType
graphql_name 'Submodule'
end
+ # rubocop: enable Graphql/AuthorizeTypes
end
end
diff --git a/app/graphql/types/tree/tree_entry_type.rb b/app/graphql/types/tree/tree_entry_type.rb
index 23ec2ef0ec2..d7faa633706 100644
--- a/app/graphql/types/tree/tree_entry_type.rb
+++ b/app/graphql/types/tree/tree_entry_type.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module Types
module Tree
+ # rubocop: disable Graphql/AuthorizeTypes
+ # This is presented through `Repository` that has its own authorization
class TreeEntryType < BaseObject
implements Types::Tree::EntryType
@@ -11,5 +13,6 @@ module Types
field :web_url, GraphQL::STRING_TYPE, null: true
end
+ # rubocop: enable Graphql/AuthorizeTypes
end
end
diff --git a/app/graphql/types/tree/tree_type.rb b/app/graphql/types/tree/tree_type.rb
index cbc448a0695..fbdc1597461 100644
--- a/app/graphql/types/tree/tree_type.rb
+++ b/app/graphql/types/tree/tree_type.rb
@@ -1,11 +1,13 @@
# frozen_string_literal: true
module Types
module Tree
+ # rubocop: disable Graphql/AuthorizeTypes
+ # This is presented through `Repository` that has its own authorization
class TreeType < BaseObject
graphql_name 'Tree'
# Complexity 10 as it triggers a Gitaly call on each render
- field :last_commit, Types::CommitType, null: true, complexity: 10, resolve: -> (tree, args, ctx) do
+ field :last_commit, Types::CommitType, null: true, complexity: 10, calls_gitaly: true, resolve: -> (tree, args, ctx) do
tree.repository.last_commit_for_path(tree.sha, tree.path)
end
@@ -13,11 +15,12 @@ module Types
Gitlab::Graphql::Representation::TreeEntry.decorate(obj.trees, obj.repository)
end
- field :submodules, Types::Tree::SubmoduleType.connection_type, null: false
+ field :submodules, Types::Tree::SubmoduleType.connection_type, null: false, calls_gitaly: true
- field :blobs, Types::Tree::BlobType.connection_type, null: false, resolve: -> (obj, args, ctx) do
+ field :blobs, Types::Tree::BlobType.connection_type, null: false, calls_gitaly: true, resolve: -> (obj, args, ctx) do
Gitlab::Graphql::Representation::TreeEntry.decorate(obj.blobs, obj.repository)
end
+ # rubocop: enable Graphql/AuthorizeTypes
end
end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index a7a4e945a99..4bf9b708401 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -187,6 +187,8 @@ module ApplicationSettingsHelper
:gitaly_timeout_default,
:gitaly_timeout_medium,
:gitaly_timeout_fast,
+ :grafana_enabled,
+ :grafana_url,
:gravatar_enabled,
:hashed_storage_enabled,
:help_page_hide_commercial_content,
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 076976175a9..31c4b27273b 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module AuthHelper
- PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze
+ PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq salesforce).freeze
LDAP_PROVIDER = /\Aldap/.freeze
def ldap_enabled?
diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb
index eadf48205fc..c759882d7f8 100644
--- a/app/helpers/branches_helper.rb
+++ b/app/helpers/branches_helper.rb
@@ -8,12 +8,4 @@ module BranchesHelper
def protected_branch?(project, branch)
ProtectedBranch.protected?(project, branch.name)
end
-
- def diverging_count_label(count)
- if count >= Repository::MAX_DIVERGING_COUNT
- "#{Repository::MAX_DIVERGING_COUNT - 1}+"
- else
- count.to_s
- end
- end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 045de105b77..67685ba4e1d 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -280,7 +280,7 @@ module IssuablesHelper
initialTaskStatus: issuable.task_status
}
- data[:hasClosingMergeRequest] = issuable.merge_requests_count != 0 if issuable.is_a?(Issue)
+ data[:hasClosingMergeRequest] = issuable.merge_requests_count(current_user) != 0 if issuable.is_a?(Issue)
if parent.is_a?(Group)
data[:groupPath] = parent.path
@@ -390,8 +390,8 @@ module IssuablesHelper
def issuable_todo_button_data(issuable, is_collapsed)
{
- todo_text: _('Add todo'),
- mark_text: _('Mark todo as done'),
+ todo_text: _('Add a To Do'),
+ mark_text: _('Mark as done'),
todo_icon: sprite_icon('todo-add'),
mark_icon: sprite_icon('todo-done', css_class: 'todo-undone'),
issuable_id: issuable[:id],
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index dfadcfc33b2..5476a7cdff6 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -137,7 +137,7 @@ module IssuesHelper
end
def create_confidential_merge_request_enabled?
- Feature.enabled?(:create_confidential_merge_request, @project)
+ Feature.enabled?(:create_confidential_merge_request, @project, default_enabled: true)
end
def show_new_branch_button?
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 766508b6609..3672d8b1b03 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -16,7 +16,7 @@ module PreferencesHelper
project_activity: _("Your Projects' Activity"),
starred_project_activity: _("Starred Projects' Activity"),
groups: _("Your Groups"),
- todos: _("Your Todos"),
+ todos: _("Your To-Do List"),
issues: _("Assigned Issues"),
merge_requests: _("Assigned Merge Requests"),
operations: _("Operations Dashboard")
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index ecb2b2d707b..6ccc1fb2ed1 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -1,6 +1,16 @@
# frozen_string_literal: true
module SnippetsHelper
+ def snippets_upload_path(snippet, user)
+ return unless user
+
+ if snippet&.persisted?
+ upload_path('personal_snippet', id: snippet.id)
+ else
+ upload_path('user', id: user.id)
+ end
+ end
+
def reliable_snippet_path(snippet, opts = nil)
if snippet.project_id?
project_snippet_path(snippet.project, snippet, opts)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index fd5aa216174..20ca4a9ab24 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -790,6 +790,10 @@ module Ci
stages.find_by!(name: name)
end
+ def error_messages
+ errors ? errors.full_messages.to_sentence : ""
+ end
+
private
def ci_yaml_from_repo
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 127430cc68f..299e413321d 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -29,7 +29,11 @@ module Issuable
# This object is used to gather issuable meta data for displaying
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests
# lists avoiding n+1 queries and improving performance.
- IssuableMeta = Struct.new(:upvotes, :downvotes, :user_notes_count, :merge_requests_count)
+ IssuableMeta = Struct.new(:upvotes, :downvotes, :user_notes_count, :mrs_count) do
+ def merge_requests_count(user = nil)
+ mrs_count
+ end
+ end
included do
cache_markdown_field :title, pipeline: :single_line
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index b0e570f52ba..33f0be91632 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -16,6 +16,14 @@ class DeployToken < ApplicationRecord
has_many :projects, through: :project_deploy_tokens
validate :ensure_at_least_one_scope
+ validates :username,
+ length: { maximum: 255 },
+ allow_nil: true,
+ format: {
+ with: /\A[a-zA-Z0-9\.\+_-]+\z/,
+ message: "can contain only letters, digits, '_', '-', '+', and '.'"
+ }
+
before_save :ensure_token
accepts_nested_attributes_for :project_deploy_tokens
@@ -39,7 +47,7 @@ class DeployToken < ApplicationRecord
end
def username
- "gitlab+deploy-token-#{id}"
+ super || default_username
end
def has_access_to?(requested_project)
@@ -75,4 +83,8 @@ class DeployToken < ApplicationRecord
def ensure_at_least_one_scope
errors.add(:base, "Scopes can't be blank") unless read_repository || read_registry
end
+
+ def default_username
+ "gitlab+deploy-token-#{id}" if persisted?
+ end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 44bc6c8288a..3f80c1373f1 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -63,6 +63,8 @@ class Group < Namespace
after_save :update_two_factor_requirement
after_update :path_changed_hook, if: :saved_change_to_path?
+ scope :with_users, -> { includes(:users) }
+
class << self
def sort_by_attribute(method)
if method == 'storage_size_desc'
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 30e29911758..982a94315bd 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -250,8 +250,8 @@ class Issue < ApplicationRecord
end
# rubocop: enable CodeReuse/ServiceClass
- def merge_requests_count
- merge_requests_closing_issues.count
+ def merge_requests_count(user = nil)
+ ::MergeRequestsClosingIssues.count_for_issue(self.id, user)
end
def labels_hook_attrs
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 82034f5946b..e96e26cc773 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -223,7 +223,13 @@ class MergeRequest < ApplicationRecord
end
def rebase_in_progress?
- strong_memoize(:rebase_in_progress) do
+ (rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid)) ||
+ gitaly_rebase_in_progress?
+ end
+
+ # TODO: remove the Gitaly lookup after v12.1, when rebase_jid will be reliable
+ def gitaly_rebase_in_progress?
+ strong_memoize(:gitaly_rebase_in_progress) do
# The source project can be deleted
next false unless source_project
@@ -389,6 +395,26 @@ class MergeRequest < ApplicationRecord
update_column(:merge_jid, jid)
end
+ # Set off a rebase asynchronously, atomically updating the `rebase_jid` of
+ # the MR so that the status of the operation can be tracked.
+ def rebase_async(user_id)
+ transaction do
+ lock!
+
+ raise ActiveRecord::StaleObjectError if !open? || rebase_in_progress?
+
+ # Although there is a race between setting rebase_jid here and clearing it
+ # in the RebaseWorker, it can't do any harm since we check both that the
+ # attribute is set *and* that the sidekiq job is still running. So a JID
+ # for a completed RebaseWorker is equivalent to a nil JID.
+ jid = Sidekiq::Worker.skipping_transaction_check do
+ RebaseWorker.perform_async(id, user_id)
+ end
+
+ update_column(:rebase_jid, jid)
+ end
+ end
+
def merge_participants
participants = [author]
@@ -1353,6 +1379,7 @@ class MergeRequest < ApplicationRecord
end
# TODO: remove once production database rename completes
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/47592
alias_attribute :allow_collaboration, :allow_maintainer_to_push
def allow_collaboration
diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb
index 61af50841ee..22cedf57b86 100644
--- a/app/models/merge_requests_closing_issues.rb
+++ b/app/models/merge_requests_closing_issues.rb
@@ -7,11 +7,38 @@ class MergeRequestsClosingIssues < ApplicationRecord
validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true
validates :issue_id, presence: true
+ scope :with_issues, ->(ids) { where(issue_id: ids) }
+ scope :with_merge_requests_enabled, -> do
+ joins(:merge_request)
+ .joins('INNER JOIN project_features ON merge_requests.target_project_id = project_features.project_id')
+ .where('project_features.merge_requests_access_level >= :access', access: ProjectFeature::ENABLED)
+ end
+
+ scope :accessible_by, ->(user) do
+ joins(:merge_request)
+ .joins('INNER JOIN project_features ON merge_requests.target_project_id = project_features.project_id')
+ .where('project_features.merge_requests_access_level >= :access OR EXISTS(:authorizations)',
+ access: ProjectFeature::ENABLED,
+ authorizations: user.authorizations_for_projects(min_access_level: Gitlab::Access::REPORTER, related_project_column: "merge_requests.target_project_id")
+ )
+ end
+
class << self
- def count_for_collection(ids)
- group(:issue_id)
- .where(issue_id: ids)
- .pluck('issue_id', 'COUNT(*) as count')
+ def count_for_collection(ids, current_user)
+ closing_merge_requests(ids, current_user).group(:issue_id).pluck('issue_id', 'COUNT(*) as count')
+ end
+
+ def count_for_issue(id, current_user)
+ closing_merge_requests(id, current_user).count
+ end
+
+ private
+
+ def closing_merge_requests(ids, current_user)
+ return with_issues(ids) if current_user&.admin?
+ return with_issues(ids).with_merge_requests_enabled if current_user.blank?
+
+ with_issues(ids).accessible_by(current_user)
end
end
end
diff --git a/app/models/namespace/aggregation_schedule.rb b/app/models/namespace/aggregation_schedule.rb
index 355593597c6..0bef352cf24 100644
--- a/app/models/namespace/aggregation_schedule.rb
+++ b/app/models/namespace/aggregation_schedule.rb
@@ -44,4 +44,12 @@ class Namespace::AggregationSchedule < ApplicationRecord
def lease_key
"namespace:namespaces_root_statistics:#{namespace_id}"
end
+
+ # Used by ExclusiveLeaseGuard
+ # Overriding value as we never release the lease
+ # before the timeout in order to prevent multiple
+ # RootStatisticsWorker to start in a short span of time
+ def lease_release?
+ false
+ end
end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index 27b7827d55e..9f5c226f4c9 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -1,18 +1,8 @@
# frozen_string_literal: true
-##
-# NOTE:
-# We'll move this class to Clusters::Platforms::Kubernetes, which contains exactly the same logic.
-# After we've migrated data, we'll remove KubernetesService. This would happen in a few months.
-# If you're modyfiyng this class, please note that you should update the same change in Clusters::Platforms::Kubernetes.
class KubernetesService < Service
- include Gitlab::Kubernetes
- include ReactiveCaching
-
default_value_for :category, 'deployment'
- self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
-
# Namespace defaults to the project path, but can be overridden in case that
# is an invalid or inappropriate name
prop_accessor :namespace
@@ -47,8 +37,6 @@ class KubernetesService < Service
message: Gitlab::Regex.kubernetes_namespace_regex_message
}
- after_save :clear_reactive_cache!
-
def self.supported_events
%w()
end
@@ -94,72 +82,6 @@ class KubernetesService < Service
]
end
- def kubernetes_namespace_for(project)
- if namespace.present?
- namespace
- else
- default_namespace
- end
- end
-
- # Check we can connect to the Kubernetes API
- def test(*args)
- kubeclient = build_kube_client!
-
- kubeclient.core_client.discover
- { success: kubeclient.core_client.discovered, result: "Checked API discovery endpoint" }
- rescue => err
- { success: false, result: err }
- end
-
- # Project param was added on
- # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22011,
- # as a way to keep this service compatible with
- # Clusters::Platforms::Kubernetes, it won't be used on this method
- # as it's only needed for Clusters::Cluster.
- def predefined_variables(project:)
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables
- .append(key: 'KUBE_URL', value: api_url)
- .append(key: 'KUBE_TOKEN', value: token, public: false, masked: true)
- .append(key: 'KUBE_NAMESPACE', value: kubernetes_namespace_for(project))
- .append(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true)
-
- if ca_pem.present?
- variables
- .append(key: 'KUBE_CA_PEM', value: ca_pem)
- .append(key: 'KUBE_CA_PEM_FILE', value: ca_pem, file: true)
- end
- end
- end
-
- # Constructs a list of terminals from the reactive cache
- #
- # Returns nil if the cache is empty, in which case you should try again a
- # short time later
- def terminals(environment)
- with_reactive_cache do |data|
- project = environment.project
-
- pods = filter_by_project_environment(data[:pods], project.full_path_slug, environment.slug)
- terminals = pods.flat_map { |pod| terminals_for_pod(api_url, kubernetes_namespace_for(project), pod) }.compact
- terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) }
- end
- end
-
- # Caches resources in the namespace so other calls don't need to block on
- # network access
- def calculate_reactive_cache
- return unless active? && project && !project.pending_delete?
-
- # We may want to cache extra things in the future
- { pods: read_pods }
- end
-
- def kubeclient
- @kubeclient ||= build_kube_client!
- end
-
def deprecated?
true
end
@@ -186,14 +108,6 @@ class KubernetesService < Service
private
- def kubeconfig
- to_kubeconfig(
- url: api_url,
- namespace: kubernetes_namespace_for(project),
- token: token,
- ca_pem: ca_pem)
- end
-
def namespace_placeholder
default_namespace || TEMPLATE_PLACEHOLDER
end
@@ -205,49 +119,6 @@ class KubernetesService < Service
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end
- def build_kube_client!
- raise "Incomplete settings" unless api_url && kubernetes_namespace_for(project) && token
-
- Gitlab::Kubernetes::KubeClient.new(
- api_url,
- auth_options: kubeclient_auth_options,
- ssl_options: kubeclient_ssl_options,
- http_proxy_uri: ENV['http_proxy']
- )
- end
-
- # Returns a hash of all pods in the namespace
- def read_pods
- kubeclient = build_kube_client!
-
- kubeclient.get_pods(namespace: kubernetes_namespace_for(project)).as_json
- rescue Kubeclient::ResourceNotFoundError
- []
- end
-
- def kubeclient_ssl_options
- opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER }
-
- if ca_pem.present?
- opts[:cert_store] = OpenSSL::X509::Store.new
- opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem))
- end
-
- opts
- end
-
- def kubeclient_auth_options
- { bearer_token: token }
- end
-
- def terminal_auth
- {
- token: token,
- ca_pem: ca_pem,
- max_session_time: Gitlab::CurrentSettings.terminal_max_session_time
- }
- end
-
def enforce_namespace_to_lower_case
self.namespace = self.namespace&.downcase
end
diff --git a/app/models/release.rb b/app/models/release.rb
index 7bbeb3c9976..459a7c29ad0 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -12,12 +12,16 @@ class Release < ApplicationRecord
has_many :links, class_name: 'Releases::Link'
+ default_value_for :released_at, allows_nil: false do
+ Time.zone.now
+ end
+
accepts_nested_attributes_for :links, allow_destroy: true
validates :description, :project, :tag, presence: true
validates :name, presence: true, on: :create
- scope :sorted, -> { order(created_at: :desc) }
+ scope :sorted, -> { order(released_at: :desc) }
delegate :repository, to: :project
@@ -44,6 +48,10 @@ class Release < ApplicationRecord
end
end
+ def upcoming_release?
+ released_at.present? && released_at > Time.zone.now
+ end
+
private
def actual_sha
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 992ed7485e5..a408db7ebbe 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -6,7 +6,6 @@ class Repository
REF_MERGE_REQUEST = 'merge-requests'.freeze
REF_KEEP_AROUND = 'keep-around'.freeze
REF_ENVIRONMENTS = 'environments'.freeze
- MAX_DIVERGING_COUNT = 1000
RESERVED_REFS_NAMES = %W[
heads
@@ -840,10 +839,10 @@ class Repository
end
end
- def merge_to_ref(user, source_sha, merge_request, target_ref, message)
+ def merge_to_ref(user, source_sha, merge_request, target_ref, message, first_parent_ref)
branch = merge_request.target_branch
- raw.merge_to_ref(user, source_sha, branch, target_ref, message)
+ raw.merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref)
end
def ff_merge(user, source, target_branch, merge_request: nil)
diff --git a/app/policies/repository_policy.rb b/app/policies/repository_policy.rb
new file mode 100644
index 00000000000..32340749858
--- /dev/null
+++ b/app/policies/repository_policy.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class RepositoryPolicy < BasePolicy
+ delegate { @subject.project }
+end
diff --git a/app/serializers/test_suite_comparer_entity.rb b/app/serializers/test_suite_comparer_entity.rb
index 9fa3a897ebe..d402a4d5718 100644
--- a/app/serializers/test_suite_comparer_entity.rb
+++ b/app/serializers/test_suite_comparer_entity.rb
@@ -1,6 +1,9 @@
# frozen_string_literal: true
class TestSuiteComparerEntity < Grape::Entity
+ DEFAULT_MAX_TESTS = 100
+ DEFAULT_MIN_TESTS = 10
+
expose :name
expose :total_status, as: :status
@@ -10,7 +13,27 @@ class TestSuiteComparerEntity < Grape::Entity
expose :failed_count, as: :failed
end
- expose :new_failures, using: TestCaseEntity
- expose :resolved_failures, using: TestCaseEntity
- expose :existing_failures, using: TestCaseEntity
+ # rubocop: disable CodeReuse/ActiveRecord
+ expose :new_failures, using: TestCaseEntity do |suite|
+ suite.new_failures.take(max_tests)
+ end
+
+ expose :existing_failures, using: TestCaseEntity do |suite|
+ suite.existing_failures.take(
+ max_tests(suite.new_failures))
+ end
+
+ expose :resolved_failures, using: TestCaseEntity do |suite|
+ suite.resolved_failures.take(
+ max_tests(suite.new_failures, suite.existing_failures))
+ end
+
+ private
+
+ def max_tests(*used)
+ return Integer::MAX unless Feature.enabled?(:ci_limit_test_reports_size, default_enabled: true)
+
+ [DEFAULT_MAX_TESTS - used.map(&:count).sum, DEFAULT_MIN_TESTS].max
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb
index d726085b89a..e06659a39cd 100644
--- a/app/services/auto_merge/base_service.rb
+++ b/app/services/auto_merge/base_service.rb
@@ -29,7 +29,7 @@ module AutoMerge
end
def cancel(merge_request)
- if cancel_auto_merge(merge_request)
+ if clear_auto_merge_parameters(merge_request)
yield if block_given?
success
@@ -38,6 +38,16 @@ module AutoMerge
end
end
+ def abort(merge_request, reason)
+ if clear_auto_merge_parameters(merge_request)
+ yield if block_given?
+
+ success
+ else
+ error("Can't abort the automatic merge", 406)
+ end
+ end
+
private
def strategy
@@ -46,7 +56,7 @@ module AutoMerge
end
end
- def cancel_auto_merge(merge_request)
+ def clear_auto_merge_parameters(merge_request)
merge_request.auto_merge_enabled = false
merge_request.merge_user = nil
diff --git a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb
index c41073a73e9..6a33ec071db 100644
--- a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb
+++ b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb
@@ -5,7 +5,7 @@ module AutoMerge
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)
+ SystemNoteService.merge_when_pipeline_succeeds(merge_request, project, current_user, merge_request.actual_head_pipeline.sha)
end
end
end
@@ -19,7 +19,13 @@ module AutoMerge
def cancel(merge_request)
super do
- SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, @project, @current_user)
+ SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, project, current_user)
+ end
+ end
+
+ def abort(merge_request, reason)
+ super do
+ SystemNoteService.abort_merge_when_pipeline_succeeds(merge_request, project, current_user, reason)
end
end
diff --git a/app/services/auto_merge_service.rb b/app/services/auto_merge_service.rb
index 926d2f5fc66..95bf2db2018 100644
--- a/app/services/auto_merge_service.rb
+++ b/app/services/auto_merge_service.rb
@@ -42,6 +42,12 @@ class AutoMergeService < BaseService
get_service_instance(merge_request.auto_merge_strategy).cancel(merge_request)
end
+ def abort(merge_request, reason)
+ return error("Can't abort the automatic merge", 406) unless merge_request.auto_merge_enabled?
+
+ get_service_instance(merge_request.auto_merge_strategy).abort(merge_request, reason)
+ end
+
def available_strategies(merge_request)
self.class.all_strategies.select do |strategy|
get_service_instance(strategy).available_for?(merge_request)
diff --git a/app/services/branches/diverging_commit_counts_service.rb b/app/services/branches/diverging_commit_counts_service.rb
index f947cec1663..a3404caf2d7 100644
--- a/app/services/branches/diverging_commit_counts_service.rb
+++ b/app/services/branches/diverging_commit_counts_service.rb
@@ -8,11 +8,7 @@ module Branches
end
def call(branch)
- if Feature.enabled?('gitaly_count_diverging_commits_no_max')
- diverging_commit_counts_without_max(branch)
- else
- diverging_commit_counts(branch)
- end
+ diverging_commit_counts(branch)
end
private
@@ -22,27 +18,9 @@ module Branches
delegate :raw_repository, to: :repository
def diverging_commit_counts(branch)
- ## TODO: deprecate the below code after 12.0
@root_ref_hash ||= raw_repository.commit(repository.root_ref).id
cache.fetch(:"diverging_commit_counts_#{branch.name}") do
number_commits_behind, number_commits_ahead =
- repository.raw_repository.diverging_commit_count(
- @root_ref_hash,
- branch.dereferenced_target.sha,
- max_count: Repository::MAX_DIVERGING_COUNT)
-
- if number_commits_behind + number_commits_ahead >= Repository::MAX_DIVERGING_COUNT
- { distance: Repository::MAX_DIVERGING_COUNT }
- else
- { behind: number_commits_behind, ahead: number_commits_ahead }
- end
- end
- end
-
- def diverging_commit_counts_without_max(branch)
- @root_ref_hash ||= raw_repository.commit(repository.root_ref).id
- cache.fetch(:"diverging_commit_counts_without_max_#{branch.name}") do
- number_commits_behind, number_commits_ahead =
raw_repository.diverging_commit_count(
@root_ref_hash,
branch.dereferenced_target.sha)
diff --git a/app/services/ci/compare_reports_base_service.rb b/app/services/ci/compare_reports_base_service.rb
index d5625857599..6c2d80d8f45 100644
--- a/app/services/ci/compare_reports_base_service.rb
+++ b/app/services/ci/compare_reports_base_service.rb
@@ -8,7 +8,7 @@ module Ci
status: :parsed,
key: key(base_pipeline, head_pipeline),
data: serializer_class
- .new(project: project)
+ .new(**serializer_params)
.represent(comparer).as_json
}
rescue Gitlab::Ci::Parsers::ParserError => e
@@ -40,6 +40,10 @@ module Ci
raise NotImplementedError
end
+ def serializer_params
+ { project: project }
+ end
+
def get_report(pipeline)
raise NotImplementedError
end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index c17712355af..cdcc4b15bea 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -65,7 +65,7 @@ module Ci
def execute!(*args, &block)
execute(*args, &block).tap do |pipeline|
unless pipeline.persisted?
- raise CreateError, pipeline.errors.full_messages.join(',')
+ raise CreateError, pipeline.error_messages
end
end
end
diff --git a/app/services/deploy_tokens/create_service.rb b/app/services/deploy_tokens/create_service.rb
index dc0122002e9..327a1dbf408 100644
--- a/app/services/deploy_tokens/create_service.rb
+++ b/app/services/deploy_tokens/create_service.rb
@@ -3,7 +3,9 @@
module DeployTokens
class CreateService < BaseService
def execute
- @project.deploy_tokens.create(params)
+ @project.deploy_tokens.create(params) do |deploy_token|
+ deploy_token.username = params[:username].presence
+ end
end
end
end
diff --git a/app/services/discussions/update_diff_position_service.rb b/app/services/discussions/update_diff_position_service.rb
index c61437fb2e3..7bdf7711155 100644
--- a/app/services/discussions/update_diff_position_service.rb
+++ b/app/services/discussions/update_diff_position_service.rb
@@ -3,7 +3,8 @@
module Discussions
class UpdateDiffPositionService < BaseService
def execute(discussion)
- result = tracer.trace(discussion.position)
+ old_position = discussion.position
+ result = tracer.trace(old_position)
return unless result
position = result[:position]
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index c34fbeb2adb..067510a8a0a 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -68,8 +68,8 @@ module MergeRequests
!merge_request.for_fork?
end
- def cancel_auto_merge(merge_request)
- AutoMergeService.new(project, current_user).cancel(merge_request)
+ def abort_auto_merge(merge_request, reason)
+ AutoMergeService.new(project, current_user).abort(merge_request, reason)
end
# Returns all origin and fork merge requests from `@project` satisfying passed arguments.
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index b81a4dd81d2..c2174d2a130 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -18,7 +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)
+ abort_auto_merge(merge_request, 'merge request was closed')
end
merge_request
diff --git a/app/services/merge_requests/merge_to_ref_service.rb b/app/services/merge_requests/merge_to_ref_service.rb
index efe4dcd6255..0ea50a5dbf5 100644
--- a/app/services/merge_requests/merge_to_ref_service.rb
+++ b/app/services/merge_requests/merge_to_ref_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module MergeRequests
- # Performs the merge between source SHA and the target branch. Instead
+ # Performs the merge between source SHA and the target branch or the specified first parent ref. Instead
# of writing the result to the MR target branch, it targets the `target_ref`.
#
# Ideally this should leave the `target_ref` state with the same state the
@@ -56,12 +56,22 @@ module MergeRequests
raise_error(error) if error
end
+ ##
+ # The parameter `target_ref` is where the merge result will be written.
+ # Default is the merge ref i.e. `refs/merge-requests/:iid/merge`.
def target_ref
- merge_request.merge_ref_path
+ params[:target_ref] || merge_request.merge_ref_path
+ end
+
+ ##
+ # The parameter `first_parent_ref` is the main line of the merge commit.
+ # Default is the target branch ref of the merge request.
+ def first_parent_ref
+ params[:first_parent_ref] || merge_request.target_branch_ref
end
def commit
- repository.merge_to_ref(current_user, source, merge_request, target_ref, commit_message)
+ repository.merge_to_ref(current_user, source, merge_request, target_ref, commit_message, first_parent_ref)
rescue Gitlab::Git::PreReceiveError => error
raise MergeError, error.message
end
diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb
index 4b9921c28ba..8d3b9b05819 100644
--- a/app/services/merge_requests/rebase_service.rb
+++ b/app/services/merge_requests/rebase_service.rb
@@ -15,7 +15,7 @@ module MergeRequests
end
def rebase
- if merge_request.rebase_in_progress?
+ if merge_request.gitaly_rebase_in_progress?
log_error('Rebase task canceled: Another rebase is already in progress', save_message_on_model: true)
return false
end
@@ -27,6 +27,8 @@ module MergeRequests
log_error(REBASE_ERROR, save_message_on_model: true)
log_error(e.message)
false
+ ensure
+ merge_request.update_column(:rebase_jid, nil)
end
end
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 4b199bd8fa8..8961d2e1023 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
- cancel_auto_merges
+ abort_auto_merges
mark_pending_todos_done
cache_merge_requests_closing_issues
@@ -142,9 +142,9 @@ module MergeRequests
end
end
- def cancel_auto_merges
+ def abort_auto_merges
merge_requests_for_source_branch.each do |merge_request|
- cancel_auto_merge(merge_request)
+ abort_auto_merge(merge_request, 'source branch was updated')
end
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 0066cd0491f..d361e96babf 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -44,7 +44,7 @@ module MergeRequests
merge_request.previous_changes['target_branch'].first,
merge_request.target_branch)
- cancel_auto_merge(merge_request)
+ abort_auto_merge(merge_request, 'target branch was changed')
end
if merge_request.assignees != old_assignees
diff --git a/app/services/prometheus/adapter_service.rb b/app/services/prometheus/adapter_service.rb
index 3be958e1613..399f4c35d66 100644
--- a/app/services/prometheus/adapter_service.rb
+++ b/app/services/prometheus/adapter_service.rb
@@ -27,12 +27,9 @@ module Prometheus
end
def cluster_prometheus_adapter
- return unless deployment_platform.respond_to?(:cluster)
+ application = deployment_platform&.cluster&.application_prometheus
- cluster = deployment_platform.cluster
- return unless cluster.application_prometheus&.available?
-
- cluster.application_prometheus
+ application if application&.available?
end
end
end
diff --git a/app/services/releases/concerns.rb b/app/services/releases/concerns.rb
index ff6b696ca96..618d96717b8 100644
--- a/app/services/releases/concerns.rb
+++ b/app/services/releases/concerns.rb
@@ -22,6 +22,10 @@ module Releases
params[:description]
end
+ def released_at
+ params[:released_at]
+ end
+
def release
strong_memoize(:release) do
project.releases.find_by_tag(tag_name)
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
index a271a7e5e49..5b13ac631ba 100644
--- a/app/services/releases/create_service.rb
+++ b/app/services/releases/create_service.rb
@@ -58,6 +58,7 @@ module Releases
author: current_user,
tag: tag.name,
sha: tag.dereferenced_target.sha,
+ released_at: released_at,
links_attributes: params.dig(:assets, 'links') || []
)
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 8f7cfe582ca..e4564bc9b00 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -221,8 +221,8 @@ module SystemNoteService
end
# Called when 'merge when pipeline succeeds' is executed
- def merge_when_pipeline_succeeds(noteable, project, author, last_commit)
- body = "enabled an automatic merge when the pipeline for #{last_commit.to_reference(project)} succeeds"
+ def merge_when_pipeline_succeeds(noteable, project, author, sha)
+ body = "enabled an automatic merge when the pipeline for #{sha} succeeds"
create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
end
@@ -234,6 +234,16 @@ module SystemNoteService
create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
end
+ # Called when 'merge when pipeline succeeds' is aborted
+ def abort_merge_when_pipeline_succeeds(noteable, project, author, reason)
+ body = "aborted the automatic merge because #{reason}"
+
+ ##
+ # TODO: Abort message should be sent by the system, not a particular user.
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63187.
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
+ end
+
def handle_merge_request_wip(noteable, project, author)
prefix = noteable.work_in_progress? ? "marked" : "unmarked"
@@ -249,7 +259,7 @@ module SystemNoteService
end
def resolve_all_discussions(merge_request, project, author)
- body = "resolved all discussions"
+ body = "resolved all threads"
create_note(NoteSummary.new(merge_request, project, author, body, action: 'discussion'))
end
@@ -268,11 +278,13 @@ module SystemNoteService
merge_request = discussion.noteable
diff_refs = change_position.diff_refs
version_index = merge_request.merge_request_diffs.viewable.count
+ position_on_text = change_position.on_text?
+ text_parts = ["changed this #{position_on_text ? 'line' : 'file'} in"]
- text_parts = ["changed this line in"]
if version_params = merge_request.version_params_for(diff_refs)
- line_code = change_position.line_code(project.repository)
- url = url_helpers.diffs_project_merge_request_path(project, merge_request, version_params.merge(anchor: line_code))
+ repository = project.repository
+ anchor = position_on_text ? change_position.line_code(repository) : change_position.file_hash
+ url = url_helpers.diffs_project_merge_request_path(project, merge_request, version_params.merge(anchor: anchor))
text_parts << "[version #{version_index} of the diff](#{url})"
else
diff --git a/app/uploaders/file_mover.rb b/app/uploaders/file_mover.rb
index 236b7ed2b3d..12be1e2bb22 100644
--- a/app/uploaders/file_mover.rb
+++ b/app/uploaders/file_mover.rb
@@ -1,22 +1,29 @@
# frozen_string_literal: true
class FileMover
- attr_reader :secret, :file_name, :model, :update_field
+ include Gitlab::Utils::StrongMemoize
- def initialize(file_path, model, update_field = :description)
+ attr_reader :secret, :file_name, :from_model, :to_model, :update_field
+
+ def initialize(file_path, update_field = :description, from_model:, to_model:)
@secret = File.split(File.dirname(file_path)).last
@file_name = File.basename(file_path)
- @model = model
+ @from_model = from_model
+ @to_model = to_model
@update_field = update_field
end
def execute
+ temp_file_uploader.retrieve_from_store!(file_name)
+
return unless valid?
+ uploader.retrieve_from_store!(file_name)
+
move
if update_markdown
- uploader.record_upload
+ update_upload_model
uploader.schedule_background_upload
end
end
@@ -24,52 +31,77 @@ class FileMover
private
def valid?
- Pathname.new(temp_file_path).realpath.to_path.start_with?(
- (Pathname(temp_file_uploader.root) + temp_file_uploader.base_dir).to_path
- )
+ if temp_file_uploader.file_storage?
+ Pathname.new(temp_file_path).realpath.to_path.start_with?(
+ (Pathname(temp_file_uploader.root) + temp_file_uploader.base_dir).to_path
+ )
+ else
+ temp_file_uploader.exists?
+ end
end
def move
- FileUtils.mkdir_p(File.dirname(file_path))
- FileUtils.move(temp_file_path, file_path)
+ if temp_file_uploader.file_storage?
+ FileUtils.mkdir_p(File.dirname(file_path))
+ FileUtils.move(temp_file_path, file_path)
+ else
+ uploader.copy_file(temp_file_uploader.file)
+ temp_file_uploader.upload.destroy!
+ end
end
def update_markdown
- updated_text = model.read_attribute(update_field)
- .gsub(temp_file_uploader.markdown_link, uploader.markdown_link)
- model.update_attribute(update_field, updated_text)
+ updated_text = to_model.read_attribute(update_field)
+ .gsub(temp_file_uploader.markdown_link, uploader.markdown_link)
+ to_model.update_attribute(update_field, updated_text)
rescue
revert
false
end
- def temp_file_path
- return @temp_file_path if @temp_file_path
+ def update_upload_model
+ return unless upload = temp_file_uploader.upload
+ return if upload.destroyed?
- temp_file_uploader.retrieve_from_store!(file_name)
+ upload.update!(model: to_model)
+ end
- @temp_file_path = temp_file_uploader.file.path
+ def temp_file_path
+ strong_memoize(:temp_file_path) do
+ temp_file_uploader.file.path
+ end
end
def file_path
- return @file_path if @file_path
-
- uploader.retrieve_from_store!(file_name)
-
- @file_path = uploader.file.path
+ strong_memoize(:file_path) do
+ uploader.file.path
+ end
end
def uploader
- @uploader ||= PersonalFileUploader.new(model, secret: secret)
+ @uploader ||=
+ begin
+ uploader = PersonalFileUploader.new(to_model, secret: secret)
+
+ # Enforcing a REMOTE object storage given FileUploader#retrieve_from_store! won't do it
+ # (there's no upload at the target yet).
+ if uploader.class.object_store_enabled?
+ uploader.object_store = ::ObjectStorage::Store::REMOTE
+ end
+
+ uploader
+ end
end
def temp_file_uploader
- @temp_file_uploader ||= PersonalFileUploader.new(nil, secret: secret)
+ @temp_file_uploader ||= PersonalFileUploader.new(from_model, secret: secret)
end
def revert
- Rails.logger.warn("Markdown not updated, file move reverted for #{model}")
+ Rails.logger.warn("Markdown not updated, file move reverted for #{to_model}")
- FileUtils.move(file_path, temp_file_path)
+ if temp_file_uploader.file_storage?
+ FileUtils.move(file_path, temp_file_path)
+ end
end
end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 1c7582533ad..b326b266017 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -203,6 +203,6 @@ class FileUploader < GitlabUploader
end
def secure_url
- File.join('/uploads', @secret, file.filename)
+ File.join('/uploads', @secret, filename)
end
end
diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb
index b43162f0935..1ac69601d18 100644
--- a/app/uploaders/personal_file_uploader.rb
+++ b/app/uploaders/personal_file_uploader.rb
@@ -93,6 +93,6 @@ class PersonalFileUploader < FileUploader
end
def secure_url
- File.join('/', base_dir, secret, file.filename)
+ File.join('/', base_dir, secret, filename)
end
end
diff --git a/app/validators/color_validator.rb b/app/validators/color_validator.rb
index 1932d042e83..974dfbbf394 100644
--- a/app/validators/color_validator.rb
+++ b/app/validators/color_validator.rb
@@ -12,7 +12,7 @@
# end
#
class ColorValidator < ActiveModel::EachValidator
- PATTERN = /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/.freeze
+ PATTERN = /\A\#(?:[0-9A-Fa-f]{3}){1,2}\Z/.freeze
def validate_each(record, attribute, value)
unless value =~ PATTERN
diff --git a/app/views/admin/application_settings/_grafana.html.haml b/app/views/admin/application_settings/_grafana.html.haml
new file mode 100644
index 00000000000..b6e02bde895
--- /dev/null
+++ b/app/views/admin/application_settings/_grafana.html.haml
@@ -0,0 +1,17 @@
+= form_for @application_setting, url: admin_application_settings_path(anchor: 'js-grafana-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ %p
+ = _("Add a Grafana button in the admin sidebar, monitoring section, to access a variety of statistics on the health and performance of GitLab.")
+ = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/grafana_configuration.md')
+ .form-group
+ .form-check
+ = f.check_box :grafana_enabled, class: 'form-check-input'
+ = f.label :grafana_enabled, class: 'form-check-label' do
+ = _('Enable access to Grafana')
+ .form-group
+ = f.label :grafana_url, _('Grafana URL'), class: 'label-bold'
+ = f.text_field :grafana_url, class: 'form-control', placeholder: '/-/grafana'
+
+ = f.submit _('Save changes'), class: "btn btn-success"
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 01d61beaf53..55a48da8342 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -24,6 +24,17 @@
.settings-content
= render 'prometheus'
+%section.settings.as-grafana.no-animate#js-grafana-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Metrics - Grafana')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Enable and configure Grafana.')
+ .settings-content
+ = render 'grafana'
+
%section.settings.qa-performance-bar-settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index db6e40a6fd0..8cdfc7369a0 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -49,5 +49,5 @@
- else
.todo-actions
= link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-add-todo', data: { href: restore_dashboard_todo_path(todo) } do
- Add todo
+ Add a To Do
= icon('spinner spin')
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 8212fb8bb33..731e763f2be 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -1,11 +1,11 @@
- @hide_top_links = true
-- page_title "Todos"
-- header_title "Todos", dashboard_todos_path
+- page_title "To-Do List"
+- header_title "To-Do List", dashboard_todos_path
= render_dashboard_gold_trial(current_user)
.page-title-holder.d-flex.align-items-center
- %h1.page-title= _('Todos')
+ %h1.page-title= _('To-Do List')
- if current_user.todos.any?
.top-area
@@ -13,7 +13,7 @@
%li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
= link_to todos_filter_path(state: 'pending') do
%span
- Todos
+ To Do
%span.badge.badge-pill
= number_with_delimiter(todos_pending_count)
%li.todos-done{ class: active_when(params[:state] == 'done') }>
@@ -102,24 +102,24 @@
%p
Are you looking for things to do? Take a look at
= succeed "," do
- = link_to "the opened issues", issues_dashboard_path
+ = link_to "open issues", issues_dashboard_path
contribute to
- = link_to "merge requests", merge_requests_dashboard_path
- or mention someone in a comment to assign a new todo automatically.
+ = link_to "a merge request\,", merge_requests_dashboard_path
+ or mention someone in a comment to automatically assign them a new to-do item.
- else
%h4.text-center
- There are no todos to show.
+ Nothing is on your to-do list. Nice work!
- else
.todos-empty
.todos-empty-hero.svg-content
= image_tag 'illustrations/todos_empty.svg'
.todos-empty-content
%h4
- Todos let you see what you should do next
+ Your To-Do List shows what to work on next
%p
- When an issue or merge request is assigned to you, or when you
+ When an issue or merge request is assigned to you, or when you receive a
%strong
@mention
- in a comment, this will trigger a new item in your todo list, automatically.
+ in a comment, this automatically triggers a new item in your To-Do List.
%p
- You will always know what to work on next.
+ It's how you always know what to work on next.
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 10187129a33..9659d416a38 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -13,12 +13,12 @@
= icon("chevron-up")
- else
= icon("chevron-down")
- = _('Toggle discussion')
+ = _('Toggle thread')
= link_to_member(@project, discussion.author, avatar: false)
.inline.discussion-headline-light
= discussion.author.to_reference
- started a discussion
+ started a thread
- url = discussion_path(discussion)
- if discussion.for_commit? && @noteable != discussion.noteable
diff --git a/app/views/discussions/_new_issue_for_discussion.html.haml b/app/views/discussions/_new_issue_for_discussion.html.haml
index 2bfe118c608..49d5378d62e 100644
--- a/app/views/discussions/_new_issue_for_discussion.html.haml
+++ b/app/views/discussions/_new_issue_for_discussion.html.haml
@@ -4,7 +4,7 @@
.btn-group{ role: "group", "v-if" => "showButton" }
= link_to custom_icon('icon_mr_issue'),
new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id),
- title: 'Resolve this discussion in a new issue',
- aria: { label: 'Resolve this discussion in a new issue' },
+ title: 'Resolve this thread in a new issue',
+ aria: { label: 'Resolve this thread in a new issue' },
data: { container: 'body' },
class: 'new-issue-for-discussion btn btn-default discussion-create-issue-btn has-tooltip'
diff --git a/app/views/layouts/fullscreen.html.haml b/app/views/layouts/fullscreen.html.haml
index fa04b5be9f2..91a7777514c 100644
--- a/app/views/layouts/fullscreen.html.haml
+++ b/app/views/layouts/fullscreen.html.haml
@@ -3,6 +3,7 @@
= render "layouts/head"
%body{ class: "#{user_application_theme} #{@body_class} fullscreen-layout", data: { page: body_data_page } }
= render 'peek/bar'
+ = header_message
= render partial: "layouts/header/default", locals: { project: @project, group: @group }
= render 'shared/outdated_browser'
.mobile-overlay
@@ -12,3 +13,4 @@
= render "layouts/flash"
.content-wrapper{ id: "content-body", class: "d-flex flex-column align-items-stretch mt-0" }
= yield
+ = footer_message
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index f8b7d0c530a..f9ee6f42e23 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -53,7 +53,7 @@
= number_with_delimiter(merge_requests_count)
- if header_link?(:todos)
= nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do
- = link_to dashboard_todos_path, title: _('Todos'), aria: { label: _('Todos') }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = link_to dashboard_todos_path, title: _('To-Do List'), aria: { label: _('To-Do List') }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('todo-done', size: 16)
%span.badge.badge-pill.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count)
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 83fe871285a..87133c7ba22 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -81,6 +81,11 @@
= link_to admin_requests_profiles_path, title: _('Requests Profiles') do
%span
= _('Requests Profiles')
+ - if Gitlab::CurrentSettings.current_application_settings.grafana_enabled?
+ = nav_link do
+ = link_to Gitlab::CurrentSettings.current_application_settings.grafana_url, target: '_blank', title: _('Metrics Dashboard') do
+ %span
+ = _('Metrics Dashboard')
= render_if_exists 'layouts/nav/ee/admin/new_monitoring_sidebar'
= nav_link(controller: :broadcast_messages) do
diff --git a/app/views/layouts/snippets.html.haml b/app/views/layouts/snippets.html.haml
index 5f986c81ff4..841b2a5e79c 100644
--- a/app/views/layouts/snippets.html.haml
+++ b/app/views/layouts/snippets.html.haml
@@ -1,9 +1,10 @@
- header_title _("Snippets"), snippets_path
+- snippets_upload_path = snippets_upload_path(@snippet, current_user)
- content_for :page_specific_javascripts do
- - if @snippet && current_user
+ - if snippets_upload_path
-# haml-lint:disable InlineJavaScript
:javascript
- window.uploads_path = "#{upload_path('personal_snippet', id: @snippet.id)}";
+ window.uploads_path = "#{snippets_upload_path}";
= render template: "layouts/application"
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 4ebfaff0860..d16e2dddbe0 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -92,21 +92,21 @@
.col-lg-8
.form-group
%h5= s_('Preferences|Time format')
- .checkbox-icon-inline-wrapper.form-check
+ .checkbox-icon-inline-wrapper
- time_format_label = capture do
= s_('Preferences|Display time in 24-hour format')
- = f.check_box :time_format_in_24h, class: 'form-check-input'
+ = f.check_box :time_format_in_24h
= f.label :time_format_in_24h do
= time_format_label
%h5= s_('Preferences|Time display')
- .checkbox-icon-inline-wrapper.form-check
+ .checkbox-icon-inline-wrapper
- time_display_label = capture do
= s_('Preferences|Use relative times')
- = f.check_box :time_display_relative, class: 'form-check-input'
+ = f.check_box :time_display_relative
= f.label :time_display_relative do
= time_display_label
- .text-muted
- = s_('Preferences|For example: 30 mins ago.')
+ .form-text.text-muted
+ = s_('Preferences|For example: 30 mins ago.')
.col-lg-4.profile-settings-sidebar
.col-lg-8
.form-group
diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml
index b2dab0b5348..d95045c9cce 100644
--- a/app/views/projects/_flash_messages.html.haml
+++ b/app/views/projects/_flash_messages.html.haml
@@ -5,6 +5,7 @@
- if current_user && can?(current_user, :download_code, project)
= render 'shared/no_ssh'
= render 'shared/no_password'
+ = render_if_exists 'shared/shared_runners_minutes_limit', project: project
- unless project.empty_repo?
= render 'shared/auto_devops_implicitly_enabled_banner', project: project
= render_if_exists 'projects/above_size_limit_warning', project: project
diff --git a/app/views/projects/deploy_tokens/_form.html.haml b/app/views/projects/deploy_tokens/_form.html.haml
index 5412fcbc9d8..f846dbd3763 100644
--- a/app/views/projects/deploy_tokens/_form.html.haml
+++ b/app/views/projects/deploy_tokens/_form.html.haml
@@ -13,6 +13,11 @@
= f.text_field :expires_at, class: 'datepicker form-control qa-deploy-token-expires-at', value: f.object.expires_at
.form-group
+ = f.label :username, class: 'label-bold'
+ = f.text_field :username, class: 'form-control qa-deploy-token-username'
+ .text-secondary= s_('DeployTokens|Default format is "gitlab+deploy-token-{n}". Enter custom username if you want to change it.')
+
+ .form-group
= f.label :scopes, class: 'label-bold'
%fieldset.form-group.form-check
= f.check_box :read_repository, class: 'form-check-input qa-deploy-token-read-repository'
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 52bb797b5b3..8d3e54dc455 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -3,13 +3,14 @@
- data_action = can_create_merge_request ? 'create-mr' : 'create-branch'
- value = can_create_merge_request ? 'Create merge request' : 'Create branch'
- value = can_create_confidential_merge_request? ? _('Create confidential merge request') : value
+ - create_mr_text = can_create_confidential_merge_request? ? _('Create confidential merge request') : _('Create merge request')
- can_create_path = can_create_branch_project_issue_path(@project, @issue)
- create_mr_path = create_merge_request_project_issue_path(@project, @issue, branch_name: @issue.to_branch_name, ref: @project.default_branch)
- create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid)
- refs_path = refs_namespace_project_path(@project.namespace, @project, search: '')
- .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 } }
+ .create-mr-dropdown-wrap.d-inline-block.full-width-mobile.js-create-mr{ data: { project_path: @project.full_path, project_id: @project.id, can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path, is_confidential: can_create_confidential_merge_request?.to_s } }
.btn-group.btn-group-sm.unavailable
%button.btn.btn-grouped{ type: 'button', disabled: 'disabled' }
= icon('spinner', class: 'fa-spin')
@@ -26,7 +27,7 @@
.droplab-dropdown
%ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-right.gl-show-field-errors{ class: ("create-confidential-merge-request-dropdown-menu" if can_create_confidential_merge_request?), data: { dropdown: true } }
- if can_create_merge_request
- %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: _('Create merge request') } }
+ %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: create_mr_text } }
.menu-item
= icon('check', class: 'icon')
- if can_create_confidential_merge_request?
@@ -41,6 +42,8 @@
%li.divider.droplab-item-ignore
%li.droplab-item-ignore.prepend-left-8.append-right-8.prepend-top-16
+ - if can_create_confidential_merge_request?
+ #js-forked-project{ data: { namespace_path: @project.namespace.full_path, project_path: @project.full_path, new_fork_path: new_project_fork_path(@project), help_page_path: help_page_path('user/project/merge_requests') } }
.form-group
%label{ for: 'new-branch-name' }
= _('Branch name')
@@ -55,4 +58,8 @@
.form-group
%button.btn.btn-success.js-create-target{ type: 'button', data: { action: 'create-mr' } }
- = _('Create merge request')
+ = create_mr_text
+
+ - if can_create_confidential_merge_request?
+ %p.text-warning.js-exposed-info-warning.hidden
+ = _('This may expose confidential information as the selected fork is in another namespace that can have other members.')
diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml
index 31a5370a5f8..71b13a5d741 100644
--- a/app/views/shared/_issuable_meta_data.html.haml
+++ b/app/views/shared/_issuable_meta_data.html.haml
@@ -2,7 +2,7 @@
- issue_votes = @issuable_meta_data[issuable.id]
- upvotes, downvotes = issue_votes.upvotes, issue_votes.downvotes
- issuable_url = @collection_type == "Issue" ? issue_path(issuable, anchor: 'notes') : merge_request_path(issuable, anchor: 'notes')
-- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count
+- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count(current_user)
- if issuable_mr > 0
%li.issuable-mr.d-none.d-sm-block.has-tooltip{ title: _('Related merge requests') }
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index c6a391ae563..1bd56e064d5 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -48,13 +48,13 @@
= hidden_field_tag 'merge_request_to_resolve_discussions_of', @merge_request_to_resolve_discussions_of.iid
- if @discussion_to_resolve
= hidden_field_tag 'discussion_to_resolve', @discussion_to_resolve.id
- Creating this issue will resolve the discussion in
+ Creating this issue will resolve the thread in
- else
- Creating this issue will resolve all discussions in
+ Creating this issue will resolve all threads in
= link_to_discussions_to_resolve(@merge_request_to_resolve_discussions_of, @discussion_to_resolve)
- else
The
- = @discussion_to_resolve ? 'discussion' : 'discussions'
+ = @discussion_to_resolve ? 'thread' : 'threads'
at
= link_to_discussions_to_resolve(@merge_request_to_resolve_discussions_of, @discussion_to_resolve)
will stay unresolved. Ask someone with permission to resolve
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index e87e560266f..b4f8377c008 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -10,7 +10,7 @@
.block.issuable-sidebar-header
- if signed_in
%span.issuable-header-text.hide-collapsed.float-left
- = _('Todo')
+ = _('To Do')
%a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
= sidebar_gutter_toggle_icon
- if signed_in
diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml
index c3f5eeb0da6..8d74eacc7dc 100644
--- a/app/views/shared/notes/_comment_button.html.haml
+++ b/app/views/shared/notes/_comment_button.html.haml
@@ -18,11 +18,11 @@
%li.divider.droplab-item-ignore
- %li#discussion{ data: { value: 'DiscussionNote', 'submit-text' => _('Start discussion'), 'close-text' => _("Start discussion & close %{noteable_name}") % { noteable_name: noteable_name }, 'reopen-text' => _("Start discussion & reopen %{noteable_name}") % { noteable_name: noteable_name } } }
+ %li#discussion{ data: { value: 'DiscussionNote', 'submit-text' => _('Start thread'), 'close-text' => _("Start thread & close %{noteable_name}") % { noteable_name: noteable_name }, 'reopen-text' => _("Start thread & reopen %{noteable_name}") % { noteable_name: noteable_name } } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
.description
- %strong= _("Start discussion")
+ %strong= _("Start thread")
%p
= succeed '.' do
- if @note.noteable.supports_resolvable_notes?
diff --git a/app/workers/rebase_worker.rb b/app/workers/rebase_worker.rb
index a6baebc1443..8d06adcd993 100644
--- a/app/workers/rebase_worker.rb
+++ b/app/workers/rebase_worker.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+# The RebaseWorker must be wrapped in important concurrency code, so should only
+# be scheduled via MergeRequest#rebase_async
class RebaseWorker
include ApplicationWorker
diff --git a/changelogs/unreleased/12550-fullscrean.yml b/changelogs/unreleased/12550-fullscrean.yml
new file mode 100644
index 00000000000..f20b191f411
--- /dev/null
+++ b/changelogs/unreleased/12550-fullscrean.yml
@@ -0,0 +1,5 @@
+---
+title: Removes EE differences for app/views/layouts/fullscreen.html.haml
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/12553-preferences.yml b/changelogs/unreleased/12553-preferences.yml
new file mode 100644
index 00000000000..c41a8c98e4e
--- /dev/null
+++ b/changelogs/unreleased/12553-preferences.yml
@@ -0,0 +1,5 @@
+---
+title: Removes EE diff for app/views/profiles/preferences/show.html.haml
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/32452-multiple-discussions.yml b/changelogs/unreleased/32452-multiple-discussions.yml
new file mode 100644
index 00000000000..5552340ee66
--- /dev/null
+++ b/changelogs/unreleased/32452-multiple-discussions.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Multiple discussions per line in merge request diffs
+merge_request: 28748
+author:
+type: added
diff --git a/changelogs/unreleased/50228-deploy-tokens-custom-username.yml b/changelogs/unreleased/50228-deploy-tokens-custom-username.yml
new file mode 100644
index 00000000000..fdafa7b1113
--- /dev/null
+++ b/changelogs/unreleased/50228-deploy-tokens-custom-username.yml
@@ -0,0 +1,5 @@
+---
+title: Allow custom username for deploy tokens
+merge_request: 29639
+author:
+type: added
diff --git a/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml b/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml
new file mode 100644
index 00000000000..908a132688c
--- /dev/null
+++ b/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add order_by and sort params to list runner jobs api
+merge_request: 29629
+author: Sujay Patel
+type: added
diff --git a/changelogs/unreleased/53357-fix-plus-in-upload-file-names.yml b/changelogs/unreleased/53357-fix-plus-in-upload-file-names.yml
new file mode 100644
index 00000000000..c11d74bd4fe
--- /dev/null
+++ b/changelogs/unreleased/53357-fix-plus-in-upload-file-names.yml
@@ -0,0 +1,5 @@
+---
+title: Fix broken URLs for uploads with a plus in the filename
+merge_request: 29915
+author:
+type: fixed
diff --git a/changelogs/unreleased/54117-transactional-rebase.yml b/changelogs/unreleased/54117-transactional-rebase.yml
new file mode 100644
index 00000000000..d0c93114c49
--- /dev/null
+++ b/changelogs/unreleased/54117-transactional-rebase.yml
@@ -0,0 +1,5 @@
+---
+title: Allow asynchronous rebase operations to be monitored
+merge_request: 29940
+author:
+type: fixed
diff --git a/changelogs/unreleased/55487-enable-group-terminals-button.yml b/changelogs/unreleased/55487-enable-group-terminals-button.yml
new file mode 100644
index 00000000000..6bf16eaadd1
--- /dev/null
+++ b/changelogs/unreleased/55487-enable-group-terminals-button.yml
@@ -0,0 +1,5 @@
+---
+title: Enable terminals button for group clusters
+merge_request: 30255
+author:
+type: added
diff --git a/changelogs/unreleased/55953-renamed-discussion-to-thread.yml b/changelogs/unreleased/55953-renamed-discussion-to-thread.yml
new file mode 100644
index 00000000000..a5203ecf2e0
--- /dev/null
+++ b/changelogs/unreleased/55953-renamed-discussion-to-thread.yml
@@ -0,0 +1,5 @@
+---
+title: "renamed discussion to thread in merge-request and issue timeline"
+merge_request: 29553
+author: Michel Engelen
+type: changed
diff --git a/changelogs/unreleased/57793-fix-line-age.yml b/changelogs/unreleased/57793-fix-line-age.yml
new file mode 100644
index 00000000000..cf4e328e54a
--- /dev/null
+++ b/changelogs/unreleased/57793-fix-line-age.yml
@@ -0,0 +1,5 @@
+---
+title: Support note position tracing on an image
+merge_request: 30158
+author:
+type: fixed
diff --git a/changelogs/unreleased/61005-grafanaInAdminSettingsMonitoringMenu.yml b/changelogs/unreleased/61005-grafanaInAdminSettingsMonitoringMenu.yml
new file mode 100644
index 00000000000..3ee512f3448
--- /dev/null
+++ b/changelogs/unreleased/61005-grafanaInAdminSettingsMonitoringMenu.yml
@@ -0,0 +1,5 @@
+---
+title: Adds link to Grafana in Admin > Monitoring settings when grafana is enabled in config
+merge_request: 28937
+author: Romain Maneschi
+type: added
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/63873-process-start-time.yml b/changelogs/unreleased/63873-process-start-time.yml
new file mode 100644
index 00000000000..b11a66ca106
--- /dev/null
+++ b/changelogs/unreleased/63873-process-start-time.yml
@@ -0,0 +1,6 @@
+---
+title: Change ruby_process_start_time_seconds metric to unix timestamp instead of
+ seconds from boot.
+merge_request: 30195
+author:
+type: fixed
diff --git a/changelogs/unreleased/63971-remove-istanbul.yml b/changelogs/unreleased/63971-remove-istanbul.yml
new file mode 100644
index 00000000000..674df82db35
--- /dev/null
+++ b/changelogs/unreleased/63971-remove-istanbul.yml
@@ -0,0 +1,5 @@
+---
+title: Remove istanbul JavaScript package
+merge_request: 30232
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/add-salesforce-logo.yml b/changelogs/unreleased/add-salesforce-logo.yml
new file mode 100644
index 00000000000..13766821b88
--- /dev/null
+++ b/changelogs/unreleased/add-salesforce-logo.yml
@@ -0,0 +1,5 @@
+---
+title: Add salesforce logo for salesforce SSO
+merge_request: 28857
+author:
+type: changed
diff --git a/changelogs/unreleased/asciidoc-enable-syntax-highlighting.yml b/changelogs/unreleased/asciidoc-enable-syntax-highlighting.yml
new file mode 100644
index 00000000000..558ee7b6224
--- /dev/null
+++ b/changelogs/unreleased/asciidoc-enable-syntax-highlighting.yml
@@ -0,0 +1,5 @@
+---
+title: "Enable syntax highlighting for AsciiDoc"
+merge_request: 29835
+author: Guillaume Grossetie
+type: added \ No newline at end of file
diff --git a/changelogs/unreleased/create-merge-train-ref-ce.yml b/changelogs/unreleased/create-merge-train-ref-ce.yml
new file mode 100644
index 00000000000..b0b95275f58
--- /dev/null
+++ b/changelogs/unreleased/create-merge-train-ref-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Extend `MergeToRefService` to create merge ref from an arbitrary ref
+merge_request: 30361
+author:
+type: added
diff --git a/changelogs/unreleased/fix-sidekiq-transaction-check-race.yml b/changelogs/unreleased/fix-sidekiq-transaction-check-race.yml
new file mode 100644
index 00000000000..89ae4abfe11
--- /dev/null
+++ b/changelogs/unreleased/fix-sidekiq-transaction-check-race.yml
@@ -0,0 +1,5 @@
+---
+title: Fix race in forbid_sidekiq_in_transactions.rb
+merge_request: 30359
+author:
+type: fixed
diff --git a/changelogs/unreleased/gitaly-version-v1.51.0.yml b/changelogs/unreleased/gitaly-version-v1.51.0.yml
new file mode 100644
index 00000000000..00d52a190f3
--- /dev/null
+++ b/changelogs/unreleased/gitaly-version-v1.51.0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Gitaly v1.51.0
+merge_request: 30353
+author:
+type: changed
diff --git a/changelogs/unreleased/issue-63222.yml b/changelogs/unreleased/issue-63222.yml
new file mode 100644
index 00000000000..bc6520b9fc5
--- /dev/null
+++ b/changelogs/unreleased/issue-63222.yml
@@ -0,0 +1,5 @@
+---
+title: Set default sort method for dashboard projects list
+merge_request: 29830
+author: David Palubin
+type: fixed
diff --git a/changelogs/unreleased/limit-amount-of-tests-returned.yml b/changelogs/unreleased/limit-amount-of-tests-returned.yml
new file mode 100644
index 00000000000..0e80a64b6b7
--- /dev/null
+++ b/changelogs/unreleased/limit-amount-of-tests-returned.yml
@@ -0,0 +1,5 @@
+---
+title: Limit amount of JUnit tests returned
+merge_request: 30274
+author:
+type: performance
diff --git a/changelogs/unreleased/osw-persist-tmp-snippet-uploads.yml b/changelogs/unreleased/osw-persist-tmp-snippet-uploads.yml
new file mode 100644
index 00000000000..9348626c41d
--- /dev/null
+++ b/changelogs/unreleased/osw-persist-tmp-snippet-uploads.yml
@@ -0,0 +1,5 @@
+---
+title: Persist tmp snippet uploads at users
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/pre-releases-38105a.yml b/changelogs/unreleased/pre-releases-38105a.yml
new file mode 100644
index 00000000000..8b7cf6065d4
--- /dev/null
+++ b/changelogs/unreleased/pre-releases-38105a.yml
@@ -0,0 +1,5 @@
+---
+title: Show an Upcoming Status for Releases
+merge_request: 29577
+author:
+type: added
diff --git a/changelogs/unreleased/rj-fix-manual-order.yml b/changelogs/unreleased/rj-fix-manual-order.yml
new file mode 100644
index 00000000000..ecc39b78b06
--- /dev/null
+++ b/changelogs/unreleased/rj-fix-manual-order.yml
@@ -0,0 +1,5 @@
+---
+title: Don't let logged out user do manual order
+merge_request: 30264
+author:
+type: fixed
diff --git a/changelogs/unreleased/security-2858-fix-color-validation.yml b/changelogs/unreleased/security-2858-fix-color-validation.yml
new file mode 100644
index 00000000000..3430207a2b6
--- /dev/null
+++ b/changelogs/unreleased/security-2858-fix-color-validation.yml
@@ -0,0 +1,5 @@
+---
+title: Fix DoS vulnerability in color validation regex
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-59581-related-merge-requests-count.yml b/changelogs/unreleased/security-59581-related-merge-requests-count.yml
new file mode 100644
index 00000000000..83faa2f7c13
--- /dev/null
+++ b/changelogs/unreleased/security-59581-related-merge-requests-count.yml
@@ -0,0 +1,5 @@
+---
+title: Expose merge requests count based on user access
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-DOS_issue_comments_banzai.yml b/changelogs/unreleased/security-DOS_issue_comments_banzai.yml
new file mode 100644
index 00000000000..2405b1a4f5f
--- /dev/null
+++ b/changelogs/unreleased/security-DOS_issue_comments_banzai.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Denial of Service for comments when rendering issues/MR comments
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-bvl-enforce-graphql-type-authorization.yml b/changelogs/unreleased/security-bvl-enforce-graphql-type-authorization.yml
new file mode 100644
index 00000000000..7dedb9f6230
--- /dev/null
+++ b/changelogs/unreleased/security-bvl-enforce-graphql-type-authorization.yml
@@ -0,0 +1,5 @@
+---
+title: Add missing authorizations in GraphQL
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-fp-prevent-billion-laughs-attack.yml b/changelogs/unreleased/security-fp-prevent-billion-laughs-attack.yml
new file mode 100644
index 00000000000..4e0cf848931
--- /dev/null
+++ b/changelogs/unreleased/security-fp-prevent-billion-laughs-attack.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent Billion Laughs attack
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-mr-head-pipeline-leak.yml b/changelogs/unreleased/security-mr-head-pipeline-leak.yml
new file mode 100644
index 00000000000..fe8c4dfb3c8
--- /dev/null
+++ b/changelogs/unreleased/security-mr-head-pipeline-leak.yml
@@ -0,0 +1,5 @@
+---
+title: Gate MR head_pipeline behind read_pipeline ability.
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-notes-in-private-snippets.yml b/changelogs/unreleased/security-notes-in-private-snippets.yml
new file mode 100644
index 00000000000..907d98cb16d
--- /dev/null
+++ b/changelogs/unreleased/security-notes-in-private-snippets.yml
@@ -0,0 +1,5 @@
+---
+title: Correctly check permissions when creating snippet notes
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-prevent-detection-of-merge-request-template-name.yml b/changelogs/unreleased/security-prevent-detection-of-merge-request-template-name.yml
new file mode 100644
index 00000000000..d7bb884cb4b
--- /dev/null
+++ b/changelogs/unreleased/security-prevent-detection-of-merge-request-template-name.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent the detection of merge request templates by unauthorized users
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/sh-cache-flipper-checks-in-memory.yml b/changelogs/unreleased/sh-cache-flipper-checks-in-memory.yml
new file mode 100644
index 00000000000..125b6244d80
--- /dev/null
+++ b/changelogs/unreleased/sh-cache-flipper-checks-in-memory.yml
@@ -0,0 +1,5 @@
+---
+title: Cache Flipper feature flags in L1 and L2 caches
+merge_request: 30276
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-fix-issue-63349.yml b/changelogs/unreleased/sh-fix-issue-63349.yml
new file mode 100644
index 00000000000..0e51a6b7b20
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-issue-63349.yml
@@ -0,0 +1,5 @@
+---
+title: Make Housekeeping button do a full garbage collection
+merge_request: 30289
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-improve-redis-peek.yml b/changelogs/unreleased/sh-improve-redis-peek.yml
new file mode 100644
index 00000000000..940be103ab7
--- /dev/null
+++ b/changelogs/unreleased/sh-improve-redis-peek.yml
@@ -0,0 +1,5 @@
+---
+title: Add Redis call details in Peek performance bar
+merge_request: 30191
+author:
+type: changed
diff --git a/changelogs/unreleased/slugify.yml b/changelogs/unreleased/slugify.yml
new file mode 100644
index 00000000000..853e90b8bed
--- /dev/null
+++ b/changelogs/unreleased/slugify.yml
@@ -0,0 +1,5 @@
+---
+title: Replace slugifyWithHyphens with improved slugify function
+merge_request: 30172
+author: Luke Ward
+type: fixed
diff --git a/changelogs/unreleased/update-todo-in-ui.yml b/changelogs/unreleased/update-todo-in-ui.yml
new file mode 100644
index 00000000000..dddcf0f3983
--- /dev/null
+++ b/changelogs/unreleased/update-todo-in-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Changes "Todo" to "To Do" in the UI for clarity
+merge_request: 28844
+author:
+type: other
diff --git a/changelogs/unreleased/use-pg-9-6-11-on-ci.yml b/changelogs/unreleased/use-pg-9-6-11-on-ci.yml
new file mode 100644
index 00000000000..785eb352895
--- /dev/null
+++ b/changelogs/unreleased/use-pg-9-6-11-on-ci.yml
@@ -0,0 +1,5 @@
+---
+title: Use PostgreSQL 9.6.11 in CI tests
+merge_request: 30270
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/winh-jest-markdown-header.yml b/changelogs/unreleased/winh-jest-markdown-header.yml
new file mode 100644
index 00000000000..6bf9d75cc93
--- /dev/null
+++ b/changelogs/unreleased/winh-jest-markdown-header.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate markdown header_spec.js to Jest
+merge_request: 30228
+author: Martin Hobert
+type: other
diff --git a/changelogs/unreleased/winh-updateResolvableDiscussionsCounts-typo.yml b/changelogs/unreleased/winh-updateResolvableDiscussionsCounts-typo.yml
new file mode 100644
index 00000000000..24b10bb3edc
--- /dev/null
+++ b/changelogs/unreleased/winh-updateResolvableDiscussionsCounts-typo.yml
@@ -0,0 +1,5 @@
+---
+title: Fix typo in updateResolvableDiscussionsCounts action
+merge_request: 30278
+author: Frank van Rest
+type: other
diff --git a/config/README.md b/config/README.md
index 2778d0d4f02..9226f71a374 100644
--- a/config/README.md
+++ b/config/README.md
@@ -39,7 +39,7 @@ If desired, the routing URL provided by these settings can be used with:
1. `host name` or IP for each Redis instance desired
2. TCP port number for each Redis instance desired
3. `database number` for each Redis instance desired
-
+
## Example URL attribute formats for GitLab Redis `.yml` configuration files
* Unix Socket, default Redis database (0)
* `url: unix:/path/to/redis.sock`
@@ -147,4 +147,3 @@ searched):
3. the configuration file pointed to by the
`GITLAB_REDIS_CONFIG_FILE` environment variable
4. the configuration file `resque.yml`
-
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index 5db72141fcd..741c8ef1ca0 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -1,15 +1,27 @@
require 'prometheus/client'
require 'prometheus/client/support/unicorn'
+# Keep separate directories for separate processes
+def prometheus_default_multiproc_dir
+ return unless Rails.env.development? || Rails.env.test?
+
+ if Sidekiq.server?
+ Rails.root.join('tmp/prometheus_multiproc_dir/sidekiq')
+ elsif defined?(Unicorn::Worker)
+ Rails.root.join('tmp/prometheus_multiproc_dir/unicorn')
+ elsif defined?(::Puma)
+ Rails.root.join('tmp/prometheus_multiproc_dir/puma')
+ else
+ Rails.root.join('tmp/prometheus_multiproc_dir')
+ end
+end
+
Prometheus::Client.configure do |config|
config.logger = Rails.logger
config.initial_mmap_file_size = 4 * 1024
- config.multiprocess_files_dir = ENV['prometheus_multiproc_dir']
- if Rails.env.development? || Rails.env.test?
- config.multiprocess_files_dir ||= Rails.root.join('tmp/prometheus_multiproc_dir')
- end
+ config.multiprocess_files_dir = ENV['prometheus_multiproc_dir'] || prometheus_default_multiproc_dir
config.pid_provider = Prometheus::Client::Support::Unicorn.method(:worker_pid_provider)
end
diff --git a/config/initializers/forbid_sidekiq_in_transactions.rb b/config/initializers/forbid_sidekiq_in_transactions.rb
index a69f1ba090e..bb190af60b5 100644
--- a/config/initializers/forbid_sidekiq_in_transactions.rb
+++ b/config/initializers/forbid_sidekiq_in_transactions.rb
@@ -2,15 +2,16 @@ module Sidekiq
module Worker
EnqueueFromTransactionError = Class.new(StandardError)
- mattr_accessor :skip_transaction_check
- self.skip_transaction_check = false
-
def self.skipping_transaction_check(&block)
- skip_transaction_check = self.skip_transaction_check
- self.skip_transaction_check = true
+ previous_skip_transaction_check = self.skip_transaction_check
+ Thread.current[:sidekiq_worker_skip_transaction_check] = true
yield
ensure
- self.skip_transaction_check = skip_transaction_check
+ Thread.current[:sidekiq_worker_skip_transaction_check] = previous_skip_transaction_check
+ end
+
+ def self.skip_transaction_check
+ Thread.current[:sidekiq_worker_skip_transaction_check]
end
module ClassMethods
diff --git a/config/initializers/peek.rb b/config/initializers/peek.rb
index cb108416b10..d492b60705d 100644
--- a/config/initializers/peek.rb
+++ b/config/initializers/peek.rb
@@ -42,3 +42,6 @@ class PEEK_DB_CLIENT
end
PEEK_DB_VIEW.prepend ::Gitlab::PerformanceBar::PeekQueryTracker
+
+require 'peek/adapters/redis'
+Peek::Adapters::Redis.prepend ::Gitlab::PerformanceBar::RedisAdapterWhenPeekEnabled
diff --git a/config/initializers/rack_attack_logging.rb b/config/initializers/rack_attack_logging.rb
index 2a3fdc8de5f..338e968cc6c 100644
--- a/config/initializers/rack_attack_logging.rb
+++ b/config/initializers/rack_attack_logging.rb
@@ -4,12 +4,22 @@
ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, request_id, req|
if [:throttle, :blacklist].include? req.env['rack.attack.match_type']
- Gitlab::AuthLogger.error(
+ rack_attack_info = {
message: 'Rack_Attack',
env: req.env['rack.attack.match_type'],
ip: req.ip,
request_method: req.request_method,
fullpath: req.fullpath
- )
+ }
+
+ if req.env['rack.attack.matched'] != 'throttle_unauthenticated'
+ user_id = req.env['rack.attack.match_discriminator']
+ user = User.find_by(id: user_id)
+
+ rack_attack_info[:user_id] = user_id
+ rack_attack_info[:username] = user.username unless user.nil?
+ end
+
+ Gitlab::AuthLogger.error(rack_attack_info)
end
end
diff --git a/config/no_todos_messages.yml b/config/no_todos_messages.yml
index da721a9b6e6..d2076f235fd 100644
--- a/config/no_todos_messages.yml
+++ b/config/no_todos_messages.yml
@@ -4,8 +4,8 @@
# If you come up with a fun one, please feel free to contribute it to GitLab!
# https://about.gitlab.com/contributing/
---
-- Good job! Looks like you don't have any todos left
-- Isn't an empty todo list beautiful?
+- Good job! Looks like you don't have anything left on your To-Do List
+- Isn't an empty To-Do List beautiful?
- Give yourself a pat on the back!
-- Nothing left to do, high five!
-- Henceforth you shall be known as "Todo Destroyer"
+- Nothing left to do. High five!
+- Henceforth, you shall be known as "To-Do Destroyer"
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 91613e3333f..c202463dadb 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -182,7 +182,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
#
# Templates
#
- get '/templates/:template_type/:key' => 'templates#show', as: :template, constraints: { key: %r{[^/]+} }
+ get '/templates/:template_type/:key' => 'templates#show',
+ as: :template,
+ defaults: { format: 'json' },
+ constraints: { key: %r{[^/]+}, template_type: %r{issue|merge_request}, format: 'json' }
resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
member do
diff --git a/config/routes/uploads.rb b/config/routes/uploads.rb
index b594f55f8a0..920f8454ce2 100644
--- a/config/routes/uploads.rb
+++ b/config/routes/uploads.rb
@@ -7,7 +7,7 @@ scope path: :uploads do
# show uploads for models, snippets (notes) available for now
get '-/system/:model/:id/:secret/:filename',
to: 'uploads#show',
- constraints: { model: /personal_snippet/, id: /\d+/, filename: %r{[^/]+} }
+ constraints: { model: /personal_snippet|user/, id: /\d+/, filename: %r{[^/]+} }
# show temporary uploads
get '-/system/temp/:secret/:filename',
@@ -28,7 +28,7 @@ scope path: :uploads do
# create uploads for models, snippets (notes) available for now
post ':model',
to: 'uploads#create',
- constraints: { model: /personal_snippet/, id: /\d+/ },
+ constraints: { model: /personal_snippet|user/, id: /\d+/ },
as: 'upload'
end
diff --git a/danger/database/Dangerfile b/danger/database/Dangerfile
index 4dadf60ad24..083e95b8da7 100644
--- a/danger/database/Dangerfile
+++ b/danger/database/Dangerfile
@@ -1,21 +1,5 @@
# frozen_string_literal: true
-# All the files/directories that should be reviewed by the DB team.
-DB_FILES = [
- 'db/',
- 'app/models/project_authorization.rb',
- 'app/services/users/refresh_authorized_projects_service.rb',
- 'lib/gitlab/background_migration.rb',
- 'lib/gitlab/background_migration/',
- 'lib/gitlab/database.rb',
- 'lib/gitlab/database/',
- 'lib/gitlab/github_import.rb',
- 'lib/gitlab/github_import/',
- 'lib/gitlab/sql/',
- 'rubocop/cop/migration',
- 'ee/lib/gitlab/database/'
-].freeze
-
SCHEMA_NOT_UPDATED_MESSAGE = <<~MSG
**New %<migrations>s added but %<schema>s wasn't updated.**
@@ -24,20 +8,6 @@ updated too (unless the migration isn't changing the DB schema
and isn't the most recent one).
MSG
-def database_paths_requiring_review(files)
- to_review = []
-
- files.each do |file|
- review = DB_FILES.any? do |pattern|
- file.start_with?(pattern)
- end
-
- to_review << file if review
- end
-
- to_review
-end
-
non_geo_db_schema_updated = !git.modified_files.grep(%r{\Adb/schema\.rb}).empty?
geo_db_schema_updated = !git.modified_files.grep(%r{\Aee/db/geo/schema\.rb}).empty?
@@ -52,7 +22,7 @@ if geo_migration_created && !geo_db_schema_updated
warn format(SCHEMA_NOT_UPDATED_MESSAGE, migrations: 'Geo migrations', schema: gitlab.html_link("ee/db/geo/schema.rb"))
end
-db_paths_to_review = database_paths_requiring_review(helper.all_changed_files)
+db_paths_to_review = helper.changes_by_category[:database]
unless db_paths_to_review.empty?
message 'This merge request adds or changes files that require a ' \
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index 7a86fe2eb7c..78ceb74da65 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -146,11 +146,13 @@ class Gitlab::Seeder::CycleAnalytics
commit_sha = issue.project.repository.create_file(@user, filename, "content", message: "Commit for #{issue.to_reference}", branch_name: branch_name)
issue.project.repository.commit(commit_sha)
- GitPushService.new(issue.project,
- @user,
- oldrev: issue.project.repository.commit("master").sha,
- newrev: commit_sha,
- ref: 'refs/heads/master').execute
+ Git::BranchPushService.new(
+ issue.project,
+ @user,
+ oldrev: issue.project.repository.commit("master").sha,
+ newrev: commit_sha,
+ ref: 'refs/heads/master'
+ ).execute
branch_name
end
diff --git a/db/migrate/20190613044655_add_username_to_deploy_tokens.rb b/db/migrate/20190613044655_add_username_to_deploy_tokens.rb
new file mode 100644
index 00000000000..793553afe35
--- /dev/null
+++ b/db/migrate/20190613044655_add_username_to_deploy_tokens.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddUsernameToDeployTokens < ActiveRecord::Migration[5.1]
+ DOWNTIME = false
+
+ def change
+ add_column :deploy_tokens, :username, :string
+ end
+end
diff --git a/db/migrate/20190617123615_add_grafana_to_settings.rb b/db/migrate/20190617123615_add_grafana_to_settings.rb
new file mode 100644
index 00000000000..f9c6f4d883e
--- /dev/null
+++ b/db/migrate/20190617123615_add_grafana_to_settings.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddGrafanaToSettings < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_column_with_default(:application_settings, :grafana_enabled, :boolean,
+ default: false, allow_null: false)
+ end
+
+ def down
+ remove_column(:application_settings, :grafana_enabled)
+ end
+end
diff --git a/db/migrate/20190621022810_add_last_ci_minutes_usage_notification_level_to_namespaces.rb b/db/migrate/20190621022810_add_last_ci_minutes_usage_notification_level_to_namespaces.rb
new file mode 100644
index 00000000000..1611340284c
--- /dev/null
+++ b/db/migrate/20190621022810_add_last_ci_minutes_usage_notification_level_to_namespaces.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddLastCiMinutesUsageNotificationLevelToNamespaces < ActiveRecord::Migration[5.1]
+ DOWNTIME = false
+
+ def change
+ add_column :namespaces, :last_ci_minutes_usage_notification_level, :integer
+ end
+end
diff --git a/db/migrate/20190621151636_add_merge_request_rebase_jid.rb b/db/migrate/20190621151636_add_merge_request_rebase_jid.rb
new file mode 100644
index 00000000000..1fed5690ead
--- /dev/null
+++ b/db/migrate/20190621151636_add_merge_request_rebase_jid.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddMergeRequestRebaseJid < ActiveRecord::Migration[5.1]
+ DOWNTIME = false
+
+ def change
+ add_column :merge_requests, :rebase_jid, :string
+ end
+end
diff --git a/db/migrate/20190624123615_add_grafana_url_to_settings.rb b/db/migrate/20190624123615_add_grafana_url_to_settings.rb
new file mode 100644
index 00000000000..61efe64a7a1
--- /dev/null
+++ b/db/migrate/20190624123615_add_grafana_url_to_settings.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddGrafanaUrlToSettings < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_column_with_default(:application_settings, :grafana_url, :string,
+ default: '/-/grafana', allow_null: false)
+ end
+
+ def down
+ remove_column(:application_settings, :grafana_url)
+ end
+end
diff --git a/db/migrate/20190628185000_add_released_at_to_releases_table.rb b/db/migrate/20190628185000_add_released_at_to_releases_table.rb
new file mode 100644
index 00000000000..ae55aa434d9
--- /dev/null
+++ b/db/migrate/20190628185000_add_released_at_to_releases_table.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddReleasedAtToReleasesTable < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column(:releases, :released_at, :datetime_with_timezone)
+ end
+end
diff --git a/db/migrate/20190628185004_backfill_and_add_not_null_constraint_to_released_at_column_on_releases_table.rb b/db/migrate/20190628185004_backfill_and_add_not_null_constraint_to_released_at_column_on_releases_table.rb
new file mode 100644
index 00000000000..1e5d3e8f235
--- /dev/null
+++ b/db/migrate/20190628185004_backfill_and_add_not_null_constraint_to_released_at_column_on_releases_table.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class BackfillAndAddNotNullConstraintToReleasedAtColumnOnReleasesTable < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ update_column_in_batches(:releases, :released_at, Arel.sql('created_at'))
+ change_column_null(:releases, :released_at, false)
+ end
+
+ def down
+ change_column_null(:releases, :released_at, true)
+ end
+end
diff --git a/db/migrate/20190703130053_remove_gitaly_feature_flags.rb b/db/migrate/20190703130053_remove_gitaly_feature_flags.rb
new file mode 100644
index 00000000000..13ac10a5e21
--- /dev/null
+++ b/db/migrate/20190703130053_remove_gitaly_feature_flags.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+class RemoveGitalyFeatureFlags < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ FEATURES = %w[
+ gitaly_batch_lfs_pointers gitaly_blame gitaly_blob_get_all_lfs_pointers gitaly_blob_get_new_lfs_pointers
+ gitaly_branch_names gitaly_branch_names_contains_sha gitaly_branches gitaly_bundle_to_disk
+ gitaly_calculate_checksum gitaly_can_be_merged gitaly_cherry_pick gitaly_commit_count
+ gitaly_commit_deltas gitaly_commit_languages gitaly_commit_messages gitaly_commit_patch
+ gitaly_commit_raw_diffs gitaly_commit_stats gitaly_commit_tree_entry gitaly_commits_between
+ gitaly_commits_by_message gitaly_conflicts_list_conflict_files gitaly_conflicts_resolve_conflicts gitaly_count_commits
+ gitaly_count_diverging_commits_no_max gitaly_create_branch gitaly_create_repo_from_bundle gitaly_create_repository
+ gitaly_delete_branch gitaly_delete_refs gitaly_delta_islands gitaly_deny_disk_acces
+ gitaly_diff_between gitaly_extract_commit_signature gitaly_fetch_ref gitaly_fetch_remote
+ gitaly_fetch_source_branch gitaly_filter_shas_with_signature gitaly_filter_shas_with_signatures gitaly_find_all_commits
+ gitaly_find_branch gitaly_find_commit gitaly_find_commits gitaly_find_ref_name
+ gitaly_force_push gitaly_fork_repository gitaly_garbage_collect gitaly_get_info_attributes
+ gitaly_git_blob_load_all_data gitaly_git_blob_raw gitaly_git_fsck gitaly_go-find-all-tags
+ gitaly_has_local_branches gitaly_import_repository gitaly_is_ancestor gitaly_last_commit_for_path
+ gitaly_license_short_name gitaly_list_blobs_by_sha_path gitaly_list_commits_by_oid gitaly_local_branches
+ gitaly_ls_files gitaly_merge_base gitaly_merged_branch_names gitaly_new_commits
+ gitaly_operation_user_add_tag gitaly_operation_user_commit_file gitaly_operation_user_commit_files gitaly_operation_user_create_branch
+ gitaly_operation_user_delete_branch gitaly_operation_user_delete_tag gitaly_operation_user_ff_branch gitaly_operation_user_merge_branch
+ gitaly_post_receive_pack gitaly_post_upload_pack gitaly_project_raw_show gitaly_raw_changes_between
+ gitaly_rebase gitaly_rebase_in_progress gitaly_ref_delete_refs gitaly_ref_exists
+ gitaly_ref_exists_branch gitaly_ref_exists_branches gitaly_ref_find_all_remote_branches gitaly_remote_add_remote
+ gitaly_remote_fetch_internal_remote gitaly_remote_remove_remote gitaly_remote_update_remote_mirror gitaly_remove_namespace
+ gitaly_repack_full gitaly_repack_incremental gitaly_repository_cleanup gitaly_repository_exists
+ gitaly_repository_size gitaly_root_ref gitaly_search_files_by_content gitaly_search_files_by_name
+ gitaly_squash gitaly_squash_in_progress gitaly_ssh_receive_pack gitaly_ssh_upload_pack
+ gitaly_submodule_url_for gitaly_tag_messages gitaly_tag_names gitaly_tag_names_contains_sha
+ gitaly_tags gitaly_tree_entries gitaly_wiki_delete_page gitaly_wiki_find_file
+ gitaly_wiki_find_page gitaly_wiki_get_all_pages gitaly_wiki_page_formatted_data gitaly_wiki_page_versions
+ gitaly_wiki_update_page gitaly_wiki_write_page gitaly_workhorse_archive gitaly_workhorse_raw_show
+ gitaly_workhorse_send_git_diff gitaly_workhorse_send_git_patch gitaly_write_config gitaly_write_ref
+ ]
+
+ class Feature < ActiveRecord::Base
+ self.table_name = 'features'
+ end
+
+ def up
+ Feature.where(key: FEATURES).delete_all
+ end
+end
diff --git a/db/post_migrate/20180202111106_remove_project_labels_group_id.rb b/db/post_migrate/20180202111106_remove_project_labels_group_id.rb
deleted file mode 100644
index 31ec84f0d6a..00000000000
--- a/db/post_migrate/20180202111106_remove_project_labels_group_id.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# See http://doc.gitlab.com/ce/development/migration_style_guide.html
-# for more information on how to write migrations for GitLab.
-
-class RemoveProjectLabelsGroupId < ActiveRecord::Migration[4.2]
- include Gitlab::Database::MigrationHelpers
-
- DOWNTIME = false
-
- disable_ddl_transaction!
-
- def up
- update_column_in_batches(:labels, :group_id, nil) do |table, query|
- query.where(table[:type].eq('ProjectLabel').and(table[:group_id].not_eq(nil)))
- end
- end
-
- def down
- end
-end
diff --git a/db/schema.rb b/db/schema.rb
index d121ad8b372..8c085d64e27 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: 20190628145246) do
+ActiveRecord::Schema.define(version: 20190703130053) 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: 20190628145246) do
t.string "required_instance_ci_template"
t.boolean "dns_rebinding_protection_enabled", default: true, null: false
t.boolean "default_project_deletion_protection", default: false, null: false
+ t.boolean "grafana_enabled", default: false, null: false
t.boolean "lock_memberships_to_ldap", default: false, null: false
t.text "help_text"
t.boolean "elasticsearch_indexing", default: false, null: false
@@ -226,6 +227,7 @@ ActiveRecord::Schema.define(version: 20190628145246) do
t.boolean "elasticsearch_limit_indexing", default: false, null: false
t.string "geo_node_allowed_ips", default: "0.0.0.0/0, ::/0"
t.boolean "time_tracking_limit_to_hours", default: false, null: false
+ t.string "grafana_url", default: "/-/grafana", null: false
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id", using: :btree
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id", using: :btree
t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
@@ -1047,6 +1049,7 @@ ActiveRecord::Schema.define(version: 20190628145246) do
t.datetime_with_timezone "created_at", null: false
t.string "name", null: false
t.string "token", null: false
+ t.string "username"
t.index ["token", "expires_at", "id"], name: "index_deploy_tokens_on_token_and_expires_at_and_id", where: "(revoked IS FALSE)", using: :btree
t.index ["token"], name: "index_deploy_tokens_on_token", unique: true, using: :btree
end
@@ -1986,6 +1989,7 @@ ActiveRecord::Schema.define(version: 20190628145246) do
t.boolean "allow_maintainer_to_push"
t.integer "state_id", limit: 2
t.integer "approvals_before_merge"
+ t.string "rebase_jid"
t.index ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
t.index ["author_id"], name: "index_merge_requests_on_author_id", using: :btree
t.index ["created_at"], name: "index_merge_requests_on_created_at", using: :btree
@@ -2112,6 +2116,7 @@ ActiveRecord::Schema.define(version: 20190628145246) do
t.integer "extra_shared_runners_minutes_limit"
t.string "ldap_sync_status", default: "ready", null: false
t.boolean "membership_lock", default: false
+ t.integer "last_ci_minutes_usage_notification_level"
t.integer "subgroup_creation_level", default: 1
t.index ["created_at"], name: "index_namespaces_on_created_at", using: :btree
t.index ["custom_project_templates_group_id", "type"], name: "index_namespaces_on_custom_project_templates_group_id_and_type", where: "(custom_project_templates_group_id IS NOT NULL)", using: :btree
@@ -2902,6 +2907,7 @@ ActiveRecord::Schema.define(version: 20190628145246) do
t.integer "author_id"
t.string "name"
t.string "sha"
+ t.datetime_with_timezone "released_at", null: false
t.index ["author_id"], name: "index_releases_on_author_id", using: :btree
t.index ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree
t.index ["project_id"], name: "index_releases_on_project_id", using: :btree
diff --git a/doc/README.md b/doc/README.md
index 489c8117b9d..5eaa998a7b8 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -111,7 +111,7 @@ The following documentation relates to the DevOps **Plan** stage:
| [Discussions](user/discussions/index.md) | Threads, comments, and resolvable discussions in issues, commits, and merge requests. |
| [Due Dates](user/project/issues/due_dates.md) | Keep track of issue deadlines. |
| [Epics](user/group/epics/index.md) **[ULTIMATE]** | Tracking groups of issues that share a theme. |
-| [Issues](user/project/issues/index.md), including [confidential issues](user/project/issues/confidential_issues.md),<br/>[issue and merge request templates](user/project/description_templates.md),<br/>and [moving issues](user/project/issues/moving_issues.md) | Project issues, restricting access to issues, create templates for submitting new issues and merge requests, and moving issues between projects. |
+| [Issues](user/project/issues/index.md), including [confidential issues](user/project/issues/confidential_issues.md),<br/>[issue and merge request templates](user/project/description_templates.md),<br/>and [moving issues](user/project/issues/managing_issues.md#moving-issues) | Project issues, restricting access to issues, create templates for submitting new issues and merge requests, and moving issues between projects. |
| [Labels](user/project/labels.md) | Categorize issues or merge requests with descriptive labels. |
| [Milestones](user/project/milestones/index.md) | Set milestones for delivery of issues and merge requests, with optional due date. |
| [Project Issue Board](user/project/issue_board.md) | Display issues on a Scrum or Kanban board. |
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index 1f2961ea39a..79ac7fe0352 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -1,6 +1,4 @@
-[//]: # (Do *NOT* modify this file in EE documentation. All changes in this)
-[//]: # (file should happen in CE, too. If the change is EE-specific, put)
-[//]: # (it in `ldap-ee.md`.)
+<!-- If the change is EE-specific, put it in `ldap-ee.md`, NOT here. -->
# LDAP
diff --git a/doc/administration/geo/disaster_recovery/background_verification.md b/doc/administration/geo/disaster_recovery/background_verification.md
index d4c8c2d3624..e19cd9bbfec 100644
--- a/doc/administration/geo/disaster_recovery/background_verification.md
+++ b/doc/administration/geo/disaster_recovery/background_verification.md
@@ -143,11 +143,13 @@ If the **primary** and **secondary** nodes have a checksum verification mismatch
1. On the project admin page get the **Gitaly storage name**, and **Gitaly relative path**:
![Project admin page](img/checksum-differences-admin-project-page.png)
-1. Navigate to the project's repository directory on both **primary** and **secondary** nodes (the path is usually `/var/opt/gitlab/git-data/repositories`). Note that if `git_data_dirs` is customized, check the directory layout on your server to be sure.
+1. Navigate to the project's repository directory on both **primary** and **secondary** nodes
+ (the path is usually `/var/opt/gitlab/git-data/repositories`). Note that if `git_data_dirs`
+ is customized, check the directory layout on your server to be sure.
- ```sh
- cd /var/opt/gitlab/git-data/repositories
- ```
+ ```sh
+ cd /var/opt/gitlab/git-data/repositories
+ ```
1. Run the following command on the **primary** node, redirecting the output to a file:
diff --git a/doc/administration/geo/disaster_recovery/bring_primary_back.md b/doc/administration/geo/disaster_recovery/bring_primary_back.md
index f4d31a98080..9a981b49349 100644
--- a/doc/administration/geo/disaster_recovery/bring_primary_back.md
+++ b/doc/administration/geo/disaster_recovery/bring_primary_back.md
@@ -21,20 +21,20 @@ To bring the former **primary** node up to date:
1. SSH into the former **primary** node that has fallen behind.
1. Make sure all the services are up:
- ```sh
- sudo gitlab-ctl start
- ```
-
- > **Note 1:** If you [disabled the **primary** node permanently][disaster-recovery-disable-primary],
- > you need to undo those steps now. For Debian/Ubuntu you just need to run
- > `sudo systemctl enable gitlab-runsvdir`. For CentOS 6, you need to install
- > the GitLab instance from scratch and set it up as a **secondary** node by
- > following [Setup instructions][setup-geo]. In this case, you don't need to follow the next step.
- >
- > **Note 2:** If you [changed the DNS records](index.md#step-4-optional-updating-the-primary-domain-dns-record)
- > for this node during disaster recovery procedure you may need to [block
- > all the writes to this node](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/gitlab-geo/planned-failover.md#block-primary-traffic)
- > during this procedure.
+ ```sh
+ sudo gitlab-ctl start
+ ```
+
+ NOTE: **Note:** If you [disabled the **primary** node permanently][disaster-recovery-disable-primary],
+ you need to undo those steps now. For Debian/Ubuntu you just need to run
+ `sudo systemctl enable gitlab-runsvdir`. For CentOS 6, you need to install
+ the GitLab instance from scratch and set it up as a **secondary** node by
+ following [Setup instructions][setup-geo]. In this case, you don't need to follow the next step.
+
+ NOTE: **Note:** If you [changed the DNS records](index.md#step-4-optional-updating-the-primary-domain-dns-record)
+ for this node during disaster recovery procedure you may need to [block
+ all the writes to this node](planned_failover.md#prevent-updates-to-the-primary-node)
+ during this procedure.
1. [Setup database replication][database-replication]. Note that in this
case, **primary** node refers to the current **primary** node, and **secondary** node refers to the
diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md
index 71dc797f281..86182b84062 100644
--- a/doc/administration/geo/disaster_recovery/index.md
+++ b/doc/administration/geo/disaster_recovery/index.md
@@ -39,48 +39,50 @@ must disable the **primary** node.
1. SSH into the **primary** node to stop and disable GitLab, if possible:
- ```sh
- sudo gitlab-ctl stop
- ```
+ ```sh
+ sudo gitlab-ctl stop
+ ```
- Prevent GitLab from starting up again if the server unexpectedly reboots:
+ Prevent GitLab from starting up again if the server unexpectedly reboots:
- ```sh
- sudo systemctl disable gitlab-runsvdir
- ```
+ ```sh
+ sudo systemctl disable gitlab-runsvdir
+ ```
- > **CentOS only**: In CentOS 6 or older, there is no easy way to prevent GitLab from being
- > started if the machine reboots isn't available (see [gitlab-org/omnibus-gitlab#3058]).
- > It may be safest to uninstall the GitLab package completely:
+ NOTE: **Note:**
+ (**CentOS only**) In CentOS 6 or older, there is no easy way to prevent GitLab from being
+ started if the machine reboots isn't available (see [gitlab-org/omnibus-gitlab#3058]).
+ It may be safest to uninstall the GitLab package completely:
- ```sh
- yum remove gitlab-ee
- ```
+ ```sh
+ yum remove gitlab-ee
+ ```
- > **Ubuntu 14.04 LTS**: If you are using an older version of Ubuntu
- > or any other distro based on the Upstart init system, you can prevent GitLab
- > from starting if the machine reboots by doing the following:
+ NOTE: **Note:**
+ (**Ubuntu 14.04 LTS**) If you are using an older version of Ubuntu
+ or any other distro based on the Upstart init system, you can prevent GitLab
+ from starting if the machine reboots by doing the following:
- ```sh
- initctl stop gitlab-runsvvdir
- echo 'manual' > /etc/init/gitlab-runsvdir.override
- initctl reload-configuration
- ```
+ ```sh
+ initctl stop gitlab-runsvvdir
+ echo 'manual' > /etc/init/gitlab-runsvdir.override
+ initctl reload-configuration
+ ```
1. If you do not have SSH access to the **primary** node, take the machine offline and
- prevent it from rebooting by any means at your disposal.
- Since there are many ways you may prefer to accomplish this, we will avoid a
- single recommendation. You may need to:
- - Reconfigure the load balancers.
- - Change DNS records (e.g., point the primary DNS record to the **secondary**
- node in order to stop usage of the **primary** node).
- - Stop the virtual servers.
- - Block traffic through a firewall.
- - Revoke object storage permissions from the **primary** node.
- - Physically disconnect a machine.
-
-1. If you plan to
- [update the primary domain DNS record](#step-4-optional-updating-the-primary-domain-dns-record),
+ prevent it from rebooting by any means at your disposal.
+ Since there are many ways you may prefer to accomplish this, we will avoid a
+ single recommendation. You may need to:
+
+ - Reconfigure the load balancers.
+ - Change DNS records (e.g., point the primary DNS record to the **secondary**
+ node in order to stop usage of the **primary** node).
+ - Stop the virtual servers.
+ - Block traffic through a firewall.
+ - Revoke object storage permissions from the **primary** node.
+ - Physically disconnect a machine.
+
+1. If you plan to [update the primary domain DNS record](#step-4-optional-updating-the-primary-domain-dns-record),
you may wish to lower the TTL now to speed up propagation.
### Step 3. Promoting a **secondary** node
@@ -94,26 +96,26 @@ the **secondary** to the **primary**.
1. SSH in to your **secondary** node and login as root:
- ```sh
- sudo -i
- ```
+ ```sh
+ sudo -i
+ ```
1. Edit `/etc/gitlab/gitlab.rb` to reflect its new status as **primary** by
removing any lines that enabled the `geo_secondary_role`:
- ```ruby
- ## In pre-11.5 documentation, the role was enabled as follows. Remove this line.
- geo_secondary_role['enable'] = true
+ ```ruby
+ ## In pre-11.5 documentation, the role was enabled as follows. Remove this line.
+ geo_secondary_role['enable'] = true
- ## In 11.5+ documentation, the role was enabled as follows. Remove this line.
- roles ['geo_secondary_role']
- ```
+ ## In 11.5+ documentation, the role was enabled as follows. Remove this line.
+ roles ['geo_secondary_role']
+ ```
1. Promote the **secondary** node to the **primary** node. Execute:
- ```sh
- gitlab-ctl promote-to-primary-node
- ```
+ ```sh
+ gitlab-ctl promote-to-primary-node
+ ```
1. Verify you can connect to the newly promoted **primary** node using the URL used
previously for the **secondary** node.
@@ -129,31 +131,31 @@ do this manually.
1. SSH in to the database node in the **secondary** and trigger PostgreSQL to
promote to read-write:
- ```bash
- sudo gitlab-pg-ctl promote
- ```
+ ```bash
+ sudo gitlab-pg-ctl promote
+ ```
1. Edit `/etc/gitlab/gitlab.rb` on every machine in the **secondary** to
reflect its new status as **primary** by removing any lines that enabled the
`geo_secondary_role`:
- ```ruby
- ## In pre-11.5 documentation, the role was enabled as follows. Remove this line.
- geo_secondary_role['enable'] = true
+ ```ruby
+ ## In pre-11.5 documentation, the role was enabled as follows. Remove this line.
+ geo_secondary_role['enable'] = true
- ## In 11.5+ documentation, the role was enabled as follows. Remove this line.
- roles ['geo_secondary_role']
- ```
+ ## In 11.5+ documentation, the role was enabled as follows. Remove this line.
+ roles ['geo_secondary_role']
+ ```
- After making these changes [Reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) each
- machine so the changes take effect.
+ After making these changes [Reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) each
+ machine so the changes take effect.
1. Promote the **secondary** to **primary**. SSH into a single application
server and execute:
- ```bash
- sudo gitlab-rake geo:set_secondary_as_primary
- ```
+ ```bash
+ sudo gitlab-rake geo:set_secondary_as_primary
+ ```
1. Verify you can connect to the newly promoted **primary** using the URL used
previously for the **secondary**.
@@ -167,37 +169,37 @@ secondary domain, like changing Git remotes and API URLs.
1. SSH into the **secondary** node and login as root:
- ```sh
- sudo -i
- ```
+ ```sh
+ sudo -i
+ ```
1. Update the primary domain's DNS record. After updating the primary domain's
DNS records to point to the **secondary** node, edit `/etc/gitlab/gitlab.rb` on the
**secondary** node to reflect the new URL:
- ```ruby
- # Change the existing external_url configuration
- external_url 'https://<new_external_url>'
- ```
+ ```ruby
+ # Change the existing external_url configuration
+ external_url 'https://<new_external_url>'
+ ```
- NOTE: **Note**
- Changing `external_url` won't prevent access via the old secondary URL, as
- long as the secondary DNS records are still intact.
+ NOTE: **Note**
+ Changing `external_url` won't prevent access via the old secondary URL, as
+ long as the secondary DNS records are still intact.
1. Reconfigure the **secondary** node for the change to take effect:
- ```sh
- gitlab-ctl reconfigure
- ```
+ ```sh
+ gitlab-ctl reconfigure
+ ```
1. Execute the command below to update the newly promoted **primary** node URL:
- ```sh
- gitlab-rake geo:update_primary_node_url
- ```
+ ```sh
+ gitlab-rake geo:update_primary_node_url
+ ```
- This command will use the changed `external_url` configuration defined
- in `/etc/gitlab/gitlab.rb`.
+ This command will use the changed `external_url` configuration defined
+ in `/etc/gitlab/gitlab.rb`.
1. Verify you can connect to the newly promoted **primary** using its URL.
If you updated the DNS records for the primary domain, these changes may
@@ -231,62 +233,61 @@ and after that you also need two extra steps.
1. SSH into the new **primary** node and login as root:
- ```sh
- sudo -i
- ```
+ ```sh
+ sudo -i
+ ```
1. Edit `/etc/gitlab/gitlab.rb`
- ```ruby
- ## Enable a Geo Primary role (if you haven't yet)
- roles ['geo_primary_role']
+ ```ruby
+ ## Enable a Geo Primary role (if you haven't yet)
+ roles ['geo_primary_role']
- ##
- # Allow PostgreSQL client authentication from the primary and secondary IPs. These IPs may be
- # public or VPC addresses in CIDR format, for example ['198.51.100.1/32', '198.51.100.2/32']
- ##
- postgresql['md5_auth_cidr_addresses'] = ['<primary_node_ip>/32', '<secondary_node_ip>/32']
+ ##
+ # Allow PostgreSQL client authentication from the primary and secondary IPs. These IPs may be
+ # public or VPC addresses in CIDR format, for example ['198.51.100.1/32', '198.51.100.2/32']
+ ##
+ postgresql['md5_auth_cidr_addresses'] = ['<primary_node_ip>/32', '<secondary_node_ip>/32']
- # Every secondary server needs to have its own slot so specify the number of secondary nodes you're going to have
- postgresql['max_replication_slots'] = 1
+ # Every secondary server needs to have its own slot so specify the number of secondary nodes you're going to have
+ postgresql['max_replication_slots'] = 1
- ##
- ## Disable automatic database migrations temporarily
- ## (until PostgreSQL is restarted and listening on the private address).
- ##
- gitlab_rails['auto_migrate'] = false
+ ##
+ ## Disable automatic database migrations temporarily
+ ## (until PostgreSQL is restarted and listening on the private address).
+ ##
+ gitlab_rails['auto_migrate'] = false
+ ```
- ```
-
- (For more details about these settings you can read [Configure the primary server][configure-the-primary-server])
+ (For more details about these settings you can read [Configure the primary server][configure-the-primary-server])
1. Save the file and reconfigure GitLab for the database listen changes and
the replication slot changes to be applied.
- ```sh
- gitlab-ctl reconfigure
- ```
+ ```sh
+ gitlab-ctl reconfigure
+ ```
- Restart PostgreSQL for its changes to take effect:
+ Restart PostgreSQL for its changes to take effect:
- ```sh
- gitlab-ctl restart postgresql
- ```
+ ```sh
+ gitlab-ctl restart postgresql
+ ```
1. Re-enable migrations now that PostgreSQL is restarted and listening on the
private address.
- Edit `/etc/gitlab/gitlab.rb` and **change** the configuration to `true`:
+ Edit `/etc/gitlab/gitlab.rb` and **change** the configuration to `true`:
- ```ruby
- gitlab_rails['auto_migrate'] = true
- ```
+ ```ruby
+ gitlab_rails['auto_migrate'] = true
+ ```
- Save the file and reconfigure GitLab:
+ Save the file and reconfigure GitLab:
- ```sh
- gitlab-ctl reconfigure
- ```
+ ```sh
+ gitlab-ctl reconfigure
+ ```
### Step 2. Initiate the replication process
diff --git a/doc/administration/geo/disaster_recovery/planned_failover.md b/doc/administration/geo/disaster_recovery/planned_failover.md
index b8071b5993f..c1a95157f8d 100644
--- a/doc/administration/geo/disaster_recovery/planned_failover.md
+++ b/doc/administration/geo/disaster_recovery/planned_failover.md
@@ -143,26 +143,26 @@ access to the **primary** node during the maintenance window.
all HTTP, HTTPS and SSH traffic to/from the **primary** node, **except** for your IP and
the **secondary** node's IP.
- For instance, you might run the following commands on the server(s) making up your **primary** node:
+ For instance, you might run the following commands on the server(s) making up your **primary** node:
- ```sh
- sudo iptables -A INPUT -p tcp -s <secondary_node_ip> --destination-port 22 -j ACCEPT
- sudo iptables -A INPUT -p tcp -s <your_ip> --destination-port 22 -j ACCEPT
- sudo iptables -A INPUT --destination-port 22 -j REJECT
+ ```sh
+ sudo iptables -A INPUT -p tcp -s <secondary_node_ip> --destination-port 22 -j ACCEPT
+ sudo iptables -A INPUT -p tcp -s <your_ip> --destination-port 22 -j ACCEPT
+ sudo iptables -A INPUT --destination-port 22 -j REJECT
- sudo iptables -A INPUT -p tcp -s <secondary_node_ip> --destination-port 80 -j ACCEPT
- sudo iptables -A INPUT -p tcp -s <your_ip> --destination-port 80 -j ACCEPT
- sudo iptables -A INPUT --tcp-dport 80 -j REJECT
+ sudo iptables -A INPUT -p tcp -s <secondary_node_ip> --destination-port 80 -j ACCEPT
+ sudo iptables -A INPUT -p tcp -s <your_ip> --destination-port 80 -j ACCEPT
+ sudo iptables -A INPUT --tcp-dport 80 -j REJECT
- sudo iptables -A INPUT -p tcp -s <secondary_node_ip> --destination-port 443 -j ACCEPT
- sudo iptables -A INPUT -p tcp -s <your_ip> --destination-port 443 -j ACCEPT
- sudo iptables -A INPUT --tcp-dport 443 -j REJECT
- ```
+ sudo iptables -A INPUT -p tcp -s <secondary_node_ip> --destination-port 443 -j ACCEPT
+ sudo iptables -A INPUT -p tcp -s <your_ip> --destination-port 443 -j ACCEPT
+ sudo iptables -A INPUT --tcp-dport 443 -j REJECT
+ ```
- From this point, users will be unable to view their data or make changes on the
- **primary** node. They will also be unable to log in to the **secondary** node.
- However, existing sessions will work for the remainder of the maintenance period, and
- public data will be accessible throughout.
+ From this point, users will be unable to view their data or make changes on the
+ **primary** node. They will also be unable to log in to the **secondary** node.
+ However, existing sessions will work for the remainder of the maintenance period, and
+ public data will be accessible throughout.
1. Verify the **primary** node is blocked to HTTP traffic by visiting it in browser via
another IP. The server should refuse connection.
@@ -187,10 +187,11 @@ access to the **primary** node during the maintenance window.
before it is completed will cause the work to be lost.
1. On the **primary** node, navigate to **Admin Area > Geo** and wait for the
following conditions to be true of the **secondary** node you are failing over to:
- - All replication meters to each 100% replicated, 0% failures.
- - All verification meters reach 100% verified, 0% failures.
- - Database replication lag is 0ms.
- - The Geo log cursor is up to date (0 events behind).
+
+ - All replication meters to each 100% replicated, 0% failures.
+ - All verification meters reach 100% verified, 0% failures.
+ - Database replication lag is 0ms.
+ - The Geo log cursor is up to date (0 events behind).
1. On the **secondary** node, navigate to **Admin Area > Monitoring > Background Jobs > Queues**
and wait for all the `geo` queues to drop to 0 queued and 0 running jobs.
diff --git a/doc/administration/geo/replication/configuration.md b/doc/administration/geo/replication/configuration.md
index 3d4f69d3abe..dd5e09c0dd7 100644
--- a/doc/administration/geo/replication/configuration.md
+++ b/doc/administration/geo/replication/configuration.md
@@ -16,11 +16,10 @@ The basic steps of configuring a **secondary** node are to:
You are encouraged to first read through all the steps before executing them
in your testing/production environment.
-> **Notes:**
-> - **Do not** setup any custom authentication for the **secondary** nodes. This will be
- handled by the **primary** node.
-> - Any change that requires access to the **Admin Area** needs to be done in the
- **primary** node because the **secondary** node is a read-only replica.
+NOTE: **Note:**
+**Do not** set up any custom authentication for the **secondary** nodes. This will be handled by the **primary** node.
+Any change that requires access to the **Admin Area** needs to be done in the
+**primary** node because the **secondary** node is a read-only replica.
### Step 1. Manually replicate secret GitLab values
@@ -31,47 +30,47 @@ they must be manually replicated to the **secondary** node.
1. SSH into the **primary** node, and execute the command below:
- ```sh
- sudo cat /etc/gitlab/gitlab-secrets.json
- ```
+ ```sh
+ sudo cat /etc/gitlab/gitlab-secrets.json
+ ```
- This will display the secrets that need to be replicated, in JSON format.
+ This will display the secrets that need to be replicated, in JSON format.
1. SSH into the **secondary** node and login as the `root` user:
- ```sh
- sudo -i
- ```
+ ```sh
+ sudo -i
+ ```
1. Make a backup of any existing secrets:
- ```sh
- mv /etc/gitlab/gitlab-secrets.json /etc/gitlab/gitlab-secrets.json.`date +%F`
- ```
+ ```sh
+ mv /etc/gitlab/gitlab-secrets.json /etc/gitlab/gitlab-secrets.json.`date +%F`
+ ```
1. Copy `/etc/gitlab/gitlab-secrets.json` from the **primary** node to the **secondary** node, or
copy-and-paste the file contents between nodes:
- ```sh
- sudo editor /etc/gitlab/gitlab-secrets.json
+ ```sh
+ sudo editor /etc/gitlab/gitlab-secrets.json
- # paste the output of the `cat` command you ran on the primary
- # save and exit
- ```
+ # paste the output of the `cat` command you ran on the primary
+ # save and exit
+ ```
1. Ensure the file permissions are correct:
- ```sh
- chown root:root /etc/gitlab/gitlab-secrets.json
- chmod 0600 /etc/gitlab/gitlab-secrets.json
- ```
+ ```sh
+ chown root:root /etc/gitlab/gitlab-secrets.json
+ chmod 0600 /etc/gitlab/gitlab-secrets.json
+ ```
1. Reconfigure the **secondary** node for the change to take effect:
- ```sh
- gitlab-ctl reconfigure
- gitlab-ctl restart
- ```
+ ```sh
+ gitlab-ctl reconfigure
+ gitlab-ctl restart
+ ```
### Step 2. Manually replicate the **primary** node's SSH host keys
@@ -89,80 +88,80 @@ keys must be manually replicated to the **secondary** node.
1. SSH into the **secondary** node and login as the `root` user:
- ```sh
- sudo -i
- ```
+ ```sh
+ sudo -i
+ ```
1. Make a backup of any existing SSH host keys:
- ```sh
- find /etc/ssh -iname ssh_host_* -exec cp {} {}.backup.`date +%F` \;
- ```
+ ```sh
+ find /etc/ssh -iname ssh_host_* -exec cp {} {}.backup.`date +%F` \;
+ ```
1. Copy OpenSSH host keys from the **primary** node:
- If you can access your **primary** node using the **root** user:
+ If you can access your **primary** node using the **root** user:
- ```sh
- # Run this from the secondary node, change `<primary_node_fqdn>` for the IP or FQDN of the server
- scp root@<primary_node_fqdn>:/etc/ssh/ssh_host_*_key* /etc/ssh
- ```
+ ```sh
+ # Run this from the secondary node, change `<primary_node_fqdn>` for the IP or FQDN of the server
+ scp root@<primary_node_fqdn>:/etc/ssh/ssh_host_*_key* /etc/ssh
+ ```
- If you only have access through a user with **sudo** privileges:
+ If you only have access through a user with **sudo** privileges:
- ```sh
- # Run this from your primary node:
- sudo tar --transform 's/.*\///g' -zcvf ~/geo-host-key.tar.gz /etc/ssh/ssh_host_*_key*
+ ```sh
+ # Run this from your primary node:
+ sudo tar --transform 's/.*\///g' -zcvf ~/geo-host-key.tar.gz /etc/ssh/ssh_host_*_key*
- # Run this from your secondary node:
- scp <user_with_sudo>@<primary_node_fqdn>:geo-host-key.tar.gz .
- tar zxvf ~/geo-host-key.tar.gz -C /etc/ssh
- ```
+ # Run this from your secondary node:
+ scp <user_with_sudo>@<primary_node_fqdn>:geo-host-key.tar.gz .
+ tar zxvf ~/geo-host-key.tar.gz -C /etc/ssh
+ ```
1. On your **secondary** node, ensure the file permissions are correct:
- ```sh
- chown root:root /etc/ssh/ssh_host_*_key*
- chmod 0600 /etc/ssh/ssh_host_*_key*
- ```
+ ```sh
+ chown root:root /etc/ssh/ssh_host_*_key*
+ chmod 0600 /etc/ssh/ssh_host_*_key*
+ ```
1. To verify key fingerprint matches, execute the following command on both nodes:
- ```sh
- for file in /etc/ssh/ssh_host_*_key; do ssh-keygen -lf $file; done
- ```
+ ```sh
+ for file in /etc/ssh/ssh_host_*_key; do ssh-keygen -lf $file; done
+ ```
- You should get an output similar to this one and they should be identical on both nodes:
+ You should get an output similar to this one and they should be identical on both nodes:
- ```sh
- 1024 SHA256:FEZX2jQa2bcsd/fn/uxBzxhKdx4Imc4raXrHwsbtP0M root@serverhostname (DSA)
- 256 SHA256:uw98R35Uf+fYEQ/UnJD9Br4NXUFPv7JAUln5uHlgSeY root@serverhostname (ECDSA)
- 256 SHA256:sqOUWcraZQKd89y/QQv/iynPTOGQxcOTIXU/LsoPmnM root@serverhostname (ED25519)
- 2048 SHA256:qwa+rgir2Oy86QI+PZi/QVR+MSmrdrpsuH7YyKknC+s root@serverhostname (RSA)
- ```
+ ```sh
+ 1024 SHA256:FEZX2jQa2bcsd/fn/uxBzxhKdx4Imc4raXrHwsbtP0M root@serverhostname (DSA)
+ 256 SHA256:uw98R35Uf+fYEQ/UnJD9Br4NXUFPv7JAUln5uHlgSeY root@serverhostname (ECDSA)
+ 256 SHA256:sqOUWcraZQKd89y/QQv/iynPTOGQxcOTIXU/LsoPmnM root@serverhostname (ED25519)
+ 2048 SHA256:qwa+rgir2Oy86QI+PZi/QVR+MSmrdrpsuH7YyKknC+s root@serverhostname (RSA)
+ ```
1. Verify that you have the correct public keys for the existing private keys:
- ```sh
- # This will print the fingerprint for private keys:
- for file in /etc/ssh/ssh_host_*_key; do ssh-keygen -lf $file; done
+ ```sh
+ # This will print the fingerprint for private keys:
+ for file in /etc/ssh/ssh_host_*_key; do ssh-keygen -lf $file; done
- # This will print the fingerprint for public keys:
- for file in /etc/ssh/ssh_host_*_key.pub; do ssh-keygen -lf $file; done
- ```
+ # This will print the fingerprint for public keys:
+ for file in /etc/ssh/ssh_host_*_key.pub; do ssh-keygen -lf $file; done
+ ```
- NOTE: **Note**:
- The output for private keys and public keys command should generate the same fingerprint.
+ NOTE: **Note:**
+ The output for private keys and public keys command should generate the same fingerprint.
1. Restart sshd on your **secondary** node:
- ```sh
- # Debian or Ubuntu installations
- sudo service ssh reload
+ ```sh
+ # Debian or Ubuntu installations
+ sudo service ssh reload
- # CentOS installations
- sudo service sshd reload
- ```
+ # CentOS installations
+ sudo service sshd reload
+ ```
### Step 3. Add the **secondary** node
@@ -176,22 +175,22 @@ keys must be manually replicated to the **secondary** node.
1. Click the **Add node** button.
1. SSH into your GitLab **secondary** server and restart the services:
- ```sh
- gitlab-ctl restart
- ```
+ ```sh
+ gitlab-ctl restart
+ ```
- Check if there are any common issue with your Geo setup by running:
+ Check if there are any common issue with your Geo setup by running:
- ```sh
- gitlab-rake gitlab:geo:check
- ```
+ ```sh
+ gitlab-rake gitlab:geo:check
+ ```
1. SSH into your **primary** server and login as root to verify the
**secondary** node is reachable or there are any common issue with your Geo setup:
- ```sh
- gitlab-rake gitlab:geo:check
- ```
+ ```sh
+ gitlab-rake gitlab:geo:check
+ ```
Once added to the admin panel and restarted, the **secondary** node will automatically start
replicating missing data from the **primary** node in a process known as **backfill**.
@@ -250,9 +249,8 @@ The two most obvious issues that can become apparent in the dashboard are:
1. Database replication not working well.
1. Instance to instance notification not working. In that case, it can be
something of the following:
- - You are using a custom certificate or custom CA (see the
- [troubleshooting document]).
- - The instance is firewalled (check your firewall rules).
+ - You are using a custom certificate or custom CA (see the [troubleshooting document](troubleshooting.md)).
+ - The instance is firewalled (check your firewall rules).
Please note that disabling a **secondary** node will stop the synchronization process.
@@ -304,5 +302,4 @@ See the [troubleshooting document](troubleshooting.md).
[gitlab-org/gitlab-ee#3789]: https://gitlab.com/gitlab-org/gitlab-ee/issues/3789
[gitlab-com/infrastructure#2821]: https://gitlab.com/gitlab-com/infrastructure/issues/2821
[omnibus-ssl]: https://docs.gitlab.com/omnibus/settings/ssl.html
-[troubleshooting document]: troubleshooting.md
[using-geo]: using_a_geo_server.md
diff --git a/doc/administration/geo/replication/database.md b/doc/administration/geo/replication/database.md
index 6658c37752a..021ed2d9f3c 100644
--- a/doc/administration/geo/replication/database.md
+++ b/doc/administration/geo/replication/database.md
@@ -52,186 +52,188 @@ There is an [issue where support is being discussed](https://gitlab.com/gitlab-o
1. SSH into your GitLab **primary** server and login as root:
- ```sh
- sudo -i
- ```
+ ```sh
+ sudo -i
+ ```
1. Execute the command below to define the node as **primary** node:
- ```sh
- gitlab-ctl set-geo-primary-node
- ```
+ ```sh
+ gitlab-ctl set-geo-primary-node
+ ```
- This command will use your defined `external_url` in `/etc/gitlab/gitlab.rb`.
+ This command will use your defined `external_url` in `/etc/gitlab/gitlab.rb`.
1. GitLab 10.4 and up only: Do the following to make sure the `gitlab` database user has a password defined:
- Generate a MD5 hash of the desired password:
+ Generate a MD5 hash of the desired password:
- ```sh
- gitlab-ctl pg-password-md5 gitlab
- # Enter password: <your_password_here>
- # Confirm password: <your_password_here>
- # fca0b89a972d69f00eb3ec98a5838484
- ```
+ ```sh
+ gitlab-ctl pg-password-md5 gitlab
+ # Enter password: <your_password_here>
+ # Confirm password: <your_password_here>
+ # fca0b89a972d69f00eb3ec98a5838484
+ ```
- Edit `/etc/gitlab/gitlab.rb`:
+ Edit `/etc/gitlab/gitlab.rb`:
- ```ruby
- # Fill with the hash generated by `gitlab-ctl pg-password-md5 gitlab`
- postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
+ ```ruby
+ # Fill with the hash generated by `gitlab-ctl pg-password-md5 gitlab`
+ postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
- # Every node that runs Unicorn or Sidekiq needs to have the database
- # password specified as below. If you have a high-availability setup, this
- # must be present in all application nodes.
- gitlab_rails['db_password'] = '<your_password_here>'
- ```
+ # Every node that runs Unicorn or Sidekiq needs to have the database
+ # password specified as below. If you have a high-availability setup, this
+ # must be present in all application nodes.
+ gitlab_rails['db_password'] = '<your_password_here>'
+ ```
1. Omnibus GitLab already has a [replication user]
called `gitlab_replicator`. You must set the password for this user manually.
You will be prompted to enter a password:
- ```sh
- gitlab-ctl set-replication-password
- ```
+ ```sh
+ gitlab-ctl set-replication-password
+ ```
- This command will also read the `postgresql['sql_replication_user']` Omnibus
- setting in case you have changed `gitlab_replicator` username to something
- else.
+ This command will also read the `postgresql['sql_replication_user']` Omnibus
+ setting in case you have changed `gitlab_replicator` username to something
+ else.
- If you are using an external database not managed by Omnibus GitLab, you need
- to create the replicator user and define a password to it manually:
+ If you are using an external database not managed by Omnibus GitLab, you need
+ to create the replicator user and define a password to it manually:
- ```sql
- --- Create a new user 'replicator'
- CREATE USER gitlab_replicator;
+ ```sql
+ --- Create a new user 'replicator'
+ CREATE USER gitlab_replicator;
- --- Set/change a password and grants replication privilege
- ALTER USER gitlab_replicator WITH REPLICATION ENCRYPTED PASSWORD '<replication_password>';
- ```
+ --- Set/change a password and grants replication privilege
+ ALTER USER gitlab_replicator WITH REPLICATION ENCRYPTED PASSWORD '<replication_password>';
+ ```
1. Configure PostgreSQL to listen on network interfaces:
- For security reasons, PostgreSQL does not listen on any network interfaces
- by default. However, Geo requires the **secondary** node to be able to
- connect to the **primary** node's database. For this reason, we need the address of
- each node. Note: For external PostgreSQL instances, see [additional instructions](external_database.md).
-
- If you are using a cloud provider, you can lookup the addresses for each
- Geo node through your cloud provider's management console.
-
- To lookup the address of a Geo node, SSH in to the Geo node and execute:
-
- ```sh
- ##
- ## Private address
- ##
- ip route get 255.255.255.255 | awk '{print "Private address:", $NF; exit}'
-
- ##
- ## Public address
- ##
- echo "External address: $(curl --silent ipinfo.io/ip)"
- ```
-
- In most cases, the following addresses will be used to configure GitLab
- Geo:
-
- | Configuration | Address |
- |:----------------------------------------|:------------------------------------------------------|
- | `postgresql['listen_address']` | **Primary** node's public or VPC private address. |
- | `postgresql['md5_auth_cidr_addresses']` | **Secondary** node's public or VPC private addresses. |
-
- If you are using Google Cloud Platform, SoftLayer, or any other vendor that
- provides a virtual private cloud (VPC) you can use the **secondary** node's private
- address (corresponds to "internal address" for Google Cloud Platform) for
- `postgresql['md5_auth_cidr_addresses']` and `postgresql['listen_address']`.
-
- The `listen_address` option opens PostgreSQL up to network connections
- with the interface corresponding to the given address. See [the PostgreSQL
- documentation][pg-docs-runtime-conn] for more details.
-
- Depending on your network configuration, the suggested addresses may not
- be correct. If your **primary** node and **secondary** nodes connect over a local
- area network, or a virtual network connecting availability zones like
- [Amazon's VPC](https://aws.amazon.com/vpc/) or [Google's VPC](https://cloud.google.com/vpc/)
- you should use the **secondary** node's private address for `postgresql['md5_auth_cidr_addresses']`.
-
- Edit `/etc/gitlab/gitlab.rb` and add the following, replacing the IP
- addresses with addresses appropriate to your network configuration:
-
- ```ruby
- ##
- ## Geo Primary role
- ## - configure dependent flags automatically to enable Geo
- ##
- roles ['geo_primary_role']
-
- ##
- ## Primary address
- ## - replace '<primary_node_ip>' with the public or VPC address of your Geo primary node
- ##
- postgresql['listen_address'] = '<primary_node_ip>'
-
- ##
- # Allow PostgreSQL client authentication from the primary and secondary IPs. These IPs may be
- # public or VPC addresses in CIDR format, for example ['198.51.100.1/32', '198.51.100.2/32']
- ##
- postgresql['md5_auth_cidr_addresses'] = ['<primary_node_ip>/32', '<secondary_node_ip>/32']
-
- ##
- ## Replication settings
- ## - set this to be the number of Geo secondary nodes you have
- ##
- postgresql['max_replication_slots'] = 1
- # postgresql['max_wal_senders'] = 10
- # postgresql['wal_keep_segments'] = 10
-
- ##
- ## Disable automatic database migrations temporarily
- ## (until PostgreSQL is restarted and listening on the private address).
- ##
- gitlab_rails['auto_migrate'] = false
- ```
+ For security reasons, PostgreSQL does not listen on any network interfaces
+ by default. However, Geo requires the **secondary** node to be able to
+ connect to the **primary** node's database. For this reason, we need the address of
+ each node.
+
+ NOTE: **Note:** For external PostgreSQL instances, see [additional instructions](external_database.md).
+
+ If you are using a cloud provider, you can lookup the addresses for each
+ Geo node through your cloud provider's management console.
+
+ To lookup the address of a Geo node, SSH in to the Geo node and execute:
+
+ ```sh
+ ##
+ ## Private address
+ ##
+ ip route get 255.255.255.255 | awk '{print "Private address:", $NF; exit}'
+
+ ##
+ ## Public address
+ ##
+ echo "External address: $(curl --silent ipinfo.io/ip)"
+ ```
+
+ In most cases, the following addresses will be used to configure GitLab
+ Geo:
+
+ | Configuration | Address |
+ |:----------------------------------------|:------------------------------------------------------|
+ | `postgresql['listen_address']` | **Primary** node's public or VPC private address. |
+ | `postgresql['md5_auth_cidr_addresses']` | **Secondary** node's public or VPC private addresses. |
+
+ If you are using Google Cloud Platform, SoftLayer, or any other vendor that
+ provides a virtual private cloud (VPC) you can use the **secondary** node's private
+ address (corresponds to "internal address" for Google Cloud Platform) for
+ `postgresql['md5_auth_cidr_addresses']` and `postgresql['listen_address']`.
+
+ The `listen_address` option opens PostgreSQL up to network connections
+ with the interface corresponding to the given address. See [the PostgreSQL
+ documentation][pg-docs-runtime-conn] for more details.
+
+ Depending on your network configuration, the suggested addresses may not
+ be correct. If your **primary** node and **secondary** nodes connect over a local
+ area network, or a virtual network connecting availability zones like
+ [Amazon's VPC](https://aws.amazon.com/vpc/) or [Google's VPC](https://cloud.google.com/vpc/)
+ you should use the **secondary** node's private address for `postgresql['md5_auth_cidr_addresses']`.
+
+ Edit `/etc/gitlab/gitlab.rb` and add the following, replacing the IP
+ addresses with addresses appropriate to your network configuration:
+
+ ```ruby
+ ##
+ ## Geo Primary role
+ ## - configure dependent flags automatically to enable Geo
+ ##
+ roles ['geo_primary_role']
+
+ ##
+ ## Primary address
+ ## - replace '<primary_node_ip>' with the public or VPC address of your Geo primary node
+ ##
+ postgresql['listen_address'] = '<primary_node_ip>'
+
+ ##
+ # Allow PostgreSQL client authentication from the primary and secondary IPs. These IPs may be
+ # public or VPC addresses in CIDR format, for example ['198.51.100.1/32', '198.51.100.2/32']
+ ##
+ postgresql['md5_auth_cidr_addresses'] = ['<primary_node_ip>/32', '<secondary_node_ip>/32']
+
+ ##
+ ## Replication settings
+ ## - set this to be the number of Geo secondary nodes you have
+ ##
+ postgresql['max_replication_slots'] = 1
+ # postgresql['max_wal_senders'] = 10
+ # postgresql['wal_keep_segments'] = 10
+
+ ##
+ ## Disable automatic database migrations temporarily
+ ## (until PostgreSQL is restarted and listening on the private address).
+ ##
+ gitlab_rails['auto_migrate'] = false
+ ```
1. Optional: If you want to add another **secondary** node, the relevant setting would look like:
- ```ruby
- postgresql['md5_auth_cidr_addresses'] = ['<primary_node_ip>/32', '<secondary_node_ip>/32', '<another_secondary_node_ip>/32']
- ```
+ ```ruby
+ postgresql['md5_auth_cidr_addresses'] = ['<primary_node_ip>/32', '<secondary_node_ip>/32', '<another_secondary_node_ip>/32']
+ ```
- You may also want to edit the `wal_keep_segments` and `max_wal_senders` to
- match your database replication requirements. Consult the [PostgreSQL -
- Replication documentation][pg-docs-runtime-replication]
- for more information.
+ You may also want to edit the `wal_keep_segments` and `max_wal_senders` to
+ match your database replication requirements. Consult the [PostgreSQL -
+ Replication documentation][pg-docs-runtime-replication]
+ for more information.
1. Save the file and reconfigure GitLab for the database listen changes and
the replication slot changes to be applied:
- ```sh
- gitlab-ctl reconfigure
- ```
+ ```sh
+ gitlab-ctl reconfigure
+ ```
- Restart PostgreSQL for its changes to take effect:
+ Restart PostgreSQL for its changes to take effect:
- ```sh
- gitlab-ctl restart postgresql
- ```
+ ```sh
+ gitlab-ctl restart postgresql
+ ```
1. Re-enable migrations now that PostgreSQL is restarted and listening on the
private address.
- Edit `/etc/gitlab/gitlab.rb` and **change** the configuration to `true`:
+ Edit `/etc/gitlab/gitlab.rb` and **change** the configuration to `true`:
- ```ruby
- gitlab_rails['auto_migrate'] = true
- ```
+ ```ruby
+ gitlab_rails['auto_migrate'] = true
+ ```
- Save the file and reconfigure GitLab:
+ Save the file and reconfigure GitLab:
- ```sh
- gitlab-ctl reconfigure
- ```
+ ```sh
+ gitlab-ctl reconfigure
+ ```
1. Now that the PostgreSQL server is set up to accept remote connections, run
`netstat -plnt | grep 5432` to make sure that PostgreSQL is listening on port
@@ -241,143 +243,143 @@ There is an [issue where support is being discussed](https://gitlab.com/gitlab-o
will be used automatically to protect your PostgreSQL traffic from
eavesdroppers, but to protect against active ("man-in-the-middle") attackers,
the **secondary** node needs a copy of the certificate. Make a copy of the PostgreSQL
- `server.crt` file on the **primary** node by running this command:
+ `server.crt` file on the **primary** node by running this command:
- ```sh
- cat ~gitlab-psql/data/server.crt
- ```
+ ```sh
+ cat ~gitlab-psql/data/server.crt
+ ```
- Copy the output into a clipboard or into a local file. You
- will need it when setting up the **secondary** node! The certificate is not sensitive
- data.
+ Copy the output into a clipboard or into a local file. You
+ will need it when setting up the **secondary** node! The certificate is not sensitive
+ data.
### Step 2. Configure the **secondary** server
1. SSH into your GitLab **secondary** server and login as root:
- ```
- sudo -i
- ```
+ ```
+ sudo -i
+ ```
1. Stop application server and Sidekiq
- ```
- gitlab-ctl stop unicorn
- gitlab-ctl stop sidekiq
- ```
+ ```
+ gitlab-ctl stop unicorn
+ gitlab-ctl stop sidekiq
+ ```
- NOTE: **Note**:
- This step is important so we don't try to execute anything before the node is fully configured.
+ NOTE: **Note:**
+ This step is important so we don't try to execute anything before the node is fully configured.
1. [Check TCP connectivity][rake-maintenance] to the **primary** node's PostgreSQL server:
- ```sh
- gitlab-rake gitlab:tcp_check[<primary_node_ip>,5432]
- ```
+ ```sh
+ gitlab-rake gitlab:tcp_check[<primary_node_ip>,5432]
+ ```
- NOTE: **Note**:
- If this step fails, you may be using the wrong IP address, or a firewall may
- be preventing access to the server. Check the IP address, paying close
- attention to the difference between public and private addresses and ensure
- that, if a firewall is present, the **secondary** node is permitted to connect to the
- **primary** node on port 5432.
+ NOTE: **Note:**
+ If this step fails, you may be using the wrong IP address, or a firewall may
+ be preventing access to the server. Check the IP address, paying close
+ attention to the difference between public and private addresses and ensure
+ that, if a firewall is present, the **secondary** node is permitted to connect to the
+ **primary** node on port 5432.
1. Create a file `server.crt` in the **secondary** server, with the content you got on the last step of the **primary** node's setup:
- ```
- editor server.crt
- ```
+ ```
+ editor server.crt
+ ```
1. Set up PostgreSQL TLS verification on the **secondary** node:
- Install the `server.crt` file:
+ Install the `server.crt` file:
- ```sh
- install \
- -D \
- -o gitlab-psql \
- -g gitlab-psql \
- -m 0400 \
- -T server.crt ~gitlab-psql/.postgresql/root.crt
- ```
+ ```sh
+ install \
+ -D \
+ -o gitlab-psql \
+ -g gitlab-psql \
+ -m 0400 \
+ -T server.crt ~gitlab-psql/.postgresql/root.crt
+ ```
- PostgreSQL will now only recognize that exact certificate when verifying TLS
- connections. The certificate can only be replicated by someone with access
- to the private key, which is **only** present on the **primary** node.
+ PostgreSQL will now only recognize that exact certificate when verifying TLS
+ connections. The certificate can only be replicated by someone with access
+ to the private key, which is **only** present on the **primary** node.
1. Test that the `gitlab-psql` user can connect to the **primary** node's database
(the default Omnibus database name is gitlabhq_production):
- ```sh
- sudo \
- -u gitlab-psql /opt/gitlab/embedded/bin/psql \
- --list \
- -U gitlab_replicator \
- -d "dbname=gitlabhq_production sslmode=verify-ca" \
- -W \
- -h <primary_node_ip>
- ```
+ ```sh
+ sudo \
+ -u gitlab-psql /opt/gitlab/embedded/bin/psql \
+ --list \
+ -U gitlab_replicator \
+ -d "dbname=gitlabhq_production sslmode=verify-ca" \
+ -W \
+ -h <primary_node_ip>
+ ```
- When prompted enter the password you set in the first step for the
- `gitlab_replicator` user. If all worked correctly, you should see
- the list of **primary** node's databases.
+ When prompted enter the password you set in the first step for the
+ `gitlab_replicator` user. If all worked correctly, you should see
+ the list of **primary** node's databases.
- A failure to connect here indicates that the TLS configuration is incorrect.
- Ensure that the contents of `~gitlab-psql/data/server.crt` on the **primary** node
- match the contents of `~gitlab-psql/.postgresql/root.crt` on the **secondary** node.
+ A failure to connect here indicates that the TLS configuration is incorrect.
+ Ensure that the contents of `~gitlab-psql/data/server.crt` on the **primary** node
+ match the contents of `~gitlab-psql/.postgresql/root.crt` on the **secondary** node.
1. Configure PostgreSQL to enable FDW support:
- This step is similar to how we configured the **primary** instance.
- We need to enable this, to enable FDW support, even if using a single node.
-
- Edit `/etc/gitlab/gitlab.rb` and add the following, replacing the IP
- addresses with addresses appropriate to your network configuration:
-
- ```ruby
- ##
- ## Geo Secondary role
- ## - configure dependent flags automatically to enable Geo
- ##
- roles ['geo_secondary_role']
-
- ##
- ## Secondary address
- ## - replace '<secondary_node_ip>' with the public or VPC address of your Geo secondary node
- ##
- postgresql['listen_address'] = '<secondary_node_ip>'
- postgresql['md5_auth_cidr_addresses'] = ['<secondary_node_ip>/32']
-
- ##
- ## Database credentials password (defined previously in primary node)
- ## - replicate same values here as defined in primary node
- ##
- postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
- gitlab_rails['db_password'] = '<your_password_here>'
-
- ##
- ## Enable FDW support for the Geo Tracking Database (improves performance)
- ##
- geo_secondary['db_fdw'] = true
- ```
-
- For external PostgreSQL instances, see [additional instructions](external_database.md).
- If you bring a former **primary** node back online to serve as a **secondary** node, then you also need to remove `roles ['geo_primary_role']` or `geo_primary_role['enable'] = true`.
+ This step is similar to how we configured the **primary** instance.
+ We need to enable this, to enable FDW support, even if using a single node.
+
+ Edit `/etc/gitlab/gitlab.rb` and add the following, replacing the IP
+ addresses with addresses appropriate to your network configuration:
+
+ ```ruby
+ ##
+ ## Geo Secondary role
+ ## - configure dependent flags automatically to enable Geo
+ ##
+ roles ['geo_secondary_role']
+
+ ##
+ ## Secondary address
+ ## - replace '<secondary_node_ip>' with the public or VPC address of your Geo secondary node
+ ##
+ postgresql['listen_address'] = '<secondary_node_ip>'
+ postgresql['md5_auth_cidr_addresses'] = ['<secondary_node_ip>/32']
+
+ ##
+ ## Database credentials password (defined previously in primary node)
+ ## - replicate same values here as defined in primary node
+ ##
+ postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
+ gitlab_rails['db_password'] = '<your_password_here>'
+
+ ##
+ ## Enable FDW support for the Geo Tracking Database (improves performance)
+ ##
+ geo_secondary['db_fdw'] = true
+ ```
+
+ For external PostgreSQL instances, see [additional instructions](external_database.md).
+ If you bring a former **primary** node back online to serve as a **secondary** node, then you also need to remove `roles ['geo_primary_role']` or `geo_primary_role['enable'] = true`.
1. Reconfigure GitLab for the changes to take effect:
- ```sh
- gitlab-ctl reconfigure
- ```
+ ```sh
+ gitlab-ctl reconfigure
+ ```
1. Restart PostgreSQL for the IP change to take effect and reconfigure again:
- ```sh
- gitlab-ctl restart postgresql
- gitlab-ctl reconfigure
- ```
+ ```sh
+ gitlab-ctl restart postgresql
+ gitlab-ctl reconfigure
+ ```
- This last reconfigure will provision the FDW configuration and enable it.
+ This last reconfigure will provision the FDW configuration and enable it.
### Step 3. Initiate the replication process
@@ -394,9 +396,9 @@ data before running `pg_basebackup`.
1. SSH into your GitLab **secondary** server and login as root:
- ```sh
- sudo -i
- ```
+ ```sh
+ sudo -i
+ ```
1. Choose a database-friendly name to use for your **secondary** node to
use as the replication slot name. For example, if your domain is
@@ -404,38 +406,40 @@ data before running `pg_basebackup`.
name as shown in the commands below.
1. Execute the command below to start a backup/restore and begin the replication
- CAUTION: **Warning:** Each Geo **secondary** node must have its own unique replication slot name.
- Using the same slot name between two secondaries will break PostgreSQL replication.
-
- ```sh
- gitlab-ctl replicate-geo-database \
- --slot-name=<secondary_node_name> \
- --host=<primary_node_ip>
- ```
-
- When prompted, enter the _plaintext_ password you set up for the `gitlab_replicator`
- user in the first step.
-
- This command also takes a number of additional options. You can use `--help`
- to list them all, but here are a couple of tips:
- - If PostgreSQL is listening on a non-standard port, add `--port=` as well.
- - If your database is too large to be transferred in 30 minutes, you will need
- to increase the timeout, e.g., `--backup-timeout=3600` if you expect the
- initial replication to take under an hour.
- - Pass `--sslmode=disable` to skip PostgreSQL TLS authentication altogether
- (e.g., you know the network path is secure, or you are using a site-to-site
- VPN). This is **not** safe over the public Internet!
- - You can read more details about each `sslmode` in the
- [PostgreSQL documentation][pg-docs-ssl];
- the instructions above are carefully written to ensure protection against
- both passive eavesdroppers and active "man-in-the-middle" attackers.
- - Change the `--slot-name` to the name of the replication slot
- to be used on the **primary** database. The script will attempt to create the
- replication slot automatically if it does not exist.
- - If you're repurposing an old server into a Geo **secondary** node, you'll need to
- add `--force` to the command line.
- - When not in a production machine you can disable backup step if you
- really sure this is what you want by adding `--skip-backup`
+
+ CAUTION: **Warning:** Each Geo **secondary** node must have its own unique replication slot name.
+ Using the same slot name between two secondaries will break PostgreSQL replication.
+
+ ```sh
+ gitlab-ctl replicate-geo-database \
+ --slot-name=<secondary_node_name> \
+ --host=<primary_node_ip>
+ ```
+
+ When prompted, enter the _plaintext_ password you set up for the `gitlab_replicator`
+ user in the first step.
+
+ This command also takes a number of additional options. You can use `--help`
+ to list them all, but here are a couple of tips:
+
+ - If PostgreSQL is listening on a non-standard port, add `--port=` as well.
+ - If your database is too large to be transferred in 30 minutes, you will need
+ to increase the timeout, e.g., `--backup-timeout=3600` if you expect the
+ initial replication to take under an hour.
+ - Pass `--sslmode=disable` to skip PostgreSQL TLS authentication altogether
+ (e.g., you know the network path is secure, or you are using a site-to-site
+ VPN). This is **not** safe over the public Internet!
+ - You can read more details about each `sslmode` in the
+ [PostgreSQL documentation][pg-docs-ssl];
+ the instructions above are carefully written to ensure protection against
+ both passive eavesdroppers and active "man-in-the-middle" attackers.
+ - Change the `--slot-name` to the name of the replication slot
+ to be used on the **primary** database. The script will attempt to create the
+ replication slot automatically if it does not exist.
+ - If you're repurposing an old server into a Geo **secondary** node, you'll need to
+ add `--force` to the command line.
+ - When not in a production machine you can disable backup step if you
+ really sure this is what you want by adding `--skip-backup`
The replication process is now complete.
@@ -452,42 +456,42 @@ it will need a separate read-only user to make [PostgreSQL FDW queries][FDW]
work:
1. On the **primary** Geo database, enter the PostgreSQL on the console as an
- admin user. If you are using an Omnibus-managed database, log onto the **primary**
- node that is running the PostgreSQL database (the default Omnibus database name is gitlabhq_production):
+ admin user. If you are using an Omnibus-managed database, log onto the **primary**
+ node that is running the PostgreSQL database (the default Omnibus database name is gitlabhq_production):
- ```sh
- sudo \
- -u gitlab-psql /opt/gitlab/embedded/bin/psql \
- -h /var/opt/gitlab/postgresql gitlabhq_production
- ```
+ ```sh
+ sudo \
+ -u gitlab-psql /opt/gitlab/embedded/bin/psql \
+ -h /var/opt/gitlab/postgresql gitlabhq_production
+ ```
1. Then create the read-only user:
- ```sql
- -- NOTE: Use the password defined earlier
- CREATE USER gitlab_geo_fdw WITH password 'mypassword';
- GRANT CONNECT ON DATABASE gitlabhq_production to gitlab_geo_fdw;
- GRANT USAGE ON SCHEMA public TO gitlab_geo_fdw;
- GRANT SELECT ON ALL TABLES IN SCHEMA public TO gitlab_geo_fdw;
- GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO gitlab_geo_fdw;
+ ```sql
+ -- NOTE: Use the password defined earlier
+ CREATE USER gitlab_geo_fdw WITH password 'mypassword';
+ GRANT CONNECT ON DATABASE gitlabhq_production to gitlab_geo_fdw;
+ GRANT USAGE ON SCHEMA public TO gitlab_geo_fdw;
+ GRANT SELECT ON ALL TABLES IN SCHEMA public TO gitlab_geo_fdw;
+ GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO gitlab_geo_fdw;
- -- Tables created by "gitlab" should be made read-only for "gitlab_geo_fdw"
- -- automatically.
- ALTER DEFAULT PRIVILEGES FOR USER gitlab IN SCHEMA public GRANT SELECT ON TABLES TO gitlab_geo_fdw;
- ALTER DEFAULT PRIVILEGES FOR USER gitlab IN SCHEMA public GRANT SELECT ON SEQUENCES TO gitlab_geo_fdw;
- ```
+ -- Tables created by "gitlab" should be made read-only for "gitlab_geo_fdw"
+ -- automatically.
+ ALTER DEFAULT PRIVILEGES FOR USER gitlab IN SCHEMA public GRANT SELECT ON TABLES TO gitlab_geo_fdw;
+ ALTER DEFAULT PRIVILEGES FOR USER gitlab IN SCHEMA public GRANT SELECT ON SEQUENCES TO gitlab_geo_fdw;
+ ```
1. On the **secondary** nodes, change `/etc/gitlab/gitlab.rb`:
- ```
- geo_postgresql['fdw_external_user'] = 'gitlab_geo_fdw'
- ```
+ ```
+ geo_postgresql['fdw_external_user'] = 'gitlab_geo_fdw'
+ ```
1. Save the file and reconfigure GitLab for the changes to be applied:
- ```sh
- gitlab-ctl reconfigure
- ```
+ ```sh
+ gitlab-ctl reconfigure
+ ```
## Troubleshooting
diff --git a/doc/administration/geo/replication/external_database.md b/doc/administration/geo/replication/external_database.md
index 177ca68613e..452e4f490a6 100644
--- a/doc/administration/geo/replication/external_database.md
+++ b/doc/administration/geo/replication/external_database.md
@@ -4,7 +4,7 @@ This document is relevant if you are using a PostgreSQL instance that is *not
managed by Omnibus*. This includes cloud-managed instances like AWS RDS, or
manually installed and configured PostgreSQL instances.
-NOTE: **Note**:
+NOTE: **Note:**
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 but we do not guarantee compatibility.
@@ -13,17 +13,17 @@ developed and tested. We aim to be compatible with most external
1. SSH into a GitLab **primary** application server and login as root:
- ```sh
- sudo -i
- ```
+ ```sh
+ sudo -i
+ ```
1. Execute the command below to define the node as **primary** node:
- ```sh
- gitlab-ctl set-geo-primary-node
- ```
+ ```sh
+ gitlab-ctl set-geo-primary-node
+ ```
- This command will use your defined `external_url` in `/etc/gitlab/gitlab.rb`.
+ This command will use your defined `external_url` in `/etc/gitlab/gitlab.rb`.
### Configure the external database to be replicated
@@ -101,26 +101,27 @@ To configure the connection to the external read-replica database and enable Log
1. SSH into a GitLab **secondary** application server and login as root:
- ```bash
- sudo -i
- ```
+ ```bash
+ sudo -i
+ ```
1. Edit `/etc/gitlab/gitlab.rb` and add the following
- ```ruby
- ##
- ## Geo Secondary role
- ## - configure dependent flags automatically to enable Geo
- ##
- roles ['geo_secondary_role']
+ ```ruby
+ ##
+ ## Geo Secondary role
+ ## - configure dependent flags automatically to enable Geo
+ ##
+ roles ['geo_secondary_role']
+
+ # note this is shared between both databases,
+ # make sure you define the same password in both
+ gitlab_rails['db_password'] = '<your_password_here>'
- # note this is shared between both databases,
- # make sure you define the same password in both
- gitlab_rails['db_password'] = '<your_password_here>'
+ gitlab_rails['db_username'] = 'gitlab'
+ gitlab_rails['db_host'] = '<database_read_replica_host>'
+ ```
- gitlab_rails['db_username'] = 'gitlab'
- gitlab_rails['db_host'] = '<database_read_replica_host>'
- ```
1. Save the file and [reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure)
### Configure the tracking database
@@ -147,73 +148,72 @@ the tracking database on port 5432.
1. SSH into a GitLab **secondary** server and login as root:
- ```bash
- sudo -i
- ```
+ ```bash
+ sudo -i
+ ```
1. Edit `/etc/gitlab/gitlab.rb` with the connection params and credentials for
- the machine with the PostgreSQL instance:
+ the machine with the PostgreSQL instance:
- ```ruby
- geo_secondary['db_username'] = 'gitlab_geo'
- geo_secondary['db_password'] = '<your_password_here>'
+ ```ruby
+ geo_secondary['db_username'] = 'gitlab_geo'
+ geo_secondary['db_password'] = '<your_password_here>'
- geo_secondary['db_host'] = '<tracking_database_host>'
- geo_secondary['db_port'] = <tracking_database_port> # change to the correct port
- geo_secondary['db_fdw'] = true # enable FDW
- geo_postgresql['enable'] = false # don't use internal managed instance
- ```
+ geo_secondary['db_host'] = '<tracking_database_host>'
+ geo_secondary['db_port'] = <tracking_database_port> # change to the correct port
+ geo_secondary['db_fdw'] = true # enable FDW
+ geo_postgresql['enable'] = false # don't use internal managed instance
+ ```
1. Save the file and [reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure)
1. Run the tracking database migrations:
- ```bash
- gitlab-rake geo:db:create
- gitlab-rake geo:db:migrate
- ```
-
-1. Configure the
- [PostgreSQL FDW](https://www.postgresql.org/docs/9.6/static/postgres-fdw.html)
- connection and credentials:
-
- Save the script below in a file, ex. `/tmp/geo_fdw.sh` and modify the connection
- params to match your environment. Execute it to set up the FDW connection.
-
- ```bash
- #!/bin/bash
-
- # Secondary Database connection params:
- DB_HOST="<public_ip_or_vpc_private_ip>"
- DB_NAME="gitlabhq_production"
- DB_USER="gitlab"
- DB_PASS="<your_password_here>"
- DB_PORT="5432"
-
- # Tracking Database connection params:
- GEO_DB_HOST="<public_ip_or_vpc_private_ip>"
- GEO_DB_NAME="gitlabhq_geo_production"
- GEO_DB_USER="gitlab_geo"
- GEO_DB_PORT="5432"
-
- query_exec () {
- gitlab-psql -h $GEO_DB_HOST -d $GEO_DB_NAME -p $GEO_DB_PORT -c "${1}"
- }
-
- query_exec "CREATE EXTENSION postgres_fdw;"
- query_exec "CREATE SERVER gitlab_secondary FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '${DB_HOST}', dbname '${DB_NAME}', port '${DB_PORT}');"
- query_exec "CREATE USER MAPPING FOR ${GEO_DB_USER} SERVER gitlab_secondary OPTIONS (user '${DB_USER}', password '${DB_PASS}');"
- query_exec "CREATE SCHEMA gitlab_secondary;"
- query_exec "GRANT USAGE ON FOREIGN SERVER gitlab_secondary TO ${GEO_DB_USER};"
- ```
-
- NOTE: **Note:** The script template above uses `gitlab-psql` as it's intended to be executed from the Geo machine,
- but you can change it to `psql` and run it from any machine that has access to the database. We also recommend using
- `psql` for AWS RDS.
+ ```bash
+ gitlab-rake geo:db:create
+ gitlab-rake geo:db:migrate
+ ```
+
+1. Configure the [PostgreSQL FDW](https://www.postgresql.org/docs/9.6/static/postgres-fdw.html)
+ connection and credentials:
+
+ Save the script below in a file, ex. `/tmp/geo_fdw.sh` and modify the connection
+ params to match your environment. Execute it to set up the FDW connection.
+
+ ```bash
+ #!/bin/bash
+
+ # Secondary Database connection params:
+ DB_HOST="<public_ip_or_vpc_private_ip>"
+ DB_NAME="gitlabhq_production"
+ DB_USER="gitlab"
+ DB_PASS="<your_password_here>"
+ DB_PORT="5432"
+
+ # Tracking Database connection params:
+ GEO_DB_HOST="<public_ip_or_vpc_private_ip>"
+ GEO_DB_NAME="gitlabhq_geo_production"
+ GEO_DB_USER="gitlab_geo"
+ GEO_DB_PORT="5432"
+
+ query_exec () {
+ gitlab-psql -h $GEO_DB_HOST -d $GEO_DB_NAME -p $GEO_DB_PORT -c "${1}"
+ }
+
+ query_exec "CREATE EXTENSION postgres_fdw;"
+ query_exec "CREATE SERVER gitlab_secondary FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '${DB_HOST}', dbname '${DB_NAME}', port '${DB_PORT}');"
+ query_exec "CREATE USER MAPPING FOR ${GEO_DB_USER} SERVER gitlab_secondary OPTIONS (user '${DB_USER}', password '${DB_PASS}');"
+ query_exec "CREATE SCHEMA gitlab_secondary;"
+ query_exec "GRANT USAGE ON FOREIGN SERVER gitlab_secondary TO ${GEO_DB_USER};"
+ ```
+
+ NOTE: **Note:** The script template above uses `gitlab-psql` as it's intended to be executed from the Geo machine,
+ but you can change it to `psql` and run it from any machine that has access to the database. We also recommend using
+ `psql` for AWS RDS.
1. Save the file and [restart GitLab](../../restart_gitlab.md#omnibus-gitlab-restart)
1. Populate the FDW tables:
- ```bash
- gitlab-rake geo:db:refresh_foreign_tables
- ```
+ ```bash
+ gitlab-rake geo:db:refresh_foreign_tables
+ ```
diff --git a/doc/administration/geo/replication/high_availability.md b/doc/administration/geo/replication/high_availability.md
index 28ad89c4446..61e18df2480 100644
--- a/doc/administration/geo/replication/high_availability.md
+++ b/doc/administration/geo/replication/high_availability.md
@@ -50,17 +50,17 @@ The following steps enable a GitLab cluster to serve as the **primary** node.
1. Edit `/etc/gitlab/gitlab.rb` and add the following:
- ```ruby
- ##
- ## Enable the Geo primary role
- ##
- roles ['geo_primary_role']
-
- ##
- ## Disable automatic migrations
- ##
- gitlab_rails['auto_migrate'] = false
- ```
+ ```ruby
+ ##
+ ## Enable the Geo primary role
+ ##
+ roles ['geo_primary_role']
+
+ ##
+ ## Disable automatic migrations
+ ##
+ gitlab_rails['auto_migrate'] = false
+ ```
After making these changes, [reconfigure GitLab][gitlab-reconfigure] so the changes take effect.
@@ -107,36 +107,36 @@ Configure the [**secondary** database](database.md) as a read-only replica of
the **primary** database. Use the following as a guide.
1. Edit `/etc/gitlab/gitlab.rb` in the replica database machine, and add the
- following:
-
- ```ruby
- ##
- ## Configure the PostgreSQL role
- ##
- roles ['postgres_role']
-
- ##
- ## Secondary address
- ## - replace '<secondary_node_ip>' with the public or VPC address of your Geo secondary node
- ## - replace '<tracking_database_ip>' with the public or VPC address of your Geo tracking database node
- ##
- postgresql['listen_address'] = '<secondary_node_ip>'
- postgresql['md5_auth_cidr_addresses'] = ['<secondary_node_ip>/32', '<tracking_database_ip>/32']
-
- ##
- ## Database credentials password (defined previously in primary node)
- ## - replicate same values here as defined in primary node
- ##
- postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
- gitlab_rails['db_password'] = '<your_password_here>'
-
- ##
- ## When running the Geo tracking database on a separate machine, disable it
- ## here and allow connections from the tracking database host. And ensure
- ## the tracking database IP is in postgresql['md5_auth_cidr_addresses'] above.
- ##
- geo_postgresql['enable'] = false
- ```
+ following:
+
+ ```ruby
+ ##
+ ## Configure the PostgreSQL role
+ ##
+ roles ['postgres_role']
+
+ ##
+ ## Secondary address
+ ## - replace '<secondary_node_ip>' with the public or VPC address of your Geo secondary node
+ ## - replace '<tracking_database_ip>' with the public or VPC address of your Geo tracking database node
+ ##
+ postgresql['listen_address'] = '<secondary_node_ip>'
+ postgresql['md5_auth_cidr_addresses'] = ['<secondary_node_ip>/32', '<tracking_database_ip>/32']
+
+ ##
+ ## Database credentials password (defined previously in primary node)
+ ## - replicate same values here as defined in primary node
+ ##
+ postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
+ gitlab_rails['db_password'] = '<your_password_here>'
+
+ ##
+ ## When running the Geo tracking database on a separate machine, disable it
+ ## here and allow connections from the tracking database host. And ensure
+ ## the tracking database IP is in postgresql['md5_auth_cidr_addresses'] above.
+ ##
+ geo_postgresql['enable'] = false
+ ```
After making these changes, [reconfigure GitLab][gitlab-reconfigure] so the changes take effect.
@@ -151,47 +151,47 @@ only a single machine, rather than as a PostgreSQL cluster.
Configure the tracking database.
1. Edit `/etc/gitlab/gitlab.rb` in the tracking database machine, and add the
- following:
-
- ```ruby
- ##
- ## Enable the Geo secondary tracking database
- ##
- geo_postgresql['enable'] = true
- geo_postgresql['listen_address'] = '<ip_address_of_this_host>'
- geo_postgresql['sql_user_password'] = '<tracking_database_password_md5_hash>'
-
- ##
- ## Configure FDW connection to the replica database
- ##
- geo_secondary['db_fdw'] = true
- geo_postgresql['fdw_external_password'] = '<replica_database_password_plaintext>'
- geo_postgresql['md5_auth_cidr_addresses'] = ['<replica_database_ip>/32']
- gitlab_rails['db_host'] = '<replica_database_ip>'
-
- # Prevent reconfigure from attempting to run migrations on the replica DB
- gitlab_rails['auto_migrate'] = false
-
- ##
- ## Disable all other services that aren't needed, since we don't have a role
- ## that does this.
- ##
- alertmanager['enable'] = false
- consul['enable'] = false
- gitaly['enable'] = false
- gitlab_monitor['enable'] = false
- gitlab_workhorse['enable'] = false
- nginx['enable'] = false
- node_exporter['enable'] = false
- pgbouncer_exporter['enable'] = false
- postgresql['enable'] = false
- prometheus['enable'] = false
- redis['enable'] = false
- redis_exporter['enable'] = false
- repmgr['enable'] = false
- sidekiq['enable'] = false
- unicorn['enable'] = false
- ```
+ following:
+
+ ```ruby
+ ##
+ ## Enable the Geo secondary tracking database
+ ##
+ geo_postgresql['enable'] = true
+ geo_postgresql['listen_address'] = '<ip_address_of_this_host>'
+ geo_postgresql['sql_user_password'] = '<tracking_database_password_md5_hash>'
+
+ ##
+ ## Configure FDW connection to the replica database
+ ##
+ geo_secondary['db_fdw'] = true
+ geo_postgresql['fdw_external_password'] = '<replica_database_password_plaintext>'
+ geo_postgresql['md5_auth_cidr_addresses'] = ['<replica_database_ip>/32']
+ gitlab_rails['db_host'] = '<replica_database_ip>'
+
+ # Prevent reconfigure from attempting to run migrations on the replica DB
+ gitlab_rails['auto_migrate'] = false
+
+ ##
+ ## Disable all other services that aren't needed, since we don't have a role
+ ## that does this.
+ ##
+ alertmanager['enable'] = false
+ consul['enable'] = false
+ gitaly['enable'] = false
+ gitlab_monitor['enable'] = false
+ gitlab_workhorse['enable'] = false
+ nginx['enable'] = false
+ node_exporter['enable'] = false
+ pgbouncer_exporter['enable'] = false
+ postgresql['enable'] = false
+ prometheus['enable'] = false
+ redis['enable'] = false
+ redis_exporter['enable'] = false
+ repmgr['enable'] = false
+ sidekiq['enable'] = false
+ unicorn['enable'] = false
+ ```
After making these changes, [reconfigure GitLab][gitlab-reconfigure] so the changes take effect.
@@ -211,50 +211,50 @@ following modifications:
1. Edit `/etc/gitlab/gitlab.rb` on each application server in the **secondary**
cluster, and add the following:
- ```ruby
- ##
- ## Enable the Geo secondary role
- ##
- roles ['geo_secondary_role', 'application_role']
-
- ##
- ## Disable automatic migrations
- ##
- gitlab_rails['auto_migrate'] = false
-
- ##
- ## Configure the connection to the tracking DB. And disable application
- ## servers from running tracking databases.
- ##
- geo_secondary['db_host'] = '<geo_tracking_db_host>'
- geo_secondary['db_password'] = '<geo_tracking_db_password>'
- geo_postgresql['enable'] = false
-
- ##
- ## Configure connection to the streaming replica database, if you haven't
- ## already
- ##
- gitlab_rails['db_host'] = '<replica_database_host>'
- gitlab_rails['db_password'] = '<replica_database_password>'
-
- ##
- ## Configure connection to Redis, if you haven't already
- ##
- gitlab_rails['redis_host'] = '<redis_host>'
- gitlab_rails['redis_password'] = '<redis_password>'
-
- ##
- ## If you are using custom users not managed by Omnibus, you need to specify
- ## UIDs and GIDs like below, and ensure they match between servers in a
- ## cluster to avoid permissions issues
- ##
- user['uid'] = 9000
- user['gid'] = 9000
- web_server['uid'] = 9001
- web_server['gid'] = 9001
- registry['uid'] = 9002
- registry['gid'] = 9002
- ```
+ ```ruby
+ ##
+ ## Enable the Geo secondary role
+ ##
+ roles ['geo_secondary_role', 'application_role']
+
+ ##
+ ## Disable automatic migrations
+ ##
+ gitlab_rails['auto_migrate'] = false
+
+ ##
+ ## Configure the connection to the tracking DB. And disable application
+ ## servers from running tracking databases.
+ ##
+ geo_secondary['db_host'] = '<geo_tracking_db_host>'
+ geo_secondary['db_password'] = '<geo_tracking_db_password>'
+ geo_postgresql['enable'] = false
+
+ ##
+ ## Configure connection to the streaming replica database, if you haven't
+ ## already
+ ##
+ gitlab_rails['db_host'] = '<replica_database_host>'
+ gitlab_rails['db_password'] = '<replica_database_password>'
+
+ ##
+ ## Configure connection to Redis, if you haven't already
+ ##
+ gitlab_rails['redis_host'] = '<redis_host>'
+ gitlab_rails['redis_password'] = '<redis_password>'
+
+ ##
+ ## If you are using custom users not managed by Omnibus, you need to specify
+ ## UIDs and GIDs like below, and ensure they match between servers in a
+ ## cluster to avoid permissions issues
+ ##
+ user['uid'] = 9000
+ user['gid'] = 9000
+ web_server['uid'] = 9001
+ web_server['gid'] = 9001
+ registry['uid'] = 9002
+ registry['gid'] = 9002
+ ```
NOTE: **Note:**
If you had set up PostgreSQL cluster using the omnibus package and you had set
diff --git a/doc/administration/geo/replication/index.md b/doc/administration/geo/replication/index.md
index 54377f7ae4e..8e1d1cb46ba 100644
--- a/doc/administration/geo/replication/index.md
+++ b/doc/administration/geo/replication/index.md
@@ -80,8 +80,8 @@ In this diagram:
- If present, the [LDAP server](#ldap) should be configured to replicate for [Disaster Recovery](../disaster_recovery/index.md) scenarios.
- A **secondary** node performs different type of synchronizations against the **primary** node, using a special
authorization protected by JWT:
- - Repositories are cloned/updated via Git over HTTPS.
- - Attachments, LFS objects, and other files are downloaded via HTTPS using a private API endpoint.
+ - Repositories are cloned/updated via Git over HTTPS.
+ - Attachments, LFS objects, and other files are downloaded via HTTPS using a private API endpoint.
From the perspective of a user performing Git operations:
@@ -107,8 +107,8 @@ The following are required to run Geo:
- An operating system that supports OpenSSH 6.9+ (needed for
[fast lookup of authorized SSH keys in the database](../../operations/fast_ssh_key_lookup.md))
The following operating systems are known to ship with a current version of OpenSSH:
- - [CentOS](https://www.centos.org) 7.4+
- - [Ubuntu](https://www.ubuntu.com) 16.04+
+ - [CentOS](https://www.centos.org) 7.4+
+ - [Ubuntu](https://www.ubuntu.com) 16.04+
- PostgreSQL 9.6+ with [FDW](https://www.postgresql.org/docs/9.6/postgres-fdw.html) support and [Streaming Replication](https://wiki.postgresql.org/wiki/Streaming_Replication)
- Git 2.9+
diff --git a/doc/administration/geo/replication/remove_geo_node.md b/doc/administration/geo/replication/remove_geo_node.md
index b190fe7d42d..6bdaad8f783 100644
--- a/doc/administration/geo/replication/remove_geo_node.md
+++ b/doc/administration/geo/replication/remove_geo_node.md
@@ -10,41 +10,42 @@ Once removed from the Geo admin page, you must stop and uninstall the **secondar
1. On the **secondary** node, stop GitLab:
- ```bash
- sudo gitlab-ctl stop
- ```
+ ```bash
+ sudo gitlab-ctl stop
+ ```
+
1. On the **secondary** node, uninstall GitLab:
- ```bash
- # Stop gitlab and remove its supervision process
- sudo gitlab-ctl uninstall
+ ```bash
+ # Stop gitlab and remove its supervision process
+ sudo gitlab-ctl uninstall
- # Debian/Ubuntu
- sudo dpkg --remove gitlab-ee
+ # Debian/Ubuntu
+ sudo dpkg --remove gitlab-ee
- # Redhat/Centos
- sudo rpm --erase gitlab-ee
- ```
+ # Redhat/Centos
+ sudo rpm --erase gitlab-ee
+ ```
Once GitLab has been uninstalled from the **secondary** node, the replication slot must be dropped from the **primary** node's database as follows:
1. On the **primary** node, start a PostgreSQL console session:
- ```bash
- sudo gitlab-psql
- ```
+ ```bash
+ sudo gitlab-psql
+ ```
- NOTE: **Note:**
- Using `gitlab-rails dbconsole` will not work, because managing replication slots requires superuser permissions.
+ NOTE: **Note:**
+ Using `gitlab-rails dbconsole` will not work, because managing replication slots requires superuser permissions.
1. Find the name of the relevant replication slot. This is the slot that is specified with `--slot-name` when running the replicate command: `gitlab-ctl replicate-geo-database`.
- ```sql
- SELECT * FROM pg_replication_slots;
- ```
+ ```sql
+ SELECT * FROM pg_replication_slots;
+ ```
1. Remove the replication slot for the **secondary** node:
- ```sql
- SELECT pg_drop_replication_slot('<name_of_slot>');
- ```
+ ```sql
+ SELECT pg_drop_replication_slot('<name_of_slot>');
+ ```
diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md
index 5bd6cc81362..c7c78407084 100644
--- a/doc/administration/geo/replication/troubleshooting.md
+++ b/doc/administration/geo/replication/troubleshooting.md
@@ -184,17 +184,17 @@ log data to build up in `pg_xlog`. Removing the unused slots can reduce the amou
1. Start a PostgreSQL console session:
- ```sh
- sudo gitlab-psql gitlabhq_production
- ```
+ ```sh
+ sudo gitlab-psql gitlabhq_production
+ ```
- > Note that using `gitlab-rails dbconsole` will not work, because managing replication slots requires superuser permissions.
+ Note: **Note:** Using `gitlab-rails dbconsole` will not work, because managing replication slots requires superuser permissions.
1. View your replication slots with:
- ```sql
- SELECT * FROM pg_replication_slots;
- ```
+ ```sql
+ SELECT * FROM pg_replication_slots;
+ ```
Slots where `active` is `f` are not active.
@@ -204,9 +204,9 @@ Slots where `active` is `f` are not active.
- If you are no longer using the slot (e.g. you no longer have Geo enabled), you can remove it with in the
PostgreSQL console session:
- ```sql
- SELECT pg_drop_replication_slot('<name_of_extra_slot>');
- ```
+ ```sql
+ SELECT pg_drop_replication_slot('<name_of_extra_slot>');
+ ```
### Very large repositories never successfully synchronize on the **secondary** node
@@ -237,82 +237,82 @@ to start again from scratch, there are a few steps that can help you:
1. Stop Sidekiq and the Geo LogCursor
- It's possible to make Sidekiq stop gracefully, but making it stop getting new jobs and
- wait until the current jobs to finish processing.
+ It's possible to make Sidekiq stop gracefully, but making it stop getting new jobs and
+ wait until the current jobs to finish processing.
- You need to send a **SIGTSTP** kill signal for the first phase and them a **SIGTERM**
- when all jobs have finished. Otherwise just use the `gitlab-ctl stop` commands.
+ You need to send a **SIGTSTP** kill signal for the first phase and them a **SIGTERM**
+ when all jobs have finished. Otherwise just use the `gitlab-ctl stop` commands.
- ```sh
- gitlab-ctl status sidekiq
- # run: sidekiq: (pid 10180) <- this is the PID you will use
- kill -TSTP 10180 # change to the correct PID
+ ```sh
+ gitlab-ctl status sidekiq
+ # run: sidekiq: (pid 10180) <- this is the PID you will use
+ kill -TSTP 10180 # change to the correct PID
- gitlab-ctl stop sidekiq
- gitlab-ctl stop geo-logcursor
- ```
+ gitlab-ctl stop sidekiq
+ gitlab-ctl stop geo-logcursor
+ ```
- You can watch sidekiq logs to know when sidekiq jobs processing have finished:
+ You can watch sidekiq logs to know when sidekiq jobs processing have finished:
- ```sh
- gitlab-ctl tail sidekiq
- ```
+ ```sh
+ gitlab-ctl tail sidekiq
+ ```
1. Rename repository storage folders and create new ones
- ```sh
- mv /var/opt/gitlab/git-data/repositories /var/opt/gitlab/git-data/repositories.old
- mkdir -p /var/opt/gitlab/git-data/repositories
- chown git:git /var/opt/gitlab/git-data/repositories
- ```
+ ```sh
+ mv /var/opt/gitlab/git-data/repositories /var/opt/gitlab/git-data/repositories.old
+ mkdir -p /var/opt/gitlab/git-data/repositories
+ chown git:git /var/opt/gitlab/git-data/repositories
+ ```
- TIP: **Tip**
- You may want to remove the `/var/opt/gitlab/git-data/repositories.old` in the future
- as soon as you confirmed that you don't need it anymore, to save disk space.
+ TIP: **Tip**
+ You may want to remove the `/var/opt/gitlab/git-data/repositories.old` in the future
+ as soon as you confirmed that you don't need it anymore, to save disk space.
1. _(Optional)_ Rename other data folders and create new ones
- CAUTION: **Caution**:
- You may still have files on the **secondary** node that have been removed from **primary** node but
- removal have not been reflected. If you skip this step, they will never be removed
- from this Geo node.
+ CAUTION: **Caution**:
+ You may still have files on the **secondary** node that have been removed from **primary** node but
+ removal have not been reflected. If you skip this step, they will never be removed
+ from this Geo node.
- Any uploaded content like file attachments, avatars or LFS objects are stored in a
- subfolder in one of the two paths below:
+ Any uploaded content like file attachments, avatars or LFS objects are stored in a
+ subfolder in one of the two paths below:
- 1. /var/opt/gitlab/gitlab-rails/shared
- 1. /var/opt/gitlab/gitlab-rails/uploads
+ - /var/opt/gitlab/gitlab-rails/shared
+ - /var/opt/gitlab/gitlab-rails/uploads
- To rename all of them:
+ To rename all of them:
- ```sh
- gitlab-ctl stop
+ ```sh
+ gitlab-ctl stop
- mv /var/opt/gitlab/gitlab-rails/shared /var/opt/gitlab/gitlab-rails/shared.old
- mkdir -p /var/opt/gitlab/gitlab-rails/shared
+ mv /var/opt/gitlab/gitlab-rails/shared /var/opt/gitlab/gitlab-rails/shared.old
+ mkdir -p /var/opt/gitlab/gitlab-rails/shared
- mv /var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/uploads.old
- mkdir -p /var/opt/gitlab/gitlab-rails/uploads
- ```
+ mv /var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/uploads.old
+ mkdir -p /var/opt/gitlab/gitlab-rails/uploads
+ ```
- Reconfigure in order to recreate the folders and make sure permissions and ownership
- are correctly
+ Reconfigure in order to recreate the folders and make sure permissions and ownership
+ are correctly
- ```sh
- gitlab-ctl reconfigure
- ```
+ ```sh
+ gitlab-ctl reconfigure
+ ```
1. Reset the Tracking Database
- ```sh
- gitlab-rake geo:db:reset
- ```
+ ```sh
+ gitlab-rake geo:db:reset
+ ```
1. Restart previously stopped services
- ```sh
- gitlab-ctl start
- ```
+ ```sh
+ gitlab-ctl start
+ ```
## Fixing Foreign Data Wrapper errors
@@ -345,108 +345,106 @@ To check the configuration:
1. Enter the database console:
- ```sh
- gitlab-geo-psql
- ```
+ ```sh
+ gitlab-geo-psql
+ ```
1. Check whether any tables are present. If everything is working, you
should see something like this:
- ```sql
- gitlabhq_geo_production=# SELECT * from information_schema.foreign_tables;
- foreign_table_catalog | foreign_table_schema | foreign_table_name | foreign_server_catalog | foreign_server_n
- ame
- -------------------------+----------------------+-------------------------------------------------+-------------------------+-----------------
- ----
- gitlabhq_geo_production | gitlab_secondary | abuse_reports | gitlabhq_geo_production | gitlab_secondary
- gitlabhq_geo_production | gitlab_secondary | appearances | gitlabhq_geo_production | gitlab_secondary
- gitlabhq_geo_production | gitlab_secondary | application_setting_terms | gitlabhq_geo_production | gitlab_secondary
- gitlabhq_geo_production | gitlab_secondary | application_settings | gitlabhq_geo_production | gitlab_secondary
- <snip>
- ```
-
- However, if the query returns with `0 rows`, then continue onto the next steps.
+ ```sql
+ gitlabhq_geo_production=# SELECT * from information_schema.foreign_tables;
+ foreign_table_catalog | foreign_table_schema | foreign_table_name | foreign_server_catalog | foreign_server_name
+ -------------------------+----------------------+-------------------------------------------------+-------------------------+---------------------
+ gitlabhq_geo_production | gitlab_secondary | abuse_reports | gitlabhq_geo_production | gitlab_secondary
+ gitlabhq_geo_production | gitlab_secondary | appearances | gitlabhq_geo_production | gitlab_secondary
+ gitlabhq_geo_production | gitlab_secondary | application_setting_terms | gitlabhq_geo_production | gitlab_secondary
+ gitlabhq_geo_production | gitlab_secondary | application_settings | gitlabhq_geo_production | gitlab_secondary
+ <snip>
+ ```
+
+ However, if the query returns with `0 rows`, then continue onto the next steps.
1. Check that the foreign server mapping is correct via `\des+`. The
results should look something like this:
- ```sql
- gitlabhq_geo_production=# \des+
- List of foreign servers
- -[ RECORD 1 ]--------+------------------------------------------------------------
- Name | gitlab_secondary
- Owner | gitlab-psql
- Foreign-data wrapper | postgres_fdw
- Access privileges | "gitlab-psql"=U/"gitlab-psql" +
- | gitlab_geo=U/"gitlab-psql"
- Type |
- Version |
- FDW Options | (host '0.0.0.0', port '5432', dbname 'gitlabhq_production')
- Description |
- ```
-
- NOTE: **Note:** Pay particular attention to the host and port under
- FDW options. That configuration should point to the Geo secondary
- database.
-
- If you need to experiment with changing the host or password, the
- following queries demonstrate how:
-
- ```sql
- ALTER SERVER gitlab_secondary OPTIONS (SET host '<my_new_host>');
- ALTER SERVER gitlab_secondary OPTIONS (SET port 5432);
- ```
-
- If you change the host and/or port, you will also have to adjust the
- following settings in `/etc/gitlab/gitlab.rb` and run `gitlab-ctl
- reconfigure`:
-
- - `gitlab_rails['db_host']`
- - `gitlab_rails['db_port']`
+ ```sql
+ gitlabhq_geo_production=# \des+
+ List of foreign servers
+ -[ RECORD 1 ]--------+------------------------------------------------------------
+ Name | gitlab_secondary
+ Owner | gitlab-psql
+ Foreign-data wrapper | postgres_fdw
+ Access privileges | "gitlab-psql"=U/"gitlab-psql" +
+ | gitlab_geo=U/"gitlab-psql"
+ Type |
+ Version |
+ FDW Options | (host '0.0.0.0', port '5432', dbname 'gitlabhq_production')
+ Description |
+ ```
+
+ NOTE: **Note:** Pay particular attention to the host and port under
+ FDW options. That configuration should point to the Geo secondary
+ database.
+
+ If you need to experiment with changing the host or password, the
+ following queries demonstrate how:
+
+ ```sql
+ ALTER SERVER gitlab_secondary OPTIONS (SET host '<my_new_host>');
+ ALTER SERVER gitlab_secondary OPTIONS (SET port 5432);
+ ```
+
+ If you change the host and/or port, you will also have to adjust the
+ following settings in `/etc/gitlab/gitlab.rb` and run `gitlab-ctl
+ reconfigure`:
+
+ - `gitlab_rails['db_host']`
+ - `gitlab_rails['db_port']`
1. Check that the user mapping is configured properly via `\deu+`:
- ```sql
- gitlabhq_geo_production=# \deu+
- List of user mappings
- Server | User name | FDW Options
- ------------------+------------+--------------------------------------------------------------------------------
- gitlab_secondary | gitlab_geo | ("user" 'gitlab', password 'YOUR-PASSWORD-HERE')
- (1 row)
- ```
+ ```sql
+ gitlabhq_geo_production=# \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ ------------------+------------+--------------------------------------------------------------------------------
+ gitlab_secondary | gitlab_geo | ("user" 'gitlab', password 'YOUR-PASSWORD-HERE')
+ (1 row)
+ ```
- Make sure the password is correct. You can test that logins work by running `psql`:
+ Make sure the password is correct. You can test that logins work by running `psql`:
- ```sh
- # Connect to the tracking database as the `gitlab_geo` user
- sudo \
- -u git /opt/gitlab/embedded/bin/psql \
- -h /var/opt/gitlab/geo-postgresql \
- -p 5431 \
- -U gitlab_geo \
- -W \
- -d gitlabhq_geo_production
- ```
+ ```sh
+ # Connect to the tracking database as the `gitlab_geo` user
+ sudo \
+ -u git /opt/gitlab/embedded/bin/psql \
+ -h /var/opt/gitlab/geo-postgresql \
+ -p 5431 \
+ -U gitlab_geo \
+ -W \
+ -d gitlabhq_geo_production
+ ```
- If you need to correct the password, the following query shows how:
+ If you need to correct the password, the following query shows how:
- ```sql
- ALTER USER MAPPING FOR gitlab_geo SERVER gitlab_secondary OPTIONS (SET password '<my_new_password>');
- ```
+ ```sql
+ ALTER USER MAPPING FOR gitlab_geo SERVER gitlab_secondary OPTIONS (SET password '<my_new_password>');
+ ```
- If you change the user or password, you will also have to adjust the
- following settings in `/etc/gitlab/gitlab.rb` and run `gitlab-ctl
- reconfigure`:
+ If you change the user or password, you will also have to adjust the
+ following settings in `/etc/gitlab/gitlab.rb` and run `gitlab-ctl
+ reconfigure`:
- - `gitlab_rails['db_username']`
- - `gitlab_rails['db_password']`
+ - `gitlab_rails['db_username']`
+ - `gitlab_rails['db_password']`
- If you are using [PgBouncer in front of the secondary
- database](database.md#pgbouncer-support-optional), be sure to update
- the following settings:
+ If you are using [PgBouncer in front of the secondary
+ database](database.md#pgbouncer-support-optional), be sure to update
+ the following settings:
- - `geo_postgresql['fdw_external_user']`
- - `geo_postgresql['fdw_external_password']`
+ - `geo_postgresql['fdw_external_user']`
+ - `geo_postgresql['fdw_external_password']`
#### Manual reload of FDW schema
@@ -456,34 +454,34 @@ reload of the FDW schema. To manually reload the FDW schema:
1. On the node running the Geo tracking database, enter the PostgreSQL console via
the `gitlab_geo` user:
- ```sh
- sudo \
- -u git /opt/gitlab/embedded/bin/psql \
- -h /var/opt/gitlab/geo-postgresql \
- -p 5431 \
- -U gitlab_geo \
- -W \
- -d gitlabhq_geo_production
- ```
+ ```sh
+ sudo \
+ -u git /opt/gitlab/embedded/bin/psql \
+ -h /var/opt/gitlab/geo-postgresql \
+ -p 5431 \
+ -U gitlab_geo \
+ -W \
+ -d gitlabhq_geo_production
+ ```
- Be sure to adjust the port and hostname for your configuration. You
- may be asked to enter a password.
+ Be sure to adjust the port and hostname for your configuration. You
+ may be asked to enter a password.
1. Reload the schema via:
- ```sql
- DROP SCHEMA IF EXISTS gitlab_secondary CASCADE;
- CREATE SCHEMA gitlab_secondary;
- GRANT USAGE ON FOREIGN SERVER gitlab_secondary TO gitlab_geo;
- IMPORT FOREIGN SCHEMA public FROM SERVER gitlab_secondary INTO gitlab_secondary;
- ```
+ ```sql
+ DROP SCHEMA IF EXISTS gitlab_secondary CASCADE;
+ CREATE SCHEMA gitlab_secondary;
+ GRANT USAGE ON FOREIGN SERVER gitlab_secondary TO gitlab_geo;
+ IMPORT FOREIGN SCHEMA public FROM SERVER gitlab_secondary INTO gitlab_secondary;
+ ```
1. Test that queries work:
- ```sql
- SELECT * from information_schema.foreign_tables;
- SELECT * FROM gitlab_secondary.projects limit 1;
- ```
+ ```sql
+ SELECT * from information_schema.foreign_tables;
+ SELECT * FROM gitlab_secondary.projects limit 1;
+ ```
[database-start-replication]: database.md#step-3-initiate-the-replication-process
[database-pg-replication]: database.md#postgresql-replication
diff --git a/doc/administration/geo/replication/updating_the_geo_nodes.md b/doc/administration/geo/replication/updating_the_geo_nodes.md
index 933a75c47d8..d56a59f4967 100644
--- a/doc/administration/geo/replication/updating_the_geo_nodes.md
+++ b/doc/administration/geo/replication/updating_the_geo_nodes.md
@@ -30,70 +30,70 @@ We now require this change as we use this password to enable the Foreign Data Wr
the Geo Tracking Database. We are also improving security by disabling the use of **trust**
authentication method.
-1. **[primary]** Login to your **primary** node and run:
+1. **(primary)** Login to your **primary** node and run:
- ```sh
- gitlab-ctl pg-password-md5 gitlab
- # Enter password: <your_password_here>
- # Confirm password: <your_password_here>
- # fca0b89a972d69f00eb3ec98a5838484
- ```
+ ```sh
+ gitlab-ctl pg-password-md5 gitlab
+ # Enter password: <your_password_here>
+ # Confirm password: <your_password_here>
+ # fca0b89a972d69f00eb3ec98a5838484
+ ```
- Copy the generated hash and edit `/etc/gitlab/gitlab.rb`:
+ Copy the generated hash and edit `/etc/gitlab/gitlab.rb`:
- ```ruby
- # Fill with the hash generated by `gitlab-ctl pg-password-md5 gitlab`
- postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
+ ```ruby
+ # Fill with the hash generated by `gitlab-ctl pg-password-md5 gitlab`
+ postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
- # Every node that runs Unicorn or Sidekiq needs to have the database
- # password specified as below. If you have a high-availability setup, this
- # must be present in all application nodes.
- gitlab_rails['db_password'] = '<your_password_here>'
- ```
+ # Every node that runs Unicorn or Sidekiq needs to have the database
+ # password specified as below. If you have a high-availability setup, this
+ # must be present in all application nodes.
+ gitlab_rails['db_password'] = '<your_password_here>'
+ ```
- Still in the configuration file, locate and remove the `trust_auth_cidr_address`:
+ Still in the configuration file, locate and remove the `trust_auth_cidr_address`:
- ```ruby
- postgresql['trust_auth_cidr_addresses'] = ['127.0.0.1/32','1.2.3.4/32'] # <- Remove this
- ```
+ ```ruby
+ postgresql['trust_auth_cidr_addresses'] = ['127.0.0.1/32','1.2.3.4/32'] # <- Remove this
+ ```
-1. **[primary]** Reconfigure and restart:
+1. **(primary)** Reconfigure and restart:
- ```sh
- sudo gitlab-ctl reconfigure
- sudo gitlab-ctl restart
- ```
+ ```sh
+ sudo gitlab-ctl reconfigure
+ sudo gitlab-ctl restart
+ ```
-1. **[secondary]** Login to all **secondary** nodes and edit `/etc/gitlab/gitlab.rb`:
+1. **(secondary)** Login to all **secondary** nodes and edit `/etc/gitlab/gitlab.rb`:
- ```ruby
- # Fill with the hash generated by `gitlab-ctl pg-password-md5 gitlab`
- postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
+ ```ruby
+ # Fill with the hash generated by `gitlab-ctl pg-password-md5 gitlab`
+ postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
- # Every node that runs Unicorn or Sidekiq needs to have the database
- # password specified as below. If you have a high-availability setup, this
- # must be present in all application nodes.
- gitlab_rails['db_password'] = '<your_password_here>'
+ # Every node that runs Unicorn or Sidekiq needs to have the database
+ # password specified as below. If you have a high-availability setup, this
+ # must be present in all application nodes.
+ gitlab_rails['db_password'] = '<your_password_here>'
- # Enable Foreign Data Wrapper
- geo_secondary['db_fdw'] = true
+ # Enable Foreign Data Wrapper
+ geo_secondary['db_fdw'] = true
- # Secondary address in CIDR format, for example '5.6.7.8/32'
- postgresql['md5_auth_cidr_addresses'] = ['<secondary_node_ip>/32']
- ```
+ # Secondary address in CIDR format, for example '5.6.7.8/32'
+ postgresql['md5_auth_cidr_addresses'] = ['<secondary_node_ip>/32']
+ ```
- Still in the configuration file, locate and remove the `trust_auth_cidr_address`:
+ Still in the configuration file, locate and remove the `trust_auth_cidr_address`:
- ```ruby
- postgresql['trust_auth_cidr_addresses'] = ['127.0.0.1/32','5.6.7.8/32'] # <- Remove this
- ```
+ ```ruby
+ postgresql['trust_auth_cidr_addresses'] = ['127.0.0.1/32','5.6.7.8/32'] # <- Remove this
+ ```
-1. **[secondary]** Reconfigure and restart:
+1. **(secondary)** Reconfigure and restart:
- ```sh
- sudo gitlab-ctl reconfigure
- sudo gitlab-ctl restart
- ```
+ ```sh
+ sudo gitlab-ctl reconfigure
+ sudo gitlab-ctl restart
+ ```
## Upgrading to GitLab 10.5
@@ -169,11 +169,11 @@ After you've verified that HTTP/HTTPS replication is working, you should remove
the now-unused SSH keys from your secondaries, as they may cause problems if the
**secondary** node if ever promoted to a **primary** node:
-1. **[secondary]** Login to **all** your **secondary** nodes and run:
+1. **(secondary)** Login to **all** your **secondary** nodes and run:
- ```ruby
- sudo -u git -H rm ~git/.ssh/id_rsa ~git/.ssh/id_rsa.pub
- ```
+ ```ruby
+ sudo -u git -H rm ~git/.ssh/id_rsa ~git/.ssh/id_rsa.pub
+ ```
### Hashed Storage
@@ -236,12 +236,12 @@ instructions below.
When in doubt, it does not hurt to do a resync. The easiest way to do this in
Omnibus is the following:
- 1. Make sure you have Omnibus GitLab on the **primary** server.
- 1. Run `gitlab-ctl reconfigure` and `gitlab-ctl restart postgresql`. This will enable replication slots on the **primary** database.
- 1. Check the steps about defining `postgresql['sql_user_password']`, `gitlab_rails['db_password']`.
- 1. Make sure `postgresql['max_replication_slots']` matches the number of **secondary** Geo nodes locations.
- 1. Install GitLab on the **secondary** server.
- 1. Re-run the [database replication process][database-replication].
+1. Make sure you have Omnibus GitLab on the **primary** server.
+1. Run `gitlab-ctl reconfigure` and `gitlab-ctl restart postgresql`. This will enable replication slots on the **primary** database.
+1. Check the steps about defining `postgresql['sql_user_password']`, `gitlab_rails['db_password']`.
+1. Make sure `postgresql['max_replication_slots']` matches the number of **secondary** Geo nodes locations.
+1. Install GitLab on the **secondary** server.
+1. Re-run the [database replication process][database-replication].
## Special update notes for 9.0.x
@@ -260,157 +260,154 @@ Make sure to follow the steps in the exact order as they appear below and pay
extra attention in what node (either **primary** or **secondary**) you execute them! Each step
is prepended with the relevant node for better clarity:
-1. **[secondary]** Login to **all** your **secondary** nodes and stop all services:
+1. **(secondary)** Login to **all** your **secondary** nodes and stop all services:
- ```ruby
- sudo gitlab-ctl stop
- ```
+ ```ruby
+ sudo gitlab-ctl stop
+ ```
-1. **[secondary]** Make a backup of the `recovery.conf` file on **all**
+1. **(secondary)** Make a backup of the `recovery.conf` file on **all**
**secondary** nodes to preserve PostgreSQL's credentials:
- ```sh
- sudo cp /var/opt/gitlab/postgresql/data/recovery.conf /var/opt/gitlab/
- ```
+ ```sh
+ sudo cp /var/opt/gitlab/postgresql/data/recovery.conf /var/opt/gitlab/
+ ```
-1. **[primary]** Update the **primary** node to GitLab 9.0 following the
+1. **(primary)** Update the **primary** node to GitLab 9.0 following the
[regular update docs][update]. At the end of the update, the **primary** node
will be running with PostgreSQL 9.6.
-1. **[primary]** To prevent a de-synchronization of the repository replication,
+1. **(primary)** To prevent a de-synchronization of the repository replication,
stop all services except `postgresql` as we will use it to re-initialize the
**secondary** node's database:
- ```sh
- sudo gitlab-ctl stop
- sudo gitlab-ctl start postgresql
- ```
+ ```sh
+ sudo gitlab-ctl stop
+ sudo gitlab-ctl start postgresql
+ ```
-1. **[secondary]** Run the following steps on each of the **secondary** nodes:
+1. **(secondary)** Run the following steps on each of the **secondary** nodes:
- 1. **[secondary]** Stop all services:
+ 1. **(secondary)** Stop all services:
- ```sh
- sudo gitlab-ctl stop
- ```
+ ```sh
+ sudo gitlab-ctl stop
+ ```
- 1. **[secondary]** Prevent running database migrations:
+ 1. **(secondary)** Prevent running database migrations:
- ```sh
- sudo touch /etc/gitlab/skip-auto-migrations
- ```
+ ```sh
+ sudo touch /etc/gitlab/skip-auto-migrations
+ ```
- 1. **[secondary]** Move the old database to another directory:
+ 1. **(secondary)** Move the old database to another directory:
- ```sh
- sudo mv /var/opt/gitlab/postgresql{,.bak}
- ```
+ ```sh
+ sudo mv /var/opt/gitlab/postgresql{,.bak}
+ ```
- 1. **[secondary]** Update to GitLab 9.0 following the [regular update docs][update].
- At the end of the update, the node will be running with PostgreSQL 9.6.
+ 1. **(secondary)** Update to GitLab 9.0 following the [regular update docs][update].
+ At the end of the update, the node will be running with PostgreSQL 9.6.
- 1. **[secondary]** Make sure all services are up:
+ 1. **(secondary)** Make sure all services are up:
- ```sh
- sudo gitlab-ctl start
- ```
+ ```sh
+ sudo gitlab-ctl start
+ ```
- 1. **[secondary]** Reconfigure GitLab:
+ 1. **(secondary)** Reconfigure GitLab:
- ```sh
- sudo gitlab-ctl reconfigure
- ```
+ ```sh
+ sudo gitlab-ctl reconfigure
+ ```
- 1. **[secondary]** Run the PostgreSQL upgrade command:
+ 1. **(secondary)** Run the PostgreSQL upgrade command:
- ```sh
- sudo gitlab-ctl pg-upgrade
- ```
+ ```sh
+ sudo gitlab-ctl pg-upgrade
+ ```
- 1. **[secondary]** See the stored credentials for the database that you will
- need to re-initialize the replication:
+ 1. **(secondary)** See the stored credentials for the database that you will
+ need to re-initialize the replication:
- ```sh
- sudo grep -s primary_conninfo /var/opt/gitlab/recovery.conf
- ```
+ ```sh
+ sudo grep -s primary_conninfo /var/opt/gitlab/recovery.conf
+ ```
- 1. **[secondary]** Create the `replica.sh` script as described in the
- [database configuration document][database-source-replication].
+ 1. **(secondary)** Save the snippet below in a file, let's say `/tmp/replica.sh`. Modify the
+ embedded paths if necessary:
- 1. 1. **[secondary]** Save the snippet below in a file, let's say `/tmp/replica.sh`. Modify the
- embedded paths if necessary:
+ ```
+ #!/bin/bash
- ```
- #!/bin/bash
+ PORT="5432"
+ USER="gitlab_replicator"
+ echo ---------------------------------------------------------------
+ echo WARNING: Make sure this script is run from the secondary server
+ echo ---------------------------------------------------------------
+ echo
+ echo Enter the IP or FQDN of the primary PostgreSQL server
+ read HOST
+ echo Enter the password for $USER@$HOST
+ read -s PASSWORD
+ echo Enter the required sslmode
+ read SSLMODE
- PORT="5432"
- USER="gitlab_replicator"
- echo ---------------------------------------------------------------
- echo WARNING: Make sure this script is run from the secondary server
- echo ---------------------------------------------------------------
- echo
- echo Enter the IP or FQDN of the primary PostgreSQL server
- read HOST
- echo Enter the password for $USER@$HOST
- read -s PASSWORD
- echo Enter the required sslmode
- read SSLMODE
+ echo Stopping PostgreSQL and all GitLab services
+ sudo service gitlab stop
+ sudo service postgresql stop
- echo Stopping PostgreSQL and all GitLab services
- sudo service gitlab stop
- sudo service postgresql stop
+ echo Backing up postgresql.conf
+ sudo -u postgres mv /var/opt/gitlab/postgresql/data/postgresql.conf /var/opt/gitlab/postgresql/
- echo Backing up postgresql.conf
- sudo -u postgres mv /var/opt/gitlab/postgresql/data/postgresql.conf /var/opt/gitlab/postgresql/
+ echo Cleaning up old cluster directory
+ sudo -u postgres rm -rf /var/opt/gitlab/postgresql/data
- echo Cleaning up old cluster directory
- sudo -u postgres rm -rf /var/opt/gitlab/postgresql/data
+ echo Starting base backup as the replicator user
+ echo Enter the password for $USER@$HOST
+ sudo -u postgres /opt/gitlab/embedded/bin/pg_basebackup -h $HOST -D /var/opt/gitlab/postgresql/data -U gitlab_replicator -v -x -P
- echo Starting base backup as the replicator user
- echo Enter the password for $USER@$HOST
- sudo -u postgres /opt/gitlab/embedded/bin/pg_basebackup -h $HOST -D /var/opt/gitlab/postgresql/data -U gitlab_replicator -v -x -P
+ echo Writing recovery.conf file
+ sudo -u postgres bash -c "cat > /var/opt/gitlab/postgresql/data/recovery.conf <<- _EOF1_
+ standby_mode = 'on'
+ primary_conninfo = 'host=$HOST port=$PORT user=$USER password=$PASSWORD sslmode=$SSLMODE'
+ _EOF1_
+ "
- echo Writing recovery.conf file
- sudo -u postgres bash -c "cat > /var/opt/gitlab/postgresql/data/recovery.conf <<- _EOF1_
- standby_mode = 'on'
- primary_conninfo = 'host=$HOST port=$PORT user=$USER password=$PASSWORD sslmode=$SSLMODE'
- _EOF1_
- "
+ echo Restoring postgresql.conf
+ sudo -u postgres mv /var/opt/gitlab/postgresql/postgresql.conf /var/opt/gitlab/postgresql/data/
- echo Restoring postgresql.conf
- sudo -u postgres mv /var/opt/gitlab/postgresql/postgresql.conf /var/opt/gitlab/postgresql/data/
+ echo Starting PostgreSQL
+ sudo service postgresql start
+ ```
- echo Starting PostgreSQL
- sudo service postgresql start
- ```
+ 1. **(secondary)** Run the recovery script using the credentials from the
+ previous step:
- 1. **[secondary]** Run the recovery script using the credentials from the
- previous step:
+ ```sh
+ sudo bash /tmp/replica.sh
+ ```
- ```sh
- sudo bash /tmp/replica.sh
- ```
+ 1. **(secondary)** Reconfigure GitLab:
- 1. **[secondary]** Reconfigure GitLab:
+ ```sh
+ sudo gitlab-ctl reconfigure
+ ```
- ```sh
- sudo gitlab-ctl reconfigure
- ```
+ 1. **(secondary)** Start all services:
- 1. **[secondary]** Start all services:
+ ```sh
+ sudo gitlab-ctl start
+ ```
- ```sh
- sudo gitlab-ctl start
- ```
+ 1. **(secondary)** Repeat the steps for the remaining **secondary** nodes.
- 1. **[secondary]** Repeat the steps for the remaining **secondary** nodes.
-
-1. **[primary]** After all **secondary** nodes are updated, start all services in
+1. **(primary)** After all **secondary** nodes are updated, start all services in
**primary** node:
- ```sh
- sudo gitlab-ctl start
- ```
+ ```sh
+ sudo gitlab-ctl start
+ ```
## Check status after updating
@@ -419,9 +416,9 @@ everything is working correctly:
1. Run the Geo raketask on all nodes, everything should be green:
- ```sh
- sudo gitlab-rake gitlab:geo:check
- ```
+ ```sh
+ sudo gitlab-rake gitlab:geo:check
+ ```
1. Check the **primary** node's Geo dashboard for any errors.
1. Test the data replication by pushing code to the **primary** node and see if it
@@ -435,9 +432,9 @@ and it is required since 10.0.
1. Run database migrations on tracking database:
- ```sh
- sudo gitlab-rake geo:db:migrate
- ```
+ ```sh
+ sudo gitlab-rake geo:db:migrate
+ ```
1. Repeat this step for each **secondary** node.
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index 0bffe2f7472..7c7bb9045c7 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -247,8 +247,10 @@ gitlab:
repositories:
storages:
default:
+ path: /mnt/gitlab/default/repositories
gitaly_address: tcp://gitaly.internal:8075
storage1:
+ path: /mnt/gitlab/storage1/repositories
gitaly_address: tcp://gitaly.internal:8075
gitaly:
@@ -267,7 +269,7 @@ repository from your GitLab server over HTTP.
Gitaly supports TLS encryption. To be able to communicate
with a Gitaly instance that listens for secure connections you will need to use `tls://` url
-scheme in the `gitaly_address` of the corresponding storage entry in the gitlab configuration.
+scheme in the `gitaly_address` of the corresponding storage entry in the GitLab configuration.
The admin needs to bring their own certificate as we do not provide that automatically.
The certificate to be used needs to be installed on all Gitaly nodes and on all client nodes that communicate with it following procedures described in [GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates).
@@ -293,8 +295,8 @@ sum(rate(gitaly_connections_total[5m])) by (type)
```ruby
# /etc/gitlab/gitlab.rb
git_data_dirs({
- 'default' => { 'path' => '/mnt/gitlab/default', 'gitaly_address' => 'tls://gitaly.internal:9999' },
- 'storage1' => { 'path' => '/mnt/gitlab/storage1', 'gitaly_address' => 'tls://gitaly.internal:9999' },
+ 'default' => { 'gitaly_address' => 'tls://gitaly.internal:9999' },
+ 'storage1' => { 'gitaly_address' => 'tls://gitaly.internal:9999' },
})
gitlab_rails['gitaly_token'] = 'abc123secret'
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index cc338725e1b..874525dd836 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -394,7 +394,7 @@ The prerequisites for a HA Redis setup are the following:
1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
> Note: You can specify multiple roles like sentinel and redis as:
-> roles ['redis_sentinel_role', 'redis_master_role']. Read more about high
+> `roles ['redis_sentinel_role', 'redis_master_role']`. Read more about high
> availability roles at <https://docs.gitlab.com/omnibus/roles/>.
### Step 2. Configuring the slave Redis instances
@@ -443,7 +443,7 @@ The prerequisites for a HA Redis setup are the following:
1. Go through the steps again for all the other slave nodes.
> Note: You can specify multiple roles like sentinel and redis as:
-> roles ['redis_sentinel_role', 'redis_slave_role']. Read more about high
+> `roles ['redis_sentinel_role', 'redis_slave_role']`. Read more about high
> availability roles at <https://docs.gitlab.com/omnibus/roles/>.
---
diff --git a/doc/administration/incoming_email.md b/doc/administration/incoming_email.md
index 8271c579f5b..84a34ae7d6e 100644
--- a/doc/administration/incoming_email.md
+++ b/doc/administration/incoming_email.md
@@ -4,7 +4,7 @@ GitLab has several features based on receiving incoming emails:
- [Reply by Email](reply_by_email.md): allow GitLab users to comment on issues
and merge requests by replying to notification emails.
-- [New issue by email](../user/project/issues/create_new_issue.md#new-issue-via-email):
+- [New issue by email](../user/project/issues/managing_issues.md#new-issue-via-email):
allow GitLab users to create a new issue by sending an email to a
user-specific email address.
- [New merge request by email](../user/project/merge_requests/index.md#create-new-merge-requests-by-email):
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 602eecb9746..f480d18ea00 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -117,7 +117,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- 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](reply_by_email.md), create [issues by email](../user/project/issues/create_new_issue.md#new-issue-via-email) and
+ users to [reply by email](reply_by_email.md), create [issues by email](../user/project/issues/managing_issues.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
diff --git a/doc/administration/issue_closing_pattern.md b/doc/administration/issue_closing_pattern.md
index 160da47c780..9c352096ecc 100644
--- a/doc/administration/issue_closing_pattern.md
+++ b/doc/administration/issue_closing_pattern.md
@@ -1,4 +1,4 @@
-# Issue closing pattern
+# Issue closing pattern **[CORE ONLY]**
>**Note:**
This is the administration documentation.
@@ -46,4 +46,4 @@ Because Rubular doesn't understand `%{issue_ref}`, you can replace this by
[gitlab.yml.example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example
[reconfigure]: restart_gitlab.md#omnibus-gitlab-reconfigure
[restart]: restart_gitlab.md#installations-from-source
-[user documentation]: ../user/project/issues/automatic_issue_closing.md
+[user documentation]: ../user/project/issues/managing_issues.md#closing-issues-automatically
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 9921ffd8ea0..b49d8c8a28f 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -288,6 +288,9 @@ installations from source.
It logs information whenever [Rack Attack] registers an abusive request.
+NOTE: **Note:**
+From [%12.1](https://gitlab.com/gitlab-org/gitlab-ce/issues/62756), user id and username are available on this log.
+
## `graphql_json.log`
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/59587) in GitLab 12.0.
diff --git a/doc/administration/monitoring/performance/grafana_configuration.md b/doc/administration/monitoring/performance/grafana_configuration.md
index 187fb2f73a1..51b0d78681d 100644
--- a/doc/administration/monitoring/performance/grafana_configuration.md
+++ b/doc/administration/monitoring/performance/grafana_configuration.md
@@ -103,6 +103,21 @@ repository for more information on this process.
[grafana-dashboards]: https://gitlab.com/gitlab-org/grafana-dashboards
+## Integration with GitLab UI
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/61005) in GitLab 12.1.
+
+If you have set up Grafana, you can enable a link to access it easily from the sidebar:
+
+1. Go to the admin area under **Settings > Metrics and profiling**
+ and expand "Metrics - Grafana".
+1. Check the "Enable access to Grafana" checkbox.
+1. If Grafana is enabled through Omnibus GitLab and on the same server,
+ leave "Grafana URL" unchanged. In any other case, enter the full URL
+ path of the Grafana instance.
+1. Click **Save changes**.
+1. The new link will be available in the admin area under **Monitoring > Metrics Dashboard**.
+
---
Read more on:
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 84b71ae6f1c..f09548aa024 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -104,11 +104,11 @@ Some basic Ruby runtime metrics are available:
| 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 |
+| ruby_process_start_time_seconds | Gauge | 12.0 | UNIX timestamp of process start time |
[GC.stat]: https://ruby-doc.org/core-2.3.0/GC.html#method-c-stat
-## Puma Metrics **[EXPERIMENTAL]**
+## Puma Metrics **(EXPERIMENTAL)**
When Puma is used instead of Unicorn, following metrics are available:
diff --git a/doc/api/group_boards.md b/doc/api/group_boards.md
index 9b0ac23b41c..4157b25477f 100644
--- a/doc/api/group_boards.md
+++ b/doc/api/group_boards.md
@@ -5,7 +5,7 @@ Every API call to group boards must be authenticated.
If a user is not a member of a group and the group is private, a `GET`
request will result in `404` status code.
-## Group Board
+## List all group issue boards in a group
Lists Issue Boards in the given group.
@@ -27,7 +27,68 @@ Example response:
[
{
"id": 1,
- "group_id": 5,
+ "name:": "group issue board",
+ "group": {
+ "id": 5,
+ "name": "Documentcloud",
+ "web_url": "http://example.com/groups/documentcloud"
+ },
+ "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
+ }
+ ]
+ }
+]
+```
+
+Users on GitLab [Premium, Silver, or higher](https://about.gitlab.com/pricing/) will see
+different parameters, due to the ability to have multiple group boards.
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "name:": "group issue board",
+ "group": {
+ "id": 5,
+ "name": "Documentcloud",
+ "web_url": "http://example.com/groups/documentcloud"
+ },
+ "milestone": {
+ "id": 12
+ "title": "10.0"
+ },
"lists" : [
{
"id" : 1,
@@ -61,9 +122,9 @@ Example response:
]
```
-## Single board
+## Single group issue board
-Gets a single board.
+Gets a single group issue board.
```
GET /groups/:id/boards/:board_id
@@ -83,7 +144,66 @@ Example response:
```json
{
"id": 1,
- "group_id": 5,
+ "name:": "group issue board",
+ "group": {
+ "id": 5,
+ "name": "Documentcloud",
+ "web_url": "http://example.com/groups/documentcloud"
+ },
+ "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
+ }
+ ]
+ }
+```
+
+Users on GitLab [Premium, Silver, or higher](https://about.gitlab.com/pricing/) will see
+different parameters, due to the ability to have multiple group issue boards.s
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "name:": "group issue board",
+ "group": {
+ "id": 5,
+ "name": "Documentcloud",
+ "web_url": "http://example.com/groups/documentcloud"
+ },
+ "milestone": {
+ "id": 12
+ "title": "10.0"
+ },
"lists" : [
{
"id" : 1,
@@ -116,7 +236,156 @@ Example response:
}
```
-## List board lists
+## Create a group issue board **[PREMIUM]**
+
+Creates a Group Issue Board.
+
+```
+POST /groups/:id/boards
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `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/groups/5/boards?name=newboard
+```
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "name": "newboard",
+ "group": {
+ "id": 5,
+ "name": "Documentcloud",
+ "web_url": "http://example.com/groups/documentcloud"
+ },
+ "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 group issue board **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/5954) in GitLab 11.1.
+
+Updates a Group Issue Board.
+
+```
+PUT /groups/:id/boards/:board_id
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `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/groups/5/boards/1?name=new_name&milestone_id=44&assignee_id=1&labels=GroupLabel&weight=4
+```
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "project": null,
+ "lists": [],
+ "name": "new_name",
+ "group": {
+ "id": 5,
+ "name": "Documentcloud",
+ "web_url": "http://example.com/groups/documentcloud"
+ },
+ "milestone": {
+ "id": 44,
+ "iid": 1,
+ "group_id": 5,
+ "title": "Group Milestone",
+ "description": "Group Milestone Desc",
+ "state": "active",
+ "created_at": "2018-07-03T07:15:19.271Z",
+ "updated_at": "2018-07-03T07:15:19.271Z",
+ "due_date": null,
+ "start_date": null,
+ "web_url": "http://example.com/groups/documentcloud/-/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": 11,
+ "name": "GroupLabel",
+ "color": "#428BCA",
+ "description": ""
+ }],
+ "weight": 4
+ }
+```
+
+## Delete a group issue board **[PREMIUM]**
+
+Deletes a Group Issue Board.
+
+```
+DELETE /groups/:id/boards/:board_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `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/groups/5/boards/1
+```
+
+## List group issue board lists
Get a list of the board's lists.
Does not include `open` and `closed` lists
@@ -168,7 +437,7 @@ Example response:
]
```
-## Single board list
+## Single group issue board list
Get a single board list.
@@ -200,7 +469,7 @@ Example response:
}
```
-## New board list
+## New group issue board list
Creates a new Issue Board list.
@@ -232,7 +501,7 @@ Example response:
}
```
-## Edit board list
+## Edit group issue board list
Updates an existing Issue Board list. This call is used to change list position.
@@ -265,7 +534,7 @@ Example response:
}
```
-## Delete a board list
+## Delete a group issue board list
Only for admins and group owners. Soft deletes the board list in question.
diff --git a/doc/api/group_milestones.md b/doc/api/group_milestones.md
index 260eb09cc38..98e81217875 100644
--- a/doc/api/group_milestones.md
+++ b/doc/api/group_milestones.md
@@ -18,13 +18,13 @@ GET /groups/:id/milestones?search=version
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `iids[]` | Array[integer] | optional | Return only the milestones having the given `iid` |
-| `state` | string | optional | Return only `active` or `closed` milestones |
-| `title` | string | optional | Return only the milestones having the given `title` |
-| `search` | string | optional | Return only milestones with a title or description matching the provided string |
+| Attribute | Type | Required | Description |
+| --------- | ------ | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `iids[]` | integer array | optional | Return only the milestones having the given `iid` |
+| `state` | string | optional | Return only `active` or `closed` milestones |
+| `title` | string | optional | Return only the milestones having the given `title` |
+| `search` | string | optional | Return only milestones with a title or description matching the provided string |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/milestones
@@ -136,3 +136,18 @@ Parameters:
- `milestone_id` (required) - The ID of a group milestone
[ce-12819]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12819
+
+## Get all burndown chart events for a single milestone **[STARTER]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/4737) in GitLab 12.1
+
+Get all burndown chart events for a single milestone.
+
+```
+GET /groups/:id/milestones/:milestone_id/burndown_events
+```
+
+Parameters:
+
+- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
+- `milestone_id` (required) - The ID of a group milestone
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 20789a1d4a4..e1bf296bc41 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -7,17 +7,17 @@ authentication, only public groups are returned.
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `skip_groups` | array of integers | no | Skip the group IDs passed |
-| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for admin); Attributes `owned` and `min_access_level` have precedence |
-| `search` | string | no | Return the list of authorized groups matching the search criteria |
-| `order_by` | string | no | Order groups by `name`, `path` or `id`. Default is `name` |
-| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
-| `statistics` | boolean | no | Include group statistics (admins only) |
-| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
-| `owned` | boolean | no | Limit to groups explicitly owned by the current user |
-| `min_access_level` | integer | no | Limit to groups where current user has at least this [access level](members.md) |
+| Attribute | Type | Required | Description |
+| ------------------------ | ----------------- | -------- | ---------- |
+| `skip_groups` | array of integers | no | Skip the group IDs passed |
+| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for admin); Attributes `owned` and `min_access_level` have precedence |
+| `search` | string | no | Return the list of authorized groups matching the search criteria |
+| `order_by` | string | no | Order groups by `name`, `path` or `id`. Default is `name` |
+| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
+| `statistics` | boolean | no | Include group statistics (admins only) |
+| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
+| `owned` | boolean | no | Limit to groups explicitly owned by the current user |
+| `min_access_level` | integer | no | Limit to groups where current user has at least this [access level](members.md) |
```
GET /groups
@@ -70,8 +70,8 @@ GET /groups?statistics=true
"repository_size" : 33,
"wiki_size" : 100,
"lfs_objects_size" : 123,
- "job_artifacts_size" : 57
-
+ "job_artifacts_size" : 57,
+ "packages_size": 0
}
}
]
@@ -94,18 +94,18 @@ When accessed without authentication, only public groups are returned.
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) of the parent group |
-| `skip_groups` | array of integers | no | Skip the group IDs passed |
-| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for admin); Attributes `owned` and `min_access_level` have precedence |
-| `search` | string | no | Return the list of authorized groups matching the search criteria |
-| `order_by` | string | no | Order groups by `name`, `path` or `id`. Default is `name` |
-| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
-| `statistics` | boolean | no | Include group statistics (admins only) |
-| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
-| `owned` | boolean | no | Limit to groups explicitly owned by the current user |
-| `min_access_level` | integer | no | Limit to groups where current user has at least this [access level](members.md) |
+| Attribute | Type | Required | Description |
+| ------------------------ | ----------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) of the parent group |
+| `skip_groups` | array of integers | no | Skip the group IDs passed |
+| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for admin); Attributes `owned` and `min_access_level` have preceden |
+| `search` | string | no | Return the list of authorized groups matching the search criteria |
+| `order_by` | string | no | Order groups by `name`, `path` or `id`. Default is `name` |
+| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
+| `statistics` | boolean | no | Include group statistics (admins only) |
+| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
+| `owned` | boolean | no | Limit to groups explicitly owned by the current user |
+| `min_access_level` | integer | no | Limit to groups where current user has at least this [access level](members.md) |
```
GET /groups/:id/subgroups
@@ -142,22 +142,22 @@ GET /groups/:id/projects
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `archived` | boolean | no | Limit by archived status |
-| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
-| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
-| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
-| `search` | string | no | Return list of authorized projects matching the search criteria |
-| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
-| `owned` | boolean | no | Limit by projects owned by the current user |
-| `starred` | boolean | no | Limit by projects starred by the current user |
-| `with_issues_enabled` | boolean | no | Limit by projects with issues feature enabled. Default is `false` |
-| `with_merge_requests_enabled` | boolean | no | Limit by projects with merge requests feature enabled. Default is `false` |
-| `with_shared` | boolean | no | Include projects shared to this group. Default is `true` |
-| `include_subgroups` | boolean | no | Include projects in subgroups of this group. Default is `false` |
-| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
+| Attribute | Type | Required | Description |
+| ----------------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
+| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
+| `owned` | boolean | no | Limit by projects owned by the current user |
+| `starred` | boolean | no | Limit by projects starred by the current user |
+| `with_issues_enabled` | boolean | no | Limit by projects with issues feature enabled. Default is `false` |
+| `with_merge_requests_enabled` | boolean | no | Limit by projects with merge requests feature enabled. Default is `false` |
+| `with_shared` | boolean | no | Include projects shared to this group. Default is `true` |
+| `include_subgroups` | boolean | no | Include projects in subgroups of this group. Default is `false` |
+| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
Example response:
@@ -214,11 +214,11 @@ GET /groups/:id
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
-| `with_projects` | boolean | no | Include details from projects that belong to the specified group (defaults to `true`). |
+| Attribute | Type | Required | Description |
+| ------------------------ | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only). |
+| `with_projects` | boolean | no | Include details from projects that belong to the specified group (defaults to `true`). |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/4
@@ -375,6 +375,21 @@ Example response:
}
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `shared_runners_minutes_limit` and `extra_shared_runners_minutes_limit` parameters:
+
+Additional response parameters:
+
+```json
+{
+ "id": 4,
+ "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
+ "shared_runners_minutes_limit": 133,
+ "extra_shared_runners_minutes_limit": 133,
+ ...
+}
+```
+
When adding the parameter `with_projects=false`, projects will not be returned.
```bash
@@ -410,15 +425,17 @@ POST /groups
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `name` | string | yes | The name of the group |
-| `path` | string | yes | The path of the group |
-| `description` | string | no | The group's description |
-| `visibility` | string | no | The group's visibility. Can be `private`, `internal`, or `public`. |
-| `lfs_enabled` | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group |
-| `request_access_enabled` | boolean | no | Allow users to request member access. |
-| `parent_id` | integer | no | The parent group id for creating nested group. |
+| Attribute | Type | Required | Description |
+| ------------------------------------ | ------- | -------- | ----------- |
+| `name` | string | yes | The name of the group. |
+| `path` | string | yes | The path of the group. |
+| `description` | string | no | The group's description. |
+| `visibility` | string | no | The group's visibility. Can be `private`, `internal`, or `public`. |
+| `lfs_enabled` | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
+| `request_access_enabled` | boolean | no | Allow users to request member access. |
+| `parent_id` | integer | no | The parent group ID for creating nested group. |
+| `shared_runners_minutes_limit` | integer | no | **[STARTER ONLY]** Pipeline minutes quota for this group. |
+| `extra_shared_runners_minutes_limit` | integer | no | **[STARTER ONLY]** Extra pipeline minutes quota for this group. |
## Transfer project to group
@@ -430,10 +447,10 @@ POST /groups/:id/projects/:project_id
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| Attribute | Type | Required | Description |
+| ------------ | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
## Update group
@@ -443,16 +460,20 @@ Updates the project group. Only available to group owners and administrators.
PUT /groups/:id
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the group |
-| `name` | string | no | The name of the group |
-| `path` | string | no | The path of the group |
-| `description` | string | no | The description of the group |
-| `visibility` | string | no | The visibility level of the group. Can be `private`, `internal`, or `public`. |
-| `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group |
-| `request_access_enabled` | boolean | no | Allow users to request member access. |
-| `file_template_project_id` | integer | no | **(Premium)** The ID of a project to load custom file templates from |
+| Attribute | Type | Required | Description |
+| ------------------------------------ | ------- | -------- | ----------- |
+| `id` | integer | yes | The ID of the group. |
+| `name` | string | no | The name of the group. |
+| `path` | string | no | The path of the group. |
+| `description` | string | no | The description of the group. |
+| `membership_lock` | boolean | no | **[STARTER]** Prevent adding new members to project membership within this group. |
+| `share_with_group_lock` | boolean | no | Prevent sharing a project with another group within this group. |
+| `visibility` | string | no | The visibility level of the group. Can be `private`, `internal`, or `public`. |
+| `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
+| `request_access_enabled` | boolean | no | Allow users to request member access. |
+| `file_template_project_id` | integer | no | **[PREMIUM]** The ID of a project to load custom file templates from. |
+| `shared_runners_minutes_limit` | integer | no | **[STARTER ONLY]** Pipeline minutes quota for this group. |
+| `extra_shared_runners_minutes_limit` | integer | no | **[STARTER ONLY]** Extra pipeline minutes quota for this group. |
```bash
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5?name=Experimental"
@@ -520,7 +541,7 @@ Example response:
## Remove group
-Removes group with all projects inside.
+Removes group with all projects inside. Only available to group owners and administrators.
```
DELETE /groups/:id
@@ -552,10 +573,62 @@ GET /groups?search=foobar
]
```
+## Sync group with LDAP **[CORE ONLY]**
+
+Syncs the group with its linked LDAP group. Only available to group owners and administrators.
+
+```
+POST /groups/:id/ldap_sync
+```
+
+Parameters:
+
+- `id` (required) - The ID or path of a user group
+
## Group members
Please consult the [Group Members](members.md) documentation.
+### Add LDAP group link **[CORE ONLY]**
+
+Adds an LDAP group link.
+
+```
+POST /groups/:id/ldap_group_links
+```
+
+Parameters:
+
+- `id` (required) - The ID of a group
+- `cn` (required) - The CN of a LDAP group
+- `group_access` (required) - Minimum access level for members of the LDAP group
+- `provider` (required) - LDAP provider for the LDAP group
+
+### Delete LDAP group link **[CORE ONLY]**
+
+Deletes an LDAP group link.
+
+```
+DELETE /groups/:id/ldap_group_links/:cn
+```
+
+Parameters:
+
+- `id` (required) - The ID of a group
+- `cn` (required) - The CN of a LDAP group
+
+Deletes a LDAP group link for a specific LDAP provider
+
+```
+DELETE /groups/:id/ldap_group_links/:provider/:cn
+```
+
+Parameters:
+
+- `id` (required) - The ID of a group
+- `cn` (required) - The CN of a LDAP group
+- `provider` (required) - Name of a LDAP provider
+
## Namespaces in groups
By default, groups only get 20 namespaces at a time because the API results are paginated.
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 0d96cfa1b21..544da1e262c 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -3,7 +3,7 @@
Every API call to issues must be authenticated.
If a user is not a member of a project and the project is private, a `GET`
-request on that project will result to a `404` status code.
+request on that project will result in a `404` status code.
## Issues pagination
@@ -39,15 +39,16 @@ GET /issues?confidential=true
| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `state` | string | no | Return `all` issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. |
-| `with_labels_details`| Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. |
+| `with_labels_details` | Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. |
+| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
+| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
-| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
+| `weight` **[STARTER]** | integer | no | Return issues with the specified `weight`. `None` returns issues with no weight assigned. `Any` returns issues with a weight assigned. |
+| `iids[]` | integer array | no | Return only the issues having the given `iid` |
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Search issues against their `title` and `description` |
@@ -144,6 +145,20 @@ Example response:
]
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `weight` parameter:
+
+```json
+[
+ {
+ "state" : "opened",
+ "description" : "Ratione dolores corrupti mollitia soluta quia.",
+ "weight": null,
+ ...
+ }
+]
+```
+
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
@@ -174,15 +189,16 @@ GET /groups/:id/issues?confidential=true
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. |
-| `with_labels_details`| Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. |
-| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
+| `with_labels_details` | Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. |
+| `iids[]` | integer array | no | Return only the issues having the given `iid` |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. |
+| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
+| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `weight` **[STARTER]** | integer | no | Return issues with the specified `weight`. `None` returns issues with no weight assigned. `Any` returns issues with a weight assigned. |
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Search group issues against their `title` and `description` |
@@ -278,6 +294,20 @@ Example response:
]
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `weight` parameter:
+
+```json
+[
+ {
+ "project_id" : 4,
+ "description" : "Omnis vero earum sunt corporis dolor et placeat.",
+ "weight": null,
+ ...
+ }
+]
+```
+
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
@@ -306,17 +336,18 @@ GET /projects/:id/issues?confidential=true
| 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 |
-| `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` |
+| `iids[]` | integer array | no | Return only the milestone having the given `iid` |
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. |
-| `with_labels_details`| Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. |
+| `with_labels_details` | Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. |
+| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
+| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `weight` **[STARTER]** | integer | no | Return issues with the specified `weight`. `None` returns issues with no weight assigned. `Any` returns issues with a weight assigned. |
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Search project issues against their `title` and `description` |
@@ -420,6 +451,20 @@ Example response:
]
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `weight` parameter:
+
+```json
+[
+ {
+ "project_id" : 4,
+ "description" : "Omnis vero earum sunt corporis dolor et placeat.",
+ "weight": null,
+ ...
+ }
+]
+```
+
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
@@ -520,6 +565,18 @@ Example response:
}
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `weight` parameter:
+
+```json
+{
+ "project_id" : 4,
+ "description" : "Omnis vero earum sunt corporis dolor et placeat.",
+ "weight": null,
+ ...
+}
+```
+
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
@@ -536,16 +593,17 @@ POST /projects/:id/issues
|-------------------------------------------|----------------|----------|--------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `iid` | integer/string | no | The internal ID of the project's issue (requires admin or project owner rights) |
-| `title` | string | yes | The title of an issue |
-| `description` | string | no | The description of an issue |
-| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
-| `assignee_ids` | Array[integer] | no | The ID of the users to assign issue |
-| `milestone_id` | integer | no | The global ID of a milestone to assign issue |
-| `labels` | string | no | Comma-separated label names for an issue |
-| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project/group owner rights) |
-| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
-| `merge_request_to_resolve_discussions_of` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values.|
-| `discussion_to_resolve` | string | no | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion as resolved. Use in combination with `merge_request_to_resolve_discussions_of`. |
+| `title` | string | yes | The title of an issue |
+| `description` | string | no | The description of an issue |
+| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
+| `assignee_ids` | integer array | no | The ID of a user to assign issue |
+| `milestone_id` | integer | no | The global ID of a milestone to assign issue |
+| `labels` | string | no | Comma-separated label names for an issue |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project/group owner rights) |
+| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
+| `merge_request_to_resolve_discussions_of` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values.|
+| `discussion_to_resolve` | string | no | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion as resolved. Use in combination with `merge_request_to_resolve_discussions_of`. |
+| `weight` **[STARTER]** | integer | no | The weight of the issue. Valid values are greater than or equal to 0. |
```bash
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/issues?title=Issues%20with%20auth&labels=bug
@@ -607,6 +665,18 @@ Example response:
}
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `weight` parameter:
+
+```json
+{
+ "project_id" : 4,
+ "description" : null,
+ "weight": null,
+ ...
+}
+```
+
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
@@ -622,17 +692,18 @@ PUT /projects/:id/issues/:issue_iid
| Attribute | Type | Required | Description |
|----------------|---------|----------|------------------------------------------------------------------------------------------------------------|
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
| `title` | string | no | The title of an issue |
| `description` | string | no | The description of an issue |
| `confidential` | boolean | no | Updates an issue to be confidential |
-| `assignee_ids` | Array[integer] | no | The ID of the user(s) to assign the issue to. Set to `0` or provide an empty value to unassign all assignees. |
+| `assignee_ids` | integer array | no | The ID of the user(s) to assign the issue to. Set to `0` or provide an empty value to unassign all assignees. |
| `milestone_id` | integer | no | The global ID of a milestone to assign the issue to. Set to `0` or provide an empty value to unassign a milestone.|
| `labels` | string | no | Comma-separated label names for an issue. Set to an empty string to unassign all labels. |
| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it |
| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
+| `weight` **[STARTER]** | integer | no | The weight of the issue. Valid values are greater than or equal to 0. 0 |
| `discussion_locked` | boolean | no | Flag indicating if the issue's discussion is locked. If the discussion is locked only project members can add or edit comments. |
```bash
@@ -702,6 +773,18 @@ Example response:
}
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `weight` parameter:
+
+```json
+{
+ "project_id" : 4,
+ "description" : null,
+ "weight": null,
+ ...
+}
+```
+
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
@@ -812,6 +895,18 @@ Example response:
}
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `weight` parameter:
+
+```json
+{
+ "project_id": 5,
+ "description": "Repellat voluptas quibusdam voluptatem exercitationem.",
+ "weight": null,
+ ...
+}
+```
+
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
@@ -901,6 +996,18 @@ Example response:
}
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `weight` parameter:
+
+```json
+{
+ "project_id": 5,
+ "description": "Repellat voluptas quibusdam voluptatem exercitationem.",
+ "weight": null,
+ ...
+}
+```
+
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
diff --git a/doc/api/issues_statistics.md b/doc/api/issues_statistics.md
index 82bc9c142cc..58a32e879d7 100644
--- a/doc/api/issues_statistics.md
+++ b/doc/api/issues_statistics.md
@@ -34,9 +34,9 @@ GET /issues_statistics?confidential=true
| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. |
| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. |
-| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
+| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. |
-| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
+| `iids[]` | integer array | no | Return only the issues having the given `iid` |
| `search` | string | no | Search issues against their `title` and `description` |
| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` |
| `created_after` | datetime | no | Return issues created on or after the given time |
@@ -86,13 +86,13 @@ GET /groups/:id/issues_statistics?confidential=true
| ------------------- | ---------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. |
-| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
+| `iids[]` | integer array | no | Return only the issues having the given `iid` |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. |
| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. |
| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. |
-| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
+| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. |
| `search` | string | no | Search group issues against their `title` and `description` |
| `created_after` | datetime | no | Return issues created on or after the given time |
@@ -141,14 +141,14 @@ GET /projects/:id/issues_statistics?confidential=true
| 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 |
-| `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` |
+| `iids[]` | integer array | no | Return only the milestone having the given `iid` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. |
| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. |
| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. |
-| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
+| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. |
| `search` | string | no | Search project issues against their `title` and `description` |
| `created_after` | datetime | no | Return issues created on or after the given time |
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index 72973b69117..223bfed91a9 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -14,7 +14,7 @@ GET /projects/:id/jobs
| `scope` | string **or** array of strings | no | Scope of jobs to show. Either one of or an array of the following: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`, or `manual`. All jobs are returned if `scope` is not provided. |
```sh
-curl --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/1/jobs?scope[]=pending&scope[]=running'
+curl --globoff --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/1/jobs?scope[]=pending&scope[]=running'
```
Example of response
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 85a07589956..850e3be42d5 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -29,27 +29,28 @@ GET /merge_requests?search=foo&in=title
Parameters:
-| Attribute | Type | Required | Description |
-| ------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
-| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
-| `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
-| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
-| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. |
-| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
-| `labels` | string | no | Return merge requests matching a comma separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. `No+Label` (Deprecated) lists all merge requests with no labels. Predefined names are case-insensitive. |
-| `created_after` | datetime | no | Return merge requests created on or after the given time |
-| `created_before` | datetime | no | Return merge requests created on or before the given time |
-| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
-| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
-| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead. |
-| `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me` |
-| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. |
-| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
-| `source_branch` | string | no | Return merge requests with the given source branch |
-| `target_branch` | string | no | Return merge requests with the given target branch |
-| `search` | string | no | Search merge requests against their `title` and `description` |
-| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` |
-| `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* WIP merge requests, `no` to return *non* WIP merge requests |
+| Attribute | Type | Required | Description |
+| ------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
+| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
+| `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
+| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
+| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. |
+| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
+| `labels` | string | no | Return merge requests matching a comma separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. `No+Label` (Deprecated) lists all merge requests with no labels. Predefined names are case-insensitive. |
+| `created_after` | datetime | no | Return merge requests created on or after the given time |
+| `created_before` | datetime | no | Return merge requests created on or before the given time |
+| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
+| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
+| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead. |
+| `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me` |
+| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. |
+| `approver_ids` **[STARTER]** | Array[integer] | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
+| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `source_branch` | string | no | Return merge requests with the given source branch |
+| `target_branch` | string | no | Return merge requests with the given target branch |
+| `search` | string | no | Search merge requests against their `title` and `description` |
+| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` |
+| `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* WIP merge requests, `no` to return *non* WIP merge requests |
```json
[
@@ -147,6 +148,20 @@ Parameters:
]
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `approvals_before_merge` parameter:
+
+```json
+[
+ {
+ "id": 1,
+ "title": "test1",
+ "approvals_before_merge": null
+ ...
+ }
+]
+```
+
## List project merge requests
Get all merge requests for this project.
@@ -177,7 +192,7 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `id` | integer | yes | The ID of a project |
-| `iids[]` | Array[integer] | no | Return the request having the given `iid` |
+| `iids[]` | integer array | no | Return the request having the given `iid` |
| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
| `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
@@ -191,6 +206,7 @@ Parameters:
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13060] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced][ce-13060] in GitLab 9.5)_ |
+| `approver_ids` **[STARTER]** | Array[integer] | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
| `source_branch` | string | no | Return merge requests with the given source branch |
| `target_branch` | string | no | Return merge requests with the given target branch |
@@ -293,6 +309,20 @@ Parameters:
]
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `approvals_before_merge` parameter:
+
+```json
+[
+ {
+ "id": 1,
+ "title": "test1",
+ "approvals_before_merge": null
+ ...
+ }
+]
+```
+
## List group merge requests
Get all merge requests for this group and its subgroups.
@@ -328,6 +358,7 @@ Parameters:
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> |
| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced][ce-13060] in GitLab 9.5)_ |
+| `approver_ids` **[STARTER]** | Array[integer] | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
| `source_branch` | string | no | Return merge requests with the given source branch |
| `target_branch` | string | no | Return merge requests with the given target branch |
@@ -427,6 +458,20 @@ Parameters:
]
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `approvals_before_merge` parameter:
+
+```json
+[
+ {
+ "id": 1,
+ "title": "test1",
+ "approvals_before_merge": null
+ ...
+ }
+]
+```
+
## Get single MR
Shows information about a single merge request.
@@ -565,6 +610,18 @@ Parameters:
}
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `approvals_before_merge` parameter:
+
+```json
+{
+ "id": 1,
+ "title": "test1",
+ "approvals_before_merge": null
+ ...
+}
+```
+
## Get single MR participants
Get a list of merge request participants.
@@ -767,18 +824,19 @@ Parameters:
## Create MR
Creates a new merge request.
+
```
POST /projects/:id/merge_requests
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `source_branch` | string | yes | The source branch |
| `target_branch` | string | yes | The target branch |
| `title` | string | yes | Title of MR |
| `assignee_id` | integer | no | Assignee user ID |
-| `assignee_ids` | Array[integer] | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
+| `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
| `description` | string | no | Description of MR |
| `target_project_id` | integer | no | The target project (numeric id) |
| `labels` | string | no | Labels for MR as a comma-separated list |
@@ -788,6 +846,15 @@ POST /projects/:id/merge_requests
| `allow_maintainer_to_push` | boolean | no | Deprecated, see allow_collaboration |
| `squash` | boolean | no | Squash commits into a single commit when merging |
+If `approvals_before_merge` **[STARTER]** is not provided, it inherits the value from the
+target project. If it is provided, then the following conditions must hold in
+order for it to take effect:
+
+1. The target project's `approvals_before_merge` must be greater than zero. A
+ value of zero disables approvals for that project.
+1. The provided value of `approvals_before_merge` must be greater than the
+ target project's `approvals_before_merge`.
+
```json
{
"id": 1,
@@ -893,6 +960,18 @@ POST /projects/:id/merge_requests
}
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `approvals_before_merge` parameter:
+
+```json
+{
+ "id": 1,
+ "title": "test1",
+ "approvals_before_merge": null
+ ...
+}
+```
+
## Update MR
Updates an existing merge request. You can change the target branch, title, or even close the MR.
@@ -908,7 +987,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| `target_branch` | string | no | The target branch |
| `title` | string | no | Title of MR |
| `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. |
-| `assignee_ids` | Array[integer] | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
+| `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
| `milestone_id` | integer | no | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.|
| `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. |
| `description` | string | no | Description of MR |
@@ -1034,6 +1113,18 @@ Must include at least one non-required attribute from above.
}
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `approvals_before_merge` parameter:
+
+```json
+{
+ "id": 1,
+ "title": "test1",
+ "approvals_before_merge": null
+ ...
+}
+```
+
## Delete a merge request
Only for admins and project owners. Soft deletes the merge request in question.
@@ -1191,9 +1282,21 @@ Parameters:
}
```
-## Returns the up to date merge-ref HEAD commit
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `approvals_before_merge` parameter:
-Merge the changes between the merge request source and target branches into `refs/merge-requests/:iid/merge`
+```json
+{
+ "id": 1,
+ "title": "test1",
+ "approvals_before_merge": null
+ ...
+}
+```
+
+## Merge to default merge ref path
+
+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.
@@ -1349,6 +1452,18 @@ Parameters:
}
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `approvals_before_merge` parameter:
+
+```json
+{
+ "id": 1,
+ "title": "test1",
+ "approvals_before_merge": null
+ ...
+}
+```
+
## Rebase a merge request
Automatically rebase the `source_branch` of the merge request against its
@@ -1370,8 +1485,14 @@ PUT /projects/:id/merge_requests/:merge_request_iid/rebase
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/76/merge_requests/1/rebase
```
-This is an asynchronous request. The API will return an empty `202 Accepted`
-response if the request is enqueued successfully.
+This is an asynchronous request. The API will return a `202 Accepted` response
+if the request is enqueued successfully, with a response containing:
+
+```json
+{
+ "rebase_in_progress": true
+}
+```
You can poll the [Get single MR](#get-single-mr) endpoint with the
`include_rebase_in_progress` parameter to check the status of the
@@ -1616,6 +1737,18 @@ Example response:
}
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `approvals_before_merge` parameter:
+
+```json
+{
+ "id": 1,
+ "title": "test1",
+ "approvals_before_merge": null
+ ...
+}
+```
+
## Unsubscribe from a merge request
Unsubscribes the authenticated user from a merge request to not receive
@@ -1749,6 +1882,18 @@ Example response:
}
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `approvals_before_merge` parameter:
+
+```json
+{
+ "id": 1,
+ "title": "test1",
+ "approvals_before_merge": null
+ ...
+}
+```
+
## Create a todo
Manually creates a todo for the current user on a merge request.
@@ -1970,6 +2115,7 @@ Example response:
}]
}
```
+
## Set a time estimate for a merge request
Sets an estimated time of work for this merge request.
@@ -2114,3 +2260,7 @@ Example response:
[ce-14016]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14016
[ce-15454]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15454
[ce-18935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18935
+
+## Approvals **[STARTER]**
+
+For approvals, please see [Merge Request Approvals](merge_request_approvals.md)
diff --git a/doc/api/milestones.md b/doc/api/milestones.md
index 90d8c033cd6..8f69378dac3 100644
--- a/doc/api/milestones.md
+++ b/doc/api/milestones.md
@@ -16,13 +16,13 @@ GET /projects/:id/milestones?search=version
Parameters:
-| 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 |
-| `iids[]` | Array[integer] | optional | Return only the milestones having the given `iid` |
-| `state` | string | optional | Return only `active` or `closed` milestones |
-| `title` | string | optional | Return only the milestones having the given `title` |
-| `search` | string | optional | Return only milestones with a title or description matching the provided string |
+| 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 |
+| `iids[]` | integer array | optional | Return only the milestones having the given `iid` |
+| `state` | string | optional | Return only `active` or `closed` milestones |
+| `title` | string | optional | Return only the milestones having the given `title` |
+| `search` | string | optional | Return only milestones with a title or description matching the provided string |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/milestones
@@ -147,3 +147,18 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `milestone_id` (required) - The ID of a project milestone
+
+## Get all burndown chart events for a single milestone **[STARTER]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/4737) in GitLab 12.1
+
+Gets all burndown chart events for a single milestone.
+
+```
+GET /projects/:id/milestones/:milestone_id/burndown_events
+```
+
+Parameters:
+
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
+- `milestone_id` (required) - The ID of a project milestone
diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md
index b66a3198ffb..5db7035fd90 100644
--- a/doc/api/namespaces.md
+++ b/doc/api/namespaces.md
@@ -54,7 +54,21 @@ Example response:
]
```
-**Note**: `members_count_with_descendants` are presented only for group maintainers/owners.
+Users on GitLab.com [Bronze or higher](https://about.gitlab.com/pricing/#gitlab-com) may also see
+the `plan` parameter associated with a namespace:
+
+```json
+[
+ {
+ "id": 1,
+ "name": "user1",
+ "plan": "bronze",
+ ...
+ }
+]
+```
+
+NOTE: **Note:** Only group maintainers/owners are presented with `members_count_with_descendants`, as well as `plan` **[BRONZE ONLY]**.
## Search for namespace
diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md
index e21e73c7dac..ccc1cccf7a4 100644
--- a/doc/api/notification_settings.md
+++ b/doc/api/notification_settings.md
@@ -1,8 +1,8 @@
# Notification settings API
->**Note:** This feature was [introduced][ce-5632] in GitLab 8.12.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5632) in GitLab 8.12.
-**Valid notification levels**
+## Valid notification levels
The notification levels are defined in the `NotificationSetting.level` model enumeration. Currently, these levels are recognized:
@@ -17,22 +17,21 @@ custom
If the `custom` level is used, specific email events can be controlled. Available events are returned by `NotificationSetting.email_events`. Currently, these events are recognized:
-```
-new_note
-new_issue
-reopen_issue
-close_issue
-reassign_issue
-issue_due
-new_merge_request
-push_to_merge_request
-reopen_merge_request
-close_merge_request
-reassign_merge_request
-merge_merge_request
-failed_pipeline
-success_pipeline
-```
+- `new_note`
+- `new_issue`
+- `reopen_issue`
+- `close_issue`
+- `reassign_issue`
+- `issue_due`
+- `new_merge_request`
+- `push_to_merge_request`
+- `reopen_merge_request`
+- `close_merge_request`
+- `reassign_merge_request`
+- `merge_merge_request`
+- `failed_pipeline`
+- `success_pipeline`
+- `new_epic` **[ULTIMATE]**
## Global notification settings
@@ -85,6 +84,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.
| `merge_merge_request` | boolean | no | Enable/disable this notification |
| `failed_pipeline` | boolean | no | Enable/disable this notification |
| `success_pipeline` | boolean | no | Enable/disable this notification |
+| `new_epic` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6626) in 11.3) **[ULTIMATE]** |
Example response:
@@ -153,6 +153,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.
| `merge_merge_request` | boolean | no | Enable/disable this notification |
| `failed_pipeline` | boolean | no | Enable/disable this notification |
| `success_pipeline` | boolean | no | Enable/disable this notification |
+| `new_epic` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6626) in 11.3) **[ULTIMATE]** |
Example responses:
@@ -182,4 +183,17 @@ Example responses:
}
```
-[ce-5632]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5632
+Users on GitLab [Ultimate or Gold](https://about.gitlab.com/pricing/) will also see
+the `new_epic` parameter:
+
+```json
+{
+ "level": "custom",
+ "events": {
+ "new_note": true,
+ "new_issue": false,
+ "new_epic": false,
+ ...
+ }
+}
+```
diff --git a/doc/api/pipeline_schedules.md b/doc/api/pipeline_schedules.md
index 470e55425f8..acf1ac90315 100644
--- a/doc/api/pipeline_schedules.md
+++ b/doc/api/pipeline_schedules.md
@@ -108,9 +108,9 @@ POST /projects/:id/pipeline_schedules
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `description` | string | yes | The description of pipeline schedule |
| `ref` | string | yes | The branch/tag name will be triggered |
-| `cron ` | string | yes | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron)) |
-| `cron_timezone ` | string | no | The timezone supported by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) (default: `'UTC'`) |
-| `active ` | boolean | no | The activation of pipeline schedule. If false is set, the pipeline schedule will deactivated initially (default: `true`) |
+| `cron` | string | yes | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron)) |
+| `cron_timezone` | string | no | The timezone supported by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) (default: `'UTC'`) |
+| `active` | boolean | no | The activation of pipeline schedule. If false is set, the pipeline schedule will deactivated initially (default: `true`) |
```sh
curl --request POST --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form description="Build packages" --form ref="master" --form cron="0 1 * * 5" --form cron_timezone="UTC" --form active="true" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules"
@@ -153,9 +153,9 @@ PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id
| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
| `description` | string | no | The description of pipeline schedule |
| `ref` | string | no | The branch/tag name will be triggered |
-| `cron ` | string | no | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron)) |
-| `cron_timezone ` | string | no | The timezone supported by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) or `TZInfo::Timezone` (e.g. `America/Los_Angeles`) |
-| `active ` | boolean | no | The activation of pipeline schedule. If false is set, the pipeline schedule will deactivated initially. |
+| `cron` | string | no | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron)) |
+| `cron_timezone` | string | no | The timezone supported by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) or `TZInfo::Timezone` (e.g. `America/Los_Angeles`) |
+| `active` | boolean | no | The activation of pipeline schedule. If false is set, the pipeline schedule will deactivated initially. |
```sh
curl --request PUT --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form cron="0 2 * * *" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13"
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index 753faec3cc8..e36f74e7c77 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -132,11 +132,11 @@ Example of response
POST /projects/:id/pipeline
```
-| 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 |
-| `ref` | string | yes | Reference to commit |
-| `variables` | array | no | An array containing the variables available in the pipeline, matching the structure [{ 'key' => 'UPLOAD_TO_S3', 'variable_type' => 'file', 'value' => 'true' }] |
+| 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 |
+| `ref` | string | yes | Reference to commit |
+| `variables` | array | no | An array containing the variables available in the pipeline, matching the structure `[{ 'key' => 'UPLOAD_TO_S3', 'variable_type' => 'file', 'value' => 'true' }]` |
```
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipeline?ref=master"
diff --git a/doc/api/project_aliases.md b/doc/api/project_aliases.md
index 65845579409..76343b4cd82 100644
--- a/doc/api/project_aliases.md
+++ b/doc/api/project_aliases.md
@@ -68,13 +68,19 @@ Add a new alias for a project. Responds with a 201 when successful,
POST /project_aliases
```
-| Attribute | Type | Required | Description |
-|--------------|--------|----------|-----------------------------------------------|
-| `project_id` | string | yes | The ID or URL-encoded path of the project. |
-| `name` | string | yes | The name of the alias. Must be unique. |
+| Attribute | Type | Required | Description |
+|--------------|----------------|----------|----------------------------------------|
+| `project_id` | integer/string | yes | The ID or path of the project. |
+| `name` | string | yes | The name of the alias. Must be unique. |
```
-curl --request POST "https://gitlab.example.com/api/v4/project_aliases" --form "project_id=gitlab-org%2Fgitlab-ee" --form "name=gitlab-ee"
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/project_aliases" --form "project_id=1" --form "name=gitlab-ee"
+```
+
+or
+
+```
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/project_aliases" --form "project_id=gitlab-org/gitlab-ee" --form "name=gitlab-ee"
```
Example response:
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index 0ccb0517e08..2b2e40fb276 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -13,6 +13,12 @@ Constants for snippet visibility levels are:
| `internal` | The snippet is visible for any logged in user |
| `public` | The snippet can be accessed without any authentication |
+NOTE: **Note:**
+From July 2019, the `Internal` visibility setting is disabled for new projects, groups,
+and snippets on GitLab.com. Existing projects, groups, and snippets using the `Internal`
+visibility setting keep this setting. You can read more about the change in the
+[relevant issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/12388).
+
## List snippets
Get a list of project snippets.
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 1d58e390d9e..e07d6ce9a42 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -14,7 +14,7 @@ Values for the project visibility level are:
The project can be cloned by any logged in user.
- `public`:
- The project can be cloned without any authentication.
+ The project can be accessed without any authentication.
## Project merge method
@@ -41,23 +41,23 @@ GET /projects
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `archived` | boolean | no | Limit by archived status |
-| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
-| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
-| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
-| `search` | string | no | Return list of projects matching the search criteria |
-| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. |
-| `owned` | boolean | no | Limit by projects explicitly owned by the current user |
-| `membership` | boolean | no | Limit by projects that the current user is a member of |
-| `starred` | boolean | no | Limit by projects starred by the current user |
-| `statistics` | boolean | no | Include project statistics |
-| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
-| `with_issues_enabled` | boolean | no | Limit by enabled issues feature |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of projects matching the search criteria |
+| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. |
+| `owned` | boolean | no | Limit by projects explicitly owned by the current user |
+| `membership` | boolean | no | Limit by projects that the current user is a member of |
+| `starred` | boolean | no | Limit by projects starred by the current user |
+| `statistics` | boolean | no | Include project statistics |
+| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
+| `with_issues_enabled` | boolean | no | Limit by enabled issues feature |
| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature |
-| `with_programming_language` | string | no | Limit by projects which use the given programming language |
-| `wiki_checksum_failed` | boolean | no | Limit projects where the wiki checksum calculation has failed _([Introduced][ee-6137] in [GitLab Premium][eep] 11.2)_ |
-| `repository_checksum_failed` | boolean | no | Limit projects where the repository checksum calculation has failed _([Introduced][ee-6137] in [GitLab Premium][eep] 11.2)_ |
-| `min_access_level` | integer | no | Limit by current user minimal [access level](members.md) |
+| `with_programming_language` | string | no | Limit by projects which use the given programming language |
+| `wiki_checksum_failed` | boolean | no | **[PREMIUM]** Limit projects where the wiki checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2) |
+| `repository_checksum_failed` | boolean | no | **[PREMIUM]** Limit projects where the repository checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2) |
+| `min_access_level` | integer | no | Limit by current user minimal [access level](members.md) |
When `simple=true` or the user is unauthenticated this returns something like:
@@ -156,7 +156,8 @@ When the user is authenticated and `simple` is not set this returns something li
"repository_size": 1038090,
"wiki_size" : 0,
"lfs_objects_size": 0,
- "job_artifacts_size": 0
+ "job_artifacts_size": 0,
+ "packages_size": 0
},
"_links": {
"self": "http://example.com/api/v4/projects",
@@ -239,7 +240,8 @@ When the user is authenticated and `simple` is not set this returns something li
"repository_size": 2066080,
"wiki_size" : 0,
"lfs_objects_size": 0,
- "job_artifacts_size": 0
+ "job_artifacts_size": 0,
+ "packages_size": 0
},
"_links": {
"self": "http://example.com/api/v4/projects",
@@ -254,6 +256,20 @@ When the user is authenticated and `simple` is not set this returns something li
]
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `approvals_before_merge` parameter:
+
+```json
+[
+ {
+ "id": 4,
+ "description": null,
+ "approvals_before_merge": 0,
+ ...
+ }
+]
+```
+
You can filter by [custom attributes](custom_attributes.md) with:
```
@@ -349,7 +365,8 @@ GET /users/:user_id/projects
"repository_size": 1038090,
"wiki_size" : 0,
"lfs_objects_size": 0,
- "job_artifacts_size": 0
+ "job_artifacts_size": 0,
+ "packages_size": 0
},
"_links": {
"self": "http://example.com/api/v4/projects",
@@ -432,7 +449,8 @@ GET /users/:user_id/projects
"repository_size": 2066080,
"wiki_size" : 0,
"lfs_objects_size": 0,
- "job_artifacts_size": 0
+ "job_artifacts_size": 0,
+ "packages_size": 0
},
"_links": {
"self": "http://example.com/api/v4/projects",
@@ -548,6 +566,7 @@ GET /projects/:id
"group_access_level": 10
}
],
+ "repository_storage": "default",
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"printing_merge_requests_link_enabled": true,
@@ -559,7 +578,8 @@ GET /projects/:id
"repository_size": 1038090,
"wiki_size" : 0,
"lfs_objects_size": 0,
- "job_artifacts_size": 0
+ "job_artifacts_size": 0,
+ "packages_size": 0
},
"_links": {
"self": "http://example.com/api/v4/projects",
@@ -573,6 +593,18 @@ GET /projects/:id
}
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `approvals_before_merge` parameter:
+
+```json
+{
+ "id": 3,
+ "description": null,
+ "approvals_before_merge": 0,
+ ...
+}
+```
+
**Note**: The `web_url` and `avatar_url` attributes on `namespace` were [introduced][ce-27427] in GitLab 11.11.
If the project is a fork, and you provide a valid token to authenticate, the
@@ -674,6 +706,7 @@ POST /projects
| `name` | string | yes if path is not provided | The name of the new project. Equals path if not provided. |
| `path` | string | yes if name is not provided | Repository name for new project. Generated based on name if not provided (generated lowercased with dashes). |
| `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) |
+| `default_branch` | string | no | `master` by default |
| `description` | string | no | Short project description |
| `issues_enabled` | boolean | no | Enable issues for this project |
| `merge_requests_enabled` | boolean | no | Enable merge requests for this project |
@@ -695,8 +728,16 @@ POST /projects
| `avatar` | mixed | no | Image file for avatar of the project |
| `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line |
| `ci_config_path` | string | no | The path to CI config file |
+| `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins |
+| `approvals_before_merge` | integer | no | **[STARTER]** How many approvers should approve merge requests by default |
+| `mirror` | boolean | no | **[STARTER]** Enables pull mirroring in a project |
+| `mirror_trigger_builds` | boolean | no | **[STARTER]** Pull mirroring triggers builds |
| `initialize_with_readme` | boolean | no | `false` by default |
+NOTE: **Note:** If your HTTP repository is not publicly accessible,
+add authentication information to the URL: `https://username:password@gitlab.company.com/group/project.git`
+where `password` is a public access key with the `api` scope enabled.
+
## Create project for user
Creates a new project owned by the specified user. Available only for admins.
@@ -710,7 +751,6 @@ POST /projects/user/:user_id
| `user_id` | integer | yes | The user ID of the project owner |
| `name` | string | yes | The name of the new project |
| `path` | string | no | Custom repository name for new project. By default generated based on name |
-| `default_branch` | string | no | `master` by default |
| `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) |
| `description` | string | no | Short project description |
| `issues_enabled` | boolean | no | Enable issues for this project |
@@ -733,6 +773,15 @@ POST /projects/user/:user_id
| `avatar` | mixed | no | Image file for avatar of the project |
| `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line |
| `ci_config_path` | string | no | The path to CI config file |
+| `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins |
+| `approvals_before_merge` | integer | no | **[STARTER]** How many approvers should approve merge requests by default |
+| `external_authorization_classification_label` | string | no | **[CORE ONLY]** The classification label for the project |
+| `mirror` | boolean | no | **[STARTER]** Enables pull mirroring in a project |
+| `mirror_trigger_builds` | boolean | no | **[STARTER]** Pull mirroring triggers builds |
+
+NOTE: **Note:** If your HTTP repository is not publicly accessible,
+add authentication information to the URL: `https://username:password@gitlab.company.com/group/project.git`
+where `password` is a public access key with the `api` scope enabled.
## Edit project
@@ -769,6 +818,19 @@ PUT /projects/:id
| `avatar` | mixed | no | Image file for avatar of the project |
| `ci_config_path` | string | no | The path to CI config file |
| `ci_default_git_depth` | integer | no | Default number of revisions for [shallow cloning](../user/project/pipelines/settings.md#git-shallow-clone) |
+| `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins |
+| `approvals_before_merge` | integer | no | **[STARTER]** How many approvers should approve merge request by default |
+| `external_authorization_classification_label` | string | no | **[CORE ONLY]** The classification label for the project |
+| `mirror` | boolean | no | **[STARTER]** Enables pull mirroring in a project |
+| `mirror_user_id` | integer | no | **[STARTER]** User responsible for all the activity surrounding a pull mirror event |
+| `mirror_trigger_builds` | boolean | no | **[STARTER]** Pull mirroring triggers builds |
+| `only_mirror_protected_branches` | boolean | no | **[STARTER]** Only mirror protected branches |
+| `mirror_overwrites_diverged_branches` | boolean | no | **[STARTER]** Pull mirror overwrites diverged branches |
+| `packages_enabled` | boolean | no | **[PREMIUM ONLY]** Enable or disable packages repository feature |
+
+NOTE: **Note:** If your HTTP repository is not publicly accessible,
+add authentication information to the URL: `https://username:password@gitlab.company.com/group/project.git`
+where `password` is a public access key with the `api` scope enabled.
## Fork project
@@ -1292,7 +1354,7 @@ Example response:
## Remove project
-Removes a project including all associated resources (issues, merge requests etc.)
+Removes a project including all associated resources (issues, merge requests etc).
```
DELETE /projects/:id
@@ -1548,9 +1610,111 @@ POST /projects/:id/housekeeping
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+
+## Push Rules **[STARTER]**
+
+### Get project push rules
+
+Get the push rules of a project.
+
+```
+GET /projects/:id/push_rule
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+
+```json
+{
+ "id": 1,
+ "project_id": 3,
+ "commit_message_regex": "Fixes \d+\..*",
+ "branch_name_regex": "",
+ "deny_delete_tag": false,
+ "created_at": "2012-10-12T17:04:47Z",
+ "member_check": false,
+ "prevent_secrets": false,
+ "author_email_regex": "",
+ "file_name_regex": "",
+ "max_file_size": 5,
+ "commit_committer_check": false
+}
+```
+
+Users on GitLab [Premium, Silver, or higher](https://about.gitlab.com/pricing/) will also see
+the `commit_committer_check` parameter:
+
+```json
+{
+ "id": 1,
+ "project_id": 3,
+ "commit_committer_check": false
+ ...
+}
+```
+
+### Add project push rule
+
+Adds a push rule to a specified project.
+
+```
+POST /projects/:id/push_rule
+```
+
+| Attribute | Type | Required | Description |
+| -------------------------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `deny_delete_tag` **[STARTER]** | boolean | no | Deny deleting a tag |
+| `member_check` **[STARTER]** | boolean | no | Restrict commits by author (email) to existing GitLab users |
+| `prevent_secrets` **[STARTER]** | boolean | no | GitLab will reject any files that are likely to contain secrets |
+| `commit_message_regex` **[STARTER]** | string | no | All commit messages must match this, e.g. `Fixed \d+\..*` |
+| `branch_name_regex` **[STARTER]** | string | no | All branch names must match this, e.g. `(feature|hotfix)\/*` |
+| `author_email_regex` **[STARTER]** | string | no | All commit author emails must match this, e.g. `@my-company.com$` |
+| `file_name_regex` **[STARTER]** | string | no | All commited filenames must **not** match this, e.g. `(jar|exe)$` |
+| `max_file_size` **[STARTER]** | integer | no | Maximum file size (MB) |
+| `commit_committer_check` **[PREMIUM]** | boolean | no | Users can only push commits to this repository that were committed with one of their own verified emails. |
+
+### Edit project push rule
+
+Edits a push rule for a specified project.
+
+```
+PUT /projects/:id/push_rule
+```
+
+| Attribute | Type | Required | Description |
+| -------------------------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `deny_delete_tag` **[STARTER]** | boolean | no | Deny deleting a tag |
+| `member_check` **[STARTER]** | boolean | no | Restrict commits by author (email) to existing GitLab users |
+| `prevent_secrets` **[STARTER]** | boolean | no | GitLab will reject any files that are likely to contain secrets |
+| `commit_message_regex` **[STARTER]** | string | no | All commit messages must match this, e.g. `Fixed \d+\..*` |
+| `branch_name_regex` **[STARTER]** | string | no | All branch names must match this, e.g. `(feature|hotfix)\/*` |
+| `author_email_regex` **[STARTER]** | string | no | All commit author emails must match this, e.g. `@my-company.com$` |
+| `file_name_regex` **[STARTER]** | string | no | All commited filenames must **not** match this, e.g. `(jar|exe)$` |
+| `max_file_size` **[STARTER]** | integer | no | Maximum file size (MB) |
+| `commit_committer_check` **[PREMIUM]** | boolean | no | Users can only push commits to this repository that were committed with one of their own verified emails. |
+
+### Delete project push rule
+
+> Introduced in [GitLab Starter](https://about.gitlab.com/pricing/) 9.0.
+
+Removes a push rule from a project. This is an idempotent method and can be called multiple times.
+Either the push rule is available or not.
+
+```
+DELETE /projects/:id/push_rule
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
-### Transfer a project to a new namespace
+## Transfer a project to a new namespace
+
+> Introduced in GitLab 11.1.
```
PUT /projects/:id/transfer
@@ -1572,6 +1736,22 @@ Read more in the [Project import/export](project_import_export.md) documentation
Read more in the [Project members](members.md) documentation.
+## Start the pull mirroring process for a Project **[STARTER]**
+
+> Introduced in [GitLab Starter](https://about.gitlab.com/pricing) 10.3.
+
+```
+POST /projects/:id/mirror/pull
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/:id/mirror/pull
+```
+
## Project badges
Read more in the [Project Badges](project_badges.md) documentation.
@@ -1602,6 +1782,4 @@ GET /projects/:id/snapshot
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `wiki` | boolean | no | Whether to download the wiki, rather than project, repository |
-[eep]: https://about.gitlab.com/pricing/ "Available only in GitLab Premium"
-[ee-6137]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6137
[ce-27427]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/27427
diff --git a/doc/api/protected_branches.md b/doc/api/protected_branches.md
index a261bb75be5..6e41584afef 100644
--- a/doc/api/protected_branches.md
+++ b/doc/api/protected_branches.md
@@ -10,6 +10,7 @@ The access levels are defined in the `ProtectedRefAccess.allowed_access_levels`
0 => No access
30 => Developer access
40 => Maintainer access
+60 => Admin access
```
## List protected branches
@@ -51,6 +52,36 @@ Example response:
]
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `user_id` and `group_id` parameters:
+
+Example response:
+
+```json
+[
+ {
+ "name": "master",
+ "push_access_levels": [
+ {
+ "access_level": 40,
+ "user_id": null,
+ "group_id": null,
+ "access_level_description": "Maintainers"
+ }
+ ],
+ "merge_access_levels": [
+ {
+ "access_level": null,
+ "user_id": null,
+ "group_id": 1234,
+ "access_level_description": "Example Merge Group"
+ }
+ ]
+ },
+ ...
+]
+```
+
## Get a single protected branch or wildcard protected branch
Gets a single protected branch or wildcard protected branch.
@@ -88,6 +119,33 @@ Example response:
}
```
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `user_id` and `group_id` parameters:
+
+Example response:
+
+```json
+{
+ "name": "master",
+ "push_access_levels": [
+ {
+ "access_level": 40,
+ "user_id": null,
+ "group_id": null,
+ "access_level_description": "Maintainers"
+ }
+ ],
+ "merge_access_levels": [
+ {
+ "access_level": null,
+ "user_id": null,
+ "group_id": 1234,
+ "access_level_description": "Example Merge Group"
+ }
+ ]
+}
+```
+
## Protect repository branches
Protects a single repository branch or several project repository
@@ -98,15 +156,47 @@ POST /projects/:id/protected_branches
```
```bash
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&push_access_level=30&merge_access_level=30'
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&push_access_level=30&merge_access_level=30&unprotect_access_level=40'
```
| 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 branch or wildcard |
-| `push_access_level` | string | no | Access levels allowed to push (defaults: `40`, maintainer access level) |
-| `merge_access_level` | string | no | Access levels allowed to merge (defaults: `40`, maintainer access level) |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `name` | string | yes | The name of the branch or wildcard |
+| `push_access_level` | string | no | Access levels allowed to push (defaults: `40`, maintainer access level) |
+| `merge_access_level` | string | no | Access levels allowed to merge (defaults: `40`, maintainer access level) |
+| `unprotect_access_level` | string | no | Access levels allowed to unprotect (defaults: `40`, maintainer access level) |
+| `allowed_to_push` | array | no | **[STARTER]** Array of access levels allowed to push, with each described by a hash |
+| `allowed_to_merge` | array | no | **[STARTER]** Array of access levels allowed to merge, with each described by a hash |
+| `allowed_to_unprotect` | array | no | **[STARTER]**Array of access levels allowed to unprotect, with each described by a hash |
+
+Example response:
+
+```json
+{
+ "name": "*-stable",
+ "push_access_levels": [
+ {
+ "access_level": 30,
+ "access_level_description": "Developers + Maintainers"
+ }
+ ],
+ "merge_access_levels": [
+ {
+ "access_level": 30,
+ "access_level_description": "Developers + Maintainers"
+ ],
+ "unprotect_access_levels": [
+ {
+ "access_level": 40,
+ "access_level_description": "Maintainers"
+ }
+ ]
+}
+```
+
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `user_id` and `group_id` parameters:
Example response:
@@ -116,13 +206,65 @@ Example response:
"push_access_levels": [
{
"access_level": 30,
+ "user_id": null,
+ "group_id": null,
"access_level_description": "Developers + Maintainers"
}
],
"merge_access_levels": [
{
"access_level": 30,
+ "user_id": null,
+ "group_id": null,
"access_level_description": "Developers + Maintainers"
+ ],
+ "unprotect_access_levels": [
+ {
+ "access_level": 40,
+ "user_id": null,
+ "group_id": null,
+ "access_level_description": "Maintainers"
+ }
+ ]
+}
+```
+
+### Example with user / group level access **[STARTER]**
+
+Elements in the `allowed_to_push` / `allowed_to_merge` / `allowed_to_unprotect` array should take the
+form `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}`. Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md). These access levels allow [more granular control over protected branch access](../user/project/protected_branches.md#restricting-push-and-merge-access-to-certain-users-starter) and were [added to the API in ][ee-3516] in GitLab 10.3 EE.
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Buser_id%5D=1'
+```
+
+Example response:
+
+```json
+{
+ "name":"*-stable",
+ "push_access_levels": [
+ {
+ "access_level":null,
+ "user_id":1,
+ "group_id":null,
+ "access_level_description":"Administrator"
+ }
+ ],
+ "merge_access_levels": [
+ {
+ "access_level":40,
+ "user_id":null,
+ "group_id":null,
+ "access_level_description":"Maintainers"
+ }
+ ],
+ "unprotect_access_levels": [
+ {
+ "access_level":40,
+ "user_id":null,
+ "group_id":null,
+ "access_level_description":"Maintainers"
}
]
}
@@ -144,3 +286,5 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" 'https://git
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the branch |
+
+[ee-3516]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3516 "ProtectedBranches API handles per user/group granularity"
diff --git a/doc/api/runners.md b/doc/api/runners.md
index 2d91428d1c1..1318b9ca828 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -44,7 +44,7 @@ GET /runners?tag_list=tag1,tag2
| `scope` | string | no | Deprecated: Use `type` or `status` instead. The scope of specific runners to show, one of: `active`, `paused`, `online`, `offline`; showing all runners if none provided |
| `type` | string | no | The type of runners to show, one of: `instance_type`, `group_type`, `project_type` |
| `status` | string | no | The status of runners to show, one of: `active`, `paused`, `online`, `offline` |
-| `tag_list` | Array[String] | no | List of of the runner's tags |
+| `tag_list` | string array | no | List of of the runner's tags |
```
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners"
@@ -95,7 +95,7 @@ GET /runners/all?tag_list=tag1,tag2
| `scope` | string | no | Deprecated: Use `type` or `status` instead. The scope of runners to show, one of: `specific`, `shared`, `active`, `paused`, `online`, `offline`; showing all runners if none provided |
| `type` | string | no | The type of runners to show, one of: `instance_type`, `group_type`, `project_type` |
| `status` | string | no | The status of runners to show, one of: `active`, `paused`, `online`, `offline` |
-| `tag_list` | Array[String] | no | List of of the runner's tags |
+| `tag_list` | string array | no | List of of the runner's tags |
```
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/all"
@@ -214,10 +214,10 @@ PUT /runners/:id
| `description` | string | no | The description of a runner |
| `active` | boolean | no | The state of a runner; can be set to `true` or `false` |
| `tag_list` | array | no | The list of tags for a runner; put array of tags, that should be finally assigned to a runner |
-| `run_untagged` | boolean | no | Flag indicating the runner can execute untagged jobs |
-| `locked` | boolean | no | Flag indicating the runner is locked |
-| `access_level` | string | no | The access_level of the runner; `not_protected` or `ref_protected` |
-| `maximum_timeout` | integer | no | Maximum timeout set when this Runner will handle the job |
+| `run_untagged`| boolean | no | Flag indicating the runner can execute untagged jobs |
+| `locked` | boolean | no | Flag indicating the runner is locked |
+| `access_level` | string | no | The access_level of the runner; `not_protected` or `ref_protected` |
+| `maximum_timeout` | integer | no | Maximum timeout set when this Runner will handle the job |
```
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
@@ -291,6 +291,8 @@ GET /runners/:id/jobs
|-----------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a runner |
| `status` | string | no | Status of the job; one of: `running`, `success`, `failed`, `canceled` |
+| `order_by`| string | no | Order jobs by `id`. |
+| `sort` | string | no | Sort jobs in `asc` or `desc` order (default: `desc`) |
```
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/1/jobs?status=running"
@@ -385,7 +387,7 @@ GET /projects/:id/runners?tag_list=tag1,tag2
| `scope` | string | no | Deprecated: Use `type` or `status` instead. The scope of specific runners to show, one of: `active`, `paused`, `online`, `offline`; showing all runners if none provided |
| `type` | string | no | The type of runners to show, one of: `instance_type`, `group_type`, `project_type` |
| `status` | string | no | The status of runners to show, one of: `active`, `paused`, `online`, `offline` |
-| `tag_list` | Array[String] | no | List of of the runner's tags |
+| `tag_list` | string array | no | List of of the runner's tags |
```
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/9/runners"
@@ -477,17 +479,17 @@ Register a new Runner for the instance.
POST /runners
```
-| Attribute | Type | Required | Description |
-|-------------|---------|----------|---------------------|
-| `token` | string | yes | [Registration token](#registration-and-authentication-tokens). |
-| `description`| string | no | Runner's description|
-| `info` | hash | no | Runner's metadata |
-| `active` | boolean| no | Whether the Runner is active |
-| `locked` | boolean| no | Whether the Runner should be locked for current project |
-| `run_untagged` | boolean | no | Whether the Runner should handle untagged jobs |
-| `tag_list` | Array[String] | no | List of Runner's tags |
-| `access_level` | string | no | The access_level of the runner; `not_protected` or `ref_protected` |
-| `maximum_timeout` | integer | no | Maximum timeout set when this Runner will handle the job |
+| Attribute | Type | Required | Description |
+|--------------|---------|----------|---------------------|
+| `token` | string | yes | [Registration token](#registration-and-authentication-tokens). |
+| `description`| string | no | Runner's description|
+| `info` | hash | no | Runner's metadata |
+| `active` | boolean | no | Whether the Runner is active |
+| `locked` | boolean | no | Whether the Runner should be locked for current project |
+| `run_untagged` | boolean | no | Whether the Runner should handle untagged jobs |
+| `tag_list` | string array | no | List of Runner's tags |
+| `access_level` | string | no | The access_level of the runner; `not_protected` or `ref_protected` |
+| `maximum_timeout` | integer | no | Maximum timeout set when this Runner will handle the job |
```
curl --request POST "https://gitlab.example.com/api/v4/runners" --form "token=<registration_token>" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
diff --git a/doc/api/services.md b/doc/api/services.md
index c811d0e84ca..4f35c17e927 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -583,7 +583,7 @@ Parameters:
| `username` | string | yes | The username of the user created to be used with GitLab/Jira. |
| `password` | string | yes | The password of the user created to be used with GitLab/Jira. |
| `active` | boolean | no | Activates or deactivates the service. Defaults to false (deactivated). |
-| `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the Jira workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. |
+| `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the Jira workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column. By default, this ID is set to `2`. |
| `commit_events` | boolean | false | Enable notifications for commit events |
| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
diff --git a/doc/api/settings.md b/doc/api/settings.md
index b01cec64837..0f46662a8ae 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -67,6 +67,19 @@ Example response:
}
```
+Users on GitLab [Premium or Ultimate](https://about.gitlab.com/pricing/) may also see
+the `file_template_project_id` or the `geo_node_allowed_ips` parameters:
+
+```json
+{
+ "id" : 1,
+ "signup_enabled" : true,
+ "file_template_project_id": 1,
+ "geo_node_allowed_ips": "0.0.0.0/0, ::/0"
+ ...
+}
+```
+
## Change application settings
Use an API call to modify GitLab instance
@@ -121,10 +134,33 @@ Example response:
"performance_bar_allowed_group_id": 42,
"instance_statistics_visibility_private": false,
"user_show_add_ssh_key_message": true,
- "local_markdown_version": 0
+ "file_template_project_id": 1,
+ "local_markdown_version": 0,
+ "geo_node_allowed_ips": "0.0.0.0/0, ::/0"
}
```
+Users on GitLab [Premium or Ultimate](https://about.gitlab.com/pricing/) may also see
+these parameters:
+
+- `external_authorization_service_enabled`
+- `external_authorization_service_url`
+- `external_authorization_service_default_label`
+- `external_authorization_service_timeout`
+- `file_template_project_id`
+- `geo_node_allowed_ips`
+
+Example responses: **[PREMIUM ONLY]**
+
+```json
+ "external_authorization_service_enabled": true,
+ "external_authorization_service_url": "https://authorize.me",
+ "external_authorization_service_default_label": "default",
+ "external_authorization_service_timeout": 0.5,
+ "file_template_project_id": 1,
+ "geo_node_allowed_ips": "0.0.0.0/0, ::/0"
+```
+
## List of settings that can be accessed via API calls
In general, all settings are optional. Certain settings though, if enabled, will
@@ -138,10 +174,14 @@ are listed in the descriptions of the relevant settings.
| `after_sign_up_text` | string | no | Text shown to the user after signing up |
| `akismet_api_key` | string | required by: `akismet_enabled` | API key for akismet spam protection. |
| `akismet_enabled` | boolean | no | (**If enabled, requires:** `akismet_api_key`) Enable or disable akismet spam protection. |
+| `allow_group_owners_to_manage_ldap` | boolean | no | **[PREMIUM]** Set to `true` to allow group owners to manage LDAP |
| `allow_local_requests_from_hooks_and_services` | boolean | no | Allow requests to the local network from hooks and services. |
| `authorized_keys_enabled` | boolean | no | By default, we write to the `authorized_keys` file to support Git over SSH without additional configuration. GitLab can be optimized to authenticate SSH keys via the database file. Only disable this if you have configured your OpenSSH server to use the AuthorizedKeysCommand. |
| `auto_devops_domain` | string | no | Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages. |
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for projects by default. It will automatically build, test, and deploy applications based on a predefined CI/CD configuration. |
+| `check_namespace_plan` | boolean | no | **[PREMIUM]** Enabling this will make only licensed EE features available to projects if the project namespace's plan includes the feature or if the project is public. |
+| `clientside_sentry_dsn` | string | required by: `clientside_sentry_enabled` | Clientside Sentry Data Source Name. |
+| `clientside_sentry_enabled` | boolean | no | (**If enabled, requires:** `clientside_sentry_dsn`) Enable Sentry error reporting for the client side. |
| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes. |
| `default_artifacts_expire_in` | string | no | Set the default expiration time for each job's artifacts. |
| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take: `0` _(not protected, both developers and maintainers can push new commits, force push, or delete the branch)_, `1` _(partially protected, developers and maintainers can push new commits, but cannot force push or delete the branch)_ or `2` _(fully protected, developers cannot push new commits, but maintainers can; no-one can force push or delete the branch)_ as a parameter. Default is `2`. |
@@ -156,10 +196,31 @@ are listed in the descriptions of the relevant settings.
| `dsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. |
| `ecdsa_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ECDSA key. Default is `0` (no restriction). `-1` disables ECDSA keys. |
| `ed25519_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ED25519 key. Default is `0` (no restriction). `-1` disables ED25519 keys. |
+| `elasticsearch_aws` | boolean | no | **[PREMIUM]** Enable the use of AWS hosted Elasticsearch |
+| `elasticsearch_aws_access_key` | string | no | **[PREMIUM]** AWS IAM access key |
+| `elasticsearch_aws_region` | string | no | **[PREMIUM]** The AWS region the elasticsearch domain is configured |
+| `elasticsearch_aws_secret_access_key` | string | no | **[PREMIUM]** AWS IAM secret access key |
+| `elasticsearch_experimental_indexer` | boolean | no | **[PREMIUM]** Use the experimental elasticsearch indexer. More info: <https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer> |
+| `elasticsearch_indexing` | boolean | no | **[PREMIUM]** Enable Elasticsearch indexing |
+| `elasticsearch_search` | boolean | no | **[PREMIUM]** Enable Elasticsearch search |
+| `elasticsearch_url` | string | no | **[PREMIUM]** The url to use for connecting to Elasticsearch. Use a comma-separated list to support cluster (e.g., `http://localhost:9200, http://localhost:9201"`). If your Elasticsearch instance is password protected, pass the `username:password` in the URL (e.g., `http://<username>:<password>@<elastic_host>:9200/`). |
+| `elasticsearch_limit_indexing` | boolean | no | **[PREMIUM]** Limit Elasticsearch to index certain namespaces and projects |
+| `elasticsearch_project_ids` | array of integers | no | **[PREMIUM]** The projects to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. |
+| `elasticsearch_namespace_ids` | array of integers | no | **[PREMIUM]** The namespaces to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. |
+| `email_additional_text` | string | no | **[PREMIUM]** Additional text added to the bottom of every email for legal/auditing/compliance reasons |
| `email_author_in_body` | boolean | no | Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. |
| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. |
| `enforce_terms` | boolean | no | (**If enabled, requires:** `terms`) Enforce application ToS to all users. |
-| `first_day_of_week` | integer | no | Start day of the week for calendar views and date pickers. Valid values are `0` (default) for Sunday, `1` for Monday, and `6` for Saturday. |
+| `external_auth_client_cert` | string | no | **[PREMIUM]** (**If enabled, requires:** `external_auth_client_key`) The certificate to use to authenticate with the external authorization service |
+| `external_auth_client_key` | string | required by: `external_auth_client_cert` | **[PREMIUM]** Private key for the certificate when authentication is required for the external authorization service, this is encrypted when stored |
+| `external_auth_client_key_pass` | string | no | **[PREMIUM]** Passphrase to use for the private key when authenticating with the external service this is encrypted when stored |
+| `external_authorization_service_enabled` | boolean | no | **[PREMIUM]** (**If enabled, requires:** `external_authorization_service_default_label`, `external_authorization_service_timeout` and `external_authorization_service_url` ) Enable using an external authorization service for accessing projects |
+| `external_authorization_service_default_label` | string | required by: `external_authorization_service_enabled` | **[PREMIUM]** The default classification label to use when requesting authorization and no classification label has been specified on the project |
+| `external_authorization_service_timeout` | float | required by: `external_authorization_service_enabled` | **[PREMIUM]** The timeout after which an authorization request is aborted, in seconds. When a request times out, access is denied to the user. (min: 0.001, max: 10, step: 0.001) |
+| `external_authorization_service_url` | string | required by: `external_authorization_service_enabled` | **[PREMIUM]** URL to which authorization requests will be directed |
+| `file_template_project_id` | integer | no | **[PREMIUM]** The ID of a project to load custom file templates from |
+| `first_day_of_week` | integer | no | Start day of the week for calendar views and date pickers. Valid values are `0` (default) for Sunday, `1` for Monday, and `6` for Saturday. |
+| `geo_status_timeout` | integer | no | **[PREMIUM]** The amount of seconds after which a request to get a secondary node status will time out. |
| `gitaly_timeout_default` | integer | no | Default Gitaly timeout, in seconds. This timeout is not enforced for git fetch/push operations or Sidekiq jobs. Set to `0` to disable timeouts. |
| `gitaly_timeout_fast` | integer | no | Gitaly fast operation timeout, in seconds. Some Gitaly operations are expected to be fast. If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' can help maintain the stability of the GitLab instance. Set to `0` to disable timeouts. |
| `gitaly_timeout_medium` | integer | no | Medium Gitaly timeout, in seconds. This should be a value between the Fast and the Default timeout. Set to `0` to disable timeouts. |
@@ -168,6 +229,7 @@ are listed in the descriptions of the relevant settings.
| `help_page_hide_commercial_content` | boolean | no | Hide marketing-related entries from help. |
| `help_page_support_url` | string | no | Alternate support URL for help page. |
| `help_page_text` | string | no | Custom text displayed on the help page. |
+| `help_text` | string | no | **[PREMIUM]** GitLab server administrator information |
| `hide_third_party_offers` | boolean | no | Do not display offers from third parties within GitLab. |
| `home_page_url` | string | no | Redirect to this URL when not logged in. |
| `housekeeping_bitmaps_enabled` | boolean | required by: `housekeeping_enabled` | Enable Git pack file bitmap creation. |
@@ -190,6 +252,9 @@ are listed in the descriptions of the relevant settings.
| `metrics_sample_interval` | integer | required by: `metrics_enabled` | The sampling interval in seconds. |
| `metrics_timeout` | integer | required by: `metrics_enabled` | The amount of seconds after which InfluxDB will time out. |
| `mirror_available` | boolean | no | Allow mirrors to be set up for projects. If disabled, only admins will be able to set up mirrors in projects. |
+| `mirror_capacity_threshold` | integer | no | **[PREMIUM]** Minimum capacity to be available before scheduling more mirrors preemptively |
+| `mirror_max_capacity` | integer | no | **[PREMIUM]** Maximum number of mirrors that can be synchronizing at the same time. |
+| `mirror_max_delay` | integer | no | **[PREMIUM]** Maximum time (in minutes) between updates that a mirror can have when scheduled to synchronize. |
| `pages_domain_verification_enabled` | boolean | no | Require users to prove ownership of custom domains. Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled. |
| `password_authentication_enabled_for_git` | boolean | no | Enable authentication for Git over HTTP(S) via a GitLab account password. Default is `true`. |
| `password_authentication_enabled_for_web` | boolean | no | Enable authentication for the web interface via a GitLab account password. Default is `true`. |
@@ -201,21 +266,28 @@ are listed in the descriptions of the relevant settings.
| `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to `0` to disable polling. |
| `project_export_enabled` | boolean | no | Enable project export. |
| `prometheus_metrics_enabled` | boolean | no | Enable prometheus metrics. |
+| `pseudonymizer_enabled` | boolean | no | **[PREMIUM]** When enabled, GitLab will run a background job that will produce pseudonymized CSVs of the GitLab database that will be uploaded to your configured object storage directory.
| `recaptcha_enabled` | boolean | no | (**If enabled, requires:** `recaptcha_private_key` and `recaptcha_site_key`) Enable recaptcha. |
| `recaptcha_private_key` | string | required by: `recaptcha_enabled` | Private key for recaptcha. |
| `recaptcha_site_key` | string | required by: `recaptcha_enabled` | Site key for recaptcha. |
| `repository_checks_enabled` | boolean | no | GitLab will periodically run `git fsck` in all project and wiki repositories to look for silent disk corruption issues. |
+| `repository_size_limit` | integer | no | **[PREMIUM]** Size limit per repository (MB) |
| `repository_storages` | array of strings | no | A list of names of enabled storage paths, taken from `gitlab.yml`. New projects will be created in one of these stores, chosen at random. |
| `require_two_factor_authentication` | boolean | no | (**If enabled, requires:** `two_factor_grace_period`) Require all users to set up Two-factor authentication. |
| `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-admin users for groups, projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is `null` which means there is no restriction. |
| `rsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded RSA key. Default is `0` (no restriction). `-1` disables RSA keys. |
| `send_user_confirmation_email` | boolean | no | Send confirmation email on sign-up. |
| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes |
-| `shared_runners_enabled` | boolean | no | (**If enabled, requires:** `shared_runners_text`) Enable shared runners for new projects. |
+| `shared_runners_enabled` | boolean | no | (**If enabled, requires:** `shared_runners_text` and `shared_runners_minutes`) Enable shared runners for new projects. |
+| `shared_runners_minutes` | integer | required by: `shared_runners_enabled` | **[PREMIUM]** Set the maximum number of pipeline minutes that a group can use on shared Runners per month. |
| `shared_runners_text` | string | required by: `shared_runners_enabled` | Shared runners text. |
| `sign_in_text` | string | no | Text on the login page. |
| `signin_enabled` | string | no | (Deprecated: Use `password_authentication_enabled_for_web` instead) Flag indicating if password authentication is enabled for the web interface. |
| `signup_enabled` | boolean | no | Enable registration. Default is `true`. |
+| `slack_app_enabled` | boolean | no | **[PREMIUM]** (**If enabled, requires:** `slack_app_id`, `slack_app_secret` and `slack_app_secret`) Enable Slack app. |
+| `slack_app_id` | string | required by: `slack_app_enabled` | **[PREMIUM]** The app id of the Slack-app. |
+| `slack_app_secret` | string | required by: `slack_app_enabled` | **[PREMIUM]** The app secret of the Slack-app. |
+| `slack_app_verification_token` | string | required by: `slack_app_enabled` | **[PREMIUM]** The verification token of the Slack-app. |
| `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to `0` for unlimited time. |
| `terms` | text | required by: `enforce_terms` | (**Required by:** `enforce_terms`) Markdown content for the ToS. |
| `throttle_authenticated_api_enabled` | boolean | no | (**If enabled, requires:** `throttle_authenticated_api_period_in_seconds` and `throttle_authenticated_api_requests_per_period`) Enable authenticated API request rate limit. Helps reduce request volume (e.g. from crawlers or abusive bots). |
@@ -238,3 +310,4 @@ are listed in the descriptions of the relevant settings.
| `user_show_add_ssh_key_message` | boolean | no | When set to `false` disable the "You won't be able to pull or push project code via SSH" warning shown to users with no uploaded SSH key. |
| `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. |
| `local_markdown_version` | integer | no | Increase this value when any cached markdown should be invalidated. |
+| `geo_node_allowed_ips` | string | yes | **[PREMIUM]** Comma-separated list of IPs and CIDRs of allowed secondary nodes. For example, `1.1.1.1, 2.2.2.0/24`. |
diff --git a/doc/api/users.md b/doc/api/users.md
index 4667a985eb9..e1fccc14df3 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -70,11 +70,11 @@ Username search is case insensitive.
GET /users
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `order_by` | string | no | Return users ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` |
-| `sort` | string | no | Return users sorted in `asc` or `desc` order. Default is `desc` |
-| `two_factor` | string | no | Filter users by Two-factor authentication. Filter values are `enabled` or `disabled`. By default it returns all users |
+| Attribute | Type | Required | Description |
+| ------------ | ------ | -------- | ----------- |
+| `order_by` | string | no | Return users ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` |
+| `sort` | string | no | Return users sorted in `asc` or `desc` order. Default is `desc` |
+| `two_factor` | string | no | Filter users by Two-factor authentication. Filter values are `enabled` or `disabled`. By default it returns all users |
```json
[
@@ -147,6 +147,24 @@ GET /users
]
```
+Users on GitLab [Silver or higher](https://about.gitlab.com/pricing/) will also see
+the `group_saml` provider option:
+
+```json
+[
+ {
+ "id": 1,
+ ...
+ "identities": [
+ {"provider": "github", "extern_uid": "2435223452345"},
+ {"provider": "bitbucket", "extern_uid": "john.smith"},
+ {"provider": "google_oauth2", "extern_uid": "8776128412476123468721346"},
+ {"provider": "group_saml", "extern_uid": "123789", "saml_provider_id": 10}
+ ],
+ ...
+ }
+]
+
You can lookup users by external UID and provider:
```
@@ -223,6 +241,8 @@ Parameters:
- `id` (required) - The ID of a user
+Example Responses:
+
```json
{
"id": 1,
@@ -258,9 +278,39 @@ Parameters:
"can_create_project": true,
"two_factor_enabled": true,
"external": false,
- "private_profile": false,
- "shared_runners_minutes_limit": 133
+ "private_profile": false
+}
+```
+
+Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
+the `shared_runners_minutes_limit` and `extra_shared_runners_minutes_limit` parameters.
+
+```json
+{
+ "id": 1,
+ "username": "john_smith",
+ "shared_runners_minutes_limit": 133,
"extra_shared_runners_minutes_limit": 133
+ ...
+}
+```
+
+Users on GitLab.com [Silver, or higher](https://about.gitlab.com/pricing/) will also
+see the `group_saml` option:
+
+```json
+{
+ "id": 1,
+ "username": "john_smith",
+ "shared_runners_minutes_limit": 133,
+ "extra_shared_runners_minutes_limit": 133
+ "identities": [
+ {"provider": "github", "extern_uid": "2435223452345"},
+ {"provider": "bitbucket", "extern_uid": "john.smith"},
+ {"provider": "google_oauth2", "extern_uid": "8776128412476123468721346"},
+ {"provider": "group_saml", "extern_uid": "123789", "saml_provider_id": 10}
+ ],
+ ...
}
```
@@ -287,32 +337,30 @@ POST /users
Parameters:
-- `email` (required) - Email
-- `password` (optional) - Password
-- `reset_password` (optional) - Send user password reset link - true or false (default)
-- `force_random_password` (optional) - Set user password to a random value - true or false (default)
-- `username` (required) - Username
-- `name` (required) - Name
-- `skype` (optional) - Skype ID
-- `linkedin` (optional) - LinkedIn
-- `twitter` (optional) - Twitter account
-- `website_url` (optional) - Website URL
-- `organization` (optional) - Organization name
-- `projects_limit` (optional) - Number of projects user can create
-- `extern_uid` (optional) - External UID
-- `provider` (optional) - External provider name
-- `group_id_for_saml` (optional) - ID of group where SAML has been configured
-- `bio` (optional) - User's biography
-- `location` (optional) - User's location
-- `public_email` (optional) - The public email of the user
-- `admin` (optional) - User is admin - true or false (default)
-- `can_create_group` (optional) - User can create groups - true or false
-- `skip_confirmation` (optional) - Skip confirmation - true or false (default)
-- `external` (optional) - Flags the user as external - true or false(default)
-- `avatar` (optional) - Image file for user's avatar
-- `private_profile` (optional) - User's profile is private - true or false
-- `shared_runners_minutes_limit` (optional) - Pipeline minutes quota for this user
-- `extra_shared_runners_minutes_limit` (optional) - Extra pipeline minutes quota for this user
+- `email` (required) - Email
+- `password` (optional) - Password
+- `reset_password` (optional) - Send user password reset link - true or false(default)
+- `username` (required) - Username
+- `name` (required) - Name
+- `skype` (optional) - Skype ID
+- `linkedin` (optional) - LinkedIn
+- `twitter` (optional) - Twitter account
+- `website_url` (optional) - Website URL
+- `organization` (optional) - Organization name
+- `projects_limit` (optional) - Number of projects user can create
+- `extern_uid` (optional) - External UID
+- `provider` (optional) - External provider name
+- `bio` (optional) - User's biography
+- `location` (optional) - User's location
+- `public_email` (optional) - The public email of the user
+- `admin` (optional) - User is admin - true or false (default)
+- `can_create_group` (optional) - User can create groups - true or false
+- `skip_confirmation` (optional) - Skip confirmation - true or false (default)
+- `external` (optional) - Flags the user as external - true or false(default)
+- `avatar` (optional) - Image file for user's avatar
+- `private_profile` (optional) - User's profile is private - true or false
+- `shared_runners_minutes_limit` (optional) - Pipeline minutes quota for this user **[STARTER]**
+- `extra_shared_runners_minutes_limit` (optional) - Extra pipeline minutes quota for this user **[STARTER]**
## User modification
@@ -348,6 +396,8 @@ Parameters:
- `extra_shared_runners_minutes_limit` (optional) - Extra pipeline minutes quota for this user
- `avatar` (optional) - Image file for user's avatar
- `private_profile` (optional) - User's profile is private - true or false
+- `shared_runners_minutes_limit` (optional) - Pipeline minutes quota for this user **[STARTER]**
+- `extra_shared_runners_minutes_limit` (optional) - Extra pipeline minutes quota for this user **[STARTER]**
On password update, user will be forced to change it upon next login.
Note, at the moment this method does only return a `404` error,
@@ -370,9 +420,7 @@ Parameters:
[moved to the ghost user](../user/profile/account/delete_account.md#associated-records)
will be deleted instead, as well as groups owned solely by this user.
-## User
-
-### For normal users
+## List current user (for normal users)
Gets currently authenticated user.
@@ -418,7 +466,7 @@ GET /user
}
```
-### For admins
+## List current user (for admins)
Parameters:
@@ -497,9 +545,9 @@ Get the status of a user.
GET /users/:id_or_username/status
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id_or_username` | string | yes | The id or username of the user to get a status of |
+| Attribute | Type | Required | Description |
+| ---------------- | ------ | -------- | ----------- |
+| `id_or_username` | string | yes | The id or username of the user to get a status of |
```bash
curl "https://gitlab.example.com/users/janedoe/status"
@@ -523,8 +571,8 @@ Set the status of the current user.
PUT /user/status
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
+| Attribute | Type | Required | Description |
+| --------- | ------ | -------- | ----------- |
| `emoji` | string | no | The name of the emoji to use as status, if omitted `speech_balloon` is used. Emoji name can be one of the specified names in the [Gemojione index][gemojione-index]. |
| `message` | string | no | The message to set as a status. It can also contain emoji codes. |
@@ -546,7 +594,7 @@ Example responses
## List user projects
-Please refer to the [List of user projects ](projects.md#list-user-projects).
+Please refer to the [List of user projects](projects.md#list-user-projects).
## List SSH keys
@@ -722,9 +770,9 @@ GET /user/gpg_keys/:key_id
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `key_id` | integer | yes | The ID of the GPG key |
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | ----------- |
+| `key_id` | integer | yes | The ID of the GPG key |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/user/gpg_keys/1
@@ -750,9 +798,9 @@ POST /user/gpg_keys
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| key | string | yes | The new GPG key |
+| Attribute | Type | Required | Description |
+| --------- | ------ | -------- | ----------- |
+| key | string | yes | The new GPG key |
```bash
curl --data "key=-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFV..." --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/user/gpg_keys
@@ -780,9 +828,9 @@ DELETE /user/gpg_keys/:key_id
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `key_id` | integer | yes | The ID of the GPG key |
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | ----------- |
+| `key_id` | integer | yes | The ID of the GPG key |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/user/gpg_keys/1
@@ -800,9 +848,9 @@ GET /users/:id/gpg_keys
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the user |
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/users/2/gpg_keys
@@ -830,10 +878,10 @@ GET /users/:id/gpg_keys/:key_id
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the user |
-| `key_id` | integer | yes | The ID of the GPG key |
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
+| `key_id` | integer | yes | The ID of the GPG key |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/users/2/gpg_keys/1
@@ -859,10 +907,10 @@ POST /users/:id/gpg_keys
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the user |
-| `key_id` | integer | yes | The ID of the GPG key |
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
+| `key_id` | integer | yes | The ID of the GPG key |
```bash
curl --data "key=-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFV..." --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/users/2/gpg_keys
@@ -890,10 +938,10 @@ DELETE /users/:id/gpg_keys/:key_id
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the user |
-| `key_id` | integer | yes | The ID of the GPG key |
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
+| `key_id` | integer | yes | The ID of the GPG key |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/users/2/gpg_keys/1
@@ -1074,10 +1122,10 @@ GET /users/:user_id/impersonation_tokens
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `user_id` | integer | yes | The ID of the user |
-| `state` | string | no | filter tokens based on state (`all`, `active`, `inactive`) |
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | ---------------------------------------------------------- |
+| `user_id` | integer | yes | The ID of the user |
+| `state` | string | no | filter tokens based on state (`all`, `active`, `inactive`) |
```
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/users/42/impersonation_tokens
@@ -1126,10 +1174,10 @@ GET /users/:user_id/impersonation_tokens/:impersonation_token_id
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `user_id` | integer | yes | The ID of the user |
-| `impersonation_token_id` | integer | yes | The ID of the impersonation token |
+| Attribute | Type | Required | Description |
+| ------------------------ | ------- | -------- | --------------------------------- |
+| `user_id` | integer | yes | The ID of the user |
+| `impersonation_token_id` | integer | yes | The ID of the impersonation token |
```
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/users/42/impersonation_tokens/2
@@ -1155,7 +1203,6 @@ Example response:
## Create an impersonation token
> Requires admin permissions.
-
> Token values are returned once. Make sure you save it - you won't be able to access it again.
It creates a new impersonation token. Note that only administrators can do this.
@@ -1167,12 +1214,12 @@ settings page.
POST /users/:user_id/impersonation_tokens
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `user_id` | integer | yes | The ID of the user |
-| `name` | string | yes | The name of the impersonation token |
-| `expires_at` | date | no | The expiration date of the impersonation token in ISO format (`YYYY-MM-DD`)|
-| `scopes` | array | yes | The array of scopes of the impersonation token (`api`, `read_user`) |
+| Attribute | Type | Required | Description |
+| ------------ | ------- | -------- | ----------- |
+| `user_id` | integer | yes | The ID of the user |
+| `name` | string | yes | The name of the impersonation token |
+| `expires_at` | date | no | The expiration date of the impersonation token in ISO format (`YYYY-MM-DD`)|
+| `scopes` | array | yes | The array of scopes of the impersonation token (`api`, `read_user`) |
```
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --data "name=mytoken" --data "expires_at=2017-04-04" --data "scopes[]=api" https://gitlab.example.com/api/v4/users/42/impersonation_tokens
@@ -1219,15 +1266,15 @@ Parameters:
### Get user activities (admin only)
->**Note:** This API endpoint is only available on 8.15 (EE) and 9.1 (CE) and above.
+NOTE: **Note:** This API endpoint is only available on 8.15 (EE) and 9.1 (CE) and above.
Get the last activity date for all users, sorted from oldest to newest.
The activities that update the timestamp are:
- - Git HTTP/SSH activities (such as clone, push)
- - User logging in into GitLab
- - User visiting pages related to Dashboards, Projects, Issues and Merge Requests ([introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/54947) in GitLab 11.8)
+- Git HTTP/SSH activities (such as clone, push)
+- User logging in into GitLab
+- User visiting pages related to Dashboards, Projects, Issues and Merge Requests ([introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/54947) in GitLab 11.8)
By default, it shows the activity for all users in the last 6 months, but this can be
amended by using the `from` parameter.
@@ -1270,4 +1317,4 @@ Example response:
Please note that `last_activity_at` is deprecated, please use `last_activity_on`.
-[gemojione-index]: https://github.com/jonathanwiesel/gemojione/blob/master/config/index.json \ No newline at end of file
+[gemojione-index]: https://github.com/jonathanwiesel/gemojione/blob/master/config/index.json
diff --git a/doc/api/vulnerabilities.md b/doc/api/vulnerabilities.md
index 390d0966244..f1b11c7bceb 100644
--- a/doc/api/vulnerabilities.md
+++ b/doc/api/vulnerabilities.md
@@ -32,13 +32,13 @@ GET /projects/:id/vulnerabilities?severity=high
GET /projects/:id/vulnerabilities?confidence=unknown,experimental
```
-| 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. |
-| `report_type` | Array[string] | no | Returns vulnerabilities belonging to specified report type. Valid values: `sast`, `dast`, `dependency_scanning`, or `container_scanning`. |
-| `scope` | string | no | Returns vulnerabilities for the given scope: `all` or `dismissed`. Defaults to `dismissed` |
-| `severity` | Array[string] | no | Returns vulnerabilities belonging to specified severity level: `undefined`, `info`, `unknown`, `low`, `medium`, `high`, or `critical`. Defaults to all' |
-| `confidence` | Array[string] | no | Returns vulnerabilities belonging to specified confidence level: `undefined`, `ignore`, `unknown`, `experimental`, `low`, `medium`, `high`, or `confirmed`. Defaults to all |
+| 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. |
+| `report_type` | string array | no | Returns vulnerabilities belonging to specified report type. Valid values: `sast`, `dast`, `dependency_scanning`, or `container_scanning`. |
+| `scope` | string | no | Returns vulnerabilities for the given scope: `all` or `dismissed`. Defaults to `dismissed` |
+| `severity` | string array | no | Returns vulnerabilities belonging to specified severity level: `undefined`, `info`, `unknown`, `low`, `medium`, `high`, or `critical`. Defaults to all' |
+| `confidence` | string array | no | Returns vulnerabilities belonging to specified confidence level: `undefined`, `ignore`, `unknown`, `experimental`, `low`, `medium`, `high`, or `confirmed`. Defaults to all |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/vulnerabilities
diff --git a/doc/ci/README.md b/doc/ci/README.md
index da864a0b3cc..d851a56ee0e 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -81,6 +81,7 @@ GitLab CI/CD supports numerous configuration options:
| [Git submodules for CI/CD](git_submodules.md) | Configure jobs for using Git submodules.|
| [SSH keys for CI/CD](ssh_keys/README.md) | Using SSH keys in your CI pipelines. |
| [Pipelines triggers](triggers/README.md) | Trigger pipelines through the API. |
+| [Pipelines for Merge Requests](merge_request_pipelines/index.md) | Design a pipeline structure for running a pipeline in merge requests. |
| [Integrate with Kubernetes clusters](../user/project/clusters/index.md) | Connect your project to Google Kubernetes Engine (GKE) or an existing Kubernetes cluster. |
| [GitLab Runner](https://docs.gitlab.com/runner/) | Configure your own GitLab Runners to execute your scripts. |
| [Optimize GitLab and Runner for large repositories](large_repositories/index.md) | Recommended strategies for handling large repos. |
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index e012f4f8595..816bd35018a 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -489,17 +489,17 @@ runtime.
### Using statically-defined credentials
There are two approaches that you can take in order to access a
private registry. Both require setting the environment variable
-`DOCKER_AUTH_LOGIN` with appropriate authentication info.
+`DOCKER_AUTH_CONFIG` with appropriate authentication info.
1. Per-job: To configure one job to access a private registry, add
- `DOCKER_AUTH_LOGIN` as a job variable.
+ `DOCKER_AUTH_CONFIG` as a job variable.
1. Per-runner: To configure a Runner so all its jobs can access a
- private registry, add `DOCKER_AUTH_LOGIN` to the environment in the
+ private registry, add `DOCKER_AUTH_CONFIG` to the environment in the
Runner's configuration.
See below for examples of each.
-#### Determining your `DOCKER_AUTH_LOGIN` data
+#### Determining your `DOCKER_AUTH_CONFIG` data
As an example, let's assume that you want to use the `registry.example.com:5000/private/image:latest`
image which is private and requires you to login into a private container registry.
diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md
index c3dbcf6a19f..e70ae0bd154 100644
--- a/doc/ci/merge_request_pipelines/index.md
+++ b/doc/ci/merge_request_pipelines/index.md
@@ -1,8 +1,9 @@
---
-type: reference
+type: reference, index
+last_update: 2019-07-03
---
-# Pipelines for merge requests
+# Pipelines for Merge Requests
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/15310) in GitLab 11.6.
@@ -75,135 +76,13 @@ when a merge request was created or updated. For example:
![Merge request page](img/merge_request.png)
-## Pipelines for merged results **[PREMIUM]**
+## Pipelines for Merged Results **[PREMIUM]**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7380) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.10.
-> This feature is disabled by default until we resolve issues with [contention handling](https://gitlab.com/gitlab-org/gitlab-ee/issues/11222), but [can be enabled manually](#enabling-pipelines-for-merged-results).
+Read the [documentation on Pipelines for Merged Results](pipelines_for_merged_results/index.md).
-It's possible for your source and target branches to diverge, which can result
-in the scenario that source branch's pipeline was green, the target's pipeline was green,
-but the combined output fails.
+### Merge Trains **[PREMIUM]**
-By having your merge request pipeline automatically
-create a new ref that contains the merge result of the source and target branch
-(then running a pipeline on that ref), we can better test that the combined result
-is also valid.
-
-GitLab can run pipelines for merge requests
-on this merged result. That is, where the source and target branches are combined into a
-new ref and a pipeline for this ref validates the result prior to merging.
-
-![Merge request pipeline as the head pipeline](img/merge_request_pipeline.png)
-
-There are some cases where creating a combined ref is not possible or not wanted.
-For example, a source branch that has conflicts with the target branch
-or a merge request that is still in WIP status. In this case, the merge request pipeline falls back to a "detached" state
-and runs on the source branch ref as if it was a regular pipeline.
-
-The detached state serves to warn you that you are working in a situation
-subjected to merge problems, and helps to highlight that you should
-get out of WIP status or resolve merge conflicts as soon as possible.
-
-### Requirements and limitations
-
-Pipelines for merged results require:
-
-- [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner) 11.9 or newer.
-- [Gitaly](https://gitlab.com/gitlab-org/gitaly) 1.21.0 or newer.
-
-In addition, pipelines for merged results have the following limitations:
-
-- Forking/cross-repo workflows are not currently supported. To follow progress,
- see [#9713](https://gitlab.com/gitlab-org/gitlab-ee/issues/9713).
-- This feature is not available for
- [fast forward merges](../../user/project/merge_requests/fast_forward_merge.md) yet.
- To follow progress, see [#58226](https://gitlab.com/gitlab-org/gitlab-ce/issues/58226).
-
-### Enabling Pipelines for merged results
-
-To enable pipelines on merged results at the project level:
-
-1. Visit your project's **Settings > General** and expand **Merge requests**.
-1. Check **Merge pipelines will try to validate the post-merge result prior to merging**.
-1. Click **Save changes** button.
-
-![Merge request pipeline config](img/merge_request_pipeline_config.png)
-
-CAUTION: **Warning:**
-Make sure your `gitlab-ci.yml` file is [configured properly for pipelines for merge requests](#configuring-pipelines-for-merge-requests),
-otherwise pipelines for merged results won't run and your merge requests will be stuck in an unresolved state.
-
-## Merge Trains **[PREMIUM]**
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9186) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.0.
-> This feature is disabled by default, but [can be enabled manually](#enabling-merge-trains).
-
-[Pipelines for merged results](#pipelines-for-merged-results-premium) introduces
-running a build on the result of the merged code prior to merging, as a way to keep master green.
-
-There's a scenario, however, for teams with a high number of changes in the target branch (typically master) where in many or even all cases,
-by the time the merged code is validated another commit has made it to master, invalidating the merged result.
-You'd need some kind of queuing, cancellation or retry mechanism for these scenarios
-in order to ensure an orderly flow of changes into the target branch.
-
-Each MR that joins a merge train joins as the last item in the train,
-just as it works in the current state. However, instead of queuing and waiting,
-each item takes the completed state of the previous (pending) merge ref, adds its own changes,
-and starts the pipeline immediately in parallel under the assumption that everything is going to pass.
-
-In this way, if all the pipelines in the train merge successfully, no pipeline time is wasted either queuing or retrying.
-If the button is subsequently pressed in a different MR, instead of creating a new pipeline for the target branch,
-it creates a new pipeline targeting the merge result of the previous MR plus the target branch.
-Pipelines invalidated through failures are immediately canceled and requeued.
-
-### Requirements and limitations
-
-Merge trains have the following requirements and limitations:
-
-- This feature requires that
- [pipelines for merged results](#pipelines-for-merged-results-premium) are
- **configured properly**.
-- Each merge train can generate a merge ref and run a pipeline **one at a time**.
- We plan to make the pipelines for merged results
- [run in parallel](https://gitlab.com/gitlab-org/gitlab-ee/issues/11222) in a
- future release.
-
-### Enabling Merge Trains
-
-To enable merge trains at the project level:
-
-1. Visit your project's **Settings > General** and expand **Merge requests**.
-1. Check **Allow merge trains**.
-1. Click **Save changes** button.
-
-![Merge request pipeline config](img/merge_train_config.png)
-
-### How to add a merge request to a merge train
-
-To add a merge request to a merge train:
-
-1. Click "Start/Add merge train" button in a merge request
-
-![Start merge train](img/merge_train_start.png)
-
-### How to remove a merge request from a merge train
-
-1. Click "Remove from merge train" button in the merge request widget.
-
-![Cancel merge train](img/merge_train_cancel.png)
-
-### Tips: Start/Add to merge train when pipeline succeeds
-
-You can add a merge request to a merge train only when the latest pipeline in the
-merge request finished. While the pipeline is running or pending, you cannot add
-the merge request to a train because the current change of the merge request may
-be broken thus it could affect the following merge requests.
-
-In this case, you can schedule to add the merge request to a merge train **when the latest
-pipeline succeeds**. You can see the following button instead of the regular "Start/Add merge train"
-button while the latest pipeline is running.
-
-![Add to merge train when pipeline succeeds](img/merge_train_start_when_pipeline_succeeds.png)
+Read the [documentation on Merge Trains](pipelines_for_merged_results/merge_trains/index.md).
## Excluding certain jobs
@@ -289,3 +168,15 @@ to integrate your job with [GitLab Merge Request API](../../api/merge_requests.m
You can find the list of available variables in [the reference sheet](../variables/predefined_variables.md).
The variable names begin with the `CI_MERGE_REQUEST_` prefix.
+
+<!-- ## 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/merge_request_pipelines/img/merge_request_pipeline.png b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/img/merge_request_pipeline.png
index 58d5581f628..58d5581f628 100644
--- a/doc/ci/merge_request_pipelines/img/merge_request_pipeline.png
+++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/img/merge_request_pipeline.png
Binary files differ
diff --git a/doc/ci/merge_request_pipelines/img/merge_request_pipeline_config.png b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/img/merge_request_pipeline_config.png
index 0a84e61d284..0a84e61d284 100644
--- a/doc/ci/merge_request_pipelines/img/merge_request_pipeline_config.png
+++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/img/merge_request_pipeline_config.png
Binary files differ
diff --git a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md
new file mode 100644
index 00000000000..3c5088089fa
--- /dev/null
+++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md
@@ -0,0 +1,78 @@
+---
+type: reference
+last_update: 2019-07-03
+---
+
+# Pipelines for Merged Results **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7380) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.10.
+> This feature is disabled by default until we resolve issues with [contention handling](https://gitlab.com/gitlab-org/gitlab-ee/issues/11222), but [can be enabled manually](#enabling-pipelines-for-merged-results).
+
+It's possible for your source and target branches to diverge, which can result
+in the scenario that source branch's pipeline was green, the target's pipeline was green,
+but the combined output fails.
+
+By having your merge request pipeline automatically
+create a new ref that contains the merge result of the source and target branch
+(then running a pipeline on that ref), we can better test that the combined result
+is also valid.
+
+GitLab can run pipelines for merge requests
+on this merged result. That is, where the source and target branches are combined into a
+new ref and a pipeline for this ref validates the result prior to merging.
+
+![Merge request pipeline as the head pipeline](img/merge_request_pipeline.png)
+
+There are some cases where creating a combined ref is not possible or not wanted.
+For example, a source branch that has conflicts with the target branch
+or a merge request that is still in WIP status. In this case, the merge request pipeline falls back to a "detached" state
+and runs on the source branch ref as if it was a regular pipeline.
+
+The detached state serves to warn you that you are working in a situation
+subjected to merge problems, and helps to highlight that you should
+get out of WIP status or resolve merge conflicts as soon as possible.
+
+## Requirements and limitations
+
+Pipelines for merged results require:
+
+- [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner) 11.9 or newer.
+- [Gitaly](https://gitlab.com/gitlab-org/gitaly) 1.21.0 or newer.
+
+In addition, pipelines for merged results have the following limitations:
+
+- Forking/cross-repo workflows are not currently supported. To follow progress,
+ see [#9713](https://gitlab.com/gitlab-org/gitlab-ee/issues/9713).
+- This feature is not available for
+ [fast forward merges](../../../user/project/merge_requests/fast_forward_merge.md) yet.
+ To follow progress, see [#58226](https://gitlab.com/gitlab-org/gitlab-ce/issues/58226).
+
+## Enabling Pipelines for Merged Results
+
+To enable pipelines on merged results at the project level:
+
+1. Visit your project's **Settings > General** and expand **Merge requests**.
+1. Check **Merge pipelines will try to validate the post-merge result prior to merging**.
+1. Click **Save changes** button.
+
+![Merge request pipeline config](img/merge_request_pipeline_config.png)
+
+CAUTION: **Warning:**
+Make sure your `gitlab-ci.yml` file is [configured properly for pipelines for merge requests](../index.md#configuring-pipelines-for-merge-requests),
+otherwise pipelines for merged results won't run and your merge requests will be stuck in an unresolved state.
+
+## Merge Trains **[PREMIUM]**
+
+Read the [documentation on Merge Trains](merge_trains/index.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/merge_request_pipelines/img/merge_train_cancel.png b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/img/merge_train_cancel_v12_0.png
index 1561fdcc7a5..1561fdcc7a5 100644
--- a/doc/ci/merge_request_pipelines/img/merge_train_cancel.png
+++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/img/merge_train_cancel_v12_0.png
Binary files differ
diff --git a/doc/ci/merge_request_pipelines/img/merge_train_config.png b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/img/merge_train_config_v12_0.png
index fb0af43556e..fb0af43556e 100644
--- a/doc/ci/merge_request_pipelines/img/merge_train_config.png
+++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/img/merge_train_config_v12_0.png
Binary files differ
diff --git a/doc/ci/merge_request_pipelines/img/merge_train_start.png b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/img/merge_train_start_v12_0.png
index f20108157d2..f20108157d2 100644
--- a/doc/ci/merge_request_pipelines/img/merge_train_start.png
+++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/img/merge_train_start_v12_0.png
Binary files differ
diff --git a/doc/ci/merge_request_pipelines/img/merge_train_start_when_pipeline_succeeds.png b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/img/merge_train_start_when_pipeline_succeeds_v12_0.png
index 62c2f2f5ff5..62c2f2f5ff5 100644
--- a/doc/ci/merge_request_pipelines/img/merge_train_start_when_pipeline_succeeds.png
+++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/img/merge_train_start_when_pipeline_succeeds_v12_0.png
Binary files differ
diff --git a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md
new file mode 100644
index 00000000000..c5ff6f9ebed
--- /dev/null
+++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md
@@ -0,0 +1,88 @@
+---
+type: reference
+last_update: 2019-07-03
+---
+
+# Merge Trains **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9186) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.0.
+> This feature is disabled by default, but [can be enabled manually](#enabling-merge-trains).
+
+[Pipelines for merged results](../index.md#pipelines-for-merged-results-premium) introduces
+running a build on the result of the merged code prior to merging, as a way to keep master green.
+
+There's a scenario, however, for teams with a high number of changes in the target branch (typically master) where in many or even all cases,
+by the time the merged code is validated another commit has made it to master, invalidating the merged result.
+You'd need some kind of queuing, cancellation or retry mechanism for these scenarios
+in order to ensure an orderly flow of changes into the target branch.
+
+Each MR that joins a merge train joins as the last item in the train,
+just as it works in the current state. However, instead of queuing and waiting,
+each item takes the completed state of the previous (pending) merge ref, adds its own changes,
+and starts the pipeline immediately in parallel under the assumption that everything is going to pass.
+
+In this way, if all the pipelines in the train merge successfully, no pipeline time is wasted either queuing or retrying.
+If the button is subsequently pressed in a different MR, instead of creating a new pipeline for the target branch,
+it creates a new pipeline targeting the merge result of the previous MR plus the target branch.
+Pipelines invalidated through failures are immediately canceled and requeued.
+
+## Requirements and limitations
+
+Merge trains have the following requirements and limitations:
+
+- This feature requires that
+ [pipelines for merged results](../index.md#pipelines-for-merged-results-premium) are
+ **configured properly**.
+- Each merge train can generate a merge ref and run a pipeline **one at a time**.
+ We plan to make the pipelines for merged results
+ [run in parallel](https://gitlab.com/gitlab-org/gitlab-ee/issues/11222) in a
+ future release.
+
+## Enabling Merge Trains
+
+To enable merge trains at the project level:
+
+1. Visit your project's **Settings > General** and expand **Merge requests**.
+1. Check **Allow merge trains**.
+1. Click **Save changes** button.
+
+![Merge request pipeline config](img/merge_train_config_v12_0.png)
+
+## How to add a merge request to a merge train
+
+To add a merge request to a merge train:
+
+1. Click "Start/Add merge train" button in a merge request
+
+![Start merge train](img/merge_train_start_v12_0.png)
+
+## How to remove a merge request from a merge train
+
+1. Click "Remove from merge train" button in the merge request widget.
+
+![Cancel merge train](img/merge_train_cancel_v12_0.png)
+
+## Start/Add to merge train when pipeline succeeds
+
+You can add a merge request to a merge train only when the latest pipeline in the
+merge request finished. While the pipeline is running or pending, you cannot add
+the merge request to a train because the current change of the merge request may
+be broken thus it could affect the following merge requests.
+
+In this case, you can schedule to add the merge request to a merge train **when the latest
+pipeline succeeds**. You can see the following button instead of the regular "Start/Add merge train"
+button while the latest pipeline is running.
+
+![Add to merge train when pipeline succeeds](img/merge_train_start_when_pipeline_succeeds_v12_0.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/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index 7dbb9af2869..e911e97d3c8 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -78,7 +78,7 @@ future GitLab releases.**
| `CI_PIPELINE_ID` | 8.10 | all | The unique id of the current pipeline that GitLab CI uses internally |
| `CI_PIPELINE_IID` | 11.0 | all | The unique id of the current pipeline scoped to project |
| `CI_PIPELINE_SOURCE` | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `trigger`, `schedule`, `api`, and `pipeline`. For pipelines created before GitLab 9.5, this will show as `unknown` |
-| `CI_PIPELINE_TRIGGERED` | all | all | The flag to indicate that job was [triggered] |
+| `CI_PIPELINE_TRIGGERED` | all | all | The flag to indicate that job was [triggered](../triggers/README.md) |
| `CI_PIPELINE_URL` | 11.1 | 0.5 | Pipeline details URL |
| `CI_PROJECT_DIR` | all | all | The full path where the repository is cloned and where the job is run. If the GitLab Runner `builds_dir` parameter is set, this variable is set relative to the value of `builds_dir`. For more information, see [Advanced configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section) for GitLab Runner. |
| `CI_PROJECT_ID` | all | all | The unique id of the current project that GitLab CI uses internally |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 2759f1c5160..3e564e4244c 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -973,6 +973,8 @@ review_app:
stop_review_app:
stage: deploy
+ variables:
+ GIT_STRATEGY: none
script: make delete-app
when: manual
environment:
@@ -987,6 +989,10 @@ Once the `review_app` job is successfully finished, it will trigger the
set it up to `manual` so it will need a [manual action](#whenmanual) via
GitLab's web interface in order to run.
+Also in the example, `GIT_STRATEGY` is set to `none` so that GitLab Runner won’t
+try to check out the code after the branch is deleted when the `stop_review_app`
+job is [automatically triggered](../environments.md#automatically-stopping-an-environment).
+
The `stop_review_app` job is **required** to have the following keywords defined:
- `when` - [reference](#when)
diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md
index 680c51e7524..9333f55ca9c 100644
--- a/doc/customization/issue_closing.md
+++ b/doc/customization/issue_closing.md
@@ -1,3 +1,5 @@
---
-redirect_to: '../user/project/issues/automatic_issue_closing.md'
+redirect_to: '../user/project/issues/managing_issues.md#closing-issues-automatically'
---
+
+This document was moved to [another location](../user/project/issues/managing_issues.md#closing-issues-automatically).
diff --git a/doc/customization/system_header_and_footer_messages.md b/doc/customization/system_header_and_footer_messages.md
index 9d6931c730d..7eee79abc77 100644
--- a/doc/customization/system_header_and_footer_messages.md
+++ b/doc/customization/system_header_and_footer_messages.md
@@ -1,6 +1,7 @@
# Adding a system message to every page
-> [Introduced][ee-1283] in [GitLab Premium][eep] 10.7.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5023) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.7.
+> [Added](https://gitlab.com/gitlab-org/gitlab-ce/issues/55057) to [GitLab Core](https://about.gitlab.com/pricing/) in 11.9.
Navigate to the **Admin** area and go to the **Appearance** page.
diff --git a/doc/development/README.md b/doc/development/README.md
index 5df6ec5fd56..1566173992a 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -82,6 +82,7 @@ description: 'Learn how to contribute to GitLab.'
- [Understanding EXPLAIN plans](understanding_explain_plans.md)
- [explain.depesz.com](https://explain.depesz.com/) for visualising the output
of `EXPLAIN`
+- [pgFormatter](http://sqlformat.darold.net/) a PostgreSQL SQL syntax beautifier
### Migrations
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index aeddad14995..c83a0427c98 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -2,13 +2,14 @@
## Deep Dive
-In March 2019, Nick Thomas hosted a [Deep Dive] on GitLab's [GraphQL API] to share his domain specific knowledge with anyone who may work in this part of the code base in the future. You can find the [recording on YouTube], and the slides on [Google Slides] and in [PDF]. Everything covered in this deep dive was accurate as of GitLab 11.9, and while specific details may have changed since then, it should still serve as a good introduction.
-
-[Deep Dive]: https://gitlab.com/gitlab-org/create-stage/issues/1
-[Pull Repository Mirroring functionality]: ../api/graphql/
-[recording on YouTube]: https://www.youtube.com/watch?v=-9L_1MWrjkg
-[Google Slides]: https://docs.google.com/presentation/d/1qOTxpkTdHIp1CRjuTvO-aXg0_rUtzE3ETfLUdnBB5uQ/edit
-[PDF]: https://gitlab.com/gitlab-org/create-stage/uploads/8e78ea7f326b2ef649e7d7d569c26d56/GraphQL_Deep_Dive__Create_.pdf
+In March 2019, Nick Thomas hosted a [Deep Dive](https://gitlab.com/gitlab-org/create-stage/issues/1)
+on GitLab's [GraphQL API](../api/graphql/index.md) to share his domain specific knowledge
+with anyone who may work in this part of the code base in the future. You can find the
+[recording on YouTube](https://www.youtube.com/watch?v=-9L_1MWrjkg), and the slides on
+[Google Slides](https://docs.google.com/presentation/d/1qOTxpkTdHIp1CRjuTvO-aXg0_rUtzE3ETfLUdnBB5uQ/edit)
+and in [PDF](https://gitlab.com/gitlab-org/create-stage/uploads/8e78ea7f326b2ef649e7d7d569c26d56/GraphQL_Deep_Dive__Create_.pdf).
+Everything covered in this deep dive was accurate as of GitLab 11.9, and while specific
+details may have changed since then, it should still serve as a good introduction.
## Authentication
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index ee6d00331e3..8319603fea2 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -267,7 +267,7 @@ GitLab CI is the open-source continuous integration service included with GitLab
#### Gitlab Workhorse
- [Project page](https://gitlab.com/gitlab-org/gitlab-workhorse/blob/master/README.md)
-- Configuration: [Omnibus][gitlab-workhorse-omnibus], [Charts][gitlab-workhorse-charts], [Source][workhorse-source]
+- Configuration: [Omnibus][workhorse-omnibus], [Charts][workhorse-charts], [Source][workhorse-source]
- Layer: Core Service (Processor)
- Process: `gitlab-workhorse`
@@ -681,7 +681,7 @@ We've also detailed [our architecture of GitLab.com](https://about.gitlab.com/ha
[pgbouncer-exporter-omnibus]: ../administration/monitoring/prometheus/pgbouncer_exporter.md
[pgbouncer-exporter-charts]: https://docs.gitlab.com/charts/installation/deployment.html#postgresql
[gitlab-monitor-omnibus]: ../administration/monitoring/prometheus/gitlab_monitor_exporter.md
-[gitab-monitor-charts]: https://docs.gitlab.com/charts/charts/gitlab/gitlab-monitor/index.html
+[gitlab-monitor-charts]: https://docs.gitlab.com/charts/charts/gitlab/gitlab-monitor/index.html
[node-exporter-omnibus]: ../administration/monitoring/prometheus/node_exporter.md
[node-exporter-charts]: https://gitlab.com/charts/gitlab/issues/1332
[mattermost-omnibus]: https://docs.gitlab.com/omnibus/gitlab-mattermost/
diff --git a/doc/development/automatic_ce_ee_merge.md b/doc/development/automatic_ce_ee_merge.md
index 3ca841353e6..423b35a9e3a 100644
--- a/doc/development/automatic_ce_ee_merge.md
+++ b/doc/development/automatic_ce_ee_merge.md
@@ -115,7 +115,7 @@ Now, every time you create an MR for CE and EE:
1. Continue cherry-picking: `git cherry-pick --continue`
1. Push to EE: `git push origin branch-example-ee`
1. Create the EE-equivalent MR and link to the CE MR from the
- description "Ports [CE-MR-LINK] to EE"
+ description `Ports [CE-MR-LINK] to EE`
1. Once all the jobs are passing in both CE and EE, you've addressed the
feedback from your own team, and got them approved, the merge requests can be merged.
1. When both MRs are ready, the EE merge request will be merged first, and the
@@ -125,42 +125,43 @@ Now, every time you create an MR for CE and EE:
- The commit SHA can be easily found from the GitLab UI. From a merge request,
open the tab **Commits** and click the copy icon to copy the commit SHA.
-- To cherry-pick a **commit range**, such as [A > B > C > D] use:
+- To cherry-pick a **commit range**, such as (A > B > C > D) use:
- ```shell
- git cherry-pick "oldest-commit-SHA^..newest-commit-SHA"
- ```
+ ```shell
+ git cherry-pick "oldest-commit-SHA^..newest-commit-SHA"
+ ```
- For example, suppose the commit A is the oldest, and its SHA is `4f5e4018c09ed797fdf446b3752f82e46f5af502`,
- and the commit D is the newest, and its SHA is `80e1c9e56783bd57bd7129828ec20b252ebc0538`.
- The cherry-pick command will be:
+ For example, suppose the commit A is the oldest, and its SHA is `4f5e4018c09ed797fdf446b3752f82e46f5af502`,
+ and the commit D is the newest, and its SHA is `80e1c9e56783bd57bd7129828ec20b252ebc0538`.
+ The cherry-pick command will be:
- ```shell
- git cherry-pick "4f5e4018c09ed797fdf446b3752f82e46f5af502^..80e1c9e56783bd57bd7129828ec20b252ebc0538"
- ```
+ ```shell
+ git cherry-pick "4f5e4018c09ed797fdf446b3752f82e46f5af502^..80e1c9e56783bd57bd7129828ec20b252ebc0538"
+ ```
- To cherry-pick a **merge commit**, use the flag `-m 1`. For example, suppose that the
merge commit SHA is `138f5e2f20289bb376caffa0303adb0cac859ce1`:
- ```shell
- git cherry-pick -m 1 138f5e2f20289bb376caffa0303adb0cac859ce1
- ```
-- To cherry-pick multiple commits, such as B and D in a range [A > B > C > D], use:
+ ```shell
+ git cherry-pick -m 1 138f5e2f20289bb376caffa0303adb0cac859ce1
+ ```
- ```shell
- git cherry-pick commit-B-SHA commit-D-SHA
- ```
+- To cherry-pick multiple commits, such as B and D in a range (A > B > C > D), use:
- For example, suppose commit B SHA = `4f5e4018c09ed797fdf446b3752f82e46f5af502`,
- and the commit D SHA = `80e1c9e56783bd57bd7129828ec20b252ebc0538`.
- The cherry-pick command will be:
+ ```shell
+ git cherry-pick commit-B-SHA commit-D-SHA
+ ```
- ```shell
- git cherry-pick 4f5e4018c09ed797fdf446b3752f82e46f5af502 80e1c9e56783bd57bd7129828ec20b252ebc0538
- ```
+ For example, suppose commit B SHA = `4f5e4018c09ed797fdf446b3752f82e46f5af502`,
+ and the commit D SHA = `80e1c9e56783bd57bd7129828ec20b252ebc0538`.
+ The cherry-pick command will be:
- This case is particularly useful when you have a merge commit in a sequence of
- commits and you want to cherry-pick all but the merge commit.
+ ```shell
+ git cherry-pick 4f5e4018c09ed797fdf446b3752f82e46f5af502 80e1c9e56783bd57bd7129828ec20b252ebc0538
+ ```
+
+ This case is particularly useful when you have a merge commit in a sequence of
+ commits and you want to cherry-pick all but the merge commit.
- If you push more commits to the CE branch, you can safely repeat the procedure
to cherry-pick them to the EE-equivalent branch. You can do that as many times as
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
index 45b3d5a23a1..33c1c3bd9e4 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -62,7 +62,7 @@ making it both concise and descriptive, err on the side of descriptive.
The first example provides no context of where the change was made, or why, or
how it benefits the user.
-- **Bad:** Copy [some text] to clipboard.
+- **Bad:** Copy (some text) to clipboard.
- **Good:** Update the "Copy to clipboard" tooltip to indicate what's being
copied.
diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md
index f1015f56106..27c349c03aa 100644
--- a/doc/development/contributing/issue_workflow.md
+++ b/doc/development/contributing/issue_workflow.md
@@ -154,8 +154,8 @@ The current group labels are:
* ~"group::ci and runner"
* ~"group::testing"
* ~"group::package"
-* ~"group::core release"
-* ~"group::supporting capabilities"
+* ~"group::progressive delivery"
+* ~"group::release management"
* ~"group::autodevops and kubernetes"
* ~"group::serverless and paas"
* ~"group::apm"
@@ -345,7 +345,7 @@ For feature proposals for EE, open an issue on the
In order to help track the feature proposals, we have created a
[`feature`][fl] label. For the time being, users that are not members
-of the project cannot add labels. You can instead ask one of the [core team]
+of the project cannot add labels. You can instead ask one of the [core team](https://about.gitlab.com/community/core-team/)
members to add the label ~feature to the issue or add the following
code snippet right after your description in a new line: `~feature`.
@@ -356,7 +356,7 @@ Please submit Feature Proposals using the ['Feature Proposal' issue template](ht
For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should
be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may
-need to ask one of the [core team] members to add the label, if you do not have permissions to do it by yourself.
+need to ask one of the [core team](https://about.gitlab.com/community/core-team/) members to add the label, if you do not have permissions to do it by yourself.
If you want to create something yourself, consider opening an issue first to
discuss whether it is interesting to include this in GitLab.
diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md
index 6064f59ed10..3f61ad7cb13 100644
--- a/doc/development/contributing/merge_request_workflow.md
+++ b/doc/development/contributing/merge_request_workflow.md
@@ -65,7 +65,7 @@ request is as follows:
1. If you are contributing documentation, choose `Documentation` from the
"Choose a template" menu and fill in the description according to the template.
1. Mention the issue(s) your merge request solves, using the `Solves #XXX` or
- `Closes #XXX` syntax to [auto-close](../../user/project/issues/automatic_issue_closing.md)
+ `Closes #XXX` syntax to [auto-close](../../user/project/issues/managing_issues.md#closing-issues-automatically)
the issue(s) once the merge request is merged.
1. If you're allowed to (Core team members, for example), set a relevant milestone
and [labels](issue_workflow.md).
@@ -193,6 +193,7 @@ requirements.
1. [Changelog entry added](../changelog.md), if necessary.
1. Reviewed by relevant (UX/FE/BE/tech writing) reviewers and all concerns are addressed.
1. Merged by a project maintainer.
+1. Confirmed to be working in the [Canary stage](https://about.gitlab.com/handbook/engineering/#canary-testing) or on GitLab.com once the contribution is deployed.
1. Added to the [release post](https://about.gitlab.com/handbook/marketing/blog/release-posts/),
if relevant.
1. Added to [the website](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/features.yml), if relevant.
diff --git a/doc/development/database_debugging.md b/doc/development/database_debugging.md
index de2c5b43411..0311eda1ff1 100644
--- a/doc/development/database_debugging.md
+++ b/doc/development/database_debugging.md
@@ -3,7 +3,7 @@
This section is to help give some copy-pasta you can use as a reference when you
run into some head-banging database problems.
-An easy first step is to search for your error in Slack or google "GitLab <my error>".
+An easy first step is to search for your error in Slack or google "GitLab (my error)".
---
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 34d41cf4958..6f4a36d4066 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -1036,7 +1036,14 @@ to avoid conflicts during CE to EE merge.
### Backporting changes from EE to CE
-When working in EE-specific features, you might have to tweak a few files that are not EE-specific. Here is a workflow to make sure those changes end up backported safely into CE too.
+Until the work completed to merge the ce and ee codebases, which is tracked on [epic &802](https://gitlab.com/groups/gitlab-org/-/epics/802), there exists times in which some changes for EE require specific changes to the CE
+code base. Examples of backports include the following:
+
+* Features intended or originally built for EE that are later decided to move to CE
+* Sometimes some code in CE may impact the EE feature
+
+Here is a workflow to make sure those changes end up backported safely into CE too.
+
(This approach does not refer to changes introduced via [csslab](https://gitlab.com/gitlab-org/csslab/).)
1. **Make your changes in the EE branch.** If possible, keep a separated commit (to be squashed) to help backporting and review.
diff --git a/doc/development/elasticsearch.md b/doc/development/elasticsearch.md
index 603a756ff56..e5da47ba16e 100644
--- a/doc/development/elasticsearch.md
+++ b/doc/development/elasticsearch.md
@@ -150,7 +150,7 @@ Uses an [Edge NGram token filter](https://www.elastic.co/guide/en/elasticsearch/
## Troubleshooting
-### Getting "flood stage disk watermark [95%] exceeded"
+### Getting `flood stage disk watermark [95%] exceeded`
You might get an error such as
diff --git a/doc/development/fe_guide/development_process.md b/doc/development/fe_guide/development_process.md
index f3fdaa3b883..ae0e2361840 100644
--- a/doc/development/fe_guide/development_process.md
+++ b/doc/development/fe_guide/development_process.md
@@ -12,8 +12,7 @@ This checklist is intended to help us during development of bigger features/refa
Please use your best judgement when to use it and please contribute new points through merge requests if something comes to your mind.
----
-
+```
### Frontend development
#### Planning development
@@ -24,15 +23,15 @@ Please use your best judgement when to use it and please contribute new points t
- [ ] Are all necessary UX specifications available that you will need in order to implement? Are there new UX components/patterns in the designs? Then contact the UI component team early on. How should error messages or validation be handled?
- [ ] **Library usage** Use Vuex as soon as you have even a medium state to manage, use Vue router if you need to have different views internally and want to link from the outside. Check what libraries we already have for which occasions.
- [ ] **Plan your implementation:**
- - [ ] **Architecture plan:** Create a plan aligned with GitLab's architecture, how you are going to do the implementation, for example Vue application setup and its components (through [onion skinning](https://gitlab.com/gitlab-org/gitlab-ce/issues/35873#note_39994091)), Store structure and data flow, which existing Vue components can you reuse. It's a good idea to go through your plan with another engineer to refine it.
- - [ ] **Backend:** The best way is to kickoff the implementation in a call and discuss with the assigned Backend engineer what you will need from the backend and also when. Can you reuse existing API's? How is the performance with the planned architecture? Maybe create together a JSON mock object to already start with development.
- - [ ] **Communication:** It also makes sense to have for bigger features an own slack channel (normally called #f_{feature_name}) and even weekly demo calls with all people involved.
- - [ ] **Dependency Plan:** Are there big dependencies in the plan between you and others, then maybe create an execution diagram to show what is blocking which part and the order of the different parts.
- - [ ] **Task list:** Create a simple checklist of the subtasks that are needed for the implementation, also consider creating even sub issues. (for example show a comment, delete a comment, update a comment, etc.). This helps you and also everyone else following the implementation
+ - [ ] **Architecture plan:** Create a plan aligned with GitLab's architecture, how you are going to do the implementation, for example Vue application setup and its components (through [onion skinning](https://gitlab.com/gitlab-org/gitlab-ce/issues/35873#note_39994091)), Store structure and data flow, which existing Vue components can you reuse. It's a good idea to go through your plan with another engineer to refine it.
+ - [ ] **Backend:** The best way is to kickoff the implementation in a call and discuss with the assigned Backend engineer what you will need from the backend and also when. Can you reuse existing API's? How is the performance with the planned architecture? Maybe create together a JSON mock object to already start with development.
+ - [ ] **Communication:** It also makes sense to have for bigger features an own slack channel (normally called #f_{feature_name}) and even weekly demo calls with all people involved.
+ - [ ] **Dependency Plan:** Are there big dependencies in the plan between you and others, then maybe create an execution diagram to show what is blocking which part and the order of the different parts.
+ - [ ] **Task list:** Create a simple checklist of the subtasks that are needed for the implementation, also consider creating even sub issues. (for example show a comment, delete a comment, update a comment, etc.). This helps you and also everyone else following the implementation
- [ ] **Keep it small** To make it easier for you and also all reviewers try to keep merge requests small and merge into a feature branch if needed. To accomplish that you need to plan that from the start. Different methods are:
- - [ ] **Skeleton based plan** Start with an MR that has the skeleton of the components with placeholder content. In following MRs you can fill the components with interactivity. This also makes it easier to spread out development on multiple people.
- - [ ] **Cookie Mode** Think about hiding the feature behind a cookie flag if the implementation is on top of existing features
- - [ ] **New route** Are you refactoring something big then you might consider adding a new route where you implement the new feature and when finished delete the current route and rename the new one. (for example 'merge_request' and 'new_merge_request')
+ - [ ] **Skeleton based plan** Start with an MR that has the skeleton of the components with placeholder content. In following MRs you can fill the components with interactivity. This also makes it easier to spread out development on multiple people.
+ - [ ] **Cookie Mode** Think about hiding the feature behind a cookie flag if the implementation is on top of existing features
+ - [ ] **New route** Are you refactoring something big then you might consider adding a new route where you implement the new feature and when finished delete the current route and rename the new one. (for example 'merge_request' and 'new_merge_request')
- [ ] **Setup** Is there any specific setup needed for your implementation (for example a kubernetes cluster)? Then let everyone know if it is not already mentioned where they can find documentation (if it doesn't exist - create it)
- [ ] **Security** Are there any new security relevant implementations? Then please contact the security team for an app security review. If you are not sure ask our [domain expert](https://about.gitlab.com/handbook/engineering/frontend/#frontend-domain-experts)
@@ -57,8 +56,7 @@ Please use your best judgement when to use it and please contribute new points t
- [ ] Are there any big changes on how and especially how frequently we use the API then let production know about it
- [ ] Smoke test of the RC on dev., staging., canary deployments and .com
- [ ] Follow up on issues that came out of the review. Create issues for discovered edge cases that should be covered in future iterations.
-
----
+```
### Share your work early
@@ -66,7 +64,7 @@ Please use your best judgement when to use it and please contribute new points t
GitLab's architecture.
1. Add a diagram to the issue and ask a frontend architect in the slack channel `#fe_architectural` about it.
- ![Diagram of Issue Boards Architecture](img/boards_diagram.png)
+ ![Diagram of Issue Boards Architecture](img/boards_diagram.png)
1. Don't take more than one week between starting work on a feature and
sharing a Merge Request with a reviewer or a maintainer.
diff --git a/doc/development/file_storage.md b/doc/development/file_storage.md
index 18e4dc2ca0c..02874d18a30 100644
--- a/doc/development/file_storage.md
+++ b/doc/development/file_storage.md
@@ -33,9 +33,9 @@ they are still not 100% standardized. You can see them below:
| User avatars | yes | uploads/-/system/user/avatar/:id/:filename | `AvatarUploader` | User |
| User snippet attachments | yes | uploads/-/system/personal_snippet/:id/:random_hex/:filename | `PersonalFileUploader` | Snippet |
| Project avatars | yes | uploads/-/system/project/avatar/:id/:filename | `AvatarUploader` | Project |
-| Issues/MR/Notes Markdown attachments | yes | uploads/:project_path_with_namespace/:random_hex/:filename | `FileUploader` | Project |
-| Issues/MR/Notes Legacy Markdown attachments | no | uploads/-/system/note/attachment/:id/:filename | `AttachmentUploader` | Note |
-| CI Artifacts (CE) | yes | shared/artifacts/:disk_hash[0..1]/:disk_hash[2..3]/:disk_hash/:year_:month_:date/:job_id/:job_artifact_id (:disk_hash is SHA256 digest of project_id) | `JobArtifactUploader` | Ci::JobArtifact |
+| Issues/MR/Notes Markdown attachments | yes | uploads/:project_path_with_namespace/:random_hex/:filename | `FileUploader` | Project |
+| Issues/MR/Notes Legacy Markdown attachments | no | uploads/-/system/note/attachment/:id/:filename | `AttachmentUploader` | Note |
+| CI Artifacts (CE) | yes | `shared/artifacts/:disk_hash[0..1]/:disk_hash[2..3]/:disk_hash/:year_:month_:date/:job_id/:job_artifact_id` (:disk_hash is SHA256 digest of project_id) | `JobArtifactUploader` | Ci::JobArtifact |
| LFS Objects (CE) | yes | shared/lfs-objects/:hex/:hex/:object_hash | `LfsObjectUploader` | LfsObject |
| External merge request diffs | yes | shared/external-diffs/merge_request_diffs/mr-:parent_id/diff-:id | `ExternalDiffUploader` | MergeRequestDiff |
diff --git a/doc/development/go_guide/index.md b/doc/development/go_guide/index.md
index 4dad8815fcb..f961a2fc837 100644
--- a/doc/development/go_guide/index.md
+++ b/doc/development/go_guide/index.md
@@ -82,7 +82,7 @@ go lint:
image: golang:1.11
script:
- go get -u golang.org/x/lint/golint
- - golint -set_exit_status
+ - golint -set_exit_status $(go list ./... | grep -v "vendor/")
```
Once [recursive includes](https://gitlab.com/gitlab-org/gitlab-ce/issues/56836)
diff --git a/doc/development/testing_guide/end_to_end/index.md b/doc/development/testing_guide/end_to_end/index.md
index 527cd350633..59eb3ecfd7e 100644
--- a/doc/development/testing_guide/end_to_end/index.md
+++ b/doc/development/testing_guide/end_to_end/index.md
@@ -79,7 +79,7 @@ subgraph gitlab-ce/ee pipeline
end
subgraph omnibus-gitlab pipeline
- A2[<b>`Trigger-docker` stage</b></b><br />`Trigger:gitlab-docker` job] -->|once done| B2
+ A2[<b>`Trigger-docker` stage</b><br />`Trigger:gitlab-docker` job] -->|once done| B2
end
subgraph gitlab-qa pipeline
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 28ebb6f0f64..98df0b5ea7c 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -560,7 +560,6 @@ end
[vue-test]: https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components
[rspec]: https://github.com/rspec/rspec-rails#feature-specs
[capybara]: https://github.com/teamcapybara/capybara
-[karma]: http://karma-runner.github.io/
[jasmine]: https://jasmine.github.io/
---
diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md
index 93ee2a6371a..c4b18391cb2 100644
--- a/doc/development/testing_guide/index.md
+++ b/doc/development/testing_guide/index.md
@@ -80,8 +80,6 @@ Everything you should know about how to run end-to-end tests using
[Return to Development documentation](../README.md)
-[^1]: /ci/yaml/README.html#dependencies
-
[rails]: http://rubyonrails.org/
[RSpec]: https://github.com/rspec/rspec-rails#feature-specs
[Capybara]: https://github.com/teamcapybara/capybara
diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md
index 0c268eff9f1..fd16047b8e4 100644
--- a/doc/gitlab-basics/README.md
+++ b/doc/gitlab-basics/README.md
@@ -22,7 +22,7 @@ The following are guides to basic GitLab functionality:
- [Fork a project](fork-project.md), to duplicate projects so they can be worked on in parallel.
- [Add a file](add-file.md), to add new files to a project's repository.
- [Add an image](add-image.md), to add new images to a project's repository.
-- [Create an issue](../user/project/issues/create_new_issue.md), to start collaborating within a project.
+- [Create an issue](../user/project/issues/managing_issues.md#create-a-new-issue), to start collaborating within a project.
- [Create a merge request](add-merge-request.md), to request changes made in a branch be merged into a project's repository.
- See how these features come together in the [GitLab Flow introduction video](https://youtu.be/InKNIvky2KE) and [GitLab Flow page](../workflow/gitlab_flow.md).
diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md
index 6e2a09fc030..5fa5f1bf2e2 100644
--- a/doc/gitlab-basics/create-issue.md
+++ b/doc/gitlab-basics/create-issue.md
@@ -1,5 +1,5 @@
---
-redirect_to: '../user/project/issues/index.md#issue-actions'
+redirect_to: '../user/project/issues/index.md#viewing-and-managing-issues'
---
-This document was moved to [another location](../user/project/issues/index.md#issue-actions).
+This document was moved to [another location](../user/project/issues/index.md#viewing-and-managing-issues).
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 4c1021d097f..70e5ab28931 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -560,7 +560,7 @@ NOTE: **Note:**
If you want to use HTTPS, see [Using HTTPS](#using-https) for the additional steps.
NOTE: **Note:**
-Make sure your hostname can be resolved on the machine itself by either a proper DNS record or an additional line in `/etc/hosts` ("127.0.0.1 hostname"). This might be necessary, for example, if you set up GitLab behind a reverse proxy. If the hostname cannot be resolved, the final installation check will fail with "Check GitLab API access: FAILED. code: 401" and pushing commits will be rejected with "[remote rejected] master -> master (hook declined)".
+Make sure your hostname can be resolved on the machine itself by either a proper DNS record or an additional line in `/etc/hosts` ("127.0.0.1 hostname"). This might be necessary, for example, if you set up GitLab behind a reverse proxy. If the hostname cannot be resolved, the final installation check will fail with `Check GitLab API access: FAILED. code: 401` and pushing commits will be rejected with `[remote rejected] master -> master (hook declined)`.
NOTE: **Note:**
GitLab Shell application startup time can be greatly reduced by disabling RubyGems. This can be done in several ways:
diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md
index f64fdb1e28b..877330b8c44 100644
--- a/doc/integration/elasticsearch.md
+++ b/doc/integration/elasticsearch.md
@@ -167,7 +167,7 @@ To disable the Elasticsearch integration:
1. Find the 'Elasticsearch' section and uncheck 'Search with Elasticsearch enabled'
and 'Elasticsearch indexing'
1. Click **Save** for the changes to take effect
-1. [Optional] Delete the existing index by running the command `sudo gitlab-rake gitlab:elastic:delete_index`
+1. (Optional) Delete the existing index by running the command `sudo gitlab-rake gitlab:elastic:delete_index`
## Adding GitLab's data to the Elasticsearch index
diff --git a/doc/intro/README.md b/doc/intro/README.md
index d9c733d4285..9a8cd925e48 100644
--- a/doc/intro/README.md
+++ b/doc/intro/README.md
@@ -15,7 +15,7 @@ Create projects and groups.
Create issues, labels, milestones, cast your vote, and review issues.
-- [Create an issue](../user/project/issues/create_new_issue.md)
+- [Create an issue](../user/project/issues/managing_issues.md#create-a-new-issue)
- [Assign labels to issues](../user/project/labels.md)
- [Use milestones as an overview of your project's tracker](../user/project/milestones/index.md)
- [Use voting to express your like/dislike to issues and merge requests](../workflow/award_emoji.md)
@@ -26,7 +26,7 @@ Create merge requests and review code.
- [Fork a project and contribute to it](../workflow/forking_workflow.md)
- [Create a new merge request](../gitlab-basics/add-merge-request.md)
-- [Automatically close issues from merge requests](../user/project/issues/automatic_issue_closing.md)
+- [Automatically close issues from merge requests](../user/project/issues/managing_issues.md#closing-issues-automatically)
- [Automatically merge when pipeline succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md)
- [Revert any commit](../user/project/merge_requests/revert_changes.md)
- [Cherry-pick any commit](../user/project/merge_requests/cherry_pick_changes.md)
diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md
index b1637181855..d04428fdbfe 100644
--- a/doc/public_access/public_access.md
+++ b/doc/public_access/public_access.md
@@ -29,6 +29,12 @@ in users.
Any logged in user will have [Guest permissions](../user/permissions.md)
on the repository.
+NOTE: **Note:**
+From July 2019, the `Internal` visibility setting is disabled for new projects, groups,
+and snippets on GitLab.com. Existing projects, groups, and snippets using the `Internal`
+visibility setting keep this setting. You can read more about the change in the
+[relevant issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/12388).
+
### Private projects
Private projects can only be cloned and viewed by project members.
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 41d12128e51..bd788cb138c 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -175,8 +175,8 @@ When using Auto DevOps, you may want to deploy different environments to
different Kubernetes clusters. This is possible due to the 1:1 connection that
[exists between them](../../user/project/clusters/index.md#multiple-kubernetes-clusters-premium).
-In the [Auto DevOps template](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml)
-(used behind the scenes by Auto DevOps), there are currently 3 defined environment names that you need to know:
+In the [Auto DevOps template] (used behind the scenes by Auto DevOps), there
+are currently 3 defined environment names that you need to know:
- `review/` (every environment starting with `review/`)
- `staging`
@@ -662,25 +662,32 @@ to the desired environment. See [Limiting environment scopes of variables](../..
### Customizing `.gitlab-ci.yml`
-If you want to modify the CI/CD pipeline used by Auto DevOps, you can copy the
-[Auto DevOps template] into your project's repo and edit as you see fit.
-
-Assuming that your project is new or it doesn't have a `.gitlab-ci.yml` file
-present:
-
-1. From your project home page, either click on the "Set up CI/CD" button, or click
- on the plus button and (`+`), then "New file"
-1. Pick `.gitlab-ci.yml` as the template type
-1. Select "Auto-DevOps" from the template dropdown
-1. Edit the template or add any jobs needed
-1. Give an appropriate commit message and hit "Commit changes"
-
-TIP: **Tip:** The Auto DevOps template includes useful comments to help you
-customize it. For example, if you want deployments to go to a staging environment
-instead of directly to a production one, you can enable the `staging` job by
-renaming `.staging` to `staging`. Then make sure to uncomment the `when` key of
-the `production` job to turn it into a manual action instead of deploying
-automatically.
+Everything about Auto DevOps is customizable since the [Auto DevOps template]
+is just an example of a [`.gitlab-ci.yml`](../../ci/yaml/README.md) and uses
+only features that are available to any `.gitlab-ci.yml`.
+
+Auto DevOps is completely customizable because the [Auto DevOps template]:
+
+- Is just an implementation of a [`.gitlab-ci.yml`](../../ci/yaml/README.md) file.
+- Uses only features available to any implementation of `.gitlab-ci.yml`.
+
+If you want to modify the CI/CD pipeline used by Auto DevOps, you can [`include`
+the template](../../ci/yaml/README.md#includetemplate) and customize as
+needed. To do this, add a `.gitlab-ci.yml` file to the root of your repository
+containing the following:
+
+```yml
+include:
+ - template: Auto-DevOps.gitlab-ci.yml
+```
+
+Then add any extra changes you want. Your additions will be merged with the
+[Auto DevOps template] using the behaviour described for
+[`include`](../../ci/yaml/README.md#include).
+
+It is also possible to copy and paste the contents of the [Auto DevOps
+template] into your project and edit this as needed. You may prefer to do it
+that way if you want to specifically remove any part of it.
### Using components of Auto-DevOps
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 001e4b6bf48..756a07e0b80 100644
--- a/doc/user/admin_area/settings/account_and_limit_settings.md
+++ b/doc/user/admin_area/settings/account_and_limit_settings.md
@@ -44,10 +44,12 @@ 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.
+**Note:** The repository size limit includes repository files and LFS, and does not include artifacts.
+
For details on manually purging files, see [reducing the repository size using Git](../../project/repository/reducing_the_repo_size_using_git.md).
NOTE: **Note:**
-For GitLab.com, the repository size limit is 10 GB.
+GitLab.com repository size [is set by GitLab](../../gitlab_com/index.md#repository-size-limit).
<!-- ## Troubleshooting
diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md
index 469a7c09250..2246ea8ed5a 100644
--- a/doc/user/clusters/applications.md
+++ b/doc/user/clusters/applications.md
@@ -111,7 +111,7 @@ file.
[Ingress](https://kubernetes.github.io/ingress-nginx/) can provide load
balancing, SSL termination, and name-based virtual hosting. It acts as a
web proxy for your applications and is useful if you want to use [Auto
-DevOps] or deploy your own web apps.
+DevOps](../../topics/autodevops/index.md) or deploy your own web apps.
NOTE: **Note:**
The
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 2fb6cec55fa..886fb6e6f55 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -352,3 +352,13 @@ High Performance TCP/HTTP Load Balancer:
[unicorn-worker-killer]: https://rubygems.org/gems/unicorn-worker-killer "unicorn-worker-killer"
[4010]: https://gitlab.com/gitlab-com/infrastructure/issues/4010 "Find a good value for maximum timeout for Shared Runners"
[4070]: https://gitlab.com/gitlab-com/infrastructure/issues/4070 "Configure per-runner timeout for shared-runners-manager-X on GitLab.com"
+
+## Other admin area settings
+
+This area highlights other noteworthy admin area settings on GitLab.com that differ from default settings. This list is not exhaustive.
+
+NOTE: **Note:**
+From July 2019, the `Internal` visibility setting is disabled for new projects, groups,
+and snippets on GitLab.com. Existing projects, groups, and snippets using the `Internal`
+visibility setting keep this setting. You can read more about the change in the
+[relevant issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/12388).
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index a08b41aaecb..eaae9964367 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -288,7 +288,7 @@ $example = array(
> If this is not rendered correctly, [view it in GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#inline-diff).
-With inline diff tags you can display {+ additions +} or [- deletions -].
+With inline diff tags you can display `{+ additions +}` or `[- deletions -]`.
The wrapping tags can be either curly braces or square brackets:
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 10184de8784..aac548020be 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -122,8 +122,8 @@ The following table depicts the various user permission levels in a project.
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
| Delete issues | | | | | ✓ |
-| Force push to protected branches [^4] | | | | | |
-| Remove protected branches [^4] | | | | | |
+| 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
diff --git a/doc/user/project/canary_deployments.md b/doc/user/project/canary_deployments.md
index cce862b0911..9bb282f1b78 100644
--- a/doc/user/project/canary_deployments.md
+++ b/doc/user/project/canary_deployments.md
@@ -44,7 +44,7 @@ Canary deployments require that you properly configure Deploy Boards:
1. Follow the steps to [enable Deploy Boards](deploy_boards.md#enabling-deploy-boards).
1. To track canary deployments you need to label your Kubernetes deployments and
- pods with `track: canary`. To get started quickly, you can use the [Auto Deploy]
+ pods with `track: canary`. To get started quickly, you can use the [Auto Deploy](../../ci/autodeploy/index.md)
template for canary deployments that GitLab provides.
Depending on the deploy, the label should be either `stable` or `canary`.
@@ -62,7 +62,6 @@ can easily notice them.
![Canary deployments on Deploy Board](img/deploy_boards_canary_deployments.png)
-[autodeploy]: ../../ci/autodeploy/index.md "GitLab Autodeploy"
[eep]: https://about.gitlab.com/pricing/
[ee-1659]: https://gitlab.com/gitlab-org/gitlab-ee/issues/1659
[kube-canary]: https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index c6ee168bad0..d21455fb5ca 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -222,7 +222,7 @@ functionalities needed to successfully build and deploy a containerized
application. Bear in mind that the same credentials are used for all the
applications running on the cluster.
-## 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.
diff --git a/doc/user/project/code_owners.md b/doc/user/project/code_owners.md
index ae04616943f..c76847616b3 100644
--- a/doc/user/project/code_owners.md
+++ b/doc/user/project/code_owners.md
@@ -1,10 +1,12 @@
# Code Owners **[STARTER]**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6916)
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6916)
in [GitLab Starter](https://about.gitlab.com/pricing/) 11.3.
+> - [Support for group namespaces](https://gitlab.com/gitlab-org/gitlab-ce/issues/53182) added in GitLab Starter 12.1.
-You can use a `CODEOWNERS` file to specify users that are responsible
-for certain files in a repository.
+You can use a `CODEOWNERS` file to specify users or
+[shared groups](members/share_project_with_groups.md)
+that are responsible for certain files in a repository.
You can choose and add the `CODEOWNERS` file in three places:
@@ -25,7 +27,8 @@ the given file.
Files can be specified using the same kind of patterns you would use
in the `.gitignore` file followed by the `@username` or email of one
-or more users that should be owners of the file.
+or more users or by the `@name` of one or more groups that should
+be owners of the file.
The order in which the paths are defined is significant: the last
pattern that matches a given path will be used to find the code
@@ -63,6 +66,10 @@ CODEOWNERS @multiple @owners @tab-separated
# owner for the LICENSE file
LICENSE @legal this_does_not_match janedoe@gitlab.com
+# Group names can be used to match groups and nested groups to specify
+# them as owners for a file
+README @group @group/with-nested/subgroup
+
# Ending a path in a `/` will specify the code owners for every file
# nested in that directory, on any level
/docs/ @all-docs
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index 7d567da1c9a..eac7fc6b195 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -29,7 +29,7 @@ to enable it.
following the [administration documentation](../../administration/container_registry.md).
If you are using GitLab.com, this is enabled by default so you can start using
the Registry immediately. Currently there is a soft (10GB) size restriction for
- registry on GitLab.com, as part of the [repository size limit](repository/index.html#repository-size).
+ registry on GitLab.com, as part of the [repository size limit](repository/index.md).
1. Go to your [project's General settings](settings/index.md#sharing-and-permissions)
and enable the **Container Registry** feature on your project. For new
projects this might be enabled by default. For existing projects
diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md
index dc97a44fd68..5d36e1d4be3 100644
--- a/doc/user/project/cycle_analytics.md
+++ b/doc/user/project/cycle_analytics.md
@@ -156,6 +156,6 @@ Learn more about Cycle Analytics in the following resources:
[environment]: ../../ci/yaml/README.md#environment
[GitLab flow]: ../../workflow/gitlab_flow.md
[idea to production]: https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab
-[issue closing pattern]: issues/automatic_issue_closing.md
+[issue closing pattern]: issues/managing_issues.md#closing-issues-automatically
[permissions]: ../permissions.md
[yml]: ../../ci/yaml/README.md
diff --git a/doc/user/project/deploy_boards.md b/doc/user/project/deploy_boards.md
index 175384bc985..0d51e8ae19a 100644
--- a/doc/user/project/deploy_boards.md
+++ b/doc/user/project/deploy_boards.md
@@ -125,6 +125,6 @@ version of your application.
[kube-service]: integrations/kubernetes.md "Kubernetes project service"
[review apps]: ../../ci/review_apps/index.md "Review Apps documentation"
[variables]: ../../ci/variables/README.md "GitLab CI variables"
-[autodeploy]: ../../ci/autodeploy/index.md "GitLab Autodeploy"
+[autodeploy]: ../../topics/autodevops/index.md#auto-deploy "GitLab Autodeploy"
[kube-image]: https://gitlab.com/gitlab-examples/kubernetes-deploy/container_registry "Kubernetes deploy Container Registry"
[runners]: ../../ci/runners/README.md
diff --git a/doc/user/project/integrations/prometheus_library/cloudwatch.md b/doc/user/project/integrations/prometheus_library/cloudwatch.md
index 66f1b587070..01da7e00d74 100644
--- a/doc/user/project/integrations/prometheus_library/cloudwatch.md
+++ b/doc/user/project/integrations/prometheus_library/cloudwatch.md
@@ -18,7 +18,7 @@ The [Prometheus service](../prometheus.md) must be enabled.
## Configuring Prometheus to monitor for Cloudwatch metrics
-To get started with Cloudwatch monitoring, you should install and configure the [Cloudwatch exporter](https://github.com/hnlq715/nginx-vts-exporter) which retrieves and parses the specified Cloudwatch metrics and translates them into a Prometheus monitoring endpoint.
+To get started with Cloudwatch monitoring, you should install and configure the [Cloudwatch exporter](https://github.com/prometheus/cloudwatch_exporter) which retrieves and parses the specified Cloudwatch metrics and translates them into a Prometheus monitoring endpoint.
Right now, the only AWS resource supported is the Elastic Load Balancer, whose Cloudwatch metrics can be found [here](http://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-cloudwatch-metrics.html).
diff --git a/doc/user/project/integrations/prometheus_library/haproxy.md b/doc/user/project/integrations/prometheus_library/haproxy.md
index abb0c01ad18..6be8fc82431 100644
--- a/doc/user/project/integrations/prometheus_library/haproxy.md
+++ b/doc/user/project/integrations/prometheus_library/haproxy.md
@@ -11,8 +11,8 @@ The [Prometheus service](../prometheus.md) must be enabled.
| Name | Query |
| ---- | ----- |
-| Throughput (req/sec) | sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) by (code) |
-| HTTP Error Rate (%) | sum(rate(haproxy_frontend_http_requests_total{code="5xx",%{environment_filter}}[2m])) / sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) |
+| Throughput (req/sec) | `sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) by (code)` |
+| HTTP Error Rate (%) | `sum(rate(haproxy_frontend_http_requests_total{code="5xx",%{environment_filter}}[2m])) / sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m]))` |
## Configuring Prometheus to monitor for HAProxy metrics
diff --git a/doc/user/project/integrations/prometheus_library/nginx.md b/doc/user/project/integrations/prometheus_library/nginx.md
index c4fea178ab5..7db9629c002 100644
--- a/doc/user/project/integrations/prometheus_library/nginx.md
+++ b/doc/user/project/integrations/prometheus_library/nginx.md
@@ -14,9 +14,9 @@ NGINX server metrics are detected, which tracks the pages and content directly s
| Name | Query |
| ---- | ----- |
-| Throughput (req/sec) | sum(rate(nginx_server_requests{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (code) |
-| Latency (ms) | avg(nginx_server_requestMsec{%{environment_filter}}) |
-| HTTP Error Rate (HTTP Errors / sec) | sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m])) |
+| Throughput (req/sec) | `sum(rate(nginx_server_requests{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (code)` |
+| Latency (ms) | `avg(nginx_server_requestMsec{%{environment_filter}})` |
+| HTTP Error Rate (HTTP Errors / sec) | `sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m]))` |
## Configuring Prometheus to monitor for NGINX metrics
diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md
index de7fc93f0a4..fd743855a8c 100644
--- a/doc/user/project/integrations/prometheus_library/nginx_ingress.md
+++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md
@@ -14,9 +14,9 @@ GitLab has support for automatically detecting and monitoring the Kubernetes NGI
| Name | Query |
| ---- | ----- |
-| Throughput (req/sec) | sum(label_replace(rate(nginx_ingress_controller_requests{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m]), "status_code", "${1}xx", "status", "(.)..")) by (status_code) |
-| Latency (ms) | sum(rate(nginx_ingress_controller_ingress_upstream_latency_seconds_sum{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) / sum(rate(nginx_ingress_controller_ingress_upstream_latency_seconds_count{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) * 1000 |
-| HTTP Error Rate (%) | sum(rate(nginx_ingress_controller_requests{status=~"5.*",namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) / sum(rate(nginx_ingress_controller_requests{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) * 100 |
+| Throughput (req/sec) | `sum(label_replace(rate(nginx_ingress_controller_requests{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m]), "status_code", "${1}xx", "status", "(.)..")) by (status_code)` |
+| Latency (ms) | `sum(rate(nginx_ingress_controller_ingress_upstream_latency_seconds_sum{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) / sum(rate(nginx_ingress_controller_ingress_upstream_latency_seconds_count{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) * 1000` |
+| HTTP Error Rate (%) | `sum(rate(nginx_ingress_controller_requests{status=~"5.*",namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) / sum(rate(nginx_ingress_controller_requests{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) * 100` |
## Configuring NGINX ingress monitoring
diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md b/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md
index 31ac53c0d14..b03787b3e0d 100644
--- a/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md
+++ b/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md
@@ -14,9 +14,9 @@ GitLab has support for automatically detecting and monitoring the Kubernetes NGI
| Name | Query |
| ---- | ----- |
-| Throughput (req/sec) | sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code) |
-| Latency (ms) | avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}) |
-| HTTP Error Rate (%) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) * 100 |
+| Throughput (req/sec) | `sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)` |
+| Latency (ms) | `avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"})` |
+| HTTP Error Rate (%) | `sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) * 100` |
## Configuring NGINX ingress monitoring
diff --git a/doc/user/project/issues/automatic_issue_closing.md b/doc/user/project/issues/automatic_issue_closing.md
index c3e06b219ff..dab79327d6a 100644
--- a/doc/user/project/issues/automatic_issue_closing.md
+++ b/doc/user/project/issues/automatic_issue_closing.md
@@ -1,63 +1,5 @@
-# Automatic issue closing
-
->**Notes:**
->
-> - This is the user docs. In order to change the default issue closing pattern,
-> follow the steps in the [administration docs].
-> - For performance reasons, automatic issue closing is disabled for the very
-> first push from an existing repository.
-
-When a commit or merge request resolves one or more issues, it is possible to
-automatically have these issues closed when the commit or merge request lands
-in the project's default branch.
-
-If a commit message or merge request description contains a sentence matching
-a certain regular expression, all issues referenced from the matched text will
-be closed. This happens when the commit is pushed to a project's
-[**default** branch](../repository/branches/index.md#default-branch), or when a
-commit or merge request is merged into it.
-
-## Default closing pattern value
-
-When not specified, the default issue closing pattern as shown below will be
-used:
-
-```bash
-((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)|[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)
-```
-
-Note that `%{issue_ref}` is a complex regular expression defined inside GitLab's
-source code that can match references to:
-
-- A local issue (`#123`).
-- A cross-project issue (`group/project#123`).
-- A link to an issue
- (`https://gitlab.example.com/group/project/issues/123`).
-
---
-
-This translates to the following keywords:
-
-- Close, Closes, Closed, Closing, close, closes, closed, closing
-- Fix, Fixes, Fixed, Fixing, fix, fixes, fixed, fixing
-- Resolve, Resolves, Resolved, Resolving, resolve, resolves, resolved, resolving
-- Implement, Implements, Implemented, Implementing, implement, implements, implemented, implementing
-
+redirect_to: 'managing_issues.md#closing-issues-automatically'
---
-For example the following commit message:
-
-```
-Awesome commit message
-
-Fix #20, Fixes #21 and Closes group/otherproject#22.
-This commit is also related to #17 and fixes #18, #19
-and https://gitlab.example.com/group/otherproject/issues/23.
-```
-
-will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed
-to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as
-it does not match the pattern. It works with multi-line commit messages as well
-as one-liners when used with `git commit -m`.
-
-[administration docs]: ../../../administration/issue_closing_pattern.md
+This document was moved to [another location](managing_issues.md#closing-issues-automatically).
diff --git a/doc/user/project/issues/closing_issues.md b/doc/user/project/issues/closing_issues.md
index 1d88745af9f..04f1c8e1a4a 100644
--- a/doc/user/project/issues/closing_issues.md
+++ b/doc/user/project/issues/closing_issues.md
@@ -1,59 +1,5 @@
-# Closing Issues
+---
+redirect_to: 'managing_issues.md#closing-issues'
+---
-Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues.
-
-## Directly
-
-Whenever you decide that's no longer need for that issue,
-close the issue using the close button:
-
-![close issue - button](img/button_close_issue.png)
-
-## Via Merge Request
-
-When a merge request resolves the discussion over an issue, you can
-make it close that issue(s) when merged.
-
-All you need is to use a [keyword](automatic_issue_closing.md)
-accompanying the issue number, add to the description of that MR.
-
-In this example, the keyword "closes" prefixing the issue number will create a relationship
-in such a way that the merge request will close the issue when merged.
-
-Mentioning various issues in the same line also works for this purpose:
-
-```md
-Closes #333, #444, #555 and #666
-```
-
-If the issue is in a different repository rather then the MR's,
-add the full URL for that issue(s):
-
-```md
-Closes #333, #444, and https://gitlab.com/<username>/<projectname>/issues/<xxx>
-```
-
-All the following keywords will produce the same behaviour:
-
-- Close, Closes, Closed, Closing, close, closes, closed, closing
-- Fix, Fixes, Fixed, Fixing, fix, fixes, fixed, fixing
-- Resolve, Resolves, Resolved, Resolving, resolve, resolves, resolved, resolving
-
-![merge request closing issue when merged](img/merge_request_closes_issue.png)
-
-If you use any other word before the issue number, the issue and the MR will
-link to each other, but the MR will NOT close the issue(s) when merged.
-
-![mention issues in MRs - closing and related](img/closing_and_related_issues.png)
-
-## From the Issue Board
-
-You can close an issue from [Issue Boards](../issue_board.md) by dragging an issue card
-from its list and dropping into **Closed**.
-
-![close issue from the Issue Board](img/close_issue_from_board.gif)
-
-## Customizing the issue closing pattern
-
-Alternatively, a GitLab **administrator** can
-[customize the issue closing pattern](../../../administration/issue_closing_pattern.md).
+This document was moved to [another location](managing_issues.md#closing-issues).
diff --git a/doc/user/project/issues/create_new_issue.md b/doc/user/project/issues/create_new_issue.md
index c2916c79876..8eec29716c1 100644
--- a/doc/user/project/issues/create_new_issue.md
+++ b/doc/user/project/issues/create_new_issue.md
@@ -1,104 +1,5 @@
-# Create a new Issue
+---
+redirect_to: 'managing_issues.md#create-a-new-issue'
+---
-Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues.
-
-When you create a new issue, you'll be prompted to fill in
-the information illustrated on the image below.
-
-![New issue from the issues list](img/new_issue.png)
-
-Read through the [issue data and actions documentation](issue_data_and_actions.md#parts-of-an-issue)
-to understand these fields one by one.
-
-## New issue from the Issue Tracker
-
-Navigate to your **Project's Dashboard** > **Issues** > **New Issue** to create a new issue:
-
-![New issue from the issue list view](img/new_issue_from_tracker_list.png)
-
-## New issue from an opened issue
-
-From an **opened issue** in your project, click **New Issue** to create a new
-issue in the same project:
-
-![New issue from an open issue](img/new_issue_from_open_issue.png)
-
-## New issue from the project's dashboard
-
-From your **Project's Dashboard**, click the plus sign (**+**) to open a dropdown
-menu with a few options. Select **New Issue** to create an issue in that project:
-
-![New issue from a project's dashboard](img/new_issue_from_projects_dashboard.png)
-
-## New issue from the Issue Board
-
-From an Issue Board, create a new issue by clicking on the plus sign (**+**) on the top of a list.
-It opens a new issue for that project labeled after its respective list.
-
-![From the issue board](img/new_issue_from_issue_board.png)
-
-## New issue via email
-
-At the bottom of a project's Issues List page, a link to **Email a new issue to this project**
-is displayed if your GitLab instance has [incoming email](../../../administration/incoming_email.md) configured.
-
-![Bottom of a project issues page](img/new_issue_from_email.png)
-
-When you click this link, an email address is displayed which belongs to you for creating issues in this project.
-You can save this address as a contact in your email client for easy acceess.
-
-CAUTION: **Caution:**
-This is a private email address, generated just for you. **Keep it to yourself**,
-as anyone who gets ahold of it can create issues or merge requests as if they
-were you. If the address is compromised, or you'd like it to be regenerated for
-any reason, click **Email a new issue to this project** again and click the reset link.
-
-Sending an email to this address will create a new issue on your behalf for
-this project, where:
-
-- The email subject becomes the issue title.
-- The email body becomes the issue description.
-- [Markdown](../../markdown.md) and [quick actions](../quick_actions.md) are supported.
-
-NOTE: **Note:**
-In GitLab 11.7, we updated the format of the generated email address.
-However the older format is still supported, allowing existing aliases
-or contacts to continue working._
-
-## New issue via Service Desk **[PREMIUM]**
-
-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.
-
-## New issue from the group-level Issue Tracker
-
-Head to the Group dashboard and click "Issues" in the sidebar to visit the Issue Tracker
-for all projects in your Group. Select the project you'd like to add an issue for
-using the dropdown button at the top-right of the page.
-
-![Select project to create issue](img/select_project_from_group_level_issue_tracker.png)
-
-We'll keep track of the project you selected most recently, and use it as the default
-for your next visit. This should save you a lot of time and clicks, if you mostly
-create issues for the same project.
-
-![Create issue from group-level issue tracker](img/create_issue_from_group_level_issue_tracker.png)
-
-## New issue via URL with prefilled fields
-
-You can link directly to the new issue page for a given project, with prefilled
-field values using query string parameters in a URL. This is useful for embedding
-a URL in an external HTML page, and also certain scenarios where you want the user to
-create an issue with certain fields prefilled.
-
-The title, description, and description template fields can be prefilled using
-this method. The description and description template fields cannot be pre-entered
-in the same URL (since a description template just populates the description field).
-
-Follow these examples to form your new issue URL with prefilled fields.
-
-- For a new issue in the GitLab Community Edition project with a pre-entered title
- and a pre-entered description, the URL would be `https://gitlab.com/gitlab-org/gitlab-ce/issues/new?issue[title]=Validate%20new%20concept&issue[description]=Research%20idea`
-- For a new issue in the GitLab Community Edition project with a pre-entered title
- and a pre-entered description template, the URL would be `https://gitlab.com/gitlab-org/gitlab-ce/issues/new?issue[title]=Validate%20new%20concept&issuable_template=Research%20proposal`
+This document was moved to [another location](managing_issues.md#create-a-new-issue).
diff --git a/doc/user/project/issues/crosslinking_issues.md b/doc/user/project/issues/crosslinking_issues.md
index ff5b1f2ce50..93dc2a2e4ca 100644
--- a/doc/user/project/issues/crosslinking_issues.md
+++ b/doc/user/project/issues/crosslinking_issues.md
@@ -25,9 +25,8 @@ git commit -m "this is my commit message. Related to https://gitlab.com/<usernam
Of course, you can replace `gitlab.com` with the URL of your own GitLab instance.
-**Note:** Linking your first commit to your issue is going to be relevant
-for tracking your process far ahead with
-[GitLab Cycle Analytics](https://about.gitlab.com/features/cycle-analytics/)).
+NOTE: **Note:** Linking your first commit to your issue is going to be relevant
+for tracking your process with [GitLab Cycle Analytics](https://about.gitlab.com/features/cycle-analytics/).
It will measure the time taken for planning the implementation of that issue,
which is the time between creating an issue and making the first commit.
@@ -35,14 +34,13 @@ which is the time between creating an issue and making the first commit.
Mentioning related issues in merge requests and other issues is useful
for your team members and collaborators to know that there are opened
-issues around that same idea.
+issues regarding the same topic.
-You do that as explained above, when
-[mentioning an issue from a commit message](#from-commit-messages).
+You do that as explained above, when [mentioning an issue from a commit message](#from-commit-messages).
-When mentioning the issue "A" in issue "B", the issue "A" will also
-display a notification in its tracker. The same is valid for mentioning
-issues in merge requests.
+When mentioning issue `#111` in issue `#222`, issue `#111` will also display a notification
+in its tracker. That is, you only need to mention the relationship once for it to
+display in both issues. The same is valid when mentioning issues in [merge requests](#from-merge-requests).
![issue mentioned in issue](img/mention_in_issue.png)
@@ -53,10 +51,7 @@ they do for [related issues](#from-related-issues).
When you mention an issue in a merge request description, it will simply
[link the issue and merge request together](#from-related-issues). Additionally,
-you can also [set an issue to close as soon as the merge request is merged](closing_issues.md#via-merge-request).
+you can also [set an issue to close automatically](managing_issues.md#closing-issues-automatically)
+as soon as the merge request is merged.
![issue mentioned in MR](img/mention_in_merge_request.png)
-
-### Close an issue by merging a merge request
-
-To [close an issue when a merge request is merged](closing_issues.md#via-merge-request), use the [automatic issue closing pattern](automatic_issue_closing.md).
diff --git a/doc/user/project/issues/csv_import.md b/doc/user/project/issues/csv_import.md
index b0b1cfcfdf7..cc0d5ac9028 100644
--- a/doc/user/project/issues/csv_import.md
+++ b/doc/user/project/issues/csv_import.md
@@ -7,7 +7,8 @@ Issues can be imported to a project by uploading a CSV file with the columns
The user uploading the CSV file will be set as the author of the imported issues.
-> **Note:** A permission level of `Developer` or higher is required to import issues.
+NOTE: **Note:** A permission level of [Developer](../../permissions.md), or higher, is required
+to import issues.
## Prepare for the import
@@ -24,42 +25,26 @@ To import issues:
1. Select the file and click the **Import issues** button.
The file is processed in the background and a notification email is sent
-to you once the import is completed.
+to you once the import is complete.
## CSV file format
-### Header row
+When importing issues from a CSV file, it must be formatted in a certain way:
-CSV files must contain a header row where the first column header is `title` and the second is `description`.
-If additional columns are present, they will be ignored.
+- **header row:** CSV files must contain a header row where the first column header
+ is `title` and the second is `description`. If additional columns are present, they
+ will be ignored.
+- **separators:** The column separator is automatically detected from the header row.
+ Supported separator characters are: commas (`,`), semicolons (`;`), and tabs (`\t`).
+ The row separator can be either `CRLF` or `LF`.
+- **double-quote character:** The double-quote (`"`) character is used to quote fields,
+ enabling the use of the column separator within a field (see the third line in the
+ sample CSV data below). To insert a double-quote (`"`) within a quoted
+ field, use two double-quote characters in succession, i.e. `""`.
+- **data rows:** After the header row, succeeding rows must follow the same column
+ order. The issue title is required while the description is optional.
-### Column separator
-
-The column separator is automatically detected from the header row.
-
-Supported separator characters are: commas (`,`), semicolons (`;`), and tabs (`\t`).
-
-### Row separator
-
-Lines ending in either `CRLF` or `LF` are supported.
-
-### Quote character
-
-The double-quote (`"`) character is used to quote fields so you can use the column separator within a field. To insert
-a double-quote (`"`) within a quoted field, use two double-quote characters in succession, i.e. `""`.
-
-### Data rows
-
-After the header row, succeeding rows must follow the same column order. The issue title is required while the
-description is optional.
-
-### File size
-
-The limit depends on the configuration value of Max Attachment Size for the GitLab instance.
-
-For GitLab.com, it is set to 10 MB.
-
-## Sample data
+Sample CSV data:
```csv
title,description
@@ -67,3 +52,9 @@ My Issue Title,My Issue Description
Another Title,"A description, with a comma"
"One More Title","One More Description"
```
+
+### File size
+
+The limit depends on the configuration value of Max Attachment Size for the GitLab instance.
+
+For GitLab.com, it is set to 10 MB.
diff --git a/doc/user/project/issues/deleting_issues.md b/doc/user/project/issues/deleting_issues.md
index 536a0de8974..e50259e0dcf 100644
--- a/doc/user/project/issues/deleting_issues.md
+++ b/doc/user/project/issues/deleting_issues.md
@@ -1,13 +1,5 @@
-# Deleting Issues
+---
+redirect_to: 'managing_issues.md#deleting-issues'
+---
-> [Introduced][ce-2982] in GitLab 8.6
-
-Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues.
-
-You can delete an issue by editing it and clicking on the delete button.
-
-![delete issue - button](img/delete_issue.png)
-
->**Note:** Only [project owners](../../permissions.md) can delete issues.
-
-[ce-2982]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2982 \ No newline at end of file
+This document was moved to [another location](managing_issues.md#deleting-issues).
diff --git a/doc/user/project/issues/due_dates.md b/doc/user/project/issues/due_dates.md
index 987c16dfab6..bd3298497d2 100644
--- a/doc/user/project/issues/due_dates.md
+++ b/doc/user/project/issues/due_dates.md
@@ -4,30 +4,32 @@
Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues.
-Due dates can be used in issues to keep track of deadlines and make sure
-features are shipped on time. Due dates require at least [Reporter permissions](../../permissions.md#project-members-permissions)
-to be able to edit them. On the contrary, they can be seen by everybody.
+Due dates can be used in issues to keep track of deadlines and make sure features are
+shipped on time. Users must have at least [Reporter permissions](../../permissions.md)
+to be able to edit them, but they can be seen by everybody with permission to view
+the issue.
## Setting a due date
-When creating or editing an issue, you can see the due date field from where
-a calendar will appear to help you choose the date you want. To remove it,
-select the date text and delete it.
+When creating or editing an issue, you can click in the **due date** field and a calendar
+will appear to help you choose the date you want. To remove the date, select the date
+text and delete it. The date is related to the server's timezone, not the timezone of
+the user setting the due date.
![Create a due date](img/due_dates_create.png)
-A quicker way to set a due date is via the issue sidebar. Simply expand the
-sidebar and select **Edit** to pick a due date or remove the existing one.
+You can also set a due date via the issue sidebar. Expand the
+sidebar and click **Edit** to pick a due date or remove the existing one.
Changes are saved immediately.
![Edit a due date via the sidebar](img/due_dates_edit_sidebar.png)
## Making use of due dates
-Issues that have a due date can be distinctively seen in the issue tracker
+Issues that have a due date can be easily seen in the issue tracker,
displaying a date next to them. Issues where the date is overdue will have
the icon and the date colored red. You can sort issues by those that are
-_Due soon_ or _Due later_ from the dropdown menu in the right.
+`Due soon` or `Due later` from the dropdown menu on the right.
![Issues with due dates in the issues index page](img/due_dates_issues_index_page.png)
@@ -36,14 +38,13 @@ Due dates also appear in your [todos list](../../../workflow/todos.md).
![Issues with due dates in the todos](img/due_dates_todos.png)
The day before an open issue is due, an email will be sent to all participants
-of the issue. Both the due date and the day before are calculated using the
+of the issue. Like the due date, the "day before the due date" is determined by the
server's timezone.
Issues with due dates can also be exported as an iCalendar feed. The URL of the
feed can be added to calendar applications. The feed is accessible by clicking
-on the _Subscribe to calendar_ button on the following pages:
+on the **Subscribe to calendar** button on the following pages:
-- on the **Assigned Issues** page that is linked on the right-hand side of the
- GitLab header
+- on the **Assigned Issues** page that is linked on the right-hand side of the GitLab header
- on the **Project Issues** page
- on the **Group Issues** page
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index 4acbb4cc3f6..f69b841e908 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -6,8 +6,9 @@ Issues are the fundamental medium for collaborating on ideas and planning work i
The GitLab issue tracker is an advanced tool for collaboratively developing ideas, solving problems, and planning work.
-Issues can allow you, your team, and your collaborators to share and discuss proposals before and during their implementation.
-However, they can be used for a variety of other purposes, customized to your needs and workflow.
+Issues can allow you, your team, and your collaborators to share and discuss proposals
+before, and during, their implementation. However, they can be used for a variety of
+other purposes, customized to your needs and workflow.
Issues are always associated with a specific project, but if you have multiple projects in a group,
you can also view all the issues collectively at the group level.
@@ -17,13 +18,15 @@ you can also view all the issues collectively at the group level.
- Discussing the implementation of a new idea
- Tracking tasks and work status
- Accepting feature proposals, questions, support requests, or bug reports
-- Elaborating new code implementations
+- Elaborating on new code implementations
-See also the blog post "[Always start a discussion with an issue](https://about.gitlab.com/2016/03/03/start-with-an-issue/)".
+See also [Always start a discussion with an issue](https://about.gitlab.com/2016/03/03/start-with-an-issue/).
## Parts of an issue
-Issues contain a variety of content and metadata, enabling a large range of flexibility in how they are used. Each issue can contain the following attributes, though some items may remain unset.
+Issues contain a variety of content and metadata, enabling a large range of flexibility
+in how they are used. Each issue can contain the following attributes, though not all items
+must be set.
<table class="borderless-table fixed-table">
<tr>
@@ -70,23 +73,36 @@ Issues contain a variety of content and metadata, enabling a large range of flex
## Viewing and managing issues
-While you can view and manage the full detail of an issue at its URL, you can also work with multiple issues at a time using the Issues List, Issue Boards, Epics **[ULTIMATE]**, and issue references.
+While you can view and manage the full details of an issue on the [issue page](#issue-page),
+you can also work with multiple issues at a time using the [Issues List](#issues-list),
+[Issue Boards](#issue-boards), Issue references, and [Epics](#epics-ultimate)**[ULTIMATE]**.
+
+Key actions for Issues include:
+
+- [Creating issues](managing_issues.md#create-a-new-issue)
+- [Moving issues](managing_issues.md#moving-issues)
+- [Closing issues](managing_issues.md#closing-issues)
+- [Deleting issues](managing_issues.md#deleting-issues)
### Issue page
![Issue view](img/issues_main_view.png)
-On an issue’s page, you can view all aspects of the issue, and you can also modify them if you you have the necessary [permissions](../../permissions.md).
-
-For more information, see the [Issue Data and Actions](issue_data_and_actions.md) page.
+On an issue's page, you can view [all aspects of the issue](issue_data_and_actions.md),
+and modify them if you you have the necessary [permissions](../../permissions.md).
### Issues list
![Project issues list view](img/project_issues_list_view.png)
-On the Issues List, you can view all issues in the current project, or from multiple projects when opening the Issues List from the higher-level group context. Filter the issue list by [any search query](../../search/index.md#issues-and-merge-requests-per-project) and/or specific metadata, such as label(s), assignees(s), status, and more. From this view, you can also make certain changes [in bulk](../bulk_editing.md) to the displayed issues.
+On the Issues List, you can view all issues in the current project, or from multiple
+projects when opening the Issues List from the higher-level group context. Filter the
+issue list with a [search query](../../search/index.md#issues-and-merge-requests-per-project),
+including specific metadata, such as label(s), assignees(s), status, and more. From this
+view, you can also make certain changes [in bulk](../bulk_editing.md) to the displayed issues.
-For more information on interacting with Issues, see the [Issue Data and Actions](issue_data_and_actions.md) page.
+For more information, see the [Issue Data and Actions](issue_data_and_actions.md) page
+for a rundown of all the fields and information in an issue.
For sorting by issue priority, see [Label Priority](../labels.md#label-priority).
@@ -94,44 +110,55 @@ For sorting by issue priority, see [Label Priority](../labels.md#label-priority)
![Issue board](img/issue_board.png)
-Issue boards are Kanban boards with columns that display issues based on their labels or their assignees**[PREMIUM]**. They offer the flexibility to manage issues using highly customizable workflows.
+[Issue boards](../issue_board.md) are Kanban boards with columns that display issues based on their labels
+or their assignees**[PREMIUM]**. They offer the flexibility to manage issues using
+highly customizable workflows.
-You can reorder issues within a column, or drag an issue card to another column; its associated label or assignee will change to match that of the new column. The entire board can also be filtered to only include issues from a certain milestone or an overarching label.
-
-For more information, see the [Issue Boards](../issue_board.md) page.
+You can reorder issues within a column. If you drag an issue card to another column, its
+associated label or assignee will change to match that of the new column. The entire
+board can also be filtered to only include issues from a certain milestone or an overarching
+label.
### Epics **[ULTIMATE]**
-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](../../group/epics/index.md) page.
+[Epics](../../group/epics/index.md) 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.
### 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](related_issues.md).
+You can mark two issues as related, so that when viewing one, the other is always
+listed in its [Related Issues](related_issues.md) section. This can help display important
+context, such as past work, dependencies, or duplicates.
### Crosslinking issues
-When you reference an issue from another issue or merge request by including its URL or ID, the referenced issue displays a message in the Activity stream about the reference, with a link to the other issue or MR.
+You can [crosslink issues](crosslinking_issues.md) by referencing an issue from another
+issue or merge request by including its URL or ID. The referenced issue displays a
+message in the Activity stream about the reference, with a link to the other issue or MR.
-For more information, see [Crosslinking issues](crosslinking_issues.md).
+### Similar issues
-## Issue actions
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22866) in GitLab 11.6.
-- [Create an issue](create_new_issue.md)
-- [Create an issue from a template](../../project/description_templates.md#using-the-templates)
-- [Close an issue](closing_issues.md)
-- [Move an issue](moving_issues.md)
-- [Delete an issue](deleting_issues.md)
-- [Create a merge request from an issue](issue_data_and_actions.md#22-create-merge-request)
+To prevent duplication of issues for the same topic, GitLab searches for similar issues
+when new issues are being created.
+
+When typing in the title in the **New Issue** page, GitLab searches titles and descriptions
+across all issues the user has access to in the current project. Up 5 similar issues,
+sorted by most recently updated, are displayed below the title box. Note that this feature
+requires [GraphQL](../../../api/graphql/index.md) to be enabled.
-## Advanced issue management
+![Similar issues](img/similar_issues.png)
-- [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.
+## Other Issue actions
+
+- [Create an issue from a template](../../project/description_templates.md#using-the-templates)
+- [Set a due date](due_dates.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](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.
+- 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 2103f331aa2..b34263f0eec 100644
--- a/doc/user/project/issues/issue_data_and_actions.md
+++ b/doc/user/project/issues/issue_data_and_actions.md
@@ -39,16 +39,19 @@ after it is closed.
![Report Abuse](img/report-abuse.png)
-#### 2. Todos
+#### 2. To Do
-You can click **add todo** to add the issue to your [GitLab Todo](../../../workflow/todos.md)
-list. If it is already on your todo list, the buttom will show **mark todo as done**,
-which you can click to mark that issue as done (which will be reflected in the Todo list).
+You can add issues to and remove issues from your [GitLab To-Do List](../../../workflow/todos.md).
+
+The button to do this has a different label depending on whether the issue is already on your To-Do List or not. If the issue is:
+
+- Already on your To-Do List: The button is labeled **Mark as done**. Click the button to remove the issue from your To-Do List.
+- Not on your To-Do List: The button is labelled **Add a To Do**. Click the button to add the issue to your To-Do List.
#### 3. Assignee
An issue can be assigned to yourself, another person, or [many people](#31-multiple-assignees-STARTER).
-The assignee(s) can be changed as much as needed. The idea is that the assignees are
+The assignee(s) can be changed as often as needed. The idea is that the assignees are
responsible for that issue until it's reassigned to someone else to take it from there.
When assigned to someone, it will appear in their assigned issues list.
@@ -206,6 +209,14 @@ You can filter what is displayed in the issue history by clicking on **Show all
and selecting either **Show comments only**, which only shows discussions and hides
updates to the issue, or **Show history only**, which hides discussions and only shows updates.
+- You can mention a user or a group present in your GitLab instance with
+ `@username` or `@groupname` and they will be notified via To-Do items
+ and email, unless they have [disabled all notifications](#13-notifications)
+ in their profile settings.
+- Mentions for yourself (the current logged in user), will be highlighted
+ in a different color, allowing you to easily see which comments involve you,
+ helping you focus on them quickly.
+
![Show all activity](img/show-all-activity.png)
#### 22. Create Merge Request
diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md
new file mode 100644
index 00000000000..91dbf0d848e
--- /dev/null
+++ b/doc/user/project/issues/managing_issues.md
@@ -0,0 +1,225 @@
+# Managing Issues
+
+[GitLab Issues](index.md) are the fundamental medium for collaborating on ideas and
+planning work in GitLab. [Creating](#create-a-new-issue), [moving](#moving-issues),
+[closing](#closing-issues), and [deleting](#deleting-issues) are key actions that
+you can do with issues.
+
+## Create a new Issue
+
+When you create a new issue, you'll be prompted to fill in the [data and fields of the issue](issue_data_and_actions.md#parts-of-an-issue), as illustrated below.
+
+![New issue from the issues list](img/new_issue.png)
+
+### Accessing the new Issue form
+
+There are many ways to get to the new Issue form from within a project:
+
+- Navigate to your **Project's Dashboard** > **Issues** > **New Issue**:
+
+ ![New issue from the issue list view](img/new_issue_from_tracker_list.png)
+
+- From an **opened issue** in your project, click **New Issue** to create a new
+ issue in the same project:
+
+ ![New issue from an open issue](img/new_issue_from_open_issue.png)
+
+- From your **Project's Dashboard**, click the plus sign (**+**) to open a dropdown
+ menu with a few options. Select **New Issue** to create an issue in that project:
+
+ ![New issue from a project's dashboard](img/new_issue_from_projects_dashboard.png)
+
+- From an **Issue Board**, create a new issue by clicking on the plus sign (**+**) at the top of a list.
+ It opens a new issue for that project, pre-labeled with its respective list.
+
+ ![From the issue board](img/new_issue_from_issue_board.png)
+
+### New issue from the group-level Issue Tracker
+
+Go to the Group dashboard and click "Issues" in the sidebar to visit the Issue Tracker
+for all projects in your Group. Select the project you'd like to add an issue for
+using the dropdown button at the top-right of the page.
+
+![Select project to create issue](img/select_project_from_group_level_issue_tracker.png)
+
+We'll keep track of the project you selected most recently, and use it as the default
+for your next visit. This should save you a lot of time and clicks, if you mostly
+create issues for the same project.
+
+![Create issue from group-level issue tracker](img/create_issue_from_group_level_issue_tracker.png)
+
+### New issue via Service Desk **[PREMIUM]**
+
+Enable [Service Desk](../service_desk.md) for 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.
+
+### New issue via email
+
+A link to **Email a new issue to this project** is displayed at the bottom of a project's
+**Issues List** page, if your GitLab instance has [incoming email](../../../administration/incoming_email.md)
+configured.
+
+![Bottom of a project issues page](img/new_issue_from_email.png)
+
+When you click this link, an email address is generated and displayed, which should be used
+by **you only**, to create issues in this project. You can save this address as a
+contact in your email client for easy acceess.
+
+CAUTION: **Caution:**
+This is a private email address, generated just for you. **Keep it to yourself**,
+as anyone who knows it can create issues or merge requests as if they
+were you. If the address is compromised, or you'd like it to be regenerated for
+any reason, click **Email a new issue to this project** again and click the reset link.
+
+Sending an email to this address will create a new issue in your name for
+this project, where:
+
+- The email subject becomes the issue title.
+- The email body becomes the issue description.
+- [Markdown](../../markdown.md) and [quick actions](../quick_actions.md) are supported.
+
+NOTE: **Note:**
+In GitLab 11.7, we updated the format of the generated email address. However the
+older format is still supported, allowing existing aliases or contacts to continue working.
+
+### New issue via URL with prefilled fields
+
+You can link directly to the new issue page for a given project, with prefilled
+field values using query string parameters in a URL. This is useful for embedding
+a URL in an external HTML page, and also certain scenarios where you want the user to
+create an issue with certain fields prefilled.
+
+The title, description, and description template fields can be prefilled using
+this method. You cannot pre-fill both the description and description template fields
+in the same URL (since a description template also populates the description field).
+
+Follow these examples to form your new issue URL with prefilled fields.
+
+- For a new issue in the GitLab Community Edition project with a pre-filled title
+ and a pre-filled description, the URL would be `https://gitlab.com/gitlab-org/gitlab-ce/issues/new?issue[title]=Validate%20new%20concept&issue[description]=Research%20idea`
+- For a new issue in the GitLab Community Edition project with a pre-filled title
+ and a pre-filled description template, the URL would be `https://gitlab.com/gitlab-org/gitlab-ce/issues/new?issue[title]=Validate%20new%20concept&issuable_template=Research%20proposal`
+
+## Moving Issues
+
+Moving an issue will copy it to a new location (project), and close it in the old project,
+but it will not be deleted. There will also be a system note added to both issues
+indicating where it came from and went to.
+
+The "Move issue" button is at the bottom of the right-sidebar when viewing the issue.
+
+![move issue - button](img/sidebar_move_issue.png)
+
+### Moving Issues in Bulk
+
+If you have advanced technical skills you can also bulk move all the issues from one project to another in the rails console. The below script will move all the issues from one project to another that are not in status **closed**.
+
+To access rails console run `sudo gitlab-rails console` on the GitLab server and run the below script. Please be sure to change **project**, **admin_user** and **target_project** to your values. We do also recommend [creating a backup](https://docs.gitlab.com/ee/raketasks/backup_restore.html#creating-a-backup-of-the-gitlab-system) before attempting any changes in the console.
+
+```ruby
+project = Project.find_by_full_path('full path of the project where issues are moved from')
+issues = project.issues
+admin_user = User.find_by_username('username of admin user') # make sure user has permissions to move the issues
+target_project = Project.find_by_full_path('full path of target project where issues moved to')
+
+issues.each do |issue|
+ if issue.state != "closed" && issue.moved_to.nil?
+ Issues::MoveService.new(project, admin_user).execute(issue, target_project)
+ else
+ puts "issue with id: #{issue.id} and title: #{issue.title} was not moved"
+ end
+end; nil
+```
+
+## Closing Issues
+
+When you decide that an issue is resolved, or no longer needed, you can close the issue
+using the close button:
+
+![close issue - button](img/button_close_issue.png)
+
+You can also close an issue from the [Issue Boards](../issue_board.md) by dragging an issue card
+from its list and dropping it into the **Closed** list.
+
+![close issue from the Issue Board](img/close_issue_from_board.gif)
+
+### Closing issues automatically
+
+NOTE: **Note:**
+For performance reasons, automatic issue closing is disabled for the very first
+push from an existing repository.
+
+When a commit or merge request resolves one or more issues, it is possible to have
+these issues closed automatically when the commit or merge request reaches the project's
+default branch.
+
+If a commit message or merge request description contains text matching a [defined pattern](#default-closing-pattern),
+all issues referenced in the matched text will be closed. This happens when the commit
+is pushed to a project's [**default** branch](../repository/branches/index.md#default-branch),
+or when a commit or merge request is merged into it.
+
+For example, if `Closes #4, #6, Related to #5` is included in a Merge Request
+description, issues `#4` and `#6` will close automatically when the MR is merged, but not `#5`.
+Using `Related to` flags `#5` as a [related issue](related_issues.md),
+but it will not close automatically.
+
+![merge request closing issue when merged](img/merge_request_closes_issue.png)
+
+If the issue is in a different repository than the MR, add the full URL for the issue(s):
+
+```md
+Closes #4, #6, and https://gitlab.com/<username>/<projectname>/issues/<xxx>
+```
+
+#### Default closing pattern
+
+When not specified, the default issue closing pattern as shown below will be used:
+
+```bash
+((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)|[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)
+```
+
+This translates to the following keywords:
+
+- Close, Closes, Closed, Closing, close, closes, closed, closing
+- Fix, Fixes, Fixed, Fixing, fix, fixes, fixed, fixing
+- Resolve, Resolves, Resolved, Resolving, resolve, resolves, resolved, resolving
+- Implement, Implements, Implemented, Implementing, implement, implements, implemented, implementing
+
+Note that `%{issue_ref}` is a complex regular expression defined inside GitLab's
+source code that can match references to:
+
+- A local issue (`#123`).
+- A cross-project issue (`group/project#123`).
+- A link to an issue (`https://gitlab.example.com/group/project/issues/123`).
+
+For example the following commit message:
+
+```
+Awesome commit message
+
+Fix #20, Fixes #21 and Closes group/otherproject#22.
+This commit is also related to #17 and fixes #18, #19
+and https://gitlab.example.com/group/otherproject/issues/23.
+```
+
+will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed to,
+as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as it does
+not match the pattern. It works with multi-line commit messages as well as one-liners
+when used from the command line with `git commit -m`.
+
+#### Customizing the issue closing pattern **[CORE ONLY]**
+
+In order to change the default issue closing pattern, GitLab administrators must edit the
+[`gitlab.rb` or `gitlab.yml` file](../../../administration/issue_closing_pattern.md)
+of your installation.
+
+## Deleting Issues
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2982) in GitLab 8.6
+
+Users with [project owner permission](../../permissions.md) can delete an issue by
+editing it and clicking on the delete button.
+
+![delete issue - button](img/delete_issue.png)
diff --git a/doc/user/project/issues/moving_issues.md b/doc/user/project/issues/moving_issues.md
index 8aac2c01444..8331f865b83 100644
--- a/doc/user/project/issues/moving_issues.md
+++ b/doc/user/project/issues/moving_issues.md
@@ -1,35 +1,5 @@
-# Moving Issues
-
-Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues.
-
-Moving an issue will close it and duplicate it on the specified project.
-There will also be a system note added to both issues indicating where it came from or went to.
-
-You can move an issue with the "Move issue" button at the bottom of the right-sidebar when viewing the issue.
-
-![move issue - button](img/sidebar_move_issue.png)
-
-## Troubleshooting
-
-### Moving Issues in Bulk
-
-If you have advanced technical skills you can also bulk move all the issues from one project to another in the rails console. The below script will move all the issues from one project to another that are not in status **closed**.
-
-To access rails console run `sudo gitlab-rails console` on the GitLab server and run the below script. Please be sure to change **project**, **admin_user** and **target_project** to your values. We do also recommend [creating a backup](https://docs.gitlab.com/ee/raketasks/backup_restore.html#creating-a-backup-of-the-gitlab-system) before attempting any changes in the console.
-
-```ruby
-project = Project.find_by_full_path('full path of the project where issues are moved from')
-issues = project.issues
-admin_user = User.find_by_username('username of admin user') # make sure user has permissions to move the issues
-target_project = Project.find_by_full_path('full path of target project where issues moved to')
-
-issues.each do |issue|
- if issue.state != "closed" && issue.moved_to.nil?
- Issues::MoveService.new(project, admin_user).execute(issue, target_project)
- else
- puts "issue with id: #{issue.id} and title: #{issue.title} was not moved"
- end
-end; nil
-
-```
+---
+redirect_to: 'managing_issues.md#moving-issues'
+---
+This document was moved to [another location](managing_issues.md#moving-issues).
diff --git a/doc/user/project/issues/similar_issues.md b/doc/user/project/issues/similar_issues.md
index e90ecd88ec6..9cbac53ee41 100644
--- a/doc/user/project/issues/similar_issues.md
+++ b/doc/user/project/issues/similar_issues.md
@@ -1,16 +1,5 @@
-# Similar issues
+---
+redirect_to: 'index.md#similar-issues'
+---
-> [Introduced][ce-22866] in GitLab 11.6.
-
-Similar issues suggests issues that are similar when new issues are being created.
-This features requires [GraphQL] to be enabled.
-
-![Similar issues](img/similar_issues.png)
-
-You can see the similar issues when typing in the title in the new issue form.
-This searches both titles and descriptions across all issues the user has access
-to in the current project. It then displays the first 5 issues sorted by most
-recently updated.
-
-[GraphQL]: ../../../api/graphql/index.md
-[ce-22866]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22866
+This document was moved to [another location](index.md#similar-issues).
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 447b338928c..169b10572b0 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -23,7 +23,7 @@ With GitLab merge requests, you can:
- Build, test, and deploy your code in a per-branch basis with built-in [GitLab CI/CD](../../../ci/README.md)
- Prevent the merge request from being merged before it's ready with [WIP MRs](#work-in-progress-merge-requests)
- View the deployment process through [Pipeline Graphs](../../../ci/pipelines.md#visualizing-pipelines)
-- [Automatically close the issue(s)](../../project/issues/closing_issues.md#via-merge-request) that originated the implementation proposed in the merge request
+- [Automatically close the issue(s)](../../project/issues/managing_issues.md#closing-issues-automatically) that originated the implementation proposed in the merge request
- Assign it to any registered user, and change the assignee how many times you need
- Assign a [milestone](../../project/milestones/index.md) and track the development of a broader implementation
- Organize your issues and merge requests consistently throughout the project with [labels](../../project/labels.md)
diff --git a/doc/user/project/merge_requests/squash_and_merge.md b/doc/user/project/merge_requests/squash_and_merge.md
index 39fd2588811..38459584eed 100644
--- a/doc/user/project/merge_requests/squash_and_merge.md
+++ b/doc/user/project/merge_requests/squash_and_merge.md
@@ -50,7 +50,10 @@ 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](revert_changes.md) if necessary.
+meaningful commit messages and:
+
+- It's simpler to [revert](revert_changes.md) if necessary.
+- The merged branch will retain the full commit history.
## Enabling squash for a merge request
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 9cd868067bc..142178ba241 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
@@ -15,7 +15,7 @@ being merged, and it will stay disabled until the "WIP" flag has been removed.
There are several ways to flag a merge request as a Work In Progress:
-- Add "[WIP]" or "WIP:" to the start of the merge request's title. Clicking on
+- Add `[WIP]` or `WIP:` to the start of the merge request's title. Clicking on
**Start the title with WIP:**, under the title box, when editing the merge request's
description will have the same effect.
- Add the `/wip` [quick action](../quick_actions.md#quick-actions-for-issues-and-merge-requests)
@@ -30,7 +30,7 @@ There are several ways to flag a merge request as a Work In Progress:
Similar to above, when a Merge Request is ready to be merged, you can remove the
"Work in Progress" flag in several ways:
-- Remove "[WIP]" or "WIP:" from the start of the merge request's title. Clicking on
+- Remove `[WIP]` or `WIP:` from the start of the merge request's title. Clicking on
**Remove the WIP: prefix from the title**, under the title box, when editing the merge
request's description, will have the same effect.
- Add the `/wip` [quick action](../quick_actions.md#quick-actions-for-issues-and-merge-requests)
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
index fa79c393b72..64b1e259292 100644
--- a/doc/user/project/pages/index.md
+++ b/doc/user/project/pages/index.md
@@ -140,7 +140,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) | Requirements, technical aspects, specific GitLab CI's configuration options, custom 404 pages, limitations, FAQ. |
+| [Exploring GitLab Pages](introduction.md) | Requirements, technical aspects, specific GitLab CI's configuration options, Access Control, 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/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md
index 002addfc043..f9feae36dbc 100644
--- a/doc/user/project/pipelines/job_artifacts.md
+++ b/doc/user/project/pipelines/job_artifacts.md
@@ -187,7 +187,8 @@ information in the UI.
DANGER: **Warning:**
This is a destructive action that leads to data loss. Use with caution.
-If you have at least Developer [permissions](../../permissions.md#gitlab-cicd-permissions)
+If you are either the owner of a given job or have Master
+[permissions](../../permissions.md#gitlab-cicd-permissions)
on the project, you can erase a single job via the UI which will also remove the
artifacts and the job's trace.
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 1281ba561b8..d20b44d4b92 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -16,8 +16,8 @@ discussions, and descriptions:
|:---------------------------|:------------------------------ |:------|:--------------|
| `/tableflip <Comment>` | Append the comment with `(╯°□°)╯︵ ┻━┻` | ✓ | ✓ |
| `/shrug <Comment>` | Append the comment with `¯\_(ツ)_/¯` | ✓ | ✓ |
-| `/todo` | Add a todo | ✓ | ✓ |
-| `/done` | Mark todo as done | ✓ | ✓ |
+| `/todo` | Add a To Do | ✓ | ✓ |
+| `/done` | Mark To Do as done | ✓ | ✓ |
| `/subscribe` | Subscribe | ✓ | ✓ |
| `/unsubscribe` | Unsubscribe | ✓ | ✓ |
| `/close` | Close | ✓ | ✓ |
@@ -75,8 +75,8 @@ The following quick actions are applicable for epics threads and description:
|:---------------------------|:----------------------------------------|
| `/tableflip <Comment>` | Append the comment with `(╯°□°)╯︵ ┻━┻` |
| `/shrug <Comment>` | Append the comment with `¯\_(ツ)_/¯` |
-| `/todo` | Add a todo |
-| `/done` | Mark todo as done |
+| `/todo` | Add a To Do |
+| `/done` | Mark To Do as done |
| `/subscribe` | Subscribe |
| `/unsubscribe` | Unsubscribe |
| `/close` | Close |
@@ -88,3 +88,5 @@ The following quick actions are applicable for epics threads and description:
| `/relabel ~label1 ~label2` | Replace label |
| <code>/child_epic &lt;&epic &#124; group&epic &#124; Epic URL&gt;</code> | Adds child epic to epic ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-ee/issues/7330)) |
| <code>/remove_child_epic &lt;&epic &#124; group&epic &#124; Epic URL&gt;</code> | Removes child epic from epic ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-ee/issues/7330)) |
+| <code>/parent_epic &lt;&epic &#124; group&epic &#124; Epic URL&gt;</code> | Sets parent epic to epic ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-ee/issues/10556)) |
+| <code>/remove_parent_epic | Removes parent epic from epic ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-ee/issues/10556)) |
diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md
index 13e4f2ce163..a81c9197ec1 100644
--- a/doc/user/project/repository/branches/index.md
+++ b/doc/user/project/repository/branches/index.md
@@ -21,7 +21,7 @@ branch for your project. You can choose another branch to be your project's
default under your project's **Settings > Repository**.
The default branch is the branch affected by the
-[issue closing pattern](../../issues/automatic_issue_closing.md),
+[issue closing pattern](../../issues/managing_issues.md#closing-issues-automatically),
which means that _an issue will be closed when a merge request is merged to
the **default branch**_.
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index 165f4c15165..5e0f4c69b5e 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -171,17 +171,14 @@ Via command line, you can commit multiple times before pushing.
- **Sign a commit:**
Use GPG to [sign your commits](gpg_signed_commits/index.md).
-## Repository size
+## Project and repository size
-A project's repository size is reported on the project's **Details** page. The reported size is
-updated every 15 minutes at most, so may not reflect recent activity.
+A project's size is reported on the project's **Details** page. The reported size is
+updated every 15 minutes at most, so may not reflect recent activity. The displayed files size includes repository files, artifacts, and LFS.
-The repository size for:
+The project size may differ slightly from one instance to another due to compression, housekeeping, and other factors.
-- GitLab.com [is set by GitLab](../../gitlab_com/index.md#repository-size-limit).
-- Self-managed instances is set by your GitLab administrators.
-
-You can [reduce a repository's size using Git](reducing_the_repo_size_using_git.md).
+[Repository size limit](../../admin_area/settings/account_and_limit_settings.md) may be set by admins. GitLab.com's repository size limit [is set by GitLab](../../gitlab_com/index.md#repository-size-limit).
## Contributors
diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md
index ce9d23bf911..253e5374f52 100644
--- a/doc/user/project/repository/web_editor.md
+++ b/doc/user/project/repository/web_editor.md
@@ -114,7 +114,7 @@ If your [project is already configured with a deployment service][project-servic
After the branch is created, you can edit files in the repository to fix
the issue. When a merge request is created based on the newly created branch,
-the description field will automatically display the [issue closing pattern]
+the description field will automatically display the [issue closing pattern](../issues/managing_issues.md#closing-issues-automatically)
`Closes #ID`, where `ID` the ID of the issue. This will close the issue once the
merge request is merged.
@@ -181,4 +181,3 @@ through the web editor, you can choose to use another of your linked email
addresses from the **User Settings > Edit Profile** page.
[ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808
-[issue closing pattern]: ../issues/automatic_issue_closing.md
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index 2bf8d4dfe7b..01763c49207 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -9,7 +9,7 @@ to your project's homepage and clicking **Settings**.
## General settings
-Under a project's general settings you can find everything concerning the
+Under a project's general settings, you can find everything concerning the
functionality of a project.
### General project settings
@@ -26,7 +26,7 @@ Set up your project's access, [visibility](../../../public_access/public_access.
![projects sharing permissions](img/sharing_and_permissions_settings.png)
-If Issues are disabled, or you can't access Issues because you're not a project member, then Lables and Milestones
+If Issues are disabled, or you can't access Issues because you're not a project member, then Labels and Milestones
links will be missing from the sidebar UI.
You can still access them with direct links if you can access Merge Requests. This is deliberate, if you can see
@@ -96,7 +96,7 @@ To rename a repository:
1. Hit **Rename project**.
Remember that this can have unintended side effects since everyone with the
-old URL will not be able to push or pull. Read more about what happens with the
+old URL will not be able to push or pull. Read more about what happens with the
[redirects when renaming repositories](../index.md#redirects-when-changing-repository-paths).
#### Transferring an existing project into another namespace
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index bb6c48471c7..d6e2f036cf2 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -97,10 +97,10 @@ quickly access issues and merge requests created or assigned to you within that
![search per project - shortcut](img/project_search.png)
-## Todos
+## To-Do List
-Your [todos](../../workflow/todos.md#gitlab-todos) can be searched by "to do" and "done".
-You can [filter](../../workflow/todos.md#filtering-your-todos) them per project,
+Your [To-Do List](../../workflow/todos.md#gitlab-to-do-list) can be searched by "to do" and "done".
+You can [filter](../../workflow/todos.md#filtering-your-to-do-list) them per project,
author, type, and action. Also, you can sort them by
[**Label priority**](../../user/project/labels.md#label-priority),
**Last created** and **Oldest created**.
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 40e2486ace5..45bd8c29a48 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -4,7 +4,7 @@ comments: false
# Workflow
-- [Automatic issue closing](../user/project/issues/automatic_issue_closing.md)
+- [Automatic issue closing](../user/project/issues/managing_issues.md#closing-issues-automatically)
- [Change your time zone](timezone.md)
- [Cycle Analytics](../user/project/cycle_analytics.md)
- [Description templates](../user/project/description_templates.md)
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 3e24557591c..2f365e42cc9 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -1,7 +1,8 @@
-![GitLab Flow](gitlab_flow.png)
# Introduction to GitLab Flow
+![GitLab Flow](img/gitlab_flow.png)
+
Git allows a wide variety of branching strategies and workflows.
Because of this, many organizations end up with workflows that are too complicated, not clearly defined, or not integrated with issue tracking systems.
Therefore, we propose GitLab flow as a clearly defined set of best practices.
@@ -11,7 +12,7 @@ Organizations coming to Git from other version control systems frequently find i
This article describes GitLab flow, which integrates the Git workflow with an issue tracking system.
It offers a simple, transparent, and effective way to work with Git.
-![Four stages (working copy, index, local repo, remote repo) and three steps between them](four_stages.png)
+![Four stages (working copy, index, local repo, remote repo) and three steps between them](img/four_stages.png)
When converting to Git, you have to get used to the fact that it takes three steps to share a commit with colleagues.
Most version control systems have only one step: committing from the working copy to a shared server.
@@ -19,7 +20,7 @@ In Git, you add files from the working copy to the staging area. After that, you
The third step is pushing to a shared remote repository.
After getting used to these three steps, the next challenge is the branching model.
-![Multiple long-running branches and merging in all directions](messy_flow.png)
+![Multiple long-running branches and merging in all directions](img/messy_flow.png)
Since many organizations new to Git have no conventions for how to work with it, their repositories can quickly become messy.
The biggest problem is that many long-running branches emerge that all contain part of the changes.
@@ -31,7 +32,7 @@ For a video introduction of how this works in GitLab, see [GitLab Flow](https://
## Git flow and its problems
-![Git Flow timeline by Vincent Driessen, used with permission](gitdashflow.png)
+![Git Flow timeline by Vincent Driessen, used with permission](img/gitdashflow.png)
Git flow was one of the first proposals to use Git branches, and it has received a lot of attention.
It suggests a `master` branch and a separate `develop` branch, as well as supporting branches for features, releases, and hotfixes.
@@ -54,7 +55,7 @@ For example, many projects do releases but don't need to do hotfixes.
## GitHub flow as a simpler alternative
-![Master branch with feature branches merged in](github_flow.png)
+![Master branch with feature branches merged in](img/github_flow.png)
In reaction to Git flow, GitHub created a simpler alternative.
[GitHub flow](https://guides.github.com/introduction/flow/index.html) has only feature branches and a `master` branch.
@@ -66,7 +67,7 @@ With GitLab flow, we offer additional guidance for these questions.
## Production branch with GitLab flow
-![Master branch and production branch with an arrow that indicates a deployment](production_branch.png)
+![Master branch and production branch with an arrow that indicates a deployment](img/production_branch.png)
GitHub flow assumes you can deploy to production every time you merge a feature branch.
While this is possible in some cases, such as SaaS applications, there are many cases where this is not possible.
@@ -82,7 +83,7 @@ This flow prevents the overhead of releasing, tagging, and merging that happens
## Environment branches with GitLab flow
-![Multiple branches with the code cascading from one to another](environment_branches.png)
+![Multiple branches with the code cascading from one to another](img/environment_branches.png)
It might be a good idea to have an environment that is automatically updated to the `master` branch.
Only, in this case, the name of this environment might differ from the branch name.
@@ -98,7 +99,7 @@ If this is not possible because more manual testing is required, you can send me
## Release branches with GitLab flow
-![Master and multiple release branches that vary in length with cherry-picks from master](release_branches.png)
+![Master and multiple release branches that vary in length with cherry-picks from master](img/release_branches.png)
You only need to work with release branches if you need to release software to the outside world.
In this case, each branch contains a minor version, for example, 2-3-stable, 2-4-stable, etc.
@@ -114,7 +115,7 @@ In this flow, it is not common to have a production branch (or Git flow `master`
## Merge/pull requests with GitLab flow
-![Merge request with inline comments](mr_inline_comments.png)
+![Merge request with inline comments](img/mr_inline_comments.png)
Merge or pull requests are created in a Git management application. They ask an assigned person to merge two branches.
Tools such as GitHub and Bitbucket choose the name "pull request" since the first manual action is to pull the feature branch.
@@ -147,11 +148,11 @@ It also ensures that if someone reopens the issue, they can use the same branch
NOTE: **Note:**
When you reopen an issue you need to create a new merge request.
-![Remove checkbox for branch in merge requests](remove_checkbox.png)
+![Remove checkbox for branch in merge requests](img/remove_checkbox.png)
## Issue tracking with GitLab flow
-![Merge request with the branch name "15-require-a-password-to-change-it" and assignee field shown](merge_request.png)
+![Merge request with the branch name "15-require-a-password-to-change-it" and assignee field shown](img/merge_request.png)
GitLab flow is a way to make the relation between the code and the issue tracker more transparent.
@@ -175,7 +176,7 @@ A merge request is an online place to discuss the change and review the code.
If you open the merge request but do not assign it to anyone, it is a "Work In Progress" merge request.
These are used to discuss the proposed implementation but are not ready for inclusion in the `master` branch yet.
-Start the title of the merge request with "[WIP]" or "WIP:" to prevent it from being merged before it's ready.
+Start the title of the merge request with `[WIP]` or `WIP:` to prevent it from being merged before it's ready.
When you think the code is ready, assign the merge request to a reviewer.
The reviewer can merge the changes when they think the code is ready for inclusion in the `master` branch.
@@ -192,7 +193,7 @@ It is possible that one feature branch solves more than one issue.
## Linking and closing issues from merge requests
-![Merge request showing the linked issues that will be closed](close_issue_mr.png)
+![Merge request showing the linked issues that will be closed](img/close_issue_mr.png)
Link to issues by mentioning them in commit messages or the description of a merge request, for example, "Fixes #16" or "Duck typing is preferred. See #12."
GitLab then creates links to the mentioned issues and creates comments in the issues linking back to the merge request.
@@ -203,7 +204,7 @@ If you have an issue that spans across multiple repositories, create an issue fo
## Squashing commits with rebase
-![Vim screen showing the rebase view](rebase.png)
+![Vim screen showing the rebase view](img/rebase.png)
With Git, you can use an interactive rebase (`rebase -i`) to squash multiple commits into one or reorder them.
This functionality is useful if you want to replace a couple of small commits with a single commit, or if you want to make the order more logical.
@@ -229,7 +230,7 @@ Git does not allow you to merge the code again otherwise.
## Reducing merge commits in feature branches
-![List of sequential merge commits](merge_commits.png)
+![List of sequential merge commits](img/merge_commits.png)
Having lots of merge commits can make your repository history messy.
Therefore, you should try to avoid merge commits in feature branches.
@@ -289,7 +290,7 @@ Sharing your work before it's complete also allows for discussion and feedback a
## How to write a good commit message
-![Good and bad commit message](good_commit.png)
+![Good and bad commit message](img/good_commit.png)
A commit message should reflect your intention, not just the contents of the commit.
It is easy to see the changes in a commit, so the commit message should explain why you made those changes.
@@ -300,7 +301,7 @@ For more information about formatting commit messages, please see this excellent
## Testing before merging
-![Merge requests showing the test states: red, yellow, and green](ci_mr.png)
+![Merge requests showing the test states: red, yellow, and green](img/ci_mr.png)
In old workflows, the continuous integration (CI) server commonly ran tests on the `master` branch only.
Developers had to ensure their code did not break the `master` branch.
@@ -317,7 +318,7 @@ As said before, if you often have feature branches that last for more than a few
## Working with feature branches
-![Shell output showing git pull output](git_pull.png)
+![Shell output showing git pull output](img/git_pull.png)
When creating a feature branch, always branch from an up-to-date `master`.
If you know before you start that your work depends on another branch, you can also branch from there.
diff --git a/doc/workflow/ci_mr.png b/doc/workflow/img/ci_mr.png
index 85a609cb814..85a609cb814 100644
--- a/doc/workflow/ci_mr.png
+++ b/doc/workflow/img/ci_mr.png
Binary files differ
diff --git a/doc/workflow/close_issue_mr.png b/doc/workflow/img/close_issue_mr.png
index 70de2fb6cee..70de2fb6cee 100644
--- a/doc/workflow/close_issue_mr.png
+++ b/doc/workflow/img/close_issue_mr.png
Binary files differ
diff --git a/doc/workflow/environment_branches.png b/doc/workflow/img/environment_branches.png
index 0aff33c6bb8..0aff33c6bb8 100644
--- a/doc/workflow/environment_branches.png
+++ b/doc/workflow/img/environment_branches.png
Binary files differ
diff --git a/doc/workflow/four_stages.png b/doc/workflow/img/four_stages.png
index 3ef6a33d2d4..3ef6a33d2d4 100644
--- a/doc/workflow/four_stages.png
+++ b/doc/workflow/img/four_stages.png
Binary files differ
diff --git a/doc/workflow/git_pull.png b/doc/workflow/img/git_pull.png
index 0e56e59471c..0e56e59471c 100644
--- a/doc/workflow/git_pull.png
+++ b/doc/workflow/img/git_pull.png
Binary files differ
diff --git a/doc/workflow/gitdashflow.png b/doc/workflow/img/gitdashflow.png
index 65900853d84..65900853d84 100644
--- a/doc/workflow/gitdashflow.png
+++ b/doc/workflow/img/gitdashflow.png
Binary files differ
diff --git a/doc/workflow/github_flow.png b/doc/workflow/img/github_flow.png
index 21a22becdb6..21a22becdb6 100644
--- a/doc/workflow/github_flow.png
+++ b/doc/workflow/img/github_flow.png
Binary files differ
diff --git a/doc/workflow/gitlab_flow.png b/doc/workflow/img/gitlab_flow.png
index a6f3c947843..a6f3c947843 100644
--- a/doc/workflow/gitlab_flow.png
+++ b/doc/workflow/img/gitlab_flow.png
Binary files differ
diff --git a/doc/workflow/good_commit.png b/doc/workflow/img/good_commit.png
index ceb0d4b1691..ceb0d4b1691 100644
--- a/doc/workflow/good_commit.png
+++ b/doc/workflow/img/good_commit.png
Binary files differ
diff --git a/doc/workflow/merge_commits.png b/doc/workflow/img/merge_commits.png
index 4a80811c6e3..4a80811c6e3 100644
--- a/doc/workflow/merge_commits.png
+++ b/doc/workflow/img/merge_commits.png
Binary files differ
diff --git a/doc/workflow/merge_request.png b/doc/workflow/img/merge_request.png
index 010e95983fc..010e95983fc 100644
--- a/doc/workflow/merge_request.png
+++ b/doc/workflow/img/merge_request.png
Binary files differ
diff --git a/doc/workflow/messy_flow.png b/doc/workflow/img/messy_flow.png
index 4fa22d2bb5d..4fa22d2bb5d 100644
--- a/doc/workflow/messy_flow.png
+++ b/doc/workflow/img/messy_flow.png
Binary files differ
diff --git a/doc/workflow/mr_inline_comments.png b/doc/workflow/img/mr_inline_comments.png
index a18801f56e4..a18801f56e4 100644
--- a/doc/workflow/mr_inline_comments.png
+++ b/doc/workflow/img/mr_inline_comments.png
Binary files differ
diff --git a/doc/workflow/production_branch.png b/doc/workflow/img/production_branch.png
index c132d51bfb6..c132d51bfb6 100644
--- a/doc/workflow/production_branch.png
+++ b/doc/workflow/img/production_branch.png
Binary files differ
diff --git a/doc/workflow/rebase.png b/doc/workflow/img/rebase.png
index fe865177ba8..fe865177ba8 100644
--- a/doc/workflow/rebase.png
+++ b/doc/workflow/img/rebase.png
Binary files differ
diff --git a/doc/workflow/release_branches.png b/doc/workflow/img/release_branches.png
index 0a7f61d0248..0a7f61d0248 100644
--- a/doc/workflow/release_branches.png
+++ b/doc/workflow/img/release_branches.png
Binary files differ
diff --git a/doc/workflow/remove_checkbox.png b/doc/workflow/img/remove_checkbox.png
index fb0e792b37b..fb0e792b37b 100644
--- a/doc/workflow/remove_checkbox.png
+++ b/doc/workflow/img/remove_checkbox.png
Binary files differ
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index 5d560f2e000..d49d29c805a 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -64,18 +64,18 @@ These settings can be configured on project page under the name of the project.
Below is the table of events users can be notified of:
-| Event | Sent to | Settings level |
-|------------------------------|-------------------------------------------------------------------|------------------------------|
-| New SSH key added | User | Security email, always sent. |
-| New email added | User | Security email, always sent. |
-| Email changed | User | Security email, always sent. |
-| Password changed | User | Security email, always sent. |
-| New user created | User | Sent on user creation, except for omniauth (LDAP)|
-| User added to project | User | Sent when user is added to project |
-| Project access level changed | User | Sent when user project access level is changed |
-| User added to group | User | Sent when user is added to group |
-| Group access level changed | User | Sent when user group access level is changed |
-| Project moved | Project members [1] | [1] not disabled |
+| Event | Sent to | Settings level |
+|------------------------------|---------------------|------------------------------|
+| New SSH key added | User | Security email, always sent. |
+| New email added | User | Security email, always sent. |
+| Email changed | User | Security email, always sent. |
+| Password changed | User | Security email, always sent. |
+| New user created | User | Sent on user creation, except for omniauth (LDAP)|
+| User added to project | User | Sent when user is added to project |
+| Project access level changed | User | Sent when user project access level is changed |
+| User added to group | User | Sent when user is added to group |
+| Group access level changed | User | Sent when user group access level is changed |
+| Project moved | Project members (1) | (1) not disabled |
### Issue / Epics / Merge request events
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index 3eac79427cf..f501a222cd5 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -1,50 +1,57 @@
-# GitLab Todos
+# GitLab To-Do List
> [Introduced][ce-2817] in GitLab 8.5.
When you log into GitLab, you normally want to see where you should spend your
-time and take some action, or what you need to keep an eye on. All without the
-mess of a huge pile of e-mail notifications. GitLab is where you do your work,
-so being able to get started quickly is very important.
+time, take some action, or know what you need to keep an eye on without
+a huge pile of e-mail notifications. GitLab is where you do your work,
+so being able to get started quickly is important.
-Todos is a chronological list of to-dos that are waiting for your input, all
+Your To-Do List offers a chronological list of items that are waiting for your input, all
in a simple dashboard.
-![Todos screenshot showing a list of items to check on](img/todos_index.png)
+![To Do screenshot showing a list of items to check on](img/todos_index.png)
---
-You can quickly access the Todos dashboard using the checkmark icon next to the
-search bar in the upper right corner. The number in blue is the number of Todos
-you still have open if the count is < 100, else it's 99+. The exact number
-will still be shown in the body of the _To do_ tab.
+You can quickly access your To-Do List by clicking the checkmark icon next to the
+search bar in the top navigation. If the count is:
-![Todos icon](img/todos_icon.png)
+- Less than 100, the number in blue is the number of To-Do items.
+- 100 or more, the number displays as 99+. The exact number displays
+ on the To-Do List.
+you still have open. Otherwise, the number displays as 99+. The exact number
+displays on the To-Do List.
-## What triggers a Todo
+![To Do icon](img/todos_icon.png)
-A Todo appears in your Todos dashboard when:
+## What triggers a To Do
-- an issue or merge request is assigned to you
-- you are `@mentioned` in the description or in a comment of an issue, merge request, or epic **[ULTIMATE]**
-- you are `@mentioned` in a comment on a commit,
-- a job in the CI pipeline running for your merge request failed, but this
- job is not allowed to fail.
-- an open merge request becomes unmergeable due to conflict, and you are either:
- - the author, or
- - have set it to automatically merge once pipeline succeeds.
+A To Do displays on your To-Do List when:
-Todo triggers are not affected by [GitLab Notification Email settings](notifications.md).
+- An issue or merge request is assigned to you
+- You are `@mentioned` in the description or comment of an:
+ - Issue
+ - Merge Request
+ - Epic **[ULTIMATE]**
+- You are `@mentioned` in a comment on a commit
+- A job in the CI pipeline running for your merge request failed, but this
+ job is not allowed to fail
+- An open merge request becomes unmergeable due to conflict, and you are either:
+ - The author
+ - Have set it to automatically merge once the pipeline succeeds
+
+To-do triggers are not affected by [GitLab Notification Email settings](notifications.md).
NOTE: **Note:**
-When an user no longer has access to a resource related to a Todo like an issue, merge request, project or group the related Todos, for security reasons, gets deleted within the next hour. The delete is delayed to prevent data loss in case user got their access revoked by mistake.
+When a user no longer has access to a resource related to a To Do (like an issue, merge request, project, or group) the related To-Do items are deleted within the next hour for security reasons. The delete is delayed to prevent data loss, in case the user's access was revoked by mistake.
-### Directly addressed Todos
+### Directly addressing a To Do
> [Introduced][ce-7926] in GitLab 9.0.
-If you are mentioned at the start of a line, the todo you receive will be listed
-as 'directly addressed'. For instance, in this comment:
+If you are mentioned at the start of a line, the To Do you receive will be listed
+as 'directly addressed'. For example, in this comment:
```markdown
@alice What do you think? cc: @bob
@@ -58,67 +65,80 @@ as 'directly addressed'. For instance, in this comment:
@erin @frank thank you!
```
-The people receiving directly addressed todos are `@alice`, `@erin`, and
-`@frank`. Directly addressed todos only differ from mention todos in their type,
-for filtering; otherwise, they appear as normal.
+The people receiving directly addressed To-Do items are `@alice`, `@erin`, and
+`@frank`. Directly addressed To-Do items only differ from mentions in their type
+for filtering purposes; otherwise, they appear as normal.
+
+### Manually creating a To Do
+
+You can also add the following to your To-Do List by clicking the **Add a To Do** button on an:
-### Manually creating a Todo
+- Issue
+- Merge Request
+- Epic **[ULTIMATE]**
-You can also add an issue, merge request or epic to your Todos dashboard by clicking
-the "Add todo" button in the sidebar of the issue, merge request, or epic **[ULTIMATE]**.
+![Adding a To Do from the issuable sidebar](img/todos_add_todo_sidebar.png)
-![Adding a Todo from the issuable sidebar](img/todos_add_todo_sidebar.png)
+## Marking a To Do as done
-## Marking a Todo as done
+Any action to the following will mark the corresponding To Do as done:
-Any action to the corresponding issue, merge request or epic **[ULTIMATE]** will mark your Todo as
-**Done**. Actions that dismiss Todos include:
+- Issue
+- Merge Request
+- Epic **[ULTIMATE]**
-- changing the assignee
-- changing the milestone
-- adding/removing a label
-- commenting on the issue
+Actions that dismiss To-Do items include:
+
+- Changing the assignee
+- Changing the milestone
+- Adding/removing a label
+- Commenting on the issue
---
-Todos are personal, and they're only marked as done if the action is coming from
-you. If you close the issue or merge request, your Todo will automatically
-be marked as done.
+Your To-Do List is personal, and items are only marked as done if the action comes from
+you. If you close the issue or merge request, your To Do is automatically
+marked as done.
+
+To prevent other users from closing issues without you being notified, if someone else closes, merges, or takes action on the any of the following, your To Do will remain pending:
-If someone else closes, merges, or takes action on the issue, epic or merge
-request, your Todo will remain pending. This prevents other users from closing issues without you being notified.
+- Issue
+- Merge request
+- Epic **[ULTIMATE]**
-There is just one Todo per issue, epic or merge request, so mentioning a user a
-hundred times in an issue will only trigger one Todo.
+There is just one To Do for each of these, so mentioning a user a hundred times in an issue will only trigger one To Do.
---
-If no action is needed, you can manually mark the Todo as done by clicking the
-corresponding **Done** button, and it will disappear from your Todo list.
+If no action is needed, you can manually mark the To Do as done by clicking the
+corresponding **Done** button, and it will disappear from your To-Do List.
+
+![A To Do in the To-Do List](img/todo_list_item.png)
-![A Todo in the Todos dashboard](img/todo_list_item.png)
+You can also mark a To Do as done by clicking the **Mark as done** button in the sidebar of the following:
-A Todo can also be marked as done from the issue, merge request or epic sidebar using
-the "Mark todo as done" button.
+- Issue
+- Merge Request
+- Epic **[ULTIMATE]**
-![Mark todo as done from the issuable sidebar](img/todos_mark_done_sidebar.png)
+![Mark as done from the issuable sidebar](img/todos_mark_done_sidebar.png)
-You can mark all your Todos as done at once by clicking on the **Mark all as
+You can mark all your To-Do items as done at once by clicking the **Mark all as
done** button.
-## Filtering your Todos
+## Filtering your To-Do List
-There are four kinds of filters you can use on your Todos dashboard.
+There are four kinds of filters you can use on your To-Do List.
| Filter | Description |
| ------- | ----------- |
| Project | Filter by project |
| Group | Filter by group |
-| Author | Filter by the author that triggered the Todo |
+| Author | Filter by the author that triggered the To Do |
| Type | Filter by issue, merge request, or epic **[ULTIMATE]** |
-| Action | Filter by the action that triggered the Todo |
+| Action | Filter by the action that triggered the To Do |
-You can also filter by more than one of these at the same time. The possible Actions are `Any Action`, `Assigned`, `Mentioned`, `Added`, `Pipelines`, and `Directly Addressed`, [as described above](#what-triggers-a-todo).
+You can also filter by more than one of these at the same time. The possible Actions are `Any Action`, `Assigned`, `Mentioned`, `Added`, `Pipelines`, and `Directly Addressed`, [as described above](#what-triggers-a-to-do).
[ce-2817]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2817
[ce-7926]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7926
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 20f8c637274..42499c5b41e 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -52,7 +52,10 @@ module API
rack_response({ 'message' => '404 Not found' }.to_json, 404)
end
- rescue_from ::Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError do
+ rescue_from(
+ ::ActiveRecord::StaleObjectError,
+ ::Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
+ ) do
rack_response({ 'message' => '409 Conflict: Resource lock' }.to_json, 409)
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index d783591c238..b9aa387ba61 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -498,9 +498,9 @@ module API
expose :state, :created_at, :updated_at
# Avoids an N+1 query when metadata is included
- def issuable_metadata(subject, options, method)
+ def issuable_metadata(subject, options, method, args = nil)
cached_subject = options.dig(:issuable_metadata, subject.id)
- (cached_subject || subject).public_send(method) # rubocop: disable GitlabSecurity/PublicSend
+ (cached_subject || subject).public_send(method, *args) # rubocop: disable GitlabSecurity/PublicSend
end
end
@@ -564,7 +564,7 @@ module API
end
expose(:user_notes_count) { |issue, options| issuable_metadata(issue, options, :user_notes_count) }
- expose(:merge_requests_count) { |issue, options| issuable_metadata(issue, options, :merge_requests_count) }
+ expose(:merge_requests_count) { |issue, options| issuable_metadata(issue, options, :merge_requests_count, options[:current_user]) }
expose(:upvotes) { |issue, options| issuable_metadata(issue, options, :upvotes) }
expose(:downvotes) { |issue, options| issuable_metadata(issue, options, :downvotes) }
expose :due_date
@@ -757,7 +757,9 @@ module API
merge_request.metrics&.pipeline
end
- expose :head_pipeline, using: 'API::Entities::Pipeline'
+ expose :head_pipeline, using: 'API::Entities::Pipeline', if: -> (_, options) do
+ Ability.allowed?(options[:current_user], :read_pipeline, options[:project])
+ end
expose :diff_refs, using: Entities::DiffRefs
@@ -1186,8 +1188,10 @@ module API
MarkupHelper.markdown_field(entity, :description)
end
expose :created_at
+ expose :released_at
expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? }
expose :commit, using: Entities::Commit, if: lambda { |_, _| can_download_code? }
+ expose :upcoming_release?, as: :upcoming_release
expose :assets do
expose :assets_count, as: :count do |release, _|
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 039ebf92187..d687acf3423 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -96,7 +96,7 @@ module API
with: Entities::Issue,
with_labels_details: declared_params[:with_labels_details],
current_user: current_user,
- issuable_metadata: issuable_meta_data(issues, 'Issue')
+ issuable_metadata: issuable_meta_data(issues, 'Issue', current_user)
}
present issues, options
@@ -122,7 +122,7 @@ module API
with: Entities::Issue,
with_labels_details: declared_params[:with_labels_details],
current_user: current_user,
- issuable_metadata: issuable_meta_data(issues, 'Issue')
+ issuable_metadata: issuable_meta_data(issues, 'Issue', current_user)
}
present issues, options
@@ -161,7 +161,7 @@ module API
with_labels_details: declared_params[:with_labels_details],
current_user: current_user,
project: user_project,
- issuable_metadata: issuable_meta_data(issues, 'Issue')
+ issuable_metadata: issuable_meta_data(issues, 'Issue', current_user)
}
present issues, options
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index bf87e9ec2ff..64ee82cd775 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -72,7 +72,7 @@ module API
if params[:view] == 'simple'
options[:with] = Entities::MergeRequestSimple
else
- options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest')
+ options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest', current_user)
end
options
@@ -429,9 +429,10 @@ module API
authorize_push_to_merge_request!(merge_request)
- RebaseWorker.perform_async(merge_request.id, current_user.id)
+ merge_request.rebase_async(current_user.id)
status :accepted
+ present rebase_in_progress: merge_request.rebase_in_progress?
end
desc 'List issues that will be closed on merge' do
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 1e14c77b5be..a7d62014509 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -474,7 +474,7 @@ module API
authorize_admin_project
begin
- ::Projects::HousekeepingService.new(user_project).execute
+ ::Projects::HousekeepingService.new(user_project, :gc).execute
rescue ::Projects::HousekeepingService::LeaseTaken => error
conflict!(error.message)
end
diff --git a/lib/api/releases.rb b/lib/api/releases.rb
index 6b17f4317db..fdd8406388e 100644
--- a/lib/api/releases.rb
+++ b/lib/api/releases.rb
@@ -54,6 +54,7 @@ module API
requires :url, type: String
end
end
+ optional :released_at, type: DateTime, desc: 'The date when the release will be/was ready. Defaults to the current time.'
end
post ':id/releases' do
authorize_create_release!
@@ -77,6 +78,7 @@ module API
requires :tag_name, type: String, desc: 'The name of the tag', as: :tag
optional :name, type: String, desc: 'The name of the release'
optional :description, type: String, desc: 'Release notes with markdown support'
+ optional :released_at, type: DateTime, desc: 'The date when the release will be/was ready. Defaults to the current time.'
end
put ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMETS do
authorize_update_release!
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index f3fea463e7f..c2d371b6867 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -115,6 +115,8 @@ module API
params do
requires :id, type: Integer, desc: 'The ID of the runner'
optional :status, type: String, desc: 'Status of the job', values: Ci::Build::AVAILABLE_STATUSES
+ optional :order_by, type: String, desc: 'Order by `id` or not', values: RunnerJobsFinder::ALLOWED_INDEXED_COLUMNS
+ optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Sort by asc (ascending) or desc (descending)'
use :pagination
end
get ':id/jobs' do
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 3c5c1a9fd5f..4275d911708 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -55,6 +55,8 @@ module API
optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
optional :gitaly_timeout_fast, type: Integer, desc: 'Gitaly fast operation timeout, in seconds. Set to 0 to disable timeouts.'
optional :gitaly_timeout_medium, type: Integer, desc: 'Medium Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
+ optional :grafana_enabled, type: Boolean, desc: 'Enable Grafana'
+ optional :grafana_url, type: String, desc: 'Grafana URL'
optional :gravatar_enabled, type: Boolean, desc: 'Flag indicating if the Gravatar service is enabled'
optional :help_page_hide_commercial_content, type: Boolean, desc: 'Hide marketing-related entries from help'
optional :help_page_support_url, type: String, desc: 'Alternate support URL for help page'
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index 871eaabc887..7260ecfb5ee 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -65,7 +65,7 @@ module API
next unless collection
targets = collection.map(&:target)
- options[type] = { issuable_metadata: issuable_meta_data(targets, type) }
+ options[type] = { issuable_metadata: issuable_meta_data(targets, type, current_user) }
end
end
end
diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index 88836e12e61..86f18679496 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -102,7 +102,7 @@ module Banzai
end
def relative_file_path(uri)
- path = Addressable::URI.unescape(uri.path)
+ path = Addressable::URI.unescape(uri.path).delete("\0")
request_path = Addressable::URI.unescape(context[:requested_path])
nested_path = build_relative_path(path, request_path)
file_exists?(nested_path) ? nested_path : path
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index fe56f9a1e33..9b66759a5fb 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -14,7 +14,7 @@ module Banzai
LANG_PARAMS_ATTR = 'data-lang-params'.freeze
def call
- doc.search('pre > code').each do |node|
+ doc.search('pre:not([data-math-style]) > code').each do |node|
highlight_node(node)
end
diff --git a/lib/banzai/pipeline/ascii_doc_pipeline.rb b/lib/banzai/pipeline/ascii_doc_pipeline.rb
index cc4af280872..6be489c6572 100644
--- a/lib/banzai/pipeline/ascii_doc_pipeline.rb
+++ b/lib/banzai/pipeline/ascii_doc_pipeline.rb
@@ -6,6 +6,7 @@ module Banzai
def self.filters
FilterArray[
Filter::SanitizationFilter,
+ Filter::SyntaxHighlightFilter,
Filter::ExternalLinkFilter,
Filter::PlantumlFilter,
Filter::AsciiDocPostProcessingFilter
diff --git a/lib/feature.rb b/lib/feature.rb
index 22420e95ea2..e28333aa58e 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -103,10 +103,27 @@ class Feature
feature_class: FlipperFeature,
gate_class: FlipperGate)
+ # Redis L2 cache
+ redis_cache_adapter =
+ Flipper::Adapters::ActiveSupportCacheStore.new(
+ active_record_adapter,
+ l2_cache_backend,
+ expires_in: 1.hour)
+
+ # Thread-local L1 cache: use a short timeout since we don't have a
+ # way to expire this cache all at once
Flipper::Adapters::ActiveSupportCacheStore.new(
- active_record_adapter,
- Rails.cache,
- expires_in: 1.hour)
+ redis_cache_adapter,
+ l1_cache_backend,
+ expires_in: 1.minute)
+ end
+
+ def l1_cache_backend
+ Gitlab::ThreadMemoryCache.cache_backend
+ end
+
+ def l2_cache_backend
+ Rails.cache
end
end
diff --git a/lib/feature/gitaly.rb b/lib/feature/gitaly.rb
index d7a8f8a0b9e..67c0b902c0c 100644
--- a/lib/feature/gitaly.rb
+++ b/lib/feature/gitaly.rb
@@ -8,7 +8,12 @@ class Feature
# CATFILE_CACHE sets an incorrect example
CATFILE_CACHE = 'catfile-cache'.freeze
- SERVER_FEATURE_FLAGS = [CATFILE_CACHE].freeze
+ SERVER_FEATURE_FLAGS =
+ [
+ CATFILE_CACHE,
+ 'get_commit_signatures'.freeze
+ ].freeze
+
DEFAULT_ON_FLAGS = Set.new([CATFILE_CACHE]).freeze
class << self
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index 7f8300a0c2f..00c87cce7b6 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -4,6 +4,7 @@ require 'asciidoctor'
require 'asciidoctor-plantuml'
require 'asciidoctor/extensions'
require 'gitlab/asciidoc/html5_converter'
+require 'gitlab/asciidoc/syntax_highlighter/html_pipeline_adapter'
module Gitlab
# Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters
@@ -16,7 +17,7 @@ module Gitlab
'idseparator' => '-',
'env' => 'gitlab',
'env-gitlab' => '',
- 'source-highlighter' => 'html-pipeline',
+ 'source-highlighter' => 'gitlab-html-pipeline',
'icons' => 'font',
'outfilesuffix' => '.adoc',
'max-include-depth' => MAX_INCLUDE_DEPTH
diff --git a/lib/gitlab/asciidoc/syntax_highlighter/html_pipeline_adapter.rb b/lib/gitlab/asciidoc/syntax_highlighter/html_pipeline_adapter.rb
new file mode 100644
index 00000000000..5fc3323f0fd
--- /dev/null
+++ b/lib/gitlab/asciidoc/syntax_highlighter/html_pipeline_adapter.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Asciidoc
+ module SyntaxHighlighter
+ class HtmlPipelineAdapter < Asciidoctor::SyntaxHighlighter::Base
+ register_for 'gitlab-html-pipeline'
+
+ def format(node, lang, opts)
+ %(<pre><code #{lang ? %[ lang="#{lang}"] : ''}>#{node.content}</code></pre>)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder.rb b/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder.rb
deleted file mode 100644
index ef50fe4adb1..00000000000
--- a/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-# rubocop:disable Style/Documentation
-
-module Gitlab
- module BackgroundMigration
- class MigrateSystemUploadsToNewFolder
- include Gitlab::Database::MigrationHelpers
- attr_reader :old_folder, :new_folder
-
- class Upload < ActiveRecord::Base
- self.table_name = 'uploads'
- include EachBatch
- end
-
- def perform(old_folder, new_folder)
- replace_sql = replace_sql(uploads[:path], old_folder, new_folder)
- affected_uploads = Upload.where(uploads[:path].matches("#{old_folder}%"))
-
- affected_uploads.each_batch do |batch|
- batch.update_all("path = #{replace_sql}")
- end
- end
-
- def uploads
- Arel::Table.new('uploads')
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/move_personal_snippet_files.rb b/lib/gitlab/background_migration/move_personal_snippet_files.rb
deleted file mode 100644
index 5b2b2af718a..00000000000
--- a/lib/gitlab/background_migration/move_personal_snippet_files.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-# frozen_string_literal: true
-# rubocop:disable Style/Documentation
-
-module Gitlab
- module BackgroundMigration
- class MovePersonalSnippetFiles
- delegate :select_all, :execute, :quote_string, to: :connection
-
- def perform(relative_source, relative_destination)
- @source_relative_location = relative_source
- @destination_relative_location = relative_destination
-
- move_personal_snippet_files
- end
-
- def move_personal_snippet_files
- query = "SELECT uploads.path, uploads.model_id FROM uploads "\
- "INNER JOIN snippets ON snippets.id = uploads.model_id WHERE uploader = 'PersonalFileUploader'"
- select_all(query).each do |upload|
- secret = upload['path'].split('/')[0]
- file_name = upload['path'].split('/')[1]
-
- move_file(upload['model_id'], secret, file_name)
- update_markdown(upload['model_id'], secret, file_name)
- end
- end
-
- def move_file(snippet_id, secret, file_name)
- source_dir = File.join(base_directory, @source_relative_location, snippet_id.to_s, secret)
- destination_dir = File.join(base_directory, @destination_relative_location, snippet_id.to_s, secret)
-
- source_file_path = File.join(source_dir, file_name)
- destination_file_path = File.join(destination_dir, file_name)
-
- unless File.exist?(source_file_path)
- say "Source file `#{source_file_path}` doesn't exist. Skipping."
- return
- end
-
- say "Moving file #{source_file_path} -> #{destination_file_path}"
-
- FileUtils.mkdir_p(destination_dir)
- FileUtils.move(source_file_path, destination_file_path)
- end
-
- def update_markdown(snippet_id, secret, file_name)
- source_markdown_path = File.join(@source_relative_location, snippet_id.to_s, secret, file_name)
- destination_markdown_path = File.join(@destination_relative_location, snippet_id.to_s, secret, file_name)
-
- source_markdown = "](#{source_markdown_path})"
- destination_markdown = "](#{destination_markdown_path})"
- quoted_source = quote_string(source_markdown)
- quoted_destination = quote_string(destination_markdown)
-
- execute("UPDATE snippets "\
- "SET description = replace(snippets.description, '#{quoted_source}', '#{quoted_destination}'), description_html = NULL "\
- "WHERE id = #{snippet_id}")
-
- query = "SELECT id, note FROM notes WHERE noteable_id = #{snippet_id} "\
- "AND noteable_type = 'Snippet' AND note IS NOT NULL"
- select_all(query).each do |note|
- text = note['note'].gsub(source_markdown, destination_markdown)
- quoted_text = quote_string(text)
-
- execute("UPDATE notes SET note = '#{quoted_text}', note_html = NULL WHERE id = #{note['id']}")
- end
- end
-
- def base_directory
- File.join(Rails.root, 'public')
- end
-
- def connection
- ActiveRecord::Base.connection
- end
-
- def say(message)
- Rails.logger.debug(message)
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index 7aeac11df55..cde042c5e0a 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -23,6 +23,11 @@ module Gitlab
@root = Entry::Root.new(@config)
@root.compose!
+
+ rescue Gitlab::Config::Loader::Yaml::DataTooLargeError => e
+ Gitlab::Sentry.track_exception(e, extra: { user: user.inspect, project: project.inspect })
+ raise Config::ConfigError, e.message
+
rescue *rescue_errors => e
raise Config::ConfigError, e.message
end
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index 65a6630365d..cf3d261c1cb 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -74,16 +74,16 @@ stages:
- cleanup
include:
- - template: Jobs/Build.gitlab-ci.yml
- - template: Jobs/Test.gitlab-ci.yml
- - template: Jobs/Code-Quality.gitlab-ci.yml
- - template: Jobs/Deploy.gitlab-ci.yml
- - template: Jobs/Browser-Performance-Testing.gitlab-ci.yml
- - template: Security/DAST.gitlab-ci.yml
- - template: Security/Container-Scanning.gitlab-ci.yml
- - template: Security/Dependency-Scanning.gitlab-ci.yml
- - template: Security/License-Management.gitlab-ci.yml
- - template: Security/SAST.gitlab-ci.yml
+ - template: Jobs/Build.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
+ - template: Jobs/Test.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
+ - template: Jobs/Code-Quality.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
+ - template: Jobs/Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+ - template: Jobs/Browser-Performance-Testing.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
+ - template: Security/DAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
+ - template: Security/Container-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
+ - template: Security/Dependency-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
+ - template: Security/License-Management.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml
+ - template: Security/SAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
# Override DAST job to exclude master branch
dast:
diff --git a/lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml
index 0d742aa282d..e7dacd3a1fc 100644
--- a/lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Jekyll.gitlab-ci.yml
@@ -4,6 +4,7 @@ image: ruby:2.3
variables:
JEKYLL_ENV: production
+ LC_ALL: C.UTF-8
before_script:
- bundle install
diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
index 8713b833011..0a97a16b83c 100644
--- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
@@ -54,6 +54,7 @@ sast:
MAVEN_PATH \
MAVEN_REPO_PATH \
SBT_PATH \
+ FAIL_NEVER \
) \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
diff --git a/lib/gitlab/config/loader/yaml.rb b/lib/gitlab/config/loader/yaml.rb
index 8159f8b8026..4cedab1545c 100644
--- a/lib/gitlab/config/loader/yaml.rb
+++ b/lib/gitlab/config/loader/yaml.rb
@@ -4,6 +4,13 @@ module Gitlab
module Config
module Loader
class Yaml
+ DataTooLargeError = Class.new(Loader::FormatError)
+
+ include Gitlab::Utils::StrongMemoize
+
+ MAX_YAML_SIZE = 1.megabyte
+ MAX_YAML_DEPTH = 100
+
def initialize(config)
@config = YAML.safe_load(config, [Symbol], [], true)
rescue Psych::Exception => e
@@ -11,16 +18,35 @@ module Gitlab
end
def valid?
- @config.is_a?(Hash)
+ hash? && !too_big?
end
def load!
- unless valid?
- raise Loader::FormatError, 'Invalid configuration format'
- end
+ raise DataTooLargeError, 'The parsed YAML is too big' if too_big?
+ raise Loader::FormatError, 'Invalid configuration format' unless hash?
@config.deep_symbolize_keys
end
+
+ private
+
+ def hash?
+ @config.is_a?(Hash)
+ end
+
+ def too_big?
+ return false unless Feature.enabled?(:ci_yaml_limit_size, default_enabled: true)
+
+ !deep_size.valid?
+ end
+
+ def deep_size
+ strong_memoize(:deep_size) do
+ Gitlab::Utils::DeepSize.new(@config,
+ max_size: MAX_YAML_SIZE,
+ max_depth: MAX_YAML_DEPTH)
+ end
+ end
end
end
end
diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb
index 1ecf4a12db7..0fc145534bf 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -103,6 +103,11 @@ module Gitlab
yarn\.lock
)\z}x => :frontend,
+ %r{\A(ee/)?db/} => :database,
+ %r{\A(ee/)?lib/gitlab/(database|background_migration|sql|github_import)(/|\.rb)} => :database,
+ %r{\A(app/models/project_authorization|app/services/users/refresh_authorized_projects_service)(/|\.rb)} => :database,
+ %r{\Arubocop/cop/migration(/|\.rb)} => :database,
+
%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,
@@ -112,7 +117,6 @@ module Gitlab
%r{\A(Dangerfile|Gemfile|Gemfile.lock|Procfile|Rakefile|\.gitlab-ci\.yml)\z} => :backend,
%r{\A[A-Z_]+_VERSION\z} => :backend,
- %r{\A(ee/)?db/} => :database,
%r{\A(ee/)?qa/} => :qa,
# Files that don't fit into any category are marked with :none
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index d349c378e53..dfa80eb4a64 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -134,6 +134,10 @@ module Gitlab
@line_code ||= diff_file(repository)&.line_code_for_position(self)
end
+ def file_hash
+ @file_hash ||= Digest::SHA1.hexdigest(file_path)
+ end
+
def on_image?
position_type == 'image'
end
diff --git a/lib/gitlab/diff/position_tracer.rb b/lib/gitlab/diff/position_tracer.rb
index af3df820422..a1c82ce9afc 100644
--- a/lib/gitlab/diff/position_tracer.rb
+++ b/lib/gitlab/diff/position_tracer.rb
@@ -17,187 +17,13 @@ module Gitlab
@paths = paths
end
- def trace(ab_position)
+ def trace(old_position)
return unless old_diff_refs&.complete? && new_diff_refs&.complete?
- return unless ab_position.diff_refs == old_diff_refs
+ return unless old_position.diff_refs == old_diff_refs
- # Suppose we have an MR with source branch `feature` and target branch `master`.
- # When the MR was created, the head of `master` was commit A, and the
- # head of `feature` was commit B, resulting in the original diff A->B.
- # Since creation, `master` was updated to C.
- # Now `feature` is being updated to D, and the newly generated MR diff is C->D.
- # It is possible that C and D are direct descendants of A and B respectively,
- # but this isn't necessarily the case as rebases and merges come into play.
- #
- # Suppose we have a diff note on the original diff A->B. Now that the MR
- # is updated, we need to find out what line in C->D corresponds to the
- # line the note was originally created on, so that we can update the diff note's
- # records and continue to display it in the right place in the diffs.
- # If we cannot find this line in the new diff, this means the diff note is now
- # outdated, and we will display that fact to the user.
- #
- # In the new diff, the file the diff note was originally created on may
- # have been renamed, deleted or even created, if the file existed in A and B,
- # but was removed in C, and restored in D.
- #
- # Every diff note stores a Position object that defines a specific location,
- # identified by paths and line numbers, within a specific diff, identified
- # by start, head and base commit ids.
- #
- # For diff notes for diff A->B, the position looks like this:
- # Position
- # start_sha - ID of commit A
- # head_sha - ID of commit B
- # base_sha - ID of base commit of A and B
- # old_path - path as of A (nil if file was newly created)
- # new_path - path as of B (nil if file was deleted)
- # old_line - line number as of A (nil if file was newly created)
- # new_line - line number as of B (nil if file was deleted)
- #
- # We can easily update `start_sha` and `head_sha` to hold the IDs of
- # commits C and D, and can trivially determine `base_sha` based on those,
- # but need to find the paths and line numbers as of C and D.
- #
- # If the file was unchanged or newly created in A->B, the path as of D can be found
- # by generating diff B->D ("head to head"), finding the diff file with
- # `diff_file.old_path == position.new_path`, and taking `diff_file.new_path`.
- # The path as of C can be found by taking diff C->D, finding the diff file
- # with that same `new_path` and taking `diff_file.old_path`.
- # The line number as of D can be found by using the LineMapper on diff B->D
- # and providing the line number as of B.
- # The line number as of C can be found by using the LineMapper on diff C->D
- # and providing the line number as of D.
- #
- # If the file was deleted in A->B, the path as of C can be found
- # by generating diff A->C ("base to base"), finding the diff file with
- # `diff_file.old_path == position.old_path`, and taking `diff_file.new_path`.
- # The path as of D can be found by taking diff C->D, finding the diff file
- # with `old_path` set to that `diff_file.new_path` and taking `diff_file.new_path`.
- # The line number as of C can be found by using the LineMapper on diff A->C
- # and providing the line number as of A.
- # The line number as of D can be found by using the LineMapper on diff C->D
- # and providing the line number as of C.
+ strategy = old_position.on_text? ? LineStrategy : ImageStrategy
- if ab_position.added?
- trace_added_line(ab_position)
- elsif ab_position.removed?
- trace_removed_line(ab_position)
- else # unchanged
- trace_unchanged_line(ab_position)
- end
- end
-
- private
-
- def trace_added_line(ab_position)
- b_path = ab_position.new_path
- b_line = ab_position.new_line
-
- bd_diff = bd_diffs.diff_file_with_old_path(b_path)
-
- d_path = bd_diff&.new_path || b_path
- d_line = LineMapper.new(bd_diff).old_to_new(b_line)
-
- if d_line
- cd_diff = cd_diffs.diff_file_with_new_path(d_path)
-
- c_path = cd_diff&.old_path || d_path
- c_line = LineMapper.new(cd_diff).new_to_old(d_line)
-
- if c_line
- # If the line is still in D but also in C, it has turned from an
- # added line into an unchanged one.
- new_position = position(cd_diff, c_line, d_line)
- if valid_position?(new_position)
- # If the line is still in the MR, we don't treat this as outdated.
- { position: new_position, outdated: false }
- else
- # If the line is no longer in the MR, we unfortunately cannot show
- # the current state on the CD diff, so we treat it as outdated.
- ac_diff = ac_diffs.diff_file_with_new_path(c_path)
-
- { position: position(ac_diff, nil, c_line), outdated: true }
- end
- else
- # If the line is still in D and not in C, it is still added.
- { position: position(cd_diff, nil, d_line), outdated: false }
- end
- else
- # If the line is no longer in D, it has been removed from the MR.
- { position: position(bd_diff, b_line, nil), outdated: true }
- end
- end
-
- def trace_removed_line(ab_position)
- a_path = ab_position.old_path
- a_line = ab_position.old_line
-
- ac_diff = ac_diffs.diff_file_with_old_path(a_path)
-
- c_path = ac_diff&.new_path || a_path
- c_line = LineMapper.new(ac_diff).old_to_new(a_line)
-
- if c_line
- cd_diff = cd_diffs.diff_file_with_old_path(c_path)
-
- d_path = cd_diff&.new_path || c_path
- d_line = LineMapper.new(cd_diff).old_to_new(c_line)
-
- if d_line
- # If the line is still in C but also in D, it has turned from a
- # removed line into an unchanged one.
- bd_diff = bd_diffs.diff_file_with_new_path(d_path)
-
- { position: position(bd_diff, nil, d_line), outdated: true }
- else
- # If the line is still in C and not in D, it is still removed.
- { position: position(cd_diff, c_line, nil), outdated: false }
- end
- else
- # If the line is no longer in C, it has been removed outside of the MR.
- { position: position(ac_diff, a_line, nil), outdated: true }
- end
- end
-
- def trace_unchanged_line(ab_position)
- a_path = ab_position.old_path
- a_line = ab_position.old_line
- b_path = ab_position.new_path
- b_line = ab_position.new_line
-
- ac_diff = ac_diffs.diff_file_with_old_path(a_path)
-
- c_path = ac_diff&.new_path || a_path
- c_line = LineMapper.new(ac_diff).old_to_new(a_line)
-
- bd_diff = bd_diffs.diff_file_with_old_path(b_path)
-
- d_line = LineMapper.new(bd_diff).old_to_new(b_line)
-
- cd_diff = cd_diffs.diff_file_with_old_path(c_path)
-
- if c_line && d_line
- # If the line is still in C and D, it is still unchanged.
- new_position = position(cd_diff, c_line, d_line)
- if valid_position?(new_position)
- # If the line is still in the MR, we don't treat this as outdated.
- { position: new_position, outdated: false }
- else
- # If the line is no longer in the MR, we unfortunately cannot show
- # the current state on the CD diff or any change on the BD diff,
- # so we treat it as outdated.
- { position: nil, outdated: true }
- end
- elsif d_line # && !c_line
- # If the line is still in D but no longer in C, it has turned from
- # an unchanged line into an added one.
- # We don't treat this as outdated since the line is still in the MR.
- { position: position(cd_diff, nil, d_line), outdated: false }
- else # !d_line && (c_line || !c_line)
- # If the line is no longer in D, it has turned from an unchanged line
- # into a removed one.
- { position: position(bd_diff, b_line, nil), outdated: true }
- end
+ strategy.new(self).trace(old_position)
end
def ac_diffs
@@ -216,18 +42,12 @@ module Gitlab
@cd_diffs ||= compare(new_diff_refs.start_sha, new_diff_refs.head_sha)
end
+ private
+
def compare(start_sha, head_sha, straight: false)
compare = CompareService.new(project, head_sha).execute(project, start_sha, straight: straight)
compare.diffs(paths: paths, expanded: true)
end
-
- def position(diff_file, old_line, new_line)
- Position.new(diff_file: diff_file, old_line: old_line, new_line: new_line)
- end
-
- def valid_position?(position)
- !!position.diff_line(project.repository)
- end
end
end
end
diff --git a/lib/gitlab/diff/position_tracer/base_strategy.rb b/lib/gitlab/diff/position_tracer/base_strategy.rb
new file mode 100644
index 00000000000..65049daabf4
--- /dev/null
+++ b/lib/gitlab/diff/position_tracer/base_strategy.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Diff
+ class PositionTracer
+ class BaseStrategy
+ attr_reader :tracer
+
+ delegate \
+ :project,
+ :ac_diffs,
+ :bd_diffs,
+ :cd_diffs,
+ to: :tracer
+
+ def initialize(tracer)
+ @tracer = tracer
+ end
+
+ def trace(position)
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/position_tracer/image_strategy.rb b/lib/gitlab/diff/position_tracer/image_strategy.rb
new file mode 100644
index 00000000000..79244a17951
--- /dev/null
+++ b/lib/gitlab/diff/position_tracer/image_strategy.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Diff
+ class PositionTracer
+ class ImageStrategy < BaseStrategy
+ def trace(position)
+ b_path = position.new_path
+
+ # If file exists in B->D (e.g. updated, renamed, removed), let the
+ # note become outdated.
+ bd_diff = bd_diffs.diff_file_with_old_path(b_path)
+
+ return { position: new_position(position, bd_diff), outdated: true } if bd_diff
+
+ # If file still exists in the new diff, update the position.
+ cd_diff = cd_diffs.diff_file_with_new_path(bd_diff&.new_path || b_path)
+
+ return { position: new_position(position, cd_diff), outdated: false } if cd_diff
+
+ # If file exists in A->C (e.g. rebased and same changes were present
+ # in target branch), let the note become outdated.
+ ac_diff = ac_diffs.diff_file_with_old_path(position.old_path)
+
+ return { position: new_position(position, ac_diff), outdated: true } if ac_diff
+
+ # If ever there's a case that the file no longer exists in any diff,
+ # don't set a change position and let the note become outdated.
+ #
+ # This should never happen given the file should exist in one of the
+ # diffs above.
+ { outdated: true }
+ end
+
+ private
+
+ def new_position(position, diff_file)
+ Position.new(
+ diff_file: diff_file,
+ x: position.x,
+ y: position.y,
+ width: position.width,
+ height: position.height,
+ position_type: position.position_type
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/position_tracer/line_strategy.rb b/lib/gitlab/diff/position_tracer/line_strategy.rb
new file mode 100644
index 00000000000..8db0fc6f963
--- /dev/null
+++ b/lib/gitlab/diff/position_tracer/line_strategy.rb
@@ -0,0 +1,201 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Diff
+ class PositionTracer
+ class LineStrategy < BaseStrategy
+ def trace(position)
+ # Suppose we have an MR with source branch `feature` and target branch `master`.
+ # When the MR was created, the head of `master` was commit A, and the
+ # head of `feature` was commit B, resulting in the original diff A->B.
+ # Since creation, `master` was updated to C.
+ # Now `feature` is being updated to D, and the newly generated MR diff is C->D.
+ # It is possible that C and D are direct descendants of A and B respectively,
+ # but this isn't necessarily the case as rebases and merges come into play.
+ #
+ # Suppose we have a diff note on the original diff A->B. Now that the MR
+ # is updated, we need to find out what line in C->D corresponds to the
+ # line the note was originally created on, so that we can update the diff note's
+ # records and continue to display it in the right place in the diffs.
+ # If we cannot find this line in the new diff, this means the diff note is now
+ # outdated, and we will display that fact to the user.
+ #
+ # In the new diff, the file the diff note was originally created on may
+ # have been renamed, deleted or even created, if the file existed in A and B,
+ # but was removed in C, and restored in D.
+ #
+ # Every diff note stores a Position object that defines a specific location,
+ # identified by paths and line numbers, within a specific diff, identified
+ # by start, head and base commit ids.
+ #
+ # For diff notes for diff A->B, the position looks like this:
+ # Position
+ # start_sha - ID of commit A
+ # head_sha - ID of commit B
+ # base_sha - ID of base commit of A and B
+ # old_path - path as of A (nil if file was newly created)
+ # new_path - path as of B (nil if file was deleted)
+ # old_line - line number as of A (nil if file was newly created)
+ # new_line - line number as of B (nil if file was deleted)
+ #
+ # We can easily update `start_sha` and `head_sha` to hold the IDs of
+ # commits C and D, and can trivially determine `base_sha` based on those,
+ # but need to find the paths and line numbers as of C and D.
+ #
+ # If the file was unchanged or newly created in A->B, the path as of D can be found
+ # by generating diff B->D ("head to head"), finding the diff file with
+ # `diff_file.old_path == position.new_path`, and taking `diff_file.new_path`.
+ # The path as of C can be found by taking diff C->D, finding the diff file
+ # with that same `new_path` and taking `diff_file.old_path`.
+ # The line number as of D can be found by using the LineMapper on diff B->D
+ # and providing the line number as of B.
+ # The line number as of C can be found by using the LineMapper on diff C->D
+ # and providing the line number as of D.
+ #
+ # If the file was deleted in A->B, the path as of C can be found
+ # by generating diff A->C ("base to base"), finding the diff file with
+ # `diff_file.old_path == position.old_path`, and taking `diff_file.new_path`.
+ # The path as of D can be found by taking diff C->D, finding the diff file
+ # with `old_path` set to that `diff_file.new_path` and taking `diff_file.new_path`.
+ # The line number as of C can be found by using the LineMapper on diff A->C
+ # and providing the line number as of A.
+ # The line number as of D can be found by using the LineMapper on diff C->D
+ # and providing the line number as of C.
+
+ if position.added?
+ trace_added_line(position)
+ elsif position.removed?
+ trace_removed_line(position)
+ else # unchanged
+ trace_unchanged_line(position)
+ end
+ end
+
+ private
+
+ def trace_added_line(position)
+ b_path = position.new_path
+ b_line = position.new_line
+
+ bd_diff = bd_diffs.diff_file_with_old_path(b_path)
+
+ d_path = bd_diff&.new_path || b_path
+ d_line = LineMapper.new(bd_diff).old_to_new(b_line)
+
+ if d_line
+ cd_diff = cd_diffs.diff_file_with_new_path(d_path)
+
+ c_path = cd_diff&.old_path || d_path
+ c_line = LineMapper.new(cd_diff).new_to_old(d_line)
+
+ if c_line
+ # If the line is still in D but also in C, it has turned from an
+ # added line into an unchanged one.
+ new_position = new_position(cd_diff, c_line, d_line)
+ if valid_position?(new_position)
+ # If the line is still in the MR, we don't treat this as outdated.
+ { position: new_position, outdated: false }
+ else
+ # If the line is no longer in the MR, we unfortunately cannot show
+ # the current state on the CD diff, so we treat it as outdated.
+ ac_diff = ac_diffs.diff_file_with_new_path(c_path)
+
+ { position: new_position(ac_diff, nil, c_line), outdated: true }
+ end
+ else
+ # If the line is still in D and not in C, it is still added.
+ { position: new_position(cd_diff, nil, d_line), outdated: false }
+ end
+ else
+ # If the line is no longer in D, it has been removed from the MR.
+ { position: new_position(bd_diff, b_line, nil), outdated: true }
+ end
+ end
+
+ def trace_removed_line(position)
+ a_path = position.old_path
+ a_line = position.old_line
+
+ ac_diff = ac_diffs.diff_file_with_old_path(a_path)
+
+ c_path = ac_diff&.new_path || a_path
+ c_line = LineMapper.new(ac_diff).old_to_new(a_line)
+
+ if c_line
+ cd_diff = cd_diffs.diff_file_with_old_path(c_path)
+
+ d_path = cd_diff&.new_path || c_path
+ d_line = LineMapper.new(cd_diff).old_to_new(c_line)
+
+ if d_line
+ # If the line is still in C but also in D, it has turned from a
+ # removed line into an unchanged one.
+ bd_diff = bd_diffs.diff_file_with_new_path(d_path)
+
+ { position: new_position(bd_diff, nil, d_line), outdated: true }
+ else
+ # If the line is still in C and not in D, it is still removed.
+ { position: new_position(cd_diff, c_line, nil), outdated: false }
+ end
+ else
+ # If the line is no longer in C, it has been removed outside of the MR.
+ { position: new_position(ac_diff, a_line, nil), outdated: true }
+ end
+ end
+
+ def trace_unchanged_line(position)
+ a_path = position.old_path
+ a_line = position.old_line
+ b_path = position.new_path
+ b_line = position.new_line
+
+ ac_diff = ac_diffs.diff_file_with_old_path(a_path)
+
+ c_path = ac_diff&.new_path || a_path
+ c_line = LineMapper.new(ac_diff).old_to_new(a_line)
+
+ bd_diff = bd_diffs.diff_file_with_old_path(b_path)
+
+ d_line = LineMapper.new(bd_diff).old_to_new(b_line)
+
+ cd_diff = cd_diffs.diff_file_with_old_path(c_path)
+
+ if c_line && d_line
+ # If the line is still in C and D, it is still unchanged.
+ new_position = new_position(cd_diff, c_line, d_line)
+ if valid_position?(new_position)
+ # If the line is still in the MR, we don't treat this as outdated.
+ { position: new_position, outdated: false }
+ else
+ # If the line is no longer in the MR, we unfortunately cannot show
+ # the current state on the CD diff or any change on the BD diff,
+ # so we treat it as outdated.
+ { position: nil, outdated: true }
+ end
+ elsif d_line # && !c_line
+ # If the line is still in D but no longer in C, it has turned from
+ # an unchanged line into an added one.
+ # We don't treat this as outdated since the line is still in the MR.
+ { position: new_position(cd_diff, nil, d_line), outdated: false }
+ else # !d_line && (c_line || !c_line)
+ # If the line is no longer in D, it has turned from an unchanged line
+ # into a removed one.
+ { position: new_position(bd_diff, b_line, nil), outdated: true }
+ end
+ end
+
+ def new_position(diff_file, old_line, new_line)
+ Position.new(
+ diff_file: diff_file,
+ old_line: old_line,
+ new_line: new_line
+ )
+ end
+
+ def valid_position?(position)
+ !!position.diff_line(project.repository)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 19b6aab1c4f..060a29be782 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -536,9 +536,9 @@ module Gitlab
tags.find { |tag| tag.name == name }
end
- def merge_to_ref(user, source_sha, branch, target_ref, message)
+ def merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref)
wrapped_gitaly_errors do
- gitaly_operation_client.user_merge_to_ref(user, source_sha, branch, target_ref, message)
+ gitaly_operation_client.user_merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref)
end
end
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index b42e6cbad8d..783c2ff0915 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -100,14 +100,15 @@ module Gitlab
end
end
- def user_merge_to_ref(user, source_sha, branch, target_ref, message)
+ def user_merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref)
request = Gitaly::UserMergeToRefRequest.new(
repository: @gitaly_repo,
source_sha: source_sha,
branch: encode_binary(branch),
target_ref: encode_binary(target_ref),
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
- message: encode_binary(message)
+ message: encode_binary(message),
+ first_parent_ref: encode_binary(first_parent_ref)
)
response = GitalyClient.call(@repository.storage, :operation_service, :user_merge_to_ref, request)
diff --git a/lib/gitlab/graphql/authorize.rb b/lib/gitlab/graphql/authorize.rb
index f8d0208e275..e83b567308b 100644
--- a/lib/gitlab/graphql/authorize.rb
+++ b/lib/gitlab/graphql/authorize.rb
@@ -8,7 +8,7 @@ module Gitlab
extend ActiveSupport::Concern
def self.use(schema_definition)
- schema_definition.instrument(:field, Instrumentation.new, after_built_ins: true)
+ schema_definition.instrument(:field, Gitlab::Graphql::Authorize::Instrumentation.new, after_built_ins: true)
end
end
end
diff --git a/lib/gitlab/graphql/authorize/authorize_field_service.rb b/lib/gitlab/graphql/authorize/authorize_field_service.rb
index 619ce100421..3b5dde2fde5 100644
--- a/lib/gitlab/graphql/authorize/authorize_field_service.rb
+++ b/lib/gitlab/graphql/authorize/authorize_field_service.rb
@@ -39,6 +39,8 @@ module Gitlab
type = node_type_for_basic_connection(type)
end
+ type = type.unwrap if type.kind.non_null?
+
Array.wrap(type.metadata[:authorize])
end
diff --git a/lib/gitlab/graphql/calls_gitaly.rb b/lib/gitlab/graphql/calls_gitaly.rb
new file mode 100644
index 00000000000..40cd74a34f2
--- /dev/null
+++ b/lib/gitlab/graphql/calls_gitaly.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ # Wraps the field resolution to count Gitaly calls before and after.
+ # Raises an error if the field calls Gitaly but hadn't declared such.
+ module CallsGitaly
+ extend ActiveSupport::Concern
+
+ def self.use(schema_definition)
+ schema_definition.instrument(:field, Gitlab::Graphql::CallsGitaly::Instrumentation.new, after_built_ins: true)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/calls_gitaly/instrumentation.rb b/lib/gitlab/graphql/calls_gitaly/instrumentation.rb
new file mode 100644
index 00000000000..fbd5e348c7d
--- /dev/null
+++ b/lib/gitlab/graphql/calls_gitaly/instrumentation.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module CallsGitaly
+ class Instrumentation
+ # Check if any `calls_gitaly: true` declarations need to be added
+ # Do nothing if a constant complexity was provided
+ def instrument(_type, field)
+ type_object = field.metadata[:type_class]
+ return field unless type_object.respond_to?(:calls_gitaly?)
+ return field if type_object.constant_complexity? || type_object.calls_gitaly?
+
+ old_resolver_proc = field.resolve_proc
+
+ gitaly_wrapped_resolve = -> (typed_object, args, ctx) do
+ previous_gitaly_call_count = Gitlab::GitalyClient.get_request_count
+ result = old_resolver_proc.call(typed_object, args, ctx)
+ current_gitaly_call_count = Gitlab::GitalyClient.get_request_count
+ calls_gitaly_check(type_object, current_gitaly_call_count - previous_gitaly_call_count)
+ result
+ end
+
+ field.redefine do
+ resolve(gitaly_wrapped_resolve)
+ end
+ end
+
+ def calls_gitaly_check(type_object, calls)
+ return if calls < 1
+
+ # Will inform you if there needs to be `calls_gitaly: true` as a kwarg in the field declaration
+ # if there is at least 1 Gitaly call involved with the field resolution.
+ error = RuntimeError.new("Gitaly is called for field '#{type_object.name}' on #{type_object.owner.try(:name)} - please either specify a constant complexity or add `calls_gitaly: true` to the field declaration")
+ Gitlab::Sentry.track_exception(error)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/mount_mutation.rb b/lib/gitlab/graphql/mount_mutation.rb
index 9048967d4e1..b10e963170a 100644
--- a/lib/gitlab/graphql/mount_mutation.rb
+++ b/lib/gitlab/graphql/mount_mutation.rb
@@ -6,11 +6,12 @@ module Gitlab
extend ActiveSupport::Concern
class_methods do
- def mount_mutation(mutation_class)
+ def mount_mutation(mutation_class, **custom_kwargs)
# Using an underscored field name symbol will make `graphql-ruby`
# standardize the field name
field mutation_class.graphql_name.underscore.to_sym,
- mutation: mutation_class
+ mutation: mutation_class,
+ **custom_kwargs
end
end
end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index a0fb051e806..01437c67fa9 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -160,6 +160,7 @@ excluded_attributes:
- :milestone_id
- :ref_fetched
- :merge_jid
+ - :rebase_jid
- :latest_merge_request_diff_id
award_emoji:
- :awardable_id
diff --git a/lib/gitlab/issuable_metadata.rb b/lib/gitlab/issuable_metadata.rb
index 351d15605e0..be73bcd5506 100644
--- a/lib/gitlab/issuable_metadata.rb
+++ b/lib/gitlab/issuable_metadata.rb
@@ -2,7 +2,7 @@
module Gitlab
module IssuableMetadata
- def issuable_meta_data(issuable_collection, collection_type)
+ def issuable_meta_data(issuable_collection, collection_type, user = nil)
# ActiveRecord uses Object#extend for null relations.
if !(issuable_collection.singleton_class < ActiveRecord::NullRelation) &&
issuable_collection.respond_to?(:limit_value) &&
@@ -23,7 +23,7 @@ module Gitlab
issuable_votes_count = ::AwardEmoji.votes_for_collection(issuable_ids, collection_type)
issuable_merge_requests_count =
if collection_type == 'Issue'
- ::MergeRequestsClosingIssues.count_for_collection(issuable_ids)
+ ::MergeRequestsClosingIssues.count_for_collection(issuable_ids, user)
else
[]
end
diff --git a/lib/gitlab/legacy_github_import/release_formatter.rb b/lib/gitlab/legacy_github_import/release_formatter.rb
index 746786b5a66..fdab6b512ea 100644
--- a/lib/gitlab/legacy_github_import/release_formatter.rb
+++ b/lib/gitlab/legacy_github_import/release_formatter.rb
@@ -10,6 +10,7 @@ module Gitlab
name: raw_data.name,
description: raw_data.body,
created_at: raw_data.created_at,
+ released_at: raw_data.published_at,
updated_at: raw_data.created_at
}
end
diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb
index 17eacbd21d8..eef802caabb 100644
--- a/lib/gitlab/metrics/samplers/ruby_sampler.rb
+++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb
@@ -6,6 +6,12 @@ module Gitlab
module Metrics
module Samplers
class RubySampler < BaseSampler
+ def initialize(interval)
+ metrics[:process_start_time_seconds].set(labels.merge(worker_label), Time.now.to_i)
+
+ super
+ end
+
def metrics
@metrics ||= init_metrics
end
@@ -47,7 +53,6 @@ module Gitlab
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
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index 34de40ca72f..5c2f07b95e2 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -31,14 +31,6 @@ module Gitlab
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
@@ -51,10 +43,6 @@ module Gitlab
def self.max_open_file_descriptors
0
end
-
- def self.process_start_time
- 0
- end
end
def self.cpu_time
diff --git a/lib/gitlab/namespaced_session_store.rb b/lib/gitlab/namespaced_session_store.rb
index 34520078bfb..f0f24c081c3 100644
--- a/lib/gitlab/namespaced_session_store.rb
+++ b/lib/gitlab/namespaced_session_store.rb
@@ -4,19 +4,24 @@ module Gitlab
class NamespacedSessionStore
delegate :[], :[]=, to: :store
- def initialize(key)
+ def initialize(key, session = Session.current)
@key = key
+ @session = session
end
def initiated?
- !Session.current.nil?
+ !session.nil?
end
def store
- return unless Session.current
+ return unless session
- Session.current[@key] ||= {}
- Session.current[@key]
+ session[@key] ||= {}
+ session[@key]
end
+
+ private
+
+ attr_reader :session
end
end
diff --git a/lib/gitlab/performance_bar.rb b/lib/gitlab/performance_bar.rb
index 4b0c7b5c7f8..07439d8e011 100644
--- a/lib/gitlab/performance_bar.rb
+++ b/lib/gitlab/performance_bar.rb
@@ -3,7 +3,8 @@
module Gitlab
module PerformanceBar
ALLOWED_USER_IDS_KEY = 'performance_bar_allowed_user_ids:v2'.freeze
- EXPIRY_TIME = 5.minutes
+ EXPIRY_TIME_L1_CACHE = 1.minute
+ EXPIRY_TIME_L2_CACHE = 5.minutes
def self.enabled?(user = nil)
return true if Rails.env.development?
@@ -19,20 +20,31 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def self.allowed_user_ids
- Rails.cache.fetch(ALLOWED_USER_IDS_KEY, expires_in: EXPIRY_TIME) do
- group = Group.find_by_id(allowed_group_id)
+ l1_cache_backend.fetch(ALLOWED_USER_IDS_KEY, expires_in: EXPIRY_TIME_L1_CACHE) do
+ l2_cache_backend.fetch(ALLOWED_USER_IDS_KEY, expires_in: EXPIRY_TIME_L2_CACHE) do
+ group = Group.find_by_id(allowed_group_id)
- if group
- GroupMembersFinder.new(group).execute.pluck(:user_id)
- else
- []
+ if group
+ GroupMembersFinder.new(group).execute.pluck(:user_id)
+ else
+ []
+ end
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
def self.expire_allowed_user_ids_cache
- Rails.cache.delete(ALLOWED_USER_IDS_KEY)
+ l1_cache_backend.delete(ALLOWED_USER_IDS_KEY)
+ l2_cache_backend.delete(ALLOWED_USER_IDS_KEY)
+ end
+
+ def self.l1_cache_backend
+ Gitlab::ThreadMemoryCache.cache_backend
+ end
+
+ def self.l2_cache_backend
+ Rails.cache
end
end
end
diff --git a/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb b/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb
new file mode 100644
index 00000000000..2d997760c46
--- /dev/null
+++ b/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+# Adapted from https://github.com/peek/peek/blob/master/lib/peek/adapters/redis.rb
+module Gitlab
+ module PerformanceBar
+ module RedisAdapterWhenPeekEnabled
+ def save
+ super unless ::Peek.request_id.blank?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/quick_actions/issuable_actions.rb b/lib/gitlab/quick_actions/issuable_actions.rb
index 572c55efcc2..f7f89d4e897 100644
--- a/lib/gitlab/quick_actions/issuable_actions.rb
+++ b/lib/gitlab/quick_actions/issuable_actions.rb
@@ -146,8 +146,8 @@ module Gitlab
@updates[:todo_event] = 'add'
end
- desc _('Mark todo as done')
- explanation _('Marks todo as done.')
+ desc _('Mark to do as done')
+ explanation _('Marks to do as done.')
types Issuable
condition do
quick_action_target.persisted? &&
diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb
index 583a970bf4e..0f890a12134 100644
--- a/lib/gitlab/sidekiq_status.rb
+++ b/lib/gitlab/sidekiq_status.rb
@@ -53,14 +53,14 @@ module Gitlab
self.num_running(job_ids).zero?
end
- # Returns true if the given job is running
+ # Returns true if the given job is running or enqueued.
#
# job_id - The Sidekiq job ID to check.
def self.running?(job_id)
num_running([job_id]) > 0
end
- # Returns the number of jobs that are running.
+ # Returns the number of jobs that are running or enqueued.
#
# job_ids - The Sidekiq job IDs to check.
def self.num_running(job_ids)
@@ -81,7 +81,7 @@ module Gitlab
# job_ids - The Sidekiq job IDs to check.
#
# Returns an array of true or false indicating job completion.
- # true = job is still running
+ # true = job is still running or enqueued
# false = job completed
def self.job_status(job_ids)
keys = job_ids.map { |jid| key_for(jid) }
diff --git a/lib/gitlab/user_extractor.rb b/lib/gitlab/user_extractor.rb
deleted file mode 100644
index ede60c9ab1d..00000000000
--- a/lib/gitlab/user_extractor.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-# This class extracts all users found in a piece of text by the username or the
-# email address
-
-module Gitlab
- class UserExtractor
- # Not using `Devise.email_regexp` to filter out any chars that an email
- # does not end with and not pinning the email to a start of end of a string.
- EMAIL_REGEXP = /(?<email>([^@\s]+@[^@\s]+(?<!\W)))/.freeze
- USERNAME_REGEXP = User.reference_pattern
-
- def initialize(text)
- # EE passes an Array to `text` in a few places, so we want to support both
- # here.
- @text = Array(text).join(' ')
- end
-
- def users
- return User.none unless @text.present?
- return User.none if references.empty?
-
- @users ||= User.from_union(union_relations)
- end
-
- def usernames
- matches[:usernames]
- end
-
- def emails
- matches[:emails]
- end
-
- def references
- @references ||= matches.values.flatten
- end
-
- def matches
- @matches ||= {
- emails: @text.scan(EMAIL_REGEXP).flatten.uniq,
- usernames: @text.scan(USERNAME_REGEXP).flatten.uniq
- }
- end
-
- private
-
- def union_relations
- relations = []
-
- relations << User.by_any_email(emails) if emails.any?
- relations << User.by_username(usernames) if usernames.any?
-
- relations
- end
- end
-end
diff --git a/lib/gitlab/utils/deep_size.rb b/lib/gitlab/utils/deep_size.rb
new file mode 100644
index 00000000000..562cf09e249
--- /dev/null
+++ b/lib/gitlab/utils/deep_size.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'objspace'
+
+module Gitlab
+ module Utils
+ class DeepSize
+ Error = Class.new(StandardError)
+ TooMuchDataError = Class.new(Error)
+
+ DEFAULT_MAX_SIZE = 1.megabyte
+ DEFAULT_MAX_DEPTH = 100
+
+ def initialize(root, max_size: DEFAULT_MAX_SIZE, max_depth: DEFAULT_MAX_DEPTH)
+ @root = root
+ @max_size = max_size
+ @max_depth = max_depth
+ @size = 0
+ @depth = 0
+
+ evaluate
+ end
+
+ def valid?
+ !too_big? && !too_deep?
+ end
+
+ private
+
+ def evaluate
+ add_object(@root)
+ rescue Error
+ # NOOP
+ end
+
+ def too_big?
+ @size > @max_size
+ end
+
+ def too_deep?
+ @depth > @max_depth
+ end
+
+ def add_object(object)
+ @size += ObjectSpace.memsize_of(object)
+ raise TooMuchDataError if @size > @max_size
+
+ add_array(object) if object.is_a?(Array)
+ add_hash(object) if object.is_a?(Hash)
+ end
+
+ def add_array(object)
+ with_nesting do
+ object.each do |n|
+ add_object(n)
+ end
+ end
+ end
+
+ def add_hash(object)
+ with_nesting do
+ object.each do |key, value|
+ add_object(key)
+ add_object(value)
+ end
+ end
+ end
+
+ def with_nesting
+ @depth += 1
+ raise TooMuchDataError if too_deep?
+
+ yield
+
+ @depth -= 1
+ end
+ end
+ end
+end
diff --git a/lib/peek/views/redis.rb b/lib/peek/views/redis.rb
new file mode 100644
index 00000000000..ad3c3c9fe01
--- /dev/null
+++ b/lib/peek/views/redis.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'redis'
+require 'peek-redis'
+
+module Gitlab
+ module Peek
+ module RedisInstrumented
+ def call(*args, &block)
+ start = Time.now
+ super(*args, &block)
+ ensure
+ duration = (Time.now - start)
+ add_call_details(duration, args)
+ end
+
+ private
+
+ def add_call_details(duration, args)
+ # redis-rb passes an array (e.g. [:get, key])
+ return unless args.length == 1
+
+ detail_store << {
+ cmd: args.first,
+ duration: duration,
+ backtrace: Gitlab::Profiler.clean_backtrace(caller)
+ }
+ end
+
+ def detail_store
+ ::Gitlab::SafeRequestStore['redis_call_details'] ||= []
+ end
+ end
+ end
+end
+
+module Peek
+ module Views
+ module RedisDetailed
+ def results
+ super.merge(details: details)
+ end
+
+ def details
+ detail_store
+ .sort { |a, b| b[:duration] <=> a[:duration] }
+ .map(&method(:format_call_details))
+ end
+
+ def detail_store
+ ::Gitlab::SafeRequestStore['redis_call_details'] ||= []
+ end
+
+ def format_call_details(call)
+ call.merge(cmd: format_command(call[:cmd]),
+ duration: (call[:duration] * 1000).round(3))
+ end
+
+ def format_command(cmd)
+ # Scrub out the value of the SET calls to avoid binary
+ # data or large data from spilling into the view
+ if cmd.length >= 2 && cmd.first =~ /set/i
+ cmd[-1] = "<redacted>"
+ end
+
+ cmd.join(' ')
+ end
+ end
+ end
+end
+
+class Redis::Client
+ prepend Gitlab::Peek::RedisInstrumented
+end
+
+module Peek
+ module Views
+ class Redis < View
+ prepend Peek::Views::RedisDetailed
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 60d4e199a9f..78bf48d66fc 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -28,6 +28,12 @@ msgstr ""
msgid " You need to do this before %{grace_period_deadline}."
msgstr ""
+msgid " and "
+msgstr ""
+
+msgid " and %{sliced}"
+msgstr ""
+
msgid " or "
msgstr ""
@@ -112,6 +118,9 @@ msgstr[1] ""
msgid "%{actionText} & %{openOrClose} %{noteable}"
msgstr ""
+msgid "%{canMergeCount}/%{assigneesCount} can merge"
+msgstr ""
+
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr ""
@@ -168,6 +177,9 @@ msgstr ""
msgid "%{link_start}Read more%{link_end} about role permissions"
msgstr ""
+msgid "%{listToShow}, and %{awardsListLength} more."
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr ""
@@ -231,6 +243,9 @@ msgid_plural "%{strong_start}%{tag_count}%{strong_end} Tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%{tabname} changed"
+msgstr ""
+
msgid "%{text} %{files}"
msgid_plural "%{text} %{files} files"
msgstr[0] ""
@@ -248,6 +263,9 @@ msgstr ""
msgid "%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc."
msgstr ""
+msgid "%{userName}'s avatar"
+msgstr ""
+
msgid "%{user_name} profile page"
msgstr ""
@@ -272,6 +290,9 @@ msgstr ""
msgid "+ %{moreCount} more"
msgstr ""
+msgid "+ %{numberOfHiddenAssignees} more"
+msgstr ""
+
msgid ", or "
msgstr ""
@@ -602,9 +623,18 @@ msgstr ""
msgid "Add a GPG key"
msgstr ""
+msgid "Add a Grafana button in the admin sidebar, monitoring section, to access a variety of statistics on the health and performance of GitLab."
+msgstr ""
+
+msgid "Add a To Do"
+msgstr ""
+
msgid "Add a bullet list"
msgstr ""
+msgid "Add a general comment to this %{noteableDisplayName}."
+msgstr ""
+
msgid "Add a general comment to this %{noteable_name}."
msgstr ""
@@ -677,9 +707,6 @@ msgstr ""
msgid "Add to review"
msgstr ""
-msgid "Add todo"
-msgstr ""
-
msgid "Add user(s) to the group:"
msgstr ""
@@ -866,9 +893,6 @@ msgstr ""
msgid "All projects"
msgstr ""
-msgid "All todos were marked as done."
-msgstr ""
-
msgid "All users"
msgstr ""
@@ -1196,6 +1220,9 @@ msgstr ""
msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
+msgid "Are you sure you want to delete this %{typeOfComment}?"
+msgstr ""
+
msgid "Are you sure you want to delete this device? This action cannot be undone."
msgstr ""
@@ -1314,7 +1341,9 @@ msgid "Assigned to me"
msgstr ""
msgid "Assignee"
-msgstr ""
+msgid_plural "%d Assignees"
+msgstr[0] ""
+msgstr[1] ""
msgid "Assignee(s)"
msgstr ""
@@ -1885,6 +1914,9 @@ msgstr ""
msgid "Cannot create the abuse report. This user has been blocked."
msgstr ""
+msgid "Cannot merge"
+msgstr ""
+
msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
@@ -2212,6 +2244,9 @@ msgstr ""
msgid "Close"
msgstr ""
+msgid "Close %{tabname}"
+msgstr ""
+
msgid "Close milestone"
msgstr ""
@@ -2764,10 +2799,10 @@ msgstr ""
msgid "Comment & reopen %{noteable_name}"
msgstr ""
-msgid "Comment & resolve discussion"
+msgid "Comment & resolve thread"
msgstr ""
-msgid "Comment & unresolve discussion"
+msgid "Comment & unresolve thread"
msgstr ""
msgid "Comment form position"
@@ -3557,6 +3592,9 @@ msgstr ""
msgid "DeployTokens|Created"
msgstr ""
+msgid "DeployTokens|Default format is \"gitlab+deploy-token-{n}\". Enter custom username if you want to change it."
+msgstr ""
+
msgid "DeployTokens|Deploy Tokens"
msgstr ""
@@ -3710,6 +3748,12 @@ msgstr ""
msgid "Discuss a specific suggestion or question that needs to be resolved"
msgstr ""
+msgid "Discuss a specific suggestion or question that needs to be resolved."
+msgstr ""
+
+msgid "Discuss a specific suggestion or question."
+msgstr ""
+
msgid "Discussion"
msgstr ""
@@ -3914,9 +3958,15 @@ msgstr ""
msgid "Enable HTML emails"
msgstr ""
+msgid "Enable access to Grafana"
+msgstr ""
+
msgid "Enable access to the Performance Bar for a given group."
msgstr ""
+msgid "Enable and configure Grafana."
+msgstr ""
+
msgid "Enable and configure InfluxDB metrics."
msgstr ""
@@ -3977,6 +4027,9 @@ msgstr ""
msgid "Enforce DNS rebinding attack protection"
msgstr ""
+msgid "Ensure connectivity is available from the GitLab server to the Prometheus server"
+msgstr ""
+
msgid "Enter at least three characters to search"
msgstr ""
@@ -4151,6 +4204,9 @@ msgstr ""
msgid "Error fetching diverging counts for branches. Please try again."
msgstr ""
+msgid "Error fetching forked projects. Please try again."
+msgstr ""
+
msgid "Error fetching labels."
msgstr ""
@@ -4202,6 +4258,9 @@ msgstr ""
msgid "Error occurred when fetching sidebar data"
msgstr ""
+msgid "Error occurred when saving assignees"
+msgstr ""
+
msgid "Error occurred when toggling the notification subscription"
msgstr ""
@@ -4223,13 +4282,16 @@ msgstr ""
msgid "Error saving label update."
msgstr ""
+msgid "Error setting up editor. Please try again."
+msgstr ""
+
msgid "Error updating %{issuableType}"
msgstr ""
-msgid "Error updating status for all todos."
+msgid "Error updating status for all to-do items."
msgstr ""
-msgid "Error updating todo status."
+msgid "Error updating status of to-do item."
msgstr ""
msgid "Error uploading file"
@@ -4328,6 +4390,9 @@ msgstr ""
msgid "Everyone can contribute"
msgstr ""
+msgid "Everything on your to-do list is marked as done."
+msgstr ""
+
msgid "Everything you need to create a GitLab Pages site using GitBook."
msgstr ""
@@ -4921,6 +4986,9 @@ msgstr ""
msgid "Got it!"
msgstr ""
+msgid "Grafana URL"
+msgstr ""
+
msgid "Grant access"
msgstr ""
@@ -6085,6 +6153,9 @@ msgstr ""
msgid "Locked"
msgstr ""
+msgid "Locked by %{fileLockUserName}"
+msgstr ""
+
msgid "Locked to current projects"
msgstr ""
@@ -6181,6 +6252,9 @@ msgstr ""
msgid "March"
msgstr ""
+msgid "Mark as done"
+msgstr ""
+
msgid "Mark as resolved"
msgstr ""
@@ -6190,7 +6264,7 @@ msgstr ""
msgid "Mark this issue as a duplicate of another issue"
msgstr ""
-msgid "Mark todo as done"
+msgid "Mark to do as done"
msgstr ""
msgid "Markdown"
@@ -6205,7 +6279,7 @@ msgstr ""
msgid "Marks this issue as a duplicate of %{duplicate_reference}."
msgstr ""
-msgid "Marks todo as done."
+msgid "Marks to do as done."
msgstr ""
msgid "Max access level"
@@ -6313,7 +6387,7 @@ msgstr ""
msgid "MergeRequests|Reply..."
msgstr ""
-msgid "MergeRequests|Resolve this discussion in a new issue"
+msgid "MergeRequests|Resolve this thread in a new issue"
msgstr ""
msgid "MergeRequests|Saving the comment failed"
@@ -6334,19 +6408,19 @@ msgstr ""
msgid "MergeRequests|commented on commit %{commitLink}"
msgstr ""
-msgid "MergeRequests|started a discussion"
+msgid "MergeRequests|started a thread"
msgstr ""
-msgid "MergeRequests|started a discussion on %{linkStart}an old version of the diff%{linkEnd}"
+msgid "MergeRequests|started a thread on %{linkStart}an old version of the diff%{linkEnd}"
msgstr ""
-msgid "MergeRequests|started a discussion on %{linkStart}the diff%{linkEnd}"
+msgid "MergeRequests|started a thread on %{linkStart}the diff%{linkEnd}"
msgstr ""
-msgid "MergeRequests|started a discussion on an outdated change in commit %{linkStart}%{commitId}%{linkEnd}"
+msgid "MergeRequests|started a thread on an outdated change in commit %{linkStart}%{commitId}%{linkEnd}"
msgstr ""
-msgid "MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}"
+msgid "MergeRequests|started a thread on commit %{linkStart}%{commitId}%{linkEnd}"
msgstr ""
msgid "MergeRequest| %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}"
@@ -6376,12 +6450,18 @@ msgstr ""
msgid "Metrics"
msgstr ""
+msgid "Metrics - Grafana"
+msgstr ""
+
msgid "Metrics - Influx"
msgstr ""
msgid "Metrics - Prometheus"
msgstr ""
+msgid "Metrics Dashboard"
+msgstr ""
+
msgid "Metrics and profiling"
msgstr ""
@@ -6792,6 +6872,9 @@ msgstr ""
msgid "No files found."
msgstr ""
+msgid "No forks available to you."
+msgstr ""
+
msgid "No job trace"
msgstr ""
@@ -6810,6 +6893,9 @@ msgstr ""
msgid "No milestones to show"
msgstr ""
+msgid "No one can merge"
+msgstr ""
+
msgid "No other labels with such name or description"
msgstr ""
@@ -6870,6 +6956,9 @@ msgstr ""
msgid "Not started"
msgstr ""
+msgid "Note"
+msgstr ""
+
msgid "Note that this invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}."
msgstr ""
@@ -7049,6 +7138,9 @@ msgstr ""
msgid "Open in Xcode"
msgstr ""
+msgid "Open in file view"
+msgstr ""
+
msgid "Open issues"
msgstr ""
@@ -7493,6 +7585,9 @@ msgstr ""
msgid "Please %{link_to_register} or %{link_to_sign_in} to comment"
msgstr ""
+msgid "Please %{startTagRegister}register%{endRegisterTag} or %{startTagSignIn}sign in%{endSignInTag} to reply"
+msgstr ""
+
msgid "Please accept the Terms of Service before continuing."
msgstr ""
@@ -7556,9 +7651,6 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
-msgid "Pre-release"
-msgstr ""
-
msgid "Preferences"
msgstr ""
@@ -8297,9 +8389,6 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
-msgid "Prometheus server"
-msgstr ""
-
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
@@ -8776,7 +8865,7 @@ msgstr ""
msgid "Reset template"
msgstr ""
-msgid "Resolve all discussions in new issue"
+msgid "Resolve all threads in new issue"
msgstr ""
msgid "Resolve conflicts on source branch"
@@ -8785,6 +8874,9 @@ msgstr ""
msgid "Resolve discussion"
msgstr ""
+msgid "Resolve thread"
+msgstr ""
+
msgid "Resolved"
msgstr ""
@@ -9156,6 +9248,9 @@ msgstr ""
msgid "Select members to invite"
msgstr ""
+msgid "Select private project"
+msgstr ""
+
msgid "Select project"
msgstr ""
@@ -9557,6 +9652,9 @@ msgstr ""
msgid "Something went wrong while adding your award. Please try again."
msgstr ""
+msgid "Something went wrong while adding your comment. Please try again."
+msgstr ""
+
msgid "Something went wrong while applying the suggestion. Please try again."
msgstr ""
@@ -9566,6 +9664,12 @@ msgstr ""
msgid "Something went wrong while deleting the source branch. Please try again."
msgstr ""
+msgid "Something went wrong while deleting your note. Please try again."
+msgstr ""
+
+msgid "Something went wrong while editing your comment. Please try again."
+msgstr ""
+
msgid "Something went wrong while fetching comments. Please try again."
msgstr ""
@@ -9809,6 +9913,9 @@ msgstr ""
msgid "Start a %{new_merge_request} with these changes"
msgstr ""
+msgid "Start a new discussion..."
+msgstr ""
+
msgid "Start a new merge request"
msgstr ""
@@ -9824,16 +9931,16 @@ msgstr ""
msgid "Start date"
msgstr ""
-msgid "Start discussion"
+msgid "Start the Runner!"
msgstr ""
-msgid "Start discussion & close %{noteable_name}"
+msgid "Start thread"
msgstr ""
-msgid "Start discussion & reopen %{noteable_name}"
+msgid "Start thread & close %{noteable_name}"
msgstr ""
-msgid "Start the Runner!"
+msgid "Start thread & reopen %{noteable_name}"
msgstr ""
msgid "Started"
@@ -10579,6 +10686,9 @@ msgstr ""
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr ""
+msgid "This comment has changed since you started editing, please review the %{startTag}updated comment%{endTag} to ensure information is not lost."
+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 ""
@@ -10714,6 +10824,9 @@ msgstr ""
msgid "This job will automatically run after its timer finishes. Often they are used for incremental roll-out deploys to production environments. When unscheduled it converts into a manual action."
msgstr ""
+msgid "This may expose confidential information as the selected fork is in another namespace that can have other members."
+msgstr ""
+
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr ""
@@ -10828,12 +10941,21 @@ msgstr ""
msgid "TimeTrackingEstimated|Est"
msgstr ""
+msgid "TimeTracking|%{startTag}Spent: %{endTag}%{timeSpentHumanReadable}"
+msgstr ""
+
msgid "TimeTracking|Estimated:"
msgstr ""
+msgid "TimeTracking|Over by %{timeRemainingHumanReadable}"
+msgstr ""
+
msgid "TimeTracking|Spent"
msgstr ""
+msgid "TimeTracking|Time remaining: %{timeRemainingHumanReadable}"
+msgstr ""
+
msgid "Timeago|%s days ago"
msgstr ""
@@ -11045,6 +11167,12 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
+msgid "To protect this issues confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgstr ""
+
+msgid "To protect this issues confidentiality, a private fork of this project was selected."
+msgstr ""
+
msgid "To see all the user's personal access tokens you must impersonate them first."
msgstr ""
@@ -11066,16 +11194,13 @@ msgstr ""
msgid "To widen your search, change or remove filters above"
msgstr ""
-msgid "Today"
-msgstr ""
-
-msgid "Todo"
+msgid "To-Do List"
msgstr ""
-msgid "Todo was successfully marked as done."
+msgid "To-do item successfully marked as done."
msgstr ""
-msgid "Todos"
+msgid "Today"
msgstr ""
msgid "Toggle Sidebar"
@@ -11093,15 +11218,18 @@ msgstr ""
msgid "Toggle commit list"
msgstr ""
-msgid "Toggle discussion"
-msgstr ""
-
msgid "Toggle emoji award"
msgstr ""
msgid "Toggle navigation"
msgstr ""
+msgid "Toggle sidebar"
+msgstr ""
+
+msgid "Toggle thread"
+msgstr ""
+
msgid "ToggleButton|Toggle Status: OFF"
msgstr ""
@@ -11216,6 +11344,12 @@ msgstr ""
msgid "Tuesday"
msgstr ""
+msgid "Turn Off"
+msgstr ""
+
+msgid "Turn On"
+msgstr ""
+
msgid "Twitter"
msgstr ""
@@ -11312,6 +11446,9 @@ msgstr ""
msgid "Unresolve discussion"
msgstr ""
+msgid "Unresolve thread"
+msgstr ""
+
msgid "Unschedule job"
msgstr ""
@@ -11357,6 +11494,9 @@ msgstr ""
msgid "Upcoming"
msgstr ""
+msgid "Upcoming Release"
+msgstr ""
+
msgid "Update"
msgstr ""
@@ -12307,7 +12447,7 @@ msgstr ""
msgid "Your SSH keys (%{count})"
msgstr ""
-msgid "Your Todos"
+msgid "Your To-Do List"
msgstr ""
msgid "Your U2F device did not send a valid JSON response."
@@ -12409,6 +12549,9 @@ msgstr ""
msgid "among other things"
msgstr ""
+msgid "assign yourself"
+msgstr ""
+
msgid "attach a new file"
msgstr ""
@@ -12427,6 +12570,9 @@ msgstr ""
msgid "cannot include leading slash or directory traversal."
msgstr ""
+msgid "comment"
+msgstr ""
+
msgid "commented on %{link_to_project}"
msgstr ""
@@ -12465,11 +12611,6 @@ msgstr ""
msgid "disabled"
msgstr ""
-msgid "discussion resolved"
-msgid_plural "discussions resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "done"
msgstr ""
@@ -12557,6 +12698,9 @@ msgstr ""
msgid "is not an email you own"
msgstr ""
+msgid "issue"
+msgstr ""
+
msgid "issue boards"
msgstr ""
@@ -12772,7 +12916,7 @@ msgstr ""
msgid "mrWidget|There are merge conflicts"
msgstr ""
-msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgid "mrWidget|There are unresolved threads. Please resolve these threads"
msgstr ""
msgid "mrWidget|This feature merges changes from the target branch to the source branch. You cannot use this feature since the source branch is protected."
@@ -12857,6 +13001,9 @@ msgstr[1] ""
msgid "password"
msgstr ""
+msgid "pending comment"
+msgstr ""
+
msgid "private"
msgstr ""
@@ -12943,6 +13090,11 @@ msgstr ""
msgid "this document"
msgstr ""
+msgid "thread resolved"
+msgid_plural "threads resolved"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "to list"
msgstr ""
diff --git a/package.json b/package.json
index da2ac7d64ef..47e7dd9c9c9 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,7 @@
"@babel/preset-env": "^7.4.4",
"@gitlab/csslab": "^1.9.0",
"@gitlab/svgs": "^1.66.0",
- "@gitlab/ui": "^5.1.0",
+ "@gitlab/ui": "^5.3.2",
"apollo-cache-inmemory": "^1.5.1",
"apollo-client": "^2.5.1",
"apollo-link": "^1.2.11",
@@ -168,7 +168,6 @@
"gettext-extractor": "^3.4.3",
"gettext-extractor-vue": "^4.0.2",
"graphql-tag": "^2.10.0",
- "istanbul": "^0.4.5",
"jasmine-core": "^2.9.0",
"jasmine-diff": "^0.1.3",
"jasmine-jquery": "^2.1.1",
diff --git a/qa/qa/page/project/operations/kubernetes/show.rb b/qa/qa/page/project/operations/kubernetes/show.rb
index c81e13e9b91..4f625c5f0f0 100644
--- a/qa/qa/page/project/operations/kubernetes/show.rb
+++ b/qa/qa/page/project/operations/kubernetes/show.rb
@@ -28,12 +28,16 @@ module QA
end
end
- def await_installed(application_name)
+ def await_installed(application_name, button_text: 'Installed')
within(".js-cluster-application-row-#{application_name}") do
- page.has_text?('Installed', wait: 300)
+ page.has_text?(button_text, wait: 300)
end
end
+ def await_uninstallable(application_name)
+ await_installed(application_name, button_text: 'Uninstall')
+ end
+
def ingress_ip
# We need to wait longer since it can take some time before the
# ip address is assigned for the ingress controller
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 1a9a2fd413f..9fd668f812b 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -5,6 +5,7 @@ module QA
module Project
class Show < Page::Base
include Page::Component::ClonePanel
+ include Page::Project::SubMenus::Settings
view 'app/views/layouts/header/_new_dropdown.haml' do
element :new_menu_toggle
diff --git a/qa/qa/page/project/sub_menus/ci_cd.rb b/qa/qa/page/project/sub_menus/ci_cd.rb
index adae2ce08c4..2f0bc8b9ba6 100644
--- a/qa/qa/page/project/sub_menus/ci_cd.rb
+++ b/qa/qa/page/project/sub_menus/ci_cd.rb
@@ -5,6 +5,8 @@ module QA
module Project
module SubMenus
module CiCd
+ include Page::Project::SubMenus::Common
+
def self.included(base)
base.class_eval do
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
diff --git a/qa/qa/page/project/sub_menus/issues.rb b/qa/qa/page/project/sub_menus/issues.rb
index f81e4f34909..8fb8fa06346 100644
--- a/qa/qa/page/project/sub_menus/issues.rb
+++ b/qa/qa/page/project/sub_menus/issues.rb
@@ -5,6 +5,8 @@ module QA
module Project
module SubMenus
module Issues
+ include Page::Project::SubMenus::Common
+
def self.included(base)
base.class_eval do
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
diff --git a/qa/qa/page/project/sub_menus/operations.rb b/qa/qa/page/project/sub_menus/operations.rb
index 24a99a9464c..d266cb21417 100644
--- a/qa/qa/page/project/sub_menus/operations.rb
+++ b/qa/qa/page/project/sub_menus/operations.rb
@@ -5,6 +5,8 @@ module QA
module Project
module SubMenus
module Operations
+ include Page::Project::SubMenus::Common
+
def self.included(base)
base.class_eval do
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
diff --git a/qa/qa/page/project/sub_menus/repository.rb b/qa/qa/page/project/sub_menus/repository.rb
index 4cc73a6b25a..c53d805c61d 100644
--- a/qa/qa/page/project/sub_menus/repository.rb
+++ b/qa/qa/page/project/sub_menus/repository.rb
@@ -5,6 +5,8 @@ module QA
module Project
module SubMenus
module Repository
+ include Page::Project::SubMenus::Common
+
def self.included(base)
base.class_eval do
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
diff --git a/qa/qa/page/project/sub_menus/settings.rb b/qa/qa/page/project/sub_menus/settings.rb
index 88b45ec55ae..1cd39fcff58 100644
--- a/qa/qa/page/project/sub_menus/settings.rb
+++ b/qa/qa/page/project/sub_menus/settings.rb
@@ -5,6 +5,8 @@ module QA
module Project
module SubMenus
module Settings
+ include Page::Project::SubMenus::Common
+
def self.included(base)
base.class_eval do
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
diff --git a/qa/qa/resource/kubernetes_cluster.rb b/qa/qa/resource/kubernetes_cluster.rb
index 27ab7b60211..1dd93dd5b88 100644
--- a/qa/qa/resource/kubernetes_cluster.rb
+++ b/qa/qa/resource/kubernetes_cluster.rb
@@ -47,7 +47,7 @@ module QA
page.install!(:runner) if @install_runner
page.await_installed(:ingress) if @install_ingress
- page.await_installed(:prometheus) if @install_prometheus
+ page.await_uninstallable(:prometheus) if @install_prometheus
page.await_installed(:runner) if @install_runner
if @install_ingress
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb
new file mode 100644
index 00000000000..013cea0a40e
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Plan' do
+ describe 'check xss occurence in @mentions in issues' do
+ let(:issue_title) { 'issue title' }
+
+ it 'user mentions a user in comment' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+
+ user = Resource::User.fabricate! do |user|
+ user.name = "eve <img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;"
+ user.password = "test1234"
+ end
+
+ project = Resource::Project.fabricate! do |resource|
+ resource.name = 'xss-test-for-mentions-project'
+ end
+ project.visit!
+
+ Page::Project::Show.perform(&:go_to_members_settings)
+ Page::Project::Settings::Members.perform do |page|
+ page.add_member(user.username)
+ end
+
+ Resource::Issue.fabricate_via_browser_ui! do |issue|
+ issue.title = issue_title
+ issue.project = project
+ end
+
+ Page::Project::Issue::Show.perform do |show_page|
+ show_page.select_all_activities_filter
+ show_page.comment('cc-ing you here @eve')
+
+ expect do
+ expect(show_page).to have_content("cc-ing you here")
+ end.not_to raise_error # Selenium::WebDriver::Error::UnhandledAlertError
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb
index 4478ea41662..2101311f065 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb
@@ -9,27 +9,33 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
- Resource::Issue.fabricate_via_browser_ui! do |issue|
+ issue = Resource::Issue.fabricate_via_api! do |issue|
issue.title = issue_title
end
+ issue.visit!
+
expect(page).to have_content(issue_title)
Page::Project::Issue::Show.perform do |show_page|
+ my_first_discussion = "My first discussion"
+ my_first_reply = "My First Reply"
+ one_reply = "1 reply"
+
show_page.select_all_activities_filter
- show_page.start_discussion("My first discussion")
- expect(show_page).to have_content("My first discussion")
+ show_page.start_discussion(my_first_discussion)
+ expect(show_page).to have_content(my_first_discussion)
- show_page.reply_to_discussion("My First Reply")
- expect(show_page).to have_content("My First Reply")
+ show_page.reply_to_discussion(my_first_reply)
+ expect(show_page).to have_content(my_first_reply)
show_page.collapse_replies
- expect(show_page).to have_content("1 reply")
- expect(show_page).not_to have_content("My First Reply")
+ expect(show_page).to have_content(one_reply)
+ expect(show_page).not_to have_content(my_first_reply)
show_page.expand_replies
- expect(show_page).to have_content("My First Reply")
- expect(show_page).not_to have_content("1 reply")
+ expect(show_page).to have_content(my_first_reply)
+ expect(show_page).not_to have_content(one_reply)
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
index ad2773b41ac..301836f5ce8 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
@@ -9,28 +9,33 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Resource::Issue.fabricate_via_browser_ui! do |issue|
+ issue = Resource::Issue.fabricate_via_api! do |issue|
issue.title = issue_title
end
+ issue.visit!
+
expect(page).to have_content(issue_title)
Page::Project::Issue::Show.perform do |show_page|
+ my_own_comment = "My own comment"
+ made_the_issue_confidential = "made the issue confidential"
+
show_page.comment('/confidential', filter: :comments_only)
- show_page.comment('My own comment', filter: :comments_only)
+ show_page.comment(my_own_comment, filter: :comments_only)
- expect(show_page).not_to have_content("made the issue confidential")
- expect(show_page).to have_content("My own comment")
+ expect(show_page).not_to have_content(made_the_issue_confidential)
+ expect(show_page).to have_content(my_own_comment)
show_page.select_all_activities_filter
- expect(show_page).to have_content("made the issue confidential")
- expect(show_page).to have_content("My own comment")
+ expect(show_page).to have_content(made_the_issue_confidential)
+ expect(show_page).to have_content(my_own_comment)
show_page.select_history_only_filter
- expect(show_page).to have_content("made the issue confidential")
- expect(show_page).not_to have_content("My own comment")
+ expect(show_page).to have_content(made_the_issue_confidential)
+ expect(show_page).not_to have_content(my_own_comment)
end
end
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 530fc684437..24dcb32f63f 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
@@ -14,7 +14,7 @@ module QA
resource.description = 'project for issue suggestions'
end
- Resource::Issue.fabricate_via_browser_ui! do |issue|
+ Resource::Issue.fabricate_via_api! do |issue|
issue.title = issue_title
issue.project = project
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb
index db33c6330ff..9e48ee7ca2a 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module QA
- # https://gitlab.com/gitlab-org/quality/staging/issues/55
- context 'Create', :quarantine do
+ context 'Create' do
describe 'Download merge request patch and diff' do
before(:context) do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb
index 23008a58af8..448d4980727 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module QA
- # https://gitlab.com/gitlab-org/quality/staging/issues/40
- context 'Create', :quarantine do
+ context 'Create' do
describe 'Push mirror a repository over HTTP' do
it 'configures and syncs a (push) mirrored repository' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
diff --git a/rubocop/cop/graphql/authorize_types.rb b/rubocop/cop/graphql/authorize_types.rb
new file mode 100644
index 00000000000..93fe80c3edf
--- /dev/null
+++ b/rubocop/cop/graphql/authorize_types.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require_relative '../../spec_helpers'
+
+module RuboCop
+ module Cop
+ module Graphql
+ class AuthorizeTypes < RuboCop::Cop::Cop
+ include SpecHelpers
+
+ MSG = 'Add an `authorize :ability` call to the type: '\
+ 'https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#type-authorization'
+
+ TYPES_DIR = 'app/graphql/types'
+
+ # We want to exclude our own basetypes and scalars
+ WHITELISTED_TYPES = %w[BaseEnum BaseScalar BasePermissionType MutationType
+ QueryType GraphQL::Schema BaseUnion].freeze
+
+ def_node_search :authorize?, <<~PATTERN
+ (send nil? :authorize ...)
+ PATTERN
+
+ def on_class(node)
+ return unless in_type?(node)
+ return if whitelisted?(class_constant(node))
+ return if whitelisted?(superclass_constant(node))
+
+ add_offense(node, location: :expression) unless authorize?(node)
+ end
+
+ private
+
+ def in_type?(node)
+ return if in_spec?(node)
+
+ path = node.location.expression.source_buffer.name
+
+ path.include?(TYPES_DIR)
+ end
+
+ def whitelisted?(class_node)
+ return false unless class_node&.const_name
+
+ WHITELISTED_TYPES.any? { |whitelisted| class_node.const_name.include?(whitelisted) }
+ end
+
+ def class_constant(node)
+ node.descendants.first
+ end
+
+ def superclass_constant(class_node)
+ # First one is the class name itself, second is it's superclass
+ _class_constant, *others = class_node.descendants
+
+ others.find { |node| node.const_type? && node&.const_name != 'Types' }
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index e2a19978839..27c63d92ae5 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -43,3 +43,4 @@ require_relative 'cop/code_reuse/serializer'
require_relative 'cop/code_reuse/active_record'
require_relative 'cop/group_public_or_visible_to_user'
require_relative 'cop/inject_enterprise_edition_module'
+require_relative 'cop/graphql/authorize_types'
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index 633ea28e96c..2bf654b1e24 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -131,6 +131,7 @@ function install_external_dns() {
if ! deploy_exists "${KUBE_NAMESPACE}" "${release_name}" || previous_deploy_failed "${release_name}" ; then
echoinfo "Installing external-dns Helm chart"
helm repo update
+ # Default requested: CPU => 0, memory => 0
helm install stable/external-dns \
-n "${release_name}" \
--namespace "${KUBE_NAMESPACE}" \
@@ -141,7 +142,11 @@ function install_external_dns() {
--set domainFilters[0]="${domain}" \
--set txtOwnerId="${KUBE_NAMESPACE}" \
--set rbac.create="true" \
- --set policy="sync"
+ --set policy="sync" \
+ --set resources.requests.cpu=50m \
+ --set resources.limits.cpu=100m \
+ --set resources.requests.memory=100M \
+ --set resources.limits.memory=200M
else
echoinfo "The external-dns Helm chart is already successfully deployed."
fi
@@ -196,45 +201,122 @@ HELM_CMD=$(cat << EOF
helm upgrade --install \
--wait \
--timeout 600 \
- --set global.appConfig.enableUsagePing=false \
--set releaseOverride="$CI_ENVIRONMENT_SLUG" \
+ --set global.appConfig.enableUsagePing=false \
--set global.imagePullPolicy=Always \
--set global.hosts.hostSuffix="$HOST_SUFFIX" \
--set global.hosts.domain="$REVIEW_APPS_DOMAIN" \
- --set certmanager.install=false \
- --set prometheus.install=false \
--set global.ingress.configureCertmanager=false \
--set global.ingress.tls.secretName=tls-cert \
--set global.ingress.annotations."external-dns\.alpha\.kubernetes\.io/ttl"="10" \
+ --set certmanager.install=false \
+ --set prometheus.install=false \
--set nginx-ingress.controller.service.enableHttp=false \
- --set nginx-ingress.defaultBackend.resources.requests.memory=7Mi \
- --set nginx-ingress.controller.resources.requests.memory=440M \
--set nginx-ingress.controller.replicaCount=2 \
- --set gitlab.unicorn.resources.requests.cpu=200m \
- --set gitlab.sidekiq.resources.requests.cpu=100m \
- --set gitlab.sidekiq.resources.requests.memory=800M \
- --set gitlab.gitlab-shell.resources.requests.cpu=100m \
- --set redis.resources.requests.cpu=100m \
- --set minio.resources.requests.cpu=100m \
+ --set nginx-ingress.controller.config.ssl-ciphers="ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4" \
--set gitlab.migrations.image.repository="$gitlab_migrations_image_repository" \
--set gitlab.migrations.image.tag="$CI_COMMIT_REF_SLUG" \
- --set gitlab.sidekiq.image.repository="$gitlab_sidekiq_image_repository" \
- --set gitlab.sidekiq.image.tag="$CI_COMMIT_REF_SLUG" \
- --set gitlab.unicorn.image.repository="$gitlab_unicorn_image_repository" \
- --set gitlab.unicorn.image.tag="$CI_COMMIT_REF_SLUG" \
- --set gitlab.task-runner.image.repository="$gitlab_task_runner_image_repository" \
- --set gitlab.task-runner.image.tag="$CI_COMMIT_REF_SLUG" \
--set gitlab.gitaly.image.repository="$gitlab_gitaly_image_repository" \
--set gitlab.gitaly.image.tag="v$GITALY_VERSION" \
--set gitlab.gitlab-shell.image.repository="$gitlab_shell_image_repository" \
--set gitlab.gitlab-shell.image.tag="v$GITLAB_SHELL_VERSION" \
+ --set gitlab.sidekiq.image.repository="$gitlab_sidekiq_image_repository" \
+ --set gitlab.sidekiq.image.tag="$CI_COMMIT_REF_SLUG" \
+ --set gitlab.unicorn.image.repository="$gitlab_unicorn_image_repository" \
+ --set gitlab.unicorn.image.tag="$CI_COMMIT_REF_SLUG" \
--set gitlab.unicorn.workhorse.image="$gitlab_workhorse_image_repository" \
--set gitlab.unicorn.workhorse.tag="$CI_COMMIT_REF_SLUG" \
- --set nginx-ingress.controller.config.ssl-ciphers="ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4" \
- --namespace="$KUBE_NAMESPACE" \
- --version="$CI_PIPELINE_ID-$CI_JOB_ID" \
- "$name" \
- .
+ --set gitlab.task-runner.image.repository="$gitlab_task_runner_image_repository" \
+ --set gitlab.task-runner.image.tag="$CI_COMMIT_REF_SLUG"
+EOF
+)
+
+# Default requested: CPU => 100m, memory => 100Mi
+HELM_CMD=$(cat << EOF
+ $HELM_CMD \
+ --set nginx-ingress.controller.resources.limits.cpu=200m \
+ --set nginx-ingress.controller.resources.requests.memory=210M \
+ --set nginx-ingress.controller.resources.limits.memory=420M
+EOF
+)
+
+# Default requested: CPU => 5m, memory => 5Mi
+HELM_CMD=$(cat << EOF
+ $HELM_CMD \
+ --set nginx-ingress.defaultBackend.resources.limits.cpu=10m \
+ --set nginx-ingress.defaultBackend.resources.requests.memory=12M \
+ --set nginx-ingress.defaultBackend.resources.limits.memory=24M
+EOF
+)
+
+# Default requested: CPU => 100m, memory => 200Mi
+HELM_CMD=$(cat << EOF
+ $HELM_CMD \
+ --set gitlab.gitaly.resources.requests.cpu=150m \
+ --set gitlab.gitaly.resources.limits.cpu=300m \
+ --set gitlab.gitaly.resources.limits.memory=420M
+EOF
+)
+
+# Default requested: CPU => 0, memory => 6M
+HELM_CMD=$(cat << EOF
+ $HELM_CMD \
+ --set gitlab.gitlab-shell.resources.requests.cpu=70m \
+ --set gitlab.gitlab-shell.resources.limits.cpu=140m \
+ --set gitlab.gitlab-shell.resources.requests.memory=20M \
+ --set gitlab.gitlab-shell.resources.limits.memory=40M
+EOF
+)
+
+# Default requested: CPU => 50m, memory => 650M
+HELM_CMD=$(cat << EOF
+ $HELM_CMD \
+ --set gitlab.sidekiq.resources.requests.cpu=200m \
+ --set gitlab.sidekiq.resources.limits.cpu=300m \
+ --set gitlab.sidekiq.resources.requests.memory=800M \
+ --set gitlab.sidekiq.resources.limits.memory=1.2G
+EOF
+)
+
+# Default requested: CPU => 300m + 100m (workhorse), memory => 1.2G + 100M (workhorse)
+HELM_CMD=$(cat << EOF
+ $HELM_CMD \
+ --set gitlab.unicorn.resources.limits.cpu=800m \
+ --set gitlab.unicorn.resources.limits.memory=2.6G
+EOF
+)
+
+# Default requested: CPU => 100m, memory => 64Mi
+HELM_CMD=$(cat << EOF
+ $HELM_CMD \
+ --set redis.resources.limits.cpu=200m \
+ --set redis.resources.limits.memory=130M
+EOF
+)
+
+# Default requested: CPU => 100m, memory => 128Mi
+HELM_CMD=$(cat << EOF
+ $HELM_CMD \
+ --set minio.resources.limits.cpu=200m \
+ --set minio.resources.limits.memory=280M
+EOF
+)
+
+# Default requested: CPU => 0, memory => 0
+HELM_CMD=$(cat << EOF
+ $HELM_CMD \
+ --set gitlab-runner.resources.requests.cpu=300m \
+ --set gitlab-runner.resources.limits.cpu=600m \
+ --set gitlab-runner.resources.requests.memory=300M \
+ --set gitlab-runner.resources.limits.memory=600M
+EOF
+)
+
+HELM_CMD=$(cat << EOF
+ $HELM_CMD \
+ --namespace="$KUBE_NAMESPACE" \
+ --version="$CI_PIPELINE_ID-$CI_JOB_ID" \
+ "$name" .
EOF
)
diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb
index ea68eae12ed..6591901a9dc 100644
--- a/spec/controllers/dashboard/projects_controller_spec.rb
+++ b/spec/controllers/dashboard/projects_controller_spec.rb
@@ -11,8 +11,10 @@ describe Dashboard::ProjectsController do
end
context 'user logged in' do
+ let(:user) { create(:user) }
+
before do
- sign_in create(:user)
+ sign_in(user)
end
context 'external authorization' do
@@ -24,6 +26,20 @@ describe Dashboard::ProjectsController do
expect(response).to have_gitlab_http_status(200)
end
end
+
+ it 'orders the projects by last activity by default' do
+ project = create(:project)
+ project.add_developer(user)
+ project.update!(last_repository_updated_at: 3.days.ago, last_activity_at: 3.days.ago)
+
+ project2 = create(:project)
+ project2.add_developer(user)
+ project2.update!(last_repository_updated_at: 10.days.ago, last_activity_at: 10.days.ago)
+
+ get :index
+
+ expect(assigns(:projects)).to eq([project, project2])
+ end
end
end
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 6ec84f5c528..1db1963476c 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -252,7 +252,7 @@ describe Projects::NotesController do
before do
service_params = ActionController::Parameters.new({
note: 'some note',
- noteable_id: merge_request.id.to_s,
+ noteable_id: merge_request.id,
noteable_type: 'MergeRequest',
commit_id: nil,
merge_request_diff_head_sha: 'sha'
diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb
index b34053fc993..7f67f67e775 100644
--- a/spec/controllers/projects/settings/repository_controller_spec.rb
+++ b/spec/controllers/projects/settings/repository_controller_spec.rb
@@ -32,4 +32,24 @@ describe Projects::Settings::RepositoryController do
expect(RepositoryCleanupWorker).to have_received(:perform_async).once
end
end
+
+ describe 'POST create_deploy_token' do
+ let(:deploy_token_params) do
+ {
+ name: 'deployer_token',
+ expires_at: 1.month.from_now.to_date.to_s,
+ username: 'deployer',
+ read_repository: '1'
+ }
+ end
+
+ subject(:create_deploy_token) { post :create_deploy_token, params: { namespace_id: project.namespace, project_id: project, deploy_token: deploy_token_params } }
+
+ it 'creates deploy token' do
+ expect { create_deploy_token }.to change { DeployToken.active.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to render_template(:show)
+ end
+ end
end
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
index bebf17728c0..9e7d34b10c0 100644
--- a/spec/controllers/projects/templates_controller_spec.rb
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -3,49 +3,101 @@
require 'spec_helper'
describe Projects::TemplatesController do
- let(:project) { create(:project, :repository) }
+ let(:project) { create(:project, :repository, :private) }
let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:file_path_1) { '.gitlab/issue_templates/bug.md' }
+ let(:file_path_1) { '.gitlab/issue_templates/issue_template.md' }
+ let(:file_path_2) { '.gitlab/merge_request_templates/merge_request_template.md' }
let(:body) { JSON.parse(response.body) }
+ let!(:file_1) { project.repository.create_file(user, file_path_1, 'issue content', message: 'message', branch_name: 'master') }
+ let!(:file_2) { project.repository.create_file(user, file_path_2, 'merge request content', message: 'message', branch_name: 'master') }
- before do
- project.add_developer(user)
- sign_in(user)
- end
+ describe '#show' do
+ shared_examples 'renders issue templates as json' do
+ it do
+ get(:show, params: { namespace_id: project.namespace, template_type: 'issue', key: 'issue_template', project_id: project }, format: :json)
- before do
- project.add_user(user, Gitlab::Access::MAINTAINER)
- project.repository.create_file(user, file_path_1, 'something valid',
- message: 'test 3', branch_name: 'master')
- end
+ expect(response.status).to eq(200)
+ expect(body['name']).to eq('issue_template')
+ expect(body['content']).to eq('issue content')
+ end
+ end
- describe '#show' do
- it 'renders template name and content as json' do
- get(:show, params: { namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project }, format: :json)
+ shared_examples 'renders merge request templates as json' do
+ it do
+ get(:show, params: { namespace_id: project.namespace, template_type: 'merge_request', key: 'merge_request_template', project_id: project }, format: :json)
+
+ expect(response.status).to eq(200)
+ expect(body['name']).to eq('merge_request_template')
+ expect(body['content']).to eq('merge request content')
+ end
+ end
+
+ shared_examples 'renders 404 when requesting an issue template' do
+ it do
+ get(:show, params: { namespace_id: project.namespace, template_type: 'issue', key: 'issue_template', project_id: project }, format: :json)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ shared_examples 'renders 404 when requesting a merge request template' do
+ it do
+ get(:show, params: { namespace_id: project.namespace, template_type: 'merge_request', key: 'merge_request_template', project_id: project }, format: :json)
- expect(response.status).to eq(200)
- expect(body["name"]).to eq("bug")
- expect(body["content"]).to eq("something valid")
+ expect(response.status).to eq(404)
+ end
end
- it 'renders 404 when unauthorized' do
- sign_in(user2)
- get(:show, params: { namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project }, format: :json)
+ shared_examples 'renders 404 when params are invalid' do
+ it 'does not route when the template type is invalid' do
+ expect do
+ get(:show, params: { namespace_id: project.namespace, template_type: 'invalid_type', key: 'issue_template', project_id: project }, format: :json)
+ end.to raise_error(ActionController::UrlGenerationError)
+ end
+
+ it 'renders 404 when the format type is invalid' do
+ get(:show, params: { namespace_id: project.namespace, template_type: 'issue', key: 'issue_template', project_id: project }, format: :html)
+
+ expect(response.status).to eq(404)
+ end
+
+ it 'renders 404 when the key is unknown' do
+ get(:show, params: { namespace_id: project.namespace, template_type: 'issue', key: 'unknown_template', project_id: project }, format: :json)
- expect(response.status).to eq(404)
+ expect(response.status).to eq(404)
+ end
end
- it 'renders 404 when template type is not found' do
- sign_in(user)
- get(:show, params: { namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project }, format: :json)
+ context 'when the user is not a member of the project' do
+ before do
+ sign_in(user)
+ end
- expect(response.status).to eq(404)
+ include_examples 'renders 404 when requesting an issue template'
+ include_examples 'renders 404 when requesting a merge request template'
+ include_examples 'renders 404 when params are invalid'
end
- it 'renders 404 without errors' do
- sign_in(user)
- expect { get(:show, params: { namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project }, format: :json) }.not_to raise_error
+ context 'when user is a member of the project' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
+
+ include_examples 'renders issue templates as json'
+ include_examples 'renders merge request templates as json'
+ include_examples 'renders 404 when params are invalid'
+ end
+
+ context 'when user is a guest of the project' do
+ before do
+ project.add_guest(user)
+ sign_in(user)
+ end
+
+ include_examples 'renders issue templates as json'
+ include_examples 'renders 404 when requesting a merge request template'
+ include_examples 'renders 404 when params are invalid'
end
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 8d2412f97ef..4e1cac67d23 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -318,6 +318,53 @@ describe ProjectsController do
end
end
+ describe '#housekeeping' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+ let(:housekeeping) { Projects::HousekeepingService.new(project) }
+
+ context 'when authenticated as owner' do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+
+ allow(Projects::HousekeepingService).to receive(:new).with(project, :gc).and_return(housekeeping)
+ end
+
+ it 'forces a full garbage collection' do
+ expect(housekeeping).to receive(:execute).once
+
+ post :housekeeping,
+ params: {
+ namespace_id: project.namespace.path,
+ id: project.path
+ }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ context 'when authenticated as developer' do
+ let(:developer) { create(:user) }
+
+ before do
+ group.add_developer(developer)
+ end
+
+ it 'does not execute housekeeping' do
+ expect(housekeeping).not_to receive(:execute)
+
+ post :housekeeping,
+ params: {
+ namespace_id: project.namespace.path,
+ id: project.path
+ }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+ end
+
describe "#update" do
render_views
diff --git a/spec/controllers/snippets/notes_controller_spec.rb b/spec/controllers/snippets/notes_controller_spec.rb
index 936d7c7dae4..586d59c2d09 100644
--- a/spec/controllers/snippets/notes_controller_spec.rb
+++ b/spec/controllers/snippets/notes_controller_spec.rb
@@ -119,6 +119,119 @@ describe Snippets::NotesController do
end
end
+ describe 'POST create' do
+ context 'when a snippet is public' do
+ let(:request_params) do
+ {
+ note: attributes_for(:note_on_personal_snippet, noteable: public_snippet),
+ snippet_id: public_snippet.id
+ }
+ end
+
+ before do
+ sign_in user
+ end
+
+ it 'returns status 302' do
+ post :create, params: request_params
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+
+ it 'creates the note' do
+ expect { post :create, params: request_params }.to change { Note.count }.by(1)
+ end
+ end
+
+ context 'when a snippet is internal' do
+ let(:request_params) do
+ {
+ note: attributes_for(:note_on_personal_snippet, noteable: internal_snippet),
+ snippet_id: internal_snippet.id
+ }
+ end
+
+ before do
+ sign_in user
+ end
+
+ it 'returns status 302' do
+ post :create, params: request_params
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+
+ it 'creates the note' do
+ expect { post :create, params: request_params }.to change { Note.count }.by(1)
+ end
+ end
+
+ context 'when a snippet is private' do
+ let(:request_params) do
+ {
+ note: attributes_for(:note_on_personal_snippet, noteable: private_snippet),
+ snippet_id: private_snippet.id
+ }
+ end
+
+ before do
+ sign_in user
+ end
+
+ context 'when user is not the author' do
+ before do
+ sign_in(user)
+ end
+
+ it 'returns status 404' do
+ post :create, params: request_params
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'does not create the note' do
+ expect { post :create, params: request_params }.not_to change { Note.count }
+ end
+
+ context 'when user sends a snippet_id for a public snippet' do
+ let(:request_params) do
+ {
+ note: attributes_for(:note_on_personal_snippet, noteable: private_snippet),
+ snippet_id: public_snippet.id
+ }
+ end
+
+ it 'returns status 302' do
+ post :create, params: request_params
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+
+ it 'creates the note on the public snippet' do
+ expect { post :create, params: request_params }.to change { Note.count }.by(1)
+ expect(Note.last.noteable).to eq public_snippet
+ end
+ end
+ end
+
+ context 'when user is the author' do
+ before do
+ sign_in(private_snippet.author)
+ end
+
+ it 'returns status 302' do
+ post :create, params: request_params
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+
+ it 'creates the note' do
+ expect { post :create, params: request_params }.to change { Note.count }.by(1)
+ end
+ end
+ end
+ end
+
describe 'DELETE destroy' do
let(:request_params) do
{
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index f8666a1986f..3aba02bf3ff 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -209,8 +209,8 @@ describe SnippetsController do
context 'when the snippet description contains a file' do
include FileMoverHelpers
- let(:picture_file) { '/-/system/temp/secret56/picture.jpg' }
- let(:text_file) { '/-/system/temp/secret78/text.txt' }
+ let(:picture_file) { "/-/system/user/#{user.id}/secret56/picture.jpg" }
+ let(:text_file) { "/-/system/user/#{user.id}/secret78/text.txt" }
let(:description) do
"Description with picture: ![picture](/uploads#{picture_file}) and "\
"text: [text.txt](/uploads#{text_file})"
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index d27658e02cb..0876502a899 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -24,121 +24,160 @@ describe UploadsController do
let!(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
describe 'POST create' do
- let(:model) { 'personal_snippet' }
- let(:snippet) { create(:personal_snippet, :public) }
let(:jpg) { fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') }
let(:txt) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') }
- context 'when a user does not have permissions to upload a file' do
- it "returns 401 when the user is not logged in" do
- post :create, params: { model: model, id: snippet.id }, format: :json
+ context 'snippet uploads' do
+ let(:model) { 'personal_snippet' }
+ let(:snippet) { create(:personal_snippet, :public) }
- expect(response).to have_gitlab_http_status(401)
- end
+ context 'when a user does not have permissions to upload a file' do
+ it "returns 401 when the user is not logged in" do
+ post :create, params: { model: model, id: snippet.id }, format: :json
- it "returns 404 when user can't comment on a snippet" do
- private_snippet = create(:personal_snippet, :private)
+ expect(response).to have_gitlab_http_status(401)
+ end
- sign_in(user)
- post :create, params: { model: model, id: private_snippet.id }, format: :json
+ it "returns 404 when user can't comment on a snippet" do
+ private_snippet = create(:personal_snippet, :private)
- expect(response).to have_gitlab_http_status(404)
- end
- end
+ sign_in(user)
+ post :create, params: { model: model, id: private_snippet.id }, format: :json
- context 'when a user is logged in' do
- before do
- sign_in(user)
+ expect(response).to have_gitlab_http_status(404)
+ end
end
- it "returns an error without file" do
- post :create, params: { model: model, id: snippet.id }, format: :json
+ context 'when a user is logged in' do
+ before do
+ sign_in(user)
+ end
- expect(response).to have_gitlab_http_status(422)
- end
+ it "returns an error without file" do
+ post :create, params: { model: model, id: snippet.id }, format: :json
- it "returns an error with invalid model" do
- expect { post :create, params: { model: 'invalid', id: snippet.id }, format: :json }
- .to raise_error(ActionController::UrlGenerationError)
- end
+ expect(response).to have_gitlab_http_status(422)
+ end
- it "returns 404 status when object not found" do
- post :create, params: { model: model, id: 9999 }, format: :json
+ it "returns an error with invalid model" do
+ expect { post :create, params: { model: 'invalid', id: snippet.id }, format: :json }
+ .to raise_error(ActionController::UrlGenerationError)
+ end
- expect(response).to have_gitlab_http_status(404)
- end
+ it "returns 404 status when object not found" do
+ post :create, params: { model: model, id: 9999 }, format: :json
- context 'with valid image' do
- before do
- post :create, params: { model: 'personal_snippet', id: snippet.id, file: jpg }, format: :json
+ expect(response).to have_gitlab_http_status(404)
end
- it 'returns a content with original filename, new link, and correct type.' do
- expect(response.body).to match '\"alt\":\"rails_sample\"'
- expect(response.body).to match "\"url\":\"/uploads"
+ context 'with valid image' do
+ before do
+ post :create, params: { model: 'personal_snippet', id: snippet.id, file: jpg }, format: :json
+ end
+
+ it 'returns a content with original filename, new link, and correct type.' do
+ expect(response.body).to match '\"alt\":\"rails_sample\"'
+ expect(response.body).to match "\"url\":\"/uploads"
+ end
+
+ it 'creates a corresponding Upload record' do
+ upload = Upload.last
+
+ aggregate_failures do
+ expect(upload).to exist
+ expect(upload.model).to eq snippet
+ end
+ end
end
- it 'creates a corresponding Upload record' do
- upload = Upload.last
+ context 'with valid non-image file' do
+ before do
+ post :create, params: { model: 'personal_snippet', id: snippet.id, file: txt }, format: :json
+ end
- aggregate_failures do
- expect(upload).to exist
- expect(upload.model).to eq snippet
+ it 'returns a content with original filename, new link, and correct type.' do
+ expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
+ expect(response.body).to match "\"url\":\"/uploads"
+ end
+
+ it 'creates a corresponding Upload record' do
+ upload = Upload.last
+
+ aggregate_failures do
+ expect(upload).to exist
+ expect(upload.model).to eq snippet
+ end
end
end
end
+ end
+
+ context 'user uploads' do
+ let(:model) { 'user' }
+
+ it 'returns 401 when the user has no access' do
+ post :create, params: { model: 'user', id: user.id }, format: :json
- context 'with valid non-image file' do
+ expect(response).to have_gitlab_http_status(401)
+ end
+
+ context 'when user is logged in' do
before do
- post :create, params: { model: 'personal_snippet', id: snippet.id, file: txt }, format: :json
+ sign_in(user)
+ end
+
+ subject do
+ post :create, params: { model: model, id: user.id, file: jpg }, format: :json
end
it 'returns a content with original filename, new link, and correct type.' do
- expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
- expect(response.body).to match "\"url\":\"/uploads"
+ subject
+
+ expect(response.body).to match '\"alt\":\"rails_sample\"'
+ expect(response.body).to match "\"url\":\"/uploads/-/system/user/#{user.id}/"
end
it 'creates a corresponding Upload record' do
+ expect { subject }.to change { Upload.count }
+
upload = Upload.last
aggregate_failures do
expect(upload).to exist
- expect(upload.model).to eq snippet
+ expect(upload.model).to eq user
end
end
- end
- context 'temporal with valid image' do
- subject do
- post :create, params: { model: 'personal_snippet', file: jpg }, format: :json
- end
+ context 'with valid non-image file' do
+ subject do
+ post :create, params: { model: model, id: user.id, file: txt }, format: :json
+ end
- it 'returns a content with original filename, new link, and correct type.' do
- subject
+ it 'returns a content with original filename, new link, and correct type.' do
+ subject
- expect(response.body).to match '\"alt\":\"rails_sample\"'
- expect(response.body).to match "\"url\":\"/uploads/-/system/temp"
- end
+ expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
+ expect(response.body).to match "\"url\":\"/uploads/-/system/user/#{user.id}/"
+ end
- it 'does not create an Upload record' do
- expect { subject }.not_to change { Upload.count }
- end
- end
+ it 'creates a corresponding Upload record' do
+ expect { subject }.to change { Upload.count }
- context 'temporal with valid non-image file' do
- subject do
- post :create, params: { model: 'personal_snippet', file: txt }, format: :json
+ upload = Upload.last
+
+ aggregate_failures do
+ expect(upload).to exist
+ expect(upload.model).to eq user
+ end
+ end
end
- it 'returns a content with original filename, new link, and correct type.' do
- subject
+ it 'returns 404 when given user is not the logged in one' do
+ another_user = create(:user)
- expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
- expect(response.body).to match "\"url\":\"/uploads/-/system/temp"
- end
+ post :create, params: { model: model, id: another_user.id, file: txt }, format: :json
- it 'does not create an Upload record' do
- expect { subject }.not_to change { Upload.count }
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/factories/releases.rb b/spec/factories/releases.rb
index cab6b4a811f..4cacc77c182 100644
--- a/spec/factories/releases.rb
+++ b/spec/factories/releases.rb
@@ -6,6 +6,7 @@ FactoryBot.define do
description "Awesome release"
project
author
+ released_at { Time.zone.parse('2018-10-20T18:00:00Z') }
trait :legacy do
sha nil
diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
index 55f5ff04d01..254bb12573c 100644
--- a/spec/features/dashboard/shortcuts_spec.rb
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -22,7 +22,7 @@ describe 'Dashboard shortcuts', :js do
find('body').send_keys([:shift, 'T'])
- check_page_title('Todos')
+ check_page_title('To-Do List')
find('body').send_keys([:shift, 'P'])
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index d58e3b2841e..c48229fc0a0 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -13,7 +13,7 @@ describe 'Dashboard Todos' do
end
it 'shows "All done" message' do
- expect(page).to have_content 'Todos let you see what you should do next'
+ expect(page).to have_content 'Your To-Do List shows what to work on next'
end
end
@@ -72,7 +72,7 @@ describe 'Dashboard Todos' do
end
it 'updates todo count' do
- expect(page).to have_content 'Todos 0'
+ expect(page).to have_content 'To Do 0'
expect(page).to have_content 'Done 1'
end
@@ -101,7 +101,7 @@ describe 'Dashboard Todos' do
end
it 'updates todo count' do
- expect(page).to have_content 'Todos 1'
+ expect(page).to have_content 'To Do 1'
expect(page).to have_content 'Done 0'
end
end
@@ -211,7 +211,7 @@ describe 'Dashboard Todos' do
describe 'restoring the todo' do
before do
within first('.todo') do
- click_link 'Add todo'
+ click_link 'Add a To Do'
end
end
@@ -220,7 +220,7 @@ describe 'Dashboard Todos' do
end
it 'updates todo count' do
- expect(page).to have_content 'Todos 1'
+ expect(page).to have_content 'To Do 1'
expect(page).to have_content 'Done 0'
end
end
@@ -276,7 +276,7 @@ describe 'Dashboard Todos' do
end
it 'shows "All done" message!' do
- expect(page).to have_content 'Todos 0'
+ expect(page).to have_content 'To Do 0'
expect(page).to have_content "You're all done!"
expect(page).not_to have_selector('.gl-pagination')
end
@@ -303,7 +303,7 @@ describe 'Dashboard Todos' do
it 'updates todo count' do
mark_all_and_undo
- expect(page).to have_content 'Todos 2'
+ expect(page).to have_content 'To Do 2'
expect(page).to have_content 'Done 0'
end
diff --git a/spec/features/discussion_comments/commit_spec.rb b/spec/features/discussion_comments/commit_spec.rb
index ea720cee74e..b3f1731ec95 100644
--- a/spec/features/discussion_comments/commit_spec.rb
+++ b/spec/features/discussion_comments/commit_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Discussion Comments Commit', :js do
+describe 'Thread Comments Commit', :js do
include RepoHelpers
let(:user) { create(:user) }
@@ -16,7 +16,7 @@ describe 'Discussion Comments Commit', :js do
visit project_commit_path(project, sample_commit.id)
end
- it_behaves_like 'discussion comments', 'commit'
+ it_behaves_like 'thread comments', 'commit'
it 'has class .js-note-emoji' do
expect(page).to have_css('.js-note-emoji')
diff --git a/spec/features/discussion_comments/issue_spec.rb b/spec/features/discussion_comments/issue_spec.rb
index 5ec19460bbd..d71a1ee4731 100644
--- a/spec/features/discussion_comments/issue_spec.rb
+++ b/spec/features/discussion_comments/issue_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Discussion Comments Issue', :js do
+describe 'Thread Comments Issue', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
@@ -12,5 +12,5 @@ describe 'Discussion Comments Issue', :js do
visit project_issue_path(project, issue)
end
- it_behaves_like 'discussion comments', 'issue'
+ it_behaves_like 'thread comments', 'issue'
end
diff --git a/spec/features/discussion_comments/merge_request_spec.rb b/spec/features/discussion_comments/merge_request_spec.rb
index f940e973923..86e3507a3ee 100644
--- a/spec/features/discussion_comments/merge_request_spec.rb
+++ b/spec/features/discussion_comments/merge_request_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Discussion Comments Merge Request', :js do
+describe 'Thread Comments Merge Request', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -12,5 +12,5 @@ describe 'Discussion Comments Merge Request', :js do
visit project_merge_request_path(project, merge_request)
end
- it_behaves_like 'discussion comments', 'merge request'
+ it_behaves_like 'thread comments', 'merge request'
end
diff --git a/spec/features/discussion_comments/snippets_spec.rb b/spec/features/discussion_comments/snippets_spec.rb
index d330e89505e..29aa3e4366c 100644
--- a/spec/features/discussion_comments/snippets_spec.rb
+++ b/spec/features/discussion_comments/snippets_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Discussion Comments Snippet', :js do
+describe 'Thread Comments Snippet', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:snippet) { create(:project_snippet, :private, project: project, author: user) }
@@ -12,5 +12,5 @@ describe 'Discussion Comments Snippet', :js do
visit project_snippet_path(project, snippet)
end
- it_behaves_like 'discussion comments', 'snippet'
+ it_behaves_like 'thread comments', 'snippet'
end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index c000165ccd9..0ada530781c 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -150,6 +150,25 @@ describe 'Group issues page' do
check_issue_order
end
+ it 'issues should not be draggable when user is not logged in', :js do
+ sign_out(user_in_group)
+
+ visit issues_group_path(group, sort: 'relative_position')
+
+ drag_to(selector: '.manual-ordering',
+ from_index: 0,
+ to_index: 2)
+
+ wait_for_requests
+
+ # Issue order should remain the same
+ page.within('.manual-ordering') do
+ expect(find('.issue:nth-child(1) .title')).to have_content('Issue #1')
+ expect(find('.issue:nth-child(2) .title')).to have_content('Issue #2')
+ expect(find('.issue:nth-child(3) .title')).to have_content('Issue #3')
+ end
+ end
+
def check_issue_order
page.within('.manual-ordering') do
expect(find('.issue:nth-child(1) .title')).to have_content('Issue #2')
diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
index ada57285abf..f6dccb5e98a 100644
--- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
@@ -1,13 +1,13 @@
require 'rails_helper'
-describe 'Resolving all open discussions in a merge request from an issue', :js do
+describe 'Resolving all open threads in a merge request from an issue', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
def resolve_all_discussions_link_selector
- text = "Resolve all discussions in new issue"
+ text = "Resolve all threads in new issue"
url = new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
%Q{a[data-original-title="#{text}"][href="#{url}"]}
end
@@ -19,15 +19,15 @@ describe 'Resolving all open discussions in a merge request from an issue', :js
visit project_merge_request_path(project, merge_request)
end
- it 'shows a button to resolve all discussions by creating a new issue' do
+ it 'shows a button to resolve all threads by creating a new issue' do
within('.line-resolve-all-container') do
expect(page).to have_selector resolve_all_discussions_link_selector
end
end
- context 'resolving the discussion' do
+ context 'resolving the thread' do
before do
- click_button 'Resolve discussion'
+ click_button 'Resolve thread'
end
it 'hides the link for creating a new issue' do
@@ -35,15 +35,15 @@ describe 'Resolving all open discussions in a merge request from an issue', :js
end
end
- context 'creating an issue for discussions' do
+ context 'creating an issue for threads' do
before do
find(resolve_all_discussions_link_selector).click
end
- it_behaves_like 'creating an issue for a discussion'
+ it_behaves_like 'creating an issue for a thread'
end
- context 'for a project where all discussions need to be resolved before merging' do
+ context 'for a project where all threads need to be resolved before merging' do
before do
project.update_attribute(:only_allow_merge_if_all_discussions_are_resolved, true)
end
@@ -59,27 +59,27 @@ describe 'Resolving all open discussions in a merge request from an issue', :js
end
end
- context 'merge request has discussions that need to be resolved' do
+ context 'merge request has threads that need to be resolved' do
before do
visit project_merge_request_path(project, merge_request)
end
- it 'shows a warning that the merge request contains unresolved discussions' do
- expect(page).to have_content 'There are unresolved discussions.'
+ it 'shows a warning that the merge request contains unresolved threads' do
+ expect(page).to have_content 'There are unresolved threads.'
end
- it 'has a link to resolve all discussions by creating an issue' do
+ it 'has a link to resolve all threads by creating an issue' do
page.within '.mr-widget-body' do
expect(page).to have_link 'Create an issue to resolve them later', href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
end
end
- context 'creating an issue for discussions' do
+ context 'creating an issue for threads' do
before do
page.click_link 'Create an issue to resolve them later', href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
end
- it_behaves_like 'creating an issue for a discussion'
+ it_behaves_like 'creating an issue for a thread'
end
end
end
@@ -92,8 +92,8 @@ describe 'Resolving all open discussions in a merge request from an issue', :js
visit new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
end
- it 'Shows a notice to ask someone else to resolve the discussions' do
- expect(page).to have_content("The discussions at #{merge_request.to_reference} will stay unresolved. Ask someone with permission to resolve them.")
+ it 'Shows a notice to ask someone else to resolve the threads' do
+ expect(page).to have_content("The threads at #{merge_request.to_reference} will stay unresolved. Ask someone with permission to resolve them.")
end
end
end
diff --git a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
index b20730bdb22..1b1a31d0723 100644
--- a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
@@ -1,13 +1,13 @@
require 'rails_helper'
-describe 'Resolve an open discussion in a merge request by creating an issue', :js do
+describe 'Resolve an open thread in a merge request by creating an issue', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
def resolve_discussion_selector
- title = 'Resolve this discussion in a new issue'
+ title = 'Resolve this thread in a new issue'
url = new_project_issue_path(project, discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid)
"a[data-original-title=\"#{title}\"][href=\"#{url}\"]"
end
@@ -30,40 +30,40 @@ describe 'Resolve an open discussion in a merge request by creating an issue', :
end
end
- context 'resolving the discussion' do
+ context 'resolving the thread' do
before do
- click_button 'Resolve discussion'
+ click_button 'Resolve thread'
end
it 'hides the link for creating a new issue' do
expect(page).not_to have_css resolve_discussion_selector
end
- it 'shows the link for creating a new issue when unresolving a discussion' do
+ it 'shows the link for creating a new issue when unresolving a thread' do
page.within '.diff-content' do
- click_button 'Unresolve discussion'
+ click_button 'Unresolve thread'
end
expect(page).to have_css resolve_discussion_selector
end
end
- it 'has a link to create a new issue for a discussion' do
+ it 'has a link to create a new issue for a thread' do
expect(page).to have_css resolve_discussion_selector
end
context 'creating the issue' do
before do
- find(resolve_discussion_selector).click
+ find(resolve_discussion_selector, match: :first).click
end
- it 'has a hidden field for the discussion' do
+ it 'has a hidden field for the thread' do
discussion_field = find('#discussion_to_resolve', visible: false)
expect(discussion_field.value).to eq(discussion.id.to_s)
end
- it_behaves_like 'creating an issue for a discussion'
+ it_behaves_like 'creating an issue for a thread'
end
end
@@ -75,8 +75,8 @@ describe 'Resolve an open discussion in a merge request by creating an issue', :
discussion_to_resolve: discussion.id)
end
- it 'Shows a notice to ask someone else to resolve the discussions' do
- expect(page).to have_content("The discussion at #{merge_request.to_reference}"\
+ it 'Shows a notice to ask someone else to resolve the threads' do
+ expect(page).to have_content("The thread at #{merge_request.to_reference}"\
" (discussion #{discussion.first_note.id}) will stay unresolved."\
" Ask someone with permission to resolve it.")
end
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index 0114178b9be..07ae159eef4 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -13,8 +13,8 @@ describe 'Manually create a todo item from issue', :js do
it 'creates todo when clicking button' do
page.within '.issuable-sidebar' do
- click_button 'Add todo'
- expect(page).to have_content 'Mark todo as done'
+ click_button 'Add a To Do'
+ expect(page).to have_content 'Mark as done'
end
page.within '.header-content .todos-count' do
@@ -30,8 +30,8 @@ describe 'Manually create a todo item from issue', :js do
it 'marks a todo as done' do
page.within '.issuable-sidebar' do
- click_button 'Add todo'
- click_button 'Mark todo as done'
+ click_button 'Add a To Do'
+ click_button 'Mark as done'
end
expect(page).to have_selector('.todos-count', visible: false)
diff --git a/spec/features/issues/user_creates_confidential_merge_request_spec.rb b/spec/features/issues/user_creates_confidential_merge_request_spec.rb
new file mode 100644
index 00000000000..7ae4af4667b
--- /dev/null
+++ b/spec/features/issues/user_creates_confidential_merge_request_spec.rb
@@ -0,0 +1,54 @@
+require 'rails_helper'
+
+describe 'User creates confidential merge request on issue page', :js do
+ include ProjectForksHelper
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository, :public) }
+ let(:issue) { create(:issue, project: project, confidential: true) }
+
+ def visit_confidential_issue
+ sign_in(user)
+ visit project_issue_path(project, issue)
+ wait_for_requests
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ context 'user has no private fork' do
+ before do
+ fork_project(project, user, repository: true)
+ visit_confidential_issue
+ end
+
+ it 'shows that user has no fork available' do
+ click_button 'Create confidential merge request'
+
+ page.within '.create-confidential-merge-request-dropdown-menu' do
+ expect(page).to have_content('No forks available to you')
+ end
+ end
+ end
+
+ describe 'user has private fork' do
+ let(:forked_project) { fork_project(project, user, repository: true) }
+
+ before do
+ forked_project.update(visibility: Gitlab::VisibilityLevel::PRIVATE)
+ visit_confidential_issue
+ end
+
+ it 'create merge request in fork' do
+ click_button 'Create confidential merge request'
+
+ page.within '.create-confidential-merge-request-dropdown-menu' do
+ expect(page).to have_button(forked_project.name_with_namespace)
+ click_button 'Create confidential merge request'
+ end
+
+ expect(page).to have_content(forked_project.namespace.name)
+ end
+ end
+end
diff --git a/spec/features/merge_request/user_comments_on_merge_request_spec.rb b/spec/features/merge_request/user_comments_on_merge_request_spec.rb
index 69bdab85d81..e6bc3780b5c 100644
--- a/spec/features/merge_request/user_comments_on_merge_request_spec.rb
+++ b/spec/features/merge_request/user_comments_on_merge_request_spec.rb
@@ -37,7 +37,7 @@ describe 'User comments on a merge request', :js do
wait_for_requests
page.within('.notes .discussion') do
- expect(page).to have_content("#{user.name} #{user.to_reference} started a discussion")
+ expect(page).to have_content("#{user.name} #{user.to_reference} started a thread")
expect(page).to have_content(sample_commit.line_code_path)
expect(page).to have_content('Line is wrong')
end
diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
index 260eec7a9ed..10fe60cb075 100644
--- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
+++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Merge request > User resolves diff notes and discussions', :js do
+describe 'Merge request > User resolves diff notes and threads', :js do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:guest) { create(:user) }
@@ -17,7 +17,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
)
end
- context 'no discussions' do
+ context 'no threads' do
before do
project.add_maintainer(user)
sign_in(user)
@@ -25,8 +25,8 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
visit_merge_request
end
- it 'displays no discussion resolved data' do
- expect(page).not_to have_content('discussion resolved')
+ it 'displays no thread resolved data' do
+ expect(page).not_to have_content('thread resolved')
expect(page).not_to have_selector('.discussion-next-btn')
end
end
@@ -38,10 +38,10 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
visit_merge_request
end
- context 'single discussion' do
- it 'shows text with how many discussions' do
+ context 'single thread' do
+ it 'shows text with how many threads' do
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 discussion resolved')
+ expect(page).to have_content('0/1 thread resolved')
end
end
@@ -54,18 +54,18 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
page.within '.diff-content' do
- expect(page).to have_selector('.btn', text: 'Unresolve discussion')
+ expect(page).to have_selector('.btn', text: 'Unresolve thread')
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_content('1/1 thread resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
- it 'allows user to mark discussion as resolved' do
+ it 'allows user to mark thread as resolved' do
page.within '.diff-content' do
- click_button 'Resolve discussion'
+ click_button 'Resolve thread'
end
expect(page).to have_selector('.discussion-body', visible: false)
@@ -75,38 +75,38 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_content('1/1 thread resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
- it 'allows user to unresolve discussion' do
+ it 'allows user to unresolve thread' do
page.within '.diff-content' do
- click_button 'Resolve discussion'
- click_button 'Unresolve discussion'
+ click_button 'Resolve thread'
+ click_button 'Unresolve thread'
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 discussion resolved')
+ expect(page).to have_content('0/1 thread resolved')
end
end
- describe 'resolved discussion' do
+ describe 'resolved thread' do
before do
page.within '.diff-content' do
- click_button 'Resolve discussion'
+ click_button 'Resolve thread'
end
visit_merge_request
end
describe 'timeline view' do
- it 'hides when resolve discussion is clicked' do
+ it 'hides when resolve thread is clicked' do
expect(page).to have_selector('.discussion-header')
expect(page).not_to have_selector('.discussion-body')
end
- it 'shows resolved discussion when toggled' do
+ it 'shows resolved thread when toggled' do
find(".timeline-content .discussion[data-discussion-id='#{note.discussion_id}'] .discussion-toggle-button").click
expect(page.find(".line-holder-placeholder")).to be_visible
@@ -130,11 +130,11 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
page.find('#parallel-diff-btn').click
end
- it 'hides when resolve discussion is clicked' do
+ it 'hides when resolve thread is clicked' do
expect(page).not_to have_selector('.diffs .diff-file .notes_holder')
end
- it 'shows resolved discussion when toggled' do
+ it 'shows resolved thread when toggled' do
find('.diff-comment-avatar-holders').click
expect(find('.diffs .diff-file .notes_holder')).to be_visible
@@ -143,7 +143,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
describe 'reply form' do
before do
- click_button 'Toggle discussion'
+ click_button 'Toggle thread'
page.within '.diff-content' do
click_button 'Reply...'
@@ -160,34 +160,34 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_content('1/1 thread resolved')
end
end
it 'allows user to unresolve from reply form without a comment' do
page.within '.diff-content' do
- click_button 'Unresolve discussion'
+ click_button 'Unresolve thread'
wait_for_requests
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 discussion resolved')
+ expect(page).to have_content('0/1 thread resolved')
expect(page).not_to have_selector('.line-resolve-btn.is-active')
end
end
- it 'allows user to comment & unresolve discussion' do
+ it 'allows user to comment & unresolve thread' do
page.within '.diff-content' do
find('.js-note-text').set 'testing'
- click_button 'Comment & unresolve discussion'
+ click_button 'Comment & unresolve thread'
wait_for_requests
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 discussion resolved')
+ expect(page).to have_content('0/1 thread resolved')
end
end
end
@@ -197,31 +197,31 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
page.within '.diff-content' do
click_button 'Reply...'
- click_button 'Resolve discussion'
+ click_button 'Resolve thread'
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_content('1/1 thread resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
- it 'allows user to comment & resolve discussion' do
+ it 'allows user to comment & resolve thread' do
page.within '.diff-content' do
click_button 'Reply...'
find('.js-note-text').set 'testing'
- click_button 'Comment & resolve discussion'
+ click_button 'Comment & resolve thread'
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_content('1/1 thread resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
- it 'allows user to quickly scroll to next unresolved discussion' do
+ it 'allows user to quickly scroll to next unresolved thread' do
page.within '.line-resolve-all-container' do
page.find('.discussion-next-btn').click
end
@@ -231,7 +231,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
it 'hides jump to next button when all resolved' do
page.within '.diff-content' do
- click_button 'Resolve discussion'
+ click_button 'Resolve thread'
end
expect(page).to have_selector('.discussion-next-btn', visible: false)
@@ -248,7 +248,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
end
- it 'hides jump to next discussion button' do
+ it 'hides jump to next thread button' do
page.within '.discussion-reply-holder' do
expect(page).not_to have_selector('.discussion-next-btn')
end
@@ -261,7 +261,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
visit_merge_request
end
- it 'does not mark discussion as resolved when resolving single note' do
+ it 'does not mark thread as resolved when resolving single note' do
page.within("#note_#{note.id}") do
first('.line-resolve-btn').click
@@ -273,11 +273,11 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
expect(page).to have_content('Last updated')
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 discussion resolved')
+ expect(page).to have_content('0/1 thread resolved')
end
end
- it 'resolves discussion' do
+ it 'resolves thread' do
resolve_buttons = page.all('.note .line-resolve-btn', count: 2)
resolve_buttons.each do |button|
button.click
@@ -290,28 +290,28 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_content('1/1 thread resolved')
end
end
end
- context 'muliple discussions' do
+ context 'muliple threads' do
before do
create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request)
visit_merge_request
end
- it 'shows text with how many discussions' do
+ it 'shows text with how many threads' do
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/2 discussions resolved')
+ expect(page).to have_content('0/2 threads resolved')
end
end
it 'allows user to mark a single note as resolved' do
- click_button('Resolve discussion', match: :first)
+ click_button('Resolve thread', match: :first)
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/2 discussions resolved')
+ expect(page).to have_content('1/2 threads resolved')
end
end
@@ -321,27 +321,27 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('2/2 discussions resolved')
+ expect(page).to have_content('2/2 threads resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
- it 'allows user to mark all discussions as resolved' do
+ it 'allows user to mark all threads as resolved' do
page.all('.discussion-reply-holder', count: 2).each do |reply_holder|
page.within reply_holder do
- click_button 'Resolve discussion'
+ click_button 'Resolve thread'
end
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('2/2 discussions resolved')
+ expect(page).to have_content('2/2 threads resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
- it 'allows user to quickly scroll to next unresolved discussion' do
+ it 'allows user to quickly scroll to next unresolved thread' do
page.within('.discussion-reply-holder', match: :first) do
- click_button 'Resolve discussion'
+ click_button 'Resolve thread'
end
page.within '.line-resolve-all-container' do
@@ -362,25 +362,25 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
end
- it 'shows jump to next discussion button on all discussions' do
+ it 'shows jump to next thread button except on last thread' do
wait_for_requests
all_discussion_replies = page.all('.discussion-reply-holder')
expect(all_discussion_replies.count).to eq(2)
- expect(all_discussion_replies.first.all('.discussion-next-btn').count).to eq(1)
- expect(all_discussion_replies.last.all('.discussion-next-btn').count).to eq(1)
+ expect(all_discussion_replies.first.all('.discussion-next-btn').count).to eq(2)
+ expect(all_discussion_replies.last.all('.discussion-next-btn').count).to eq(2)
end
- it 'displays next discussion even if hidden' do
+ it 'displays next thread even if hidden' do
page.all('.note-discussion', count: 2).each do |discussion|
page.within discussion do
- click_button 'Toggle discussion'
+ click_button 'Toggle thread'
end
end
page.within('.issuable-discussion #notes') do
- expect(page).not_to have_selector('.btn', text: 'Resolve discussion')
+ expect(page).not_to have_selector('.btn', text: 'Resolve thread')
end
page.within '.line-resolve-all-container' do
@@ -388,19 +388,19 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
page.all('.note-discussion').first do
- expect(page.find('.discussion-with-resolve-btn')).to have_selector('.btn', text: 'Resolve discussion')
+ expect(page.find('.discussion-with-resolve-btn')).to have_selector('.btn', text: 'Resolve thread')
end
page.all('.note-discussion').last do
- expect(page.find('.discussion-with-resolve-btn')).not.to have_selector('.btn', text: 'Resolve discussion')
+ expect(page.find('.discussion-with-resolve-btn')).not.to have_selector('.btn', text: 'Resolve thread')
end
end
end
context 'changes tab' do
- it 'shows text with how many discussions' do
+ it 'shows text with how many threads' do
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 discussion resolved')
+ expect(page).to have_content('0/1 thread resolved')
end
end
@@ -412,18 +412,18 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
page.within '.diff-content' do
- expect(page).to have_selector('.btn', text: 'Unresolve discussion')
+ expect(page).to have_selector('.btn', text: 'Unresolve thread')
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_content('1/1 thread resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
- it 'allows user to mark discussion as resolved' do
+ it 'allows user to mark thread as resolved' do
page.within '.diff-content' do
- click_button 'Resolve discussion'
+ click_button 'Resolve thread'
end
page.within '.diff-content .note' do
@@ -431,50 +431,50 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_content('1/1 thread resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
- it 'allows user to unresolve discussion' do
+ it 'allows user to unresolve thread' do
page.within '.diff-content' do
- click_button 'Resolve discussion'
- click_button 'Unresolve discussion'
+ click_button 'Resolve thread'
+ click_button 'Unresolve thread'
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 discussion resolved')
+ expect(page).to have_content('0/1 thread resolved')
end
end
- it 'allows user to comment & resolve discussion' do
+ it 'allows user to comment & resolve thread' do
page.within '.diff-content' do
click_button 'Reply...'
find('.js-note-text').set 'testing'
- click_button 'Comment & resolve discussion'
+ click_button 'Comment & resolve thread'
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_content('1/1 thread resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
- it 'allows user to comment & unresolve discussion' do
+ it 'allows user to comment & unresolve thread' do
page.within '.diff-content' do
- click_button 'Resolve discussion'
+ click_button 'Resolve thread'
click_button 'Reply...'
find('.js-note-text').set 'testing'
- click_button 'Comment & unresolve discussion'
+ click_button 'Comment & unresolve thread'
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 discussion resolved')
+ expect(page).to have_content('0/1 thread resolved')
end
end
end
@@ -497,13 +497,13 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 discussion resolved')
+ expect(page).to have_content('0/1 thread resolved')
end
end
- it 'does not allow user to mark discussion as resolved' do
+ it 'does not allow user to mark thread as resolved' do
page.within '.diff-content .note' do
- expect(page).not_to have_selector('.btn', text: 'Resolve discussion')
+ expect(page).not_to have_selector('.btn', text: 'Resolve thread')
end
end
end
@@ -523,11 +523,11 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
page.within '.diff-content' do
- expect(page).to have_selector('.btn', text: 'Unresolve discussion')
+ expect(page).to have_selector('.btn', text: 'Unresolve thread')
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_content('1/1 thread resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
@@ -546,7 +546,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 discussion resolved')
+ expect(page).to have_content('0/1 thread resolved')
end
end
end
@@ -558,15 +558,15 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
it 'shows resolved icon' do
- expect(page).to have_content '1/1 discussion resolved'
+ expect(page).to have_content '1/1 thread resolved'
- click_button 'Toggle discussion'
+ click_button 'Toggle thread'
expect(page).to have_selector('.line-resolve-btn.is-active')
end
it 'does not allow user to click resolve button' do
expect(page).to have_selector('.line-resolve-btn.is-disabled')
- click_button 'Toggle discussion'
+ click_button 'Toggle thread'
expect(page).to have_selector('.line-resolve-btn.is-disabled')
end
diff --git a/spec/features/merge_request/user_sees_discussions_spec.rb b/spec/features/merge_request/user_sees_discussions_spec.rb
index 57be1d06708..39258b48295 100644
--- a/spec/features/merge_request/user_sees_discussions_spec.rb
+++ b/spec/features/merge_request/user_sees_discussions_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Merge request > User sees discussions', :js do
+describe 'Merge request > User sees threads', :js do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -30,7 +30,7 @@ describe 'Merge request > User sees discussions', :js do
visit project_merge_request_path(project, merge_request)
end
- context 'active discussions' do
+ context 'active threads' do
it 'shows a link to the diff' do
within(".discussion[data-discussion-id='#{active_discussion.id}']") do
path = diffs_project_merge_request_path(project, merge_request, anchor: active_discussion.line_code)
@@ -39,7 +39,7 @@ describe 'Merge request > User sees discussions', :js do
end
end
- context 'outdated discussions' do
+ context 'outdated threads' do
it 'shows a link to the outdated diff' do
within(".discussion[data-discussion-id='#{outdated_discussion.id}']") do
path = diffs_project_merge_request_path(project, merge_request, diff_id: old_merge_request_diff.id, anchor: outdated_discussion.line_code)
@@ -85,7 +85,7 @@ describe 'Merge request > User sees discussions', :js do
it_behaves_like 'a functional discussion'
it 'displays correct header' do
- expect(page).to have_content "started a discussion on commit #{note.commit_id[0...7]}"
+ expect(page).to have_content "started a thread on commit #{note.commit_id[0...7]}"
end
end
diff --git a/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb b/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb
index f6b771facf8..cf398a7df18 100644
--- a/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Merge request > User sees merge button depending on unresolved discussions', :js do
+describe 'Merge request > User sees merge button depending on unresolved threads', :js do
let(:project) { create(:project, :repository) }
let(:user) { project.creator }
let!(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) }
@@ -16,14 +16,14 @@ describe 'Merge request > User sees merge button depending on unresolved discuss
visit project_merge_request_path(project, merge_request)
end
- context 'with unresolved discussions' do
+ context 'with unresolved threads' do
it 'does not allow to merge' do
expect(page).not_to have_button 'Merge'
- expect(page).to have_content('There are unresolved discussions.')
+ expect(page).to have_content('There are unresolved threads.')
end
end
- context 'with all discussions resolved' do
+ context 'with all threads resolved' do
before do
merge_request.discussions.each { |d| d.resolve!(user) }
visit project_merge_request_path(project, merge_request)
@@ -41,13 +41,13 @@ describe 'Merge request > User sees merge button depending on unresolved discuss
visit project_merge_request_path(project, merge_request)
end
- context 'with unresolved discussions' do
+ context 'with unresolved threads' do
it 'does not allow to merge' do
expect(page).to have_button 'Merge'
end
end
- context 'with all discussions resolved' do
+ context 'with all threads resolved' do
before do
merge_request.discussions.each { |d| d.resolve!(user) }
visit project_merge_request_path(project, merge_request)
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 733e8aa3eba..30e30751693 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -519,6 +519,8 @@ describe 'Merge request > User sees merge widget', :js do
end
before do
+ allow_any_instance_of(TestSuiteComparerEntity)
+ .to receive(:max_tests).and_return(2)
allow_any_instance_of(MergeRequest)
.to receive(:has_test_reports?).and_return(true)
allow_any_instance_of(MergeRequest)
@@ -551,7 +553,7 @@ describe 'Merge request > User sees merge widget', :js do
expect(page).to have_content('rspec found no changed test results out of 1 total test')
expect(page).to have_content('junit found 1 failed test result out of 1 total test')
expect(page).to have_content('New')
- expect(page).to have_content('subtractTest')
+ expect(page).to have_content('addTest')
end
end
end
@@ -562,7 +564,7 @@ describe 'Merge request > User sees merge widget', :js do
click_button 'Expand'
within(".js-report-section-container") do
- click_button 'subtractTest'
+ click_button 'addTest'
expect(page).to have_content('6.66')
expect(page).to have_content(sample_java_failed_message.gsub!(/\s+/, ' ').strip)
@@ -596,7 +598,7 @@ describe 'Merge request > User sees merge widget', :js do
expect(page).to have_content('rspec found 1 failed test result out of 1 total test')
expect(page).to have_content('junit found no changed test results out of 1 total test')
expect(page).not_to have_content('New')
- expect(page).to have_content('Test#sum when a is 2 and b is 2 returns summary')
+ expect(page).to have_content('Test#sum when a is 1 and b is 3 returns summary')
end
end
end
@@ -607,7 +609,7 @@ describe 'Merge request > User sees merge widget', :js do
click_button 'Expand'
within(".js-report-section-container") do
- click_button 'Test#sum when a is 2 and b is 2 returns summary'
+ click_button 'Test#sum when a is 1 and b is 3 returns summary'
expect(page).to have_content('2.22')
expect(page).to have_content(sample_rspec_failed_message.gsub!(/\s+/, ' ').strip)
@@ -628,13 +630,7 @@ describe 'Merge request > User sees merge widget', :js do
let(:head_reports) do
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
- reports.get_suite('junit').add_test_case(create_test_case_java_resolved)
- end
- end
-
- let(:create_test_case_java_resolved) do
- create_test_case_java_failed.tap do |test_case|
- test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
+ reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
end
@@ -646,7 +642,7 @@ describe 'Merge request > User sees merge widget', :js do
within(".js-report-section-container") do
expect(page).to have_content('rspec found no changed test results out of 1 total test')
expect(page).to have_content('junit found 1 fixed test result out of 1 total test')
- expect(page).to have_content('subtractTest')
+ expect(page).to have_content('addTest')
end
end
end
@@ -657,15 +653,53 @@ describe 'Merge request > User sees merge widget', :js do
click_button 'Expand'
within(".js-report-section-container") do
- click_button 'subtractTest'
+ click_button 'addTest'
- expect(page).to have_content('6.66')
+ expect(page).to have_content('5.55')
end
end
end
end
end
+ context 'properly truncates the report' do
+ let(:base_reports) do
+ Gitlab::Ci::Reports::TestReports.new.tap do |reports|
+ 10.times do |index|
+ reports.get_suite('rspec').add_test_case(
+ create_test_case_rspec_failed(index))
+ reports.get_suite('junit').add_test_case(
+ create_test_case_java_success(index))
+ end
+ end
+ end
+
+ let(:head_reports) do
+ Gitlab::Ci::Reports::TestReports.new.tap do |reports|
+ 10.times do |index|
+ reports.get_suite('rspec').add_test_case(
+ create_test_case_rspec_failed(index))
+ reports.get_suite('junit').add_test_case(
+ create_test_case_java_failed(index))
+ end
+ end
+ end
+
+ it 'shows test reports summary which includes the resolved failure' do
+ within(".js-reports-container") do
+ click_button 'Expand'
+
+ expect(page).to have_content('Test summary contained 20 failed test results out of 20 total tests')
+ within(".js-report-section-container") do
+ expect(page).to have_content('rspec found 10 failed test results out of 10 total tests')
+ expect(page).to have_content('junit found 10 failed test results out of 10 total tests')
+
+ expect(page).to have_content('Test#sum when a is 1 and b is 3 returns summary', count: 2)
+ end
+ end
+ end
+ end
+
def comparer
Gitlab::Ci::Reports::TestReportsComparer.new(base_reports, head_reports)
end
diff --git a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
index e8b4fc8f160..4363b359038 100644
--- a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
+++ b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
@@ -186,12 +186,12 @@ describe 'User comments on a diff', :js do
it 'resolves discussion when applied' do
page.within('.diff-discussions') do
- expect(page).not_to have_content('Unresolve discussion')
+ expect(page).not_to have_content('Unresolve thread')
click_button('Apply suggestion')
wait_for_requests
- expect(page).to have_content('Unresolve discussion')
+ expect(page).to have_content('Unresolve thread')
end
end
end
diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb
index 5ebfc32952d..86331728f88 100644
--- a/spec/features/oauth_login_spec.rb
+++ b/spec/features/oauth_login_spec.rb
@@ -16,16 +16,8 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
providers = [:github, :twitter, :bitbucket, :gitlab, :google_oauth2,
:facebook, :cas3, :auth0, :authentiq, :salesforce]
- before(:all) do
- # The OmniAuth `full_host` parameter doesn't get set correctly (it gets set to something like `http://localhost`
- # here), and causes integration tests to fail with 404s. We set the `full_host` by removing the request path (and
- # anything after it) from the request URI.
- @omniauth_config_full_host = OmniAuth.config.full_host
- OmniAuth.config.full_host = ->(request) { request['REQUEST_URI'].sub(/#{request['REQUEST_PATH']}.*/, '') }
- end
-
- after(:all) do
- OmniAuth.config.full_host = @omniauth_config_full_host
+ around(:all) do |example|
+ with_omniauth_full_host { example.run }
end
def login_with_provider(provider, enter_two_factor: false)
diff --git a/spec/features/projects/releases/user_views_releases_spec.rb b/spec/features/projects/releases/user_views_releases_spec.rb
index 317ffb6a2ff..725d7173bce 100644
--- a/spec/features/projects/releases/user_views_releases_spec.rb
+++ b/spec/features/projects/releases/user_views_releases_spec.rb
@@ -16,6 +16,7 @@ describe 'User views releases', :js do
expect(page).to have_content(release.name)
expect(page).to have_content(release.tag)
+ expect(page).not_to have_content('Upcoming Release')
end
context 'when there is a link as an asset' do
@@ -43,4 +44,15 @@ describe 'User views releases', :js do
end
end
end
+
+ context 'with an upcoming release' do
+ let(:tomorrow) { Time.zone.now + 1.day }
+ let!(:release) { create(:release, project: project, released_at: tomorrow ) }
+
+ it 'sees the upcoming tag' do
+ visit project_releases_path(project)
+
+ expect(page).to have_content('Upcoming Release')
+ end
+ end
end
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index 8c7bc192c50..1edfee705c8 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -112,11 +112,17 @@ describe 'Projects > Settings > Repository settings' do
it 'add a new deploy token' do
fill_in 'deploy_token_name', with: 'new_deploy_key'
fill_in 'deploy_token_expires_at', with: (Date.today + 1.month).to_s
+ fill_in 'deploy_token_username', with: 'deployer'
check 'deploy_token_read_repository'
check 'deploy_token_read_registry'
click_button 'Create deploy token'
expect(page).to have_content('Your new project deploy token has been created')
+
+ within('.created-deploy-token-container') do
+ expect(page).to have_selector("input[name='deploy-token-user'][value='deployer']")
+ expect(page).to have_selector("input[name='deploy-token'][readonly='readonly']")
+ end
end
end
diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb
index 1c97d5ec5b4..93d77d5b5ce 100644
--- a/spec/features/snippets/user_creates_snippet_spec.rb
+++ b/spec/features/snippets/user_creates_snippet_spec.rb
@@ -41,7 +41,7 @@ describe 'User creates snippet', :js do
expect(page).to have_content('My Snippet')
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
- expect(link).to match(%r{/uploads/-/system/temp/\h{32}/banana_sample\.gif\z})
+ expect(link).to match(%r{/uploads/-/system/user/#{user.id}/\h{32}/banana_sample\.gif\z})
reqs = inspect_requests { visit(link) }
expect(reqs.first.status_code).to eq(200)
diff --git a/spec/finders/releases_finder_spec.rb b/spec/finders/releases_finder_spec.rb
index 32ee15134a2..5ffb8c74bf5 100644
--- a/spec/finders/releases_finder_spec.rb
+++ b/spec/finders/releases_finder_spec.rb
@@ -12,8 +12,8 @@ describe ReleasesFinder do
subject { described_class.new(project, user)}
before do
- v1_0_0.update_attribute(:created_at, 2.days.ago)
- v1_1_0.update_attribute(:created_at, 1.day.ago)
+ v1_0_0.update_attribute(:released_at, 2.days.ago)
+ v1_1_0.update_attribute(:released_at, 1.day.ago)
end
describe '#execute' do
@@ -30,7 +30,7 @@ describe ReleasesFinder do
project.add_developer(user)
end
- it 'sorts by creation date' do
+ it 'sorts by release date' do
releases = subject.execute
expect(releases).to be_present
diff --git a/spec/finders/runner_jobs_finder_spec.rb b/spec/finders/runner_jobs_finder_spec.rb
index 97304170c4e..01f45a37ba8 100644
--- a/spec/finders/runner_jobs_finder_spec.rb
+++ b/spec/finders/runner_jobs_finder_spec.rb
@@ -35,5 +35,27 @@ describe RunnerJobsFinder do
end
end
end
+
+ context 'when order_by and sort are specified' do
+ context 'when order_by id and sort is asc' do
+ let(:params) { { order_by: 'id', sort: 'asc' } }
+ let!(:jobs) { create_list(:ci_build, 2, runner: runner, project: project, user: create(:user)) }
+
+ it 'sorts as id: :asc' do
+ is_expected.to eq(jobs.sort_by(&:id))
+ end
+ end
+ end
+
+ context 'when order_by is specified and sort is not specified' do
+ context 'when order_by id and sort is not specified' do
+ let(:params) { { order_by: 'id' } }
+ let!(:jobs) { create_list(:ci_build, 2, runner: runner, project: project, user: create(:user)) }
+
+ it 'sorts as id: :desc' do
+ is_expected.to eq(jobs.sort_by(&:id).reverse)
+ end
+ end
+ end
end
end
diff --git a/spec/fixtures/api/schemas/public_api/v4/release.json b/spec/fixtures/api/schemas/public_api/v4/release.json
index 6ea0781c1ed..ec3fa59cdb1 100644
--- a/spec/fixtures/api/schemas/public_api/v4/release.json
+++ b/spec/fixtures/api/schemas/public_api/v4/release.json
@@ -1,12 +1,14 @@
{
"type": "object",
- "required": ["name", "tag_name", "commit"],
+ "required": ["name", "tag_name", "commit", "released_at"],
"properties": {
"name": { "type": "string" },
"tag_name": { "type": "string" },
"description": { "type": "string" },
"description_html": { "type": "string" },
"created_at": { "type": "date" },
+ "released_at": { "type": "date" },
+ "upcoming_release": { "type": "boolean" },
"commit": {
"oneOf": [{ "type": "null" }, { "$ref": "commit/basic.json" }]
},
diff --git a/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json b/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json
index e78398ad1d5..0c1e8fd5fb3 100644
--- a/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json
+++ b/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json
@@ -1,11 +1,13 @@
{
"type": "object",
- "required": ["name"],
+ "required": ["name", "released_at"],
"properties": {
"name": { "type": "string" },
"description": { "type": "string" },
"description_html": { "type": "string" },
"created_at": { "type": "date" },
+ "released_at": { "type": "date" },
+ "upcoming_release": { "type": "boolean" },
"author": {
"oneOf": [{ "type": "null" }, { "$ref": "../user/basic.json" }]
},
diff --git a/spec/frontend/boards/services/board_service_spec.js b/spec/frontend/boards/services/board_service_spec.js
index de9fc998360..a8a322e7237 100644
--- a/spec/frontend/boards/services/board_service_spec.js
+++ b/spec/frontend/boards/services/board_service_spec.js
@@ -2,6 +2,7 @@ import BoardService from '~/boards/services/board_service';
import { TEST_HOST } from 'helpers/test_constants';
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import boardsStore from '~/boards/stores/boards_store';
describe('BoardService', () => {
const dummyResponse = "without type checking this doesn't matter";
@@ -18,10 +19,11 @@ describe('BoardService', () => {
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
- service = new BoardService({
+ boardsStore.setEndpoints({
...endpoints,
boardId,
});
+ service = new BoardService();
});
describe('all', () => {
diff --git a/spec/frontend/branches/divergence_graph_spec.js b/spec/frontend/branches/divergence_graph_spec.js
index 4ed77c3a036..8283bc966e4 100644
--- a/spec/frontend/branches/divergence_graph_spec.js
+++ b/spec/frontend/branches/divergence_graph_spec.js
@@ -10,12 +10,14 @@ describe('Divergence graph', () => {
mock.onGet('/-/diverging_counts').reply(200, {
master: { ahead: 1, behind: 1 },
+ 'test/hello-world': { ahead: 1, behind: 1 },
});
jest.spyOn(axios, 'get');
document.body.innerHTML = `
- <div class="js-branch-item" data-name="master"></div>
+ <div class="js-branch-item" data-name="master"><div class="js-branch-divergence-graph"></div></div>
+ <div class="js-branch-item" data-name="test/hello-world"><div class="js-branch-divergence-graph"></div></div>
`;
});
@@ -26,7 +28,13 @@ describe('Divergence graph', () => {
it('calls axos get with list of branch names', () =>
init('/-/diverging_counts').then(() => {
expect(axios.get).toHaveBeenCalledWith('/-/diverging_counts', {
- params: { names: ['master'] },
+ params: { names: ['master', 'test/hello-world'] },
});
}));
+
+ it('creates Vue components', () =>
+ init('/-/diverging_counts').then(() => {
+ expect(document.querySelector('[data-name="master"]').innerHTML).not.toEqual('');
+ expect(document.querySelector('[data-name="test/hello-world"]').innerHTML).not.toEqual('');
+ }));
});
diff --git a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
new file mode 100644
index 00000000000..a241c764df7
--- /dev/null
+++ b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
@@ -0,0 +1,101 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Confidential merge request project form group component renders empty state when response is empty 1`] = `
+<div
+ class="form-group"
+>
+ <label>
+ Project
+ </label>
+
+ <div>
+ <!---->
+
+ <p
+ class="text-muted mt-1 mb-0"
+ >
+
+ No forks available to you.
+ <br />
+
+ <span>
+ To protect this issues confidentiality,
+ <a
+ class="help-link"
+ href="https://test.com"
+ >
+ fork the project
+ </a>
+ and set the forks visiblity to private.
+ </span>
+
+ <gllink-stub
+ class="help-link"
+ href="/help"
+ target="_blank"
+ >
+ <span
+ class="sr-only"
+ >
+ Read more
+ </span>
+
+ <i
+ aria-hidden="true"
+ class="fa fa-question-circle"
+ />
+ </gllink-stub>
+ </p>
+ </div>
+</div>
+`;
+
+exports[`Confidential merge request project form group component renders fork dropdown 1`] = `
+<div
+ class="form-group"
+>
+ <label>
+ Project
+ </label>
+
+ <div>
+ <!---->
+
+ <p
+ class="text-muted mt-1 mb-0"
+ >
+
+ No forks available to you.
+ <br />
+
+ <span>
+ To protect this issues confidentiality,
+ <a
+ class="help-link"
+ href="https://test.com"
+ >
+ fork the project
+ </a>
+ and set the forks visiblity to private.
+ </span>
+
+ <gllink-stub
+ class="help-link"
+ href="/help"
+ target="_blank"
+ >
+ <span
+ class="sr-only"
+ >
+ Read more
+ </span>
+
+ <i
+ aria-hidden="true"
+ class="fa fa-question-circle"
+ />
+ </gllink-stub>
+ </p>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/confidential_merge_request/components/dropdown_spec.js b/spec/frontend/confidential_merge_request/components/dropdown_spec.js
new file mode 100644
index 00000000000..69495f3c161
--- /dev/null
+++ b/spec/frontend/confidential_merge_request/components/dropdown_spec.js
@@ -0,0 +1,56 @@
+import { mount } from '@vue/test-utils';
+import { GlDropdownItem } from '@gitlab/ui';
+import Dropdown from '~/confidential_merge_request/components/dropdown.vue';
+
+let vm;
+
+function factory(projects = []) {
+ vm = mount(Dropdown, {
+ propsData: {
+ projects,
+ selectedProject: projects[0],
+ },
+ });
+}
+
+describe('Confidential merge request project dropdown component', () => {
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ it('renders dropdown items', () => {
+ factory([
+ {
+ id: 1,
+ name: 'test',
+ },
+ {
+ id: 2,
+ name: 'test',
+ },
+ ]);
+
+ expect(vm.findAll(GlDropdownItem).length).toBe(2);
+ });
+
+ it('renders selected project icon', () => {
+ factory([
+ {
+ id: 1,
+ name: 'test',
+ },
+ {
+ id: 2,
+ name: 'test 2',
+ },
+ ]);
+
+ expect(vm.find('.js-active-project-check').classes()).not.toContain('icon');
+ expect(
+ vm
+ .findAll('.js-active-project-check')
+ .at(1)
+ .classes(),
+ ).toContain('icon');
+ });
+});
diff --git a/spec/frontend/confidential_merge_request/components/project_form_group_spec.js b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
new file mode 100644
index 00000000000..3001363f7b9
--- /dev/null
+++ b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
@@ -0,0 +1,77 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import ProjectFormGroup from '~/confidential_merge_request/components/project_form_group.vue';
+
+const localVue = createLocalVue();
+const mockData = [
+ {
+ id: 1,
+ name_with_namespace: 'root / gitlab-ce',
+ path_with_namespace: 'root/gitlab-ce',
+ namespace: {
+ full_path: 'root',
+ },
+ },
+ {
+ id: 2,
+ name_with_namespace: 'test / gitlab-ce',
+ path_with_namespace: 'test/gitlab-ce',
+ namespace: {
+ full_path: 'test',
+ },
+ },
+];
+let vm;
+let mock;
+
+function factory(projects = mockData) {
+ mock = new MockAdapter(axios);
+ mock.onGet(/api\/(.*)\/projects\/gitlab-org%2Fgitlab-ce\/forks/).reply(200, projects);
+
+ vm = shallowMount(ProjectFormGroup, {
+ localVue,
+ propsData: {
+ namespacePath: 'gitlab-org',
+ projectPath: 'gitlab-org/gitlab-ce',
+ newForkPath: 'https://test.com',
+ helpPagePath: '/help',
+ },
+ });
+}
+
+describe('Confidential merge request project form group component', () => {
+ afterEach(() => {
+ mock.restore();
+ vm.destroy();
+ });
+
+ it('renders fork dropdown', () => {
+ factory();
+
+ return localVue.nextTick(() => {
+ expect(vm.element).toMatchSnapshot();
+ });
+ });
+
+ it('sets selected project as first fork', () => {
+ factory();
+
+ return localVue.nextTick(() => {
+ expect(vm.vm.selectedProject).toEqual({
+ id: 1,
+ name: 'root / gitlab-ce',
+ pathWithNamespace: 'root/gitlab-ce',
+ namespaceFullpath: 'root',
+ });
+ });
+ });
+
+ it('renders empty state when response is empty', () => {
+ factory([]);
+
+ return localVue.nextTick(() => {
+ expect(vm.element).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/javascripts/create_merge_request_dropdown_spec.js b/spec/frontend/create_merge_request_dropdown_spec.js
index 00fe3f451f5..6e41fdabdce 100644
--- a/spec/javascripts/create_merge_request_dropdown_spec.js
+++ b/spec/frontend/create_merge_request_dropdown_spec.js
@@ -1,7 +1,7 @@
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
import CreateMergeRequestDropdown from '~/create_merge_request_dropdown';
-import { TEST_HOST } from 'spec/test_constants';
+import { TEST_HOST } from './helpers/test_constants';
describe('CreateMergeRequestDropdown', () => {
let axiosMock;
@@ -10,7 +10,7 @@ describe('CreateMergeRequestDropdown', () => {
beforeEach(() => {
axiosMock = new MockAdapter(axios);
- setFixtures(`
+ document.body.innerHTML = `
<div id="dummy-wrapper-element">
<div class="available"></div>
<div class="unavailable">
@@ -18,11 +18,12 @@ describe('CreateMergeRequestDropdown', () => {
<div class="text"></div>
</div>
<div class="js-ref"></div>
+ <div class="js-create-mr"></div>
<div class="js-create-merge-request"></div>
<div class="js-create-target"></div>
<div class="js-dropdown-toggle"></div>
</div>
- `);
+ `;
const dummyElement = document.getElementById('dummy-wrapper-element');
dropdown = new CreateMergeRequestDropdown(dummyElement);
@@ -36,7 +37,7 @@ describe('CreateMergeRequestDropdown', () => {
describe('getRef', () => {
it('escapes branch names correctly', done => {
const endpoint = `${dropdown.refsPath}contains%23hash`;
- spyOn(axios, 'get').and.callThrough();
+ jest.spyOn(axios, 'get');
axiosMock.onGet(endpoint).replyOnce({});
dropdown
diff --git a/spec/frontend/diffs/components/diff_discussion_reply_spec.js b/spec/frontend/diffs/components/diff_discussion_reply_spec.js
new file mode 100644
index 00000000000..28689ab07de
--- /dev/null
+++ b/spec/frontend/diffs/components/diff_discussion_reply_spec.js
@@ -0,0 +1,90 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import DiffDiscussionReply from '~/diffs/components/diff_discussion_reply.vue';
+import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
+import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('DiffDiscussionReply', () => {
+ let wrapper;
+ let getters;
+ let store;
+
+ const createComponent = (props = {}, slots = {}) => {
+ wrapper = shallowMount(DiffDiscussionReply, {
+ store,
+ localVue,
+ sync: false,
+ propsData: {
+ ...props,
+ },
+ slots: {
+ ...slots,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('if user can reply', () => {
+ beforeEach(() => {
+ getters = {
+ userCanReply: () => true,
+ getUserData: () => ({
+ path: 'test-path',
+ avatar_url: 'avatar_url',
+ name: 'John Doe',
+ }),
+ };
+
+ store = new Vuex.Store({
+ getters,
+ });
+ });
+
+ it('should render a form if component has form', () => {
+ createComponent(
+ {
+ renderReplyPlaceholder: false,
+ hasForm: true,
+ },
+ {
+ form: `<div id="test-form"></div>`,
+ },
+ );
+
+ expect(wrapper.find('#test-form').exists()).toBe(true);
+ });
+
+ it('should render a reply placeholder if there is no form', () => {
+ createComponent({
+ renderReplyPlaceholder: true,
+ hasForm: false,
+ });
+
+ expect(wrapper.find(ReplyPlaceholder).exists()).toBe(true);
+ });
+ });
+
+ it('renders a signed out widget when user is not logged in', () => {
+ getters = {
+ userCanReply: () => false,
+ getUserData: () => null,
+ };
+
+ store = new Vuex.Store({
+ getters,
+ });
+
+ createComponent({
+ renderReplyPlaceholder: false,
+ hasForm: false,
+ });
+
+ expect(wrapper.find(NoteSignedOutWidget).exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
new file mode 100644
index 00000000000..48ee5c63f35
--- /dev/null
+++ b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
@@ -0,0 +1,113 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
+import discussionsMockData from '../mock_data/diff_discussions';
+
+const localVue = createLocalVue();
+const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
+
+describe('DiffGutterAvatars', () => {
+ let wrapper;
+
+ const findCollapseButton = () => wrapper.find('.diff-notes-collapse');
+ const findMoreCount = () => wrapper.find('.diff-comments-more-count');
+ const findUserAvatars = () => wrapper.findAll('.diff-comment-avatar');
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(DiffGutterAvatars, {
+ localVue,
+ sync: false,
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when expanded', () => {
+ beforeEach(() => {
+ createComponent({
+ discussions: getDiscussionsMockData(),
+ discussionsExpanded: true,
+ });
+ });
+
+ it('renders a collapse button when discussions are expanded', () => {
+ expect(findCollapseButton().exists()).toBe(true);
+ });
+
+ it('should emit toggleDiscussions event on button click', () => {
+ findCollapseButton().trigger('click');
+
+ expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy();
+ });
+ });
+
+ describe('when collapsed', () => {
+ beforeEach(() => {
+ createComponent({
+ discussions: getDiscussionsMockData(),
+ discussionsExpanded: false,
+ });
+ });
+
+ it('renders user avatars and moreCount text', () => {
+ expect(findUserAvatars().exists()).toBe(true);
+ expect(findMoreCount().exists()).toBe(true);
+ });
+
+ it('renders correct amount of user avatars', () => {
+ expect(findUserAvatars().length).toBe(3);
+ });
+
+ it('renders correct moreCount number', () => {
+ expect(findMoreCount().text()).toBe('+2');
+ });
+
+ it('should emit toggleDiscussions event on avatars click', () => {
+ findUserAvatars()
+ .at(0)
+ .trigger('click');
+
+ expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy();
+ });
+
+ it('should emit toggleDiscussions event on more count text click', () => {
+ findMoreCount().trigger('click');
+
+ expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy();
+ });
+ });
+
+ it('renders an empty more count string if there are no discussions', () => {
+ createComponent({
+ discussions: [],
+ discussionsExpanded: false,
+ });
+
+ expect(findMoreCount().exists()).toBe(false);
+ });
+
+ describe('tooltip text', () => {
+ beforeEach(() => {
+ createComponent({
+ discussions: getDiscussionsMockData(),
+ discussionsExpanded: false,
+ });
+ });
+
+ it('returns original comment if it is shorter than max length', () => {
+ const note = wrapper.vm.discussions[0].notes[0];
+
+ expect(wrapper.vm.getTooltipText(note)).toEqual('Administrator: comment 1');
+ });
+
+ it('returns truncated version of comment if it is longer than max length', () => {
+ const note = wrapper.vm.discussions[0].notes[1];
+
+ expect(wrapper.vm.getTooltipText(note)).toEqual('Fatih Acet: comment 2 is r...');
+ });
+ });
+});
diff --git a/spec/frontend/diffs/mock_data/diff_discussions.js b/spec/frontend/diffs/mock_data/diff_discussions.js
new file mode 100644
index 00000000000..711ab543411
--- /dev/null
+++ b/spec/frontend/diffs/mock_data/diff_discussions.js
@@ -0,0 +1,529 @@
+export default {
+ id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ reply_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ position: {
+ old_line: null,
+ new_line: 2,
+ old_path: 'CHANGELOG',
+ new_path: 'CHANGELOG',
+ base_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a',
+ start_sha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962',
+ head_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ },
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ expanded: true,
+ notes: [
+ {
+ id: '1749',
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-04-03T21:06:21.521Z',
+ updated_at: '2018-04-08T08:50:41.762Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 1',
+ note_html: '<p dir="auto">comment 1</p>',
+ last_edited_at: '2018-04-08T08:50:41.762Z',
+ last_edited_by: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1749/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1749&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1749',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1749',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ {
+ id: '1753',
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Fatih Acet',
+ username: 'fatihacet',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/fatihacevt',
+ },
+ created_at: '2018-04-08T08:49:35.804Z',
+ updated_at: '2018-04-08T08:50:45.915Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 2 is really long one',
+ note_html: '<p dir="auto">comment 2 is really long one</p>',
+ last_edited_at: '2018-04-08T08:50:45.915Z',
+ last_edited_by: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1753/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1753&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1753',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1753',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ {
+ id: '1754',
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-04-08T08:50:48.294Z',
+ updated_at: '2018-04-08T08:50:48.294Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 3',
+ note_html: '<p dir="auto">comment 3</p>',
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1754/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1754&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1754',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1754',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ {
+ id: '1755',
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-04-08T08:50:50.911Z',
+ updated_at: '2018-04-08T08:50:50.911Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 4',
+ note_html: '<p dir="auto">comment 4</p>',
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1755/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1755&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1755',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1755',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ {
+ id: '1756',
+ type: 'DiffNote',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-04-08T08:50:53.895Z',
+ updated_at: '2018-04-08T08:50:53.895Z',
+ system: false,
+ noteable_id: 51,
+ noteable_type: 'MergeRequest',
+ noteable_iid: 20,
+ human_access: 'Owner',
+ note: 'comment 5',
+ note_html: '<p dir="auto">comment 5</p>',
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ resolved: false,
+ resolvable: true,
+ resolved_by: null,
+ discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-test/notes/1756/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1756&user_id=1',
+ path: '/gitlab-org/gitlab-test/notes/1756',
+ noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1756',
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ },
+ ],
+ individual_note: false,
+ resolvable: true,
+ resolved: false,
+ resolve_path:
+ '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve',
+ resolve_with_issue_path:
+ '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20',
+ diff_file: {
+ submodule: false,
+ submodule_link: null,
+ blob: {
+ id: '9e10516ca50788acf18c518a231914a21e5f16f7',
+ path: 'CHANGELOG',
+ name: 'CHANGELOG',
+ mode: '100644',
+ readable_text: true,
+ icon: 'file-text-o',
+ },
+ blob_path: 'CHANGELOG',
+ blob_name: 'CHANGELOG',
+ blob_icon: '<i aria-hidden="true" data-hidden="true" class="fa fa-file-text-o fa-fw"></i>',
+ file_hash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a',
+ file_path: 'CHANGELOG.rb',
+ new_file: false,
+ deleted_file: false,
+ renamed_file: false,
+ old_path: 'CHANGELOG',
+ new_path: 'CHANGELOG',
+ mode_changed: false,
+ a_mode: '100644',
+ b_mode: '100644',
+ text: true,
+ added_lines: 2,
+ removed_lines: 0,
+ diff_refs: {
+ base_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a',
+ start_sha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962',
+ head_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ },
+ content_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ stored_externally: null,
+ external_storage: null,
+ old_path_html: 'CHANGELOG_OLD',
+ new_path_html: 'CHANGELOG',
+ is_fully_expanded: true,
+ context_lines_path:
+ '/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff',
+ highlighted_diff_lines: [
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
+ type: 'new',
+ old_line: null,
+ new_line: 1,
+ text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ type: 'new',
+ old_line: null,
+ new_line: 2,
+ text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ old_line: 1,
+ new_line: 3,
+ text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ old_line: 2,
+ new_line: 4,
+ text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ old_line: 3,
+ new_line: 5,
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ ],
+ parallel_diff_lines: [
+ {
+ left: null,
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
+ type: 'new',
+ old_line: null,
+ new_line: 1,
+ text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: null,
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ type: 'new',
+ old_line: null,
+ new_line: 2,
+ text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ old_line: 1,
+ new_line: 3,
+ text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ meta_data: null,
+ },
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ old_line: 1,
+ new_line: 3,
+ text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ old_line: 2,
+ new_line: 4,
+ text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ old_line: 2,
+ new_line: 4,
+ text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ old_line: 3,
+ new_line: 5,
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
+ },
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ old_line: 3,
+ new_line: 5,
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ right: {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ },
+ ],
+ viewer: {
+ name: 'text',
+ error: null,
+ },
+ },
+ diff_discussion: true,
+ truncated_diff_lines: [
+ {
+ text: 'line',
+ rich_text:
+ '<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="1">\n1\n</td>\n<td class="line_content new"><span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n</td>\n</tr>\n<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="2">\n2\n</td>\n<td class="line_content new"><span id="LC2" class="line" lang="plaintext"></span>\n</td>\n</tr>\n',
+ can_receive_suggestion: true,
+ line_code: '6f209374f7e565f771b95720abf46024c41d1885_1_1',
+ type: 'new',
+ old_line: null,
+ new_line: 1,
+ meta_data: null,
+ },
+ ],
+};
+
+export const imageDiffDiscussions = [
+ {
+ id: '1',
+ position: {
+ x: 10,
+ y: 10,
+ width: 100,
+ height: 200,
+ },
+ },
+ {
+ id: '2',
+ position: {
+ x: 5,
+ y: 5,
+ width: 100,
+ height: 200,
+ },
+ },
+];
diff --git a/spec/javascripts/error_tracking_settings/components/app_spec.js b/spec/frontend/error_tracking_settings/components/app_spec.js
index 2e52a45fd34..022f12ef191 100644
--- a/spec/javascripts/error_tracking_settings/components/app_spec.js
+++ b/spec/frontend/error_tracking_settings/components/app_spec.js
@@ -4,7 +4,7 @@ import ErrorTrackingSettings from '~/error_tracking_settings/components/app.vue'
import ErrorTrackingForm from '~/error_tracking_settings/components/error_tracking_form.vue';
import ProjectDropdown from '~/error_tracking_settings/components/project_dropdown.vue';
import createStore from '~/error_tracking_settings/store';
-import { TEST_HOST } from 'spec/test_constants';
+import { TEST_HOST } from 'helpers/test_constants';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/javascripts/error_tracking_settings/components/error_tracking_form_spec.js b/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
index 23e57c4bbf1..23e57c4bbf1 100644
--- a/spec/javascripts/error_tracking_settings/components/error_tracking_form_spec.js
+++ b/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
diff --git a/spec/javascripts/error_tracking_settings/components/project_dropdown_spec.js b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
index 8e5dbe28452..8e5dbe28452 100644
--- a/spec/javascripts/error_tracking_settings/components/project_dropdown_spec.js
+++ b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
diff --git a/spec/javascripts/error_tracking_settings/mock.js b/spec/frontend/error_tracking_settings/mock.js
index 32cdba33c14..42233f82d54 100644
--- a/spec/javascripts/error_tracking_settings/mock.js
+++ b/spec/frontend/error_tracking_settings/mock.js
@@ -1,5 +1,5 @@
import createStore from '~/error_tracking_settings/store';
-import { TEST_HOST } from 'spec/test_constants';
+import { TEST_HOST } from '../helpers/test_constants';
const defaultStore = createStore();
diff --git a/spec/javascripts/error_tracking_settings/store/actions_spec.js b/spec/frontend/error_tracking_settings/store/actions_spec.js
index 0255b3a7aa4..1eab0f7470b 100644
--- a/spec/javascripts/error_tracking_settings/store/actions_spec.js
+++ b/spec/frontend/error_tracking_settings/store/actions_spec.js
@@ -1,13 +1,16 @@
import MockAdapter from 'axios-mock-adapter';
-import testAction from 'spec/helpers/vuex_action_helper';
-import { TEST_HOST } from 'spec/test_constants';
+import testAction from 'helpers/vuex_action_helper';
+import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import actionsDefaultExport, * as actions from '~/error_tracking_settings/store/actions';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import * as actions from '~/error_tracking_settings/store/actions';
import * as types from '~/error_tracking_settings/store/mutation_types';
import defaultState from '~/error_tracking_settings/store/state';
import { projectList } from '../mock';
+jest.mock('~/lib/utils/url_utility');
+
describe('error tracking settings actions', () => {
let state;
@@ -21,6 +24,7 @@ describe('error tracking settings actions', () => {
afterEach(() => {
mock.restore();
+ refreshCurrentPage.mockClear();
});
it('should request and transform the project list', done => {
@@ -111,7 +115,6 @@ describe('error tracking settings actions', () => {
});
it('should save the page', done => {
- const refreshCurrentPage = spyOnDependency(actionsDefaultExport, 'refreshCurrentPage');
mock.onPatch(TEST_HOST).reply(200);
testAction(actions.updateSettings, null, state, [], [{ type: 'requestSettings' }], () => {
expect(mock.history.patch.length).toBe(1);
diff --git a/spec/javascripts/error_tracking_settings/store/getters_spec.js b/spec/frontend/error_tracking_settings/store/getters_spec.js
index 2c5ff084b8a..2c5ff084b8a 100644
--- a/spec/javascripts/error_tracking_settings/store/getters_spec.js
+++ b/spec/frontend/error_tracking_settings/store/getters_spec.js
diff --git a/spec/javascripts/error_tracking_settings/store/mutation_spec.js b/spec/frontend/error_tracking_settings/store/mutation_spec.js
index bb1f1da784e..fa188462c3f 100644
--- a/spec/javascripts/error_tracking_settings/store/mutation_spec.js
+++ b/spec/frontend/error_tracking_settings/store/mutation_spec.js
@@ -1,4 +1,4 @@
-import { TEST_HOST } from 'spec/test_constants';
+import { TEST_HOST } from 'helpers/test_constants';
import mutations from '~/error_tracking_settings/store/mutations';
import defaultState from '~/error_tracking_settings/store/state';
import * as types from '~/error_tracking_settings/store/mutation_types';
diff --git a/spec/javascripts/error_tracking_settings/utils_spec.js b/spec/frontend/error_tracking_settings/utils_spec.js
index 4b144f7daf1..4b144f7daf1 100644
--- a/spec/javascripts/error_tracking_settings/utils_spec.js
+++ b/spec/frontend/error_tracking_settings/utils_spec.js
diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js
index 9e920d59093..dc886d0db3b 100644
--- a/spec/frontend/lib/utils/text_utility_spec.js
+++ b/spec/frontend/lib/utils/text_utility_spec.js
@@ -55,9 +55,24 @@ describe('text_utility', () => {
});
});
- describe('slugifyWithHyphens', () => {
+ describe('slugify', () => {
+ it('should remove accents and convert to lower case', () => {
+ expect(textUtils.slugify('João')).toEqual('jo-o');
+ });
it('should replaces whitespaces with hyphens and convert to lower case', () => {
- expect(textUtils.slugifyWithHyphens('My Input String')).toEqual('my-input-string');
+ expect(textUtils.slugify('My Input String')).toEqual('my-input-string');
+ });
+ it('should remove trailing whitespace and replace whitespaces within string with a hyphen', () => {
+ expect(textUtils.slugify(' a new project ')).toEqual('a-new-project');
+ });
+ it('should only remove non-allowed special characters', () => {
+ expect(textUtils.slugify('test!_pro-ject~')).toEqual('test-_pro-ject-');
+ });
+ it('should squash multiple hypens', () => {
+ expect(textUtils.slugify('test!!!!_pro-ject~')).toEqual('test-_pro-ject-');
+ });
+ it('should return empty string if only non-allowed characters', () => {
+ expect(textUtils.slugify('здрасти')).toEqual('');
});
});
diff --git a/spec/frontend/monitoring/__snapshots__/dashboard_state_spec.js.snap b/spec/frontend/monitoring/__snapshots__/dashboard_state_spec.js.snap
new file mode 100644
index 00000000000..5f24bab600c
--- /dev/null
+++ b/spec/frontend/monitoring/__snapshots__/dashboard_state_spec.js.snap
@@ -0,0 +1,37 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EmptyState shows gettingStarted state 1`] = `
+<glemptystate-stub
+ description="Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments."
+ primarybuttonlink="/clustersPath"
+ primarybuttontext="Install on clusters"
+ secondarybuttonlink="/settingsPath"
+ secondarybuttontext="Configure existing installation"
+ svgpath="/path/to/getting-started.svg"
+ title="Get started with performance monitoring"
+/>
+`;
+
+exports[`EmptyState shows loading state 1`] = `
+<glemptystate-stub
+ description="Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available."
+ primarybuttonlink="/documentationPath"
+ primarybuttontext="View documentation"
+ secondarybuttonlink=""
+ secondarybuttontext=""
+ svgpath="/path/to/loading.svg"
+ title="Waiting for performance data"
+/>
+`;
+
+exports[`EmptyState shows unableToConnect state 1`] = `
+<glemptystate-stub
+ description="Ensure connectivity is available from the GitLab server to the Prometheus server"
+ primarybuttonlink="/documentationPath"
+ primarybuttontext="View documentation"
+ secondarybuttonlink="/settingsPath"
+ secondarybuttontext="Configure Prometheus"
+ svgpath="/path/to/unable-to-connect.svg"
+ title="Unable to connect to Prometheus server"
+/>
+`;
diff --git a/spec/frontend/monitoring/dashboard_state_spec.js b/spec/frontend/monitoring/dashboard_state_spec.js
new file mode 100644
index 00000000000..950422911eb
--- /dev/null
+++ b/spec/frontend/monitoring/dashboard_state_spec.js
@@ -0,0 +1,43 @@
+import { shallowMount } from '@vue/test-utils';
+import EmptyState from '~/monitoring/components/empty_state.vue';
+
+function createComponent(props) {
+ return shallowMount(EmptyState, {
+ propsData: {
+ ...props,
+ settingsPath: '/settingsPath',
+ clustersPath: '/clustersPath',
+ documentationPath: '/documentationPath',
+ emptyGettingStartedSvgPath: '/path/to/getting-started.svg',
+ emptyLoadingSvgPath: '/path/to/loading.svg',
+ emptyNoDataSvgPath: '/path/to/no-data.svg',
+ emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
+ },
+ });
+}
+
+describe('EmptyState', () => {
+ it('shows gettingStarted state', () => {
+ const wrapper = createComponent({
+ selectedState: 'gettingStarted',
+ });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('shows loading state', () => {
+ const wrapper = createComponent({
+ selectedState: 'loading',
+ });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('shows unableToConnect state', () => {
+ const wrapper = createComponent({
+ selectedState: 'unableToConnect',
+ });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index 394666403ee..58d367077e8 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -87,7 +87,7 @@ describe('DiscussionNotes', () => {
discussion.notes[0],
];
discussion.notes = notesData;
- createComponent({ discussion });
+ createComponent({ discussion, shouldRenderDiffs: true });
const notes = wrapper.findAll('.notes > li');
expect(notes.at(0).is(PlaceholderSystemNote)).toBe(true);
diff --git a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
index 07a366cf339..e008f4ed093 100644
--- a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
+++ b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
@@ -2,13 +2,19 @@ import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vu
import { shallowMount, createLocalVue } from '@vue/test-utils';
const localVue = createLocalVue();
+const buttonText = 'Test Button Text';
describe('ReplyPlaceholder', () => {
let wrapper;
+ const findButton = () => wrapper.find({ ref: 'button' });
+
beforeEach(() => {
wrapper = shallowMount(ReplyPlaceholder, {
localVue,
+ propsData: {
+ buttonText,
+ },
});
});
@@ -17,9 +23,7 @@ describe('ReplyPlaceholder', () => {
});
it('emits onClick even on button click', () => {
- const button = wrapper.find({ ref: 'button' });
-
- button.trigger('click');
+ findButton().trigger('click');
expect(wrapper.emitted()).toEqual({
onClick: [[]],
@@ -27,8 +31,6 @@ describe('ReplyPlaceholder', () => {
});
it('should render reply button', () => {
- const button = wrapper.find({ ref: 'button' });
-
- expect(button.text()).toEqual('Reply...');
+ expect(findButton().text()).toEqual(buttonText);
});
});
diff --git a/spec/javascripts/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index af92e5f5ae2..aa0b544f948 100644
--- a/spec/javascripts/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -53,7 +53,7 @@ describe('Markdown field header component', () => {
});
it('emits toggle markdown event when clicking preview', () => {
- spyOn(vm, '$emit');
+ jest.spyOn(vm, '$emit').mockImplementation();
vm.$el.querySelector('.js-preview-link').click();
@@ -65,7 +65,7 @@ describe('Markdown field header component', () => {
});
it('does not emit toggle markdown event when triggered from another form', () => {
- spyOn(vm, '$emit');
+ jest.spyOn(vm, '$emit').mockImplementation();
$(document).triggerHandler('markdown-preview:show', [
$(
@@ -76,17 +76,13 @@ describe('Markdown field header component', () => {
expect(vm.$emit).not.toHaveBeenCalled();
});
- it('blurs preview link after click', done => {
+ it('blurs preview link after click', () => {
const link = vm.$el.querySelector('li:nth-child(2) button');
- spyOn(HTMLElement.prototype, 'blur');
+ jest.spyOn(HTMLElement.prototype, 'blur').mockImplementation();
link.click();
- setTimeout(() => {
- expect(link.blur).toHaveBeenCalled();
-
- done();
- });
+ expect(link.blur).toHaveBeenCalled();
});
it('renders markdown table template', () => {
diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb
index d36e428a8ee..93b86b9b812 100644
--- a/spec/graphql/gitlab_schema_spec.rb
+++ b/spec/graphql/gitlab_schema_spec.rb
@@ -21,6 +21,10 @@ describe GitlabSchema do
expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::Present::Instrumentation))
end
+ it 'enables using gitaly call checker' do
+ expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::CallsGitaly::Instrumentation))
+ end
+
it 'has the base mutation' do
expect(described_class.mutation).to eq(::Types::MutationType.to_graphql)
end
diff --git a/spec/graphql/types/base_field_spec.rb b/spec/graphql/types/base_field_spec.rb
index 0d3c3e37daf..77ef8933717 100644
--- a/spec/graphql/types/base_field_spec.rb
+++ b/spec/graphql/types/base_field_spec.rb
@@ -22,6 +22,24 @@ describe Types::BaseField do
expect(field.to_graphql.complexity).to eq 1
end
+ describe '#base_complexity' do
+ context 'with no gitaly calls' do
+ it 'defaults to 1' do
+ field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true)
+
+ expect(field.base_complexity).to eq 1
+ end
+ end
+
+ context 'with a gitaly call' do
+ it 'adds 1 to the default value' do
+ field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true)
+
+ expect(field.base_complexity).to eq 2
+ end
+ end
+ end
+
it 'has specified value' do
field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, complexity: 12)
@@ -52,5 +70,46 @@ describe Types::BaseField do
end
end
end
+
+ context 'calls_gitaly' do
+ context 'for fields with a resolver' do
+ it 'adds 1 if true' do
+ with_gitaly_field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, null: true, calls_gitaly: true)
+ without_gitaly_field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, null: true)
+ base_result = without_gitaly_field.to_graphql.complexity.call({}, {}, 2)
+
+ expect(with_gitaly_field.to_graphql.complexity.call({}, {}, 2)).to eq base_result + 1
+ end
+ end
+
+ context 'for fields without a resolver' do
+ it 'adds 1 if true' do
+ field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true)
+
+ expect(field.to_graphql.complexity).to eq 2
+ end
+ end
+
+ it 'defaults to false' do
+ field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true)
+
+ expect(field.base_complexity).to eq Types::BaseField::DEFAULT_COMPLEXITY
+ end
+
+ context 'with declared constant complexity value' do
+ it 'has complexity set to that constant' do
+ field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, complexity: 12)
+
+ expect(field.to_graphql.complexity).to eq 12
+ end
+
+ it 'does not raise an error even with Gitaly calls' do
+ allow(Gitlab::GitalyClient).to receive(:get_request_count).and_return([0, 1])
+ field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, complexity: 12)
+
+ expect(field.to_graphql.complexity).to eq 12
+ end
+ end
+ end
end
end
diff --git a/spec/graphql/types/label_type_spec.rb b/spec/graphql/types/label_type_spec.rb
index f498b32f9ed..8e7b2c69eff 100644
--- a/spec/graphql/types/label_type_spec.rb
+++ b/spec/graphql/types/label_type_spec.rb
@@ -7,4 +7,6 @@ describe GitlabSchema.types['Label'] do
is_expected.to have_graphql_fields(*expected_fields)
end
+
+ it { is_expected.to require_graphql_authorizations(:read_label) }
end
diff --git a/spec/graphql/types/metadata_type_spec.rb b/spec/graphql/types/metadata_type_spec.rb
index 55205bf5b6a..5236380e477 100644
--- a/spec/graphql/types/metadata_type_spec.rb
+++ b/spec/graphql/types/metadata_type_spec.rb
@@ -2,4 +2,5 @@ require 'spec_helper'
describe GitlabSchema.types['Metadata'] do
it { expect(described_class.graphql_name).to eq('Metadata') }
+ it { is_expected.to require_graphql_authorizations(:read_instance_metadata) }
end
diff --git a/spec/graphql/types/namespace_type_spec.rb b/spec/graphql/types/namespace_type_spec.rb
index 77fd590586e..e1153832cc9 100644
--- a/spec/graphql/types/namespace_type_spec.rb
+++ b/spec/graphql/types/namespace_type_spec.rb
@@ -13,4 +13,6 @@ describe GitlabSchema.types['Namespace'] do
is_expected.to have_graphql_fields(*expected_fields)
end
+
+ it { is_expected.to require_graphql_authorizations(:read_namespace) }
end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index af1972a2513..bc3b8a42392 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -34,9 +34,5 @@ describe GitlabSchema.types['Query'] do
is_expected.to have_graphql_type(Types::MetadataType)
is_expected.to have_graphql_resolver(Resolvers::MetadataResolver)
end
-
- it 'authorizes with read_instance_metadata' do
- is_expected.to require_graphql_authorizations(:read_instance_metadata)
- end
end
end
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index db0d45c3692..554c08add2d 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -28,7 +28,7 @@ describe PreferencesHelper do
["Your Projects' Activity", 'project_activity'],
["Starred Projects' Activity", 'starred_project_activity'],
["Your Groups", 'groups'],
- ["Your Todos", 'todos'],
+ ["Your To-Do List", 'todos'],
["Assigned Issues", 'issues'],
["Assigned Merge Requests", 'merge_requests']
]
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index 9854cf49e97..ea22ae5c4e7 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -1,4 +1,5 @@
import BoardService from '~/boards/services/board_service';
+import boardsStore from '~/boards/stores/boards_store';
export const boardObj = {
id: 1,
@@ -76,12 +77,14 @@ export const mockBoardService = (opts = {}) => {
const bulkUpdatePath = opts.bulkUpdatePath || '';
const boardId = opts.boardId || '1';
- return new BoardService({
+ boardsStore.setEndpoints({
boardsEndpoint,
listsEndpoint,
bulkUpdatePath,
boardId,
});
+
+ return new BoardService();
};
export const mockAssigneesList = [
diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js
index bb90e53e525..f75d63c8f57 100644
--- a/spec/javascripts/collapsed_sidebar_todo_spec.js
+++ b/spec/javascripts/collapsed_sidebar_todo_spec.js
@@ -58,7 +58,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
it('sets default tooltip title', () => {
expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('title'),
- ).toBe('Add todo');
+ ).toBe('Add a To Do');
});
it('toggle todo state', done => {
@@ -85,7 +85,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
setTimeout(() => {
expect(
document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
- ).toBe('Mark todo as done');
+ ).toBe('Mark as done');
done();
});
@@ -99,7 +99,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
document
.querySelector('.js-issuable-todo.sidebar-collapsed-icon')
.getAttribute('data-original-title'),
- ).toBe('Mark todo as done');
+ ).toBe('Mark as done');
done();
});
@@ -124,13 +124,13 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
expect(
document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
- ).toBe('Add todo');
+ ).toBe('Add a To Do');
})
.then(done)
.catch(done.fail);
});
- it('updates aria-label to mark todo as done', done => {
+ it('updates aria-label to Mark as done', done => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
setTimeout(() => {
@@ -138,7 +138,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
document
.querySelector('.js-issuable-todo.sidebar-collapsed-icon')
.getAttribute('aria-label'),
- ).toBe('Mark todo as done');
+ ).toBe('Mark as done');
done();
});
@@ -153,7 +153,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
document
.querySelector('.js-issuable-todo.sidebar-collapsed-icon')
.getAttribute('aria-label'),
- ).toBe('Mark todo as done');
+ ).toBe('Mark as done');
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
})
@@ -163,7 +163,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
document
.querySelector('.js-issuable-todo.sidebar-collapsed-icon')
.getAttribute('aria-label'),
- ).toBe('Add todo');
+ ).toBe('Add a To Do');
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js
index 596a1ba5ad2..d4280d3ec2c 100644
--- a/spec/javascripts/diffs/components/diff_file_header_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_header_spec.js
@@ -521,7 +521,7 @@ describe('diff_file_header', () => {
});
describe('with discussions', () => {
- it('dispatches toggleFileDiscussions when user clicks on toggle discussions button', () => {
+ it('dispatches toggleFileDiscussionWrappers when user clicks on toggle discussions button', () => {
const propsCopy = Object.assign({}, props);
propsCopy.diffFile.submodule = false;
propsCopy.diffFile.blob = {
@@ -552,11 +552,11 @@ describe('diff_file_header', () => {
}),
});
- spyOn(vm, 'toggleFileDiscussions');
+ spyOn(vm, 'toggleFileDiscussionWrappers');
vm.$el.querySelector('.js-btn-vue-toggle-comments').click();
- expect(vm.toggleFileDiscussions).toHaveBeenCalled();
+ expect(vm.toggleFileDiscussionWrappers).toHaveBeenCalled();
});
});
});
diff --git a/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js b/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js
deleted file mode 100644
index cdd30919b09..00000000000
--- a/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js
+++ /dev/null
@@ -1,146 +0,0 @@
-import Vue from 'vue';
-import DiffGutterAvatarsComponent from '~/diffs/components/diff_gutter_avatars.vue';
-import { COUNT_OF_AVATARS_IN_GUTTER } from '~/diffs/constants';
-import store from '~/mr_notes/stores';
-import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import discussionsMockData from '../mock_data/diff_discussions';
-
-describe('DiffGutterAvatars', () => {
- let component;
- const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
-
- beforeEach(() => {
- component = createComponentWithStore(Vue.extend(DiffGutterAvatarsComponent), store, {
- discussions: getDiscussionsMockData(),
- }).$mount();
- });
-
- describe('computed', () => {
- describe('discussionsExpanded', () => {
- it('should return true when all discussions are expanded', () => {
- expect(component.discussionsExpanded).toEqual(true);
- });
-
- it('should return false when all discussions are not expanded', () => {
- component.discussions[0].expanded = false;
-
- expect(component.discussionsExpanded).toEqual(false);
- });
- });
-
- describe('allDiscussions', () => {
- it('should return an array of notes', () => {
- expect(component.allDiscussions).toEqual([...component.discussions[0].notes]);
- });
- });
-
- describe('notesInGutter', () => {
- it('should return a subset of discussions to show in gutter', () => {
- expect(component.notesInGutter.length).toEqual(COUNT_OF_AVATARS_IN_GUTTER);
- expect(component.notesInGutter[0]).toEqual({
- note: component.discussions[0].notes[0].note,
- author: component.discussions[0].notes[0].author,
- });
- });
- });
-
- describe('moreCount', () => {
- it('should return count of remaining discussions from gutter', () => {
- expect(component.moreCount).toEqual(2);
- });
- });
-
- describe('moreText', () => {
- it('should return proper text if moreCount > 0', () => {
- expect(component.moreText).toEqual('2 more comments');
- });
-
- it('should return empty string if there is no discussion', () => {
- component.discussions = [];
-
- expect(component.moreText).toEqual('');
- });
- });
- });
-
- describe('methods', () => {
- describe('getTooltipText', () => {
- it('should return original comment if it is shorter than max length', () => {
- const note = component.discussions[0].notes[0];
-
- expect(component.getTooltipText(note)).toEqual('Administrator: comment 1');
- });
-
- it('should return truncated version of comment', () => {
- const note = component.discussions[0].notes[1];
-
- expect(component.getTooltipText(note)).toEqual('Fatih Acet: comment 2 is r...');
- });
- });
-
- describe('toggleDiscussions', () => {
- it('should toggle all discussions', () => {
- expect(component.discussions[0].expanded).toEqual(true);
-
- component.$store.dispatch('setInitialNotes', getDiscussionsMockData());
- component.discussions = component.$store.state.notes.discussions;
- component.toggleDiscussions();
-
- expect(component.discussions[0].expanded).toEqual(false);
- component.$store.dispatch('setInitialNotes', []);
- });
-
- it('forces expansion of all discussions', () => {
- spyOn(component.$store, 'dispatch');
-
- component.discussions[0].expanded = true;
- component.discussions.push({
- ...component.discussions[0],
- id: '123test',
- expanded: false,
- });
-
- component.toggleDiscussions();
-
- expect(component.$store.dispatch.calls.argsFor(0)).toEqual([
- 'toggleDiscussion',
- {
- discussionId: component.discussions[0].id,
- forceExpanded: true,
- },
- ]);
-
- expect(component.$store.dispatch.calls.argsFor(1)).toEqual([
- 'toggleDiscussion',
- {
- discussionId: component.discussions[1].id,
- forceExpanded: true,
- },
- ]);
- });
- });
- });
-
- describe('template', () => {
- const buttonSelector = '.js-diff-comment-button';
- const svgSelector = `${buttonSelector} svg`;
- const avatarSelector = '.js-diff-comment-avatar';
- const plusCountSelector = '.js-diff-comment-plus';
-
- it('should have button to collapse discussions when the discussions expanded', () => {
- expect(component.$el.querySelector(buttonSelector)).toBeDefined();
- expect(component.$el.querySelector(svgSelector)).toBeDefined();
- });
-
- it('should have user avatars when discussions collapsed', () => {
- component.discussions[0].expanded = false;
-
- Vue.nextTick(() => {
- expect(component.$el.querySelector(buttonSelector)).toBeNull();
- expect(component.$el.querySelectorAll(avatarSelector).length).toEqual(4);
- expect(component.$el.querySelector(plusCountSelector)).toBeDefined();
- expect(component.$el.querySelector(plusCountSelector).textContent).toEqual('+2');
- });
- });
- });
-});
diff --git a/spec/javascripts/diffs/components/inline_diff_view_spec.js b/spec/javascripts/diffs/components/inline_diff_view_spec.js
index 4452106580a..0b3890b68d6 100644
--- a/spec/javascripts/diffs/components/inline_diff_view_spec.js
+++ b/spec/javascripts/diffs/components/inline_diff_view_spec.js
@@ -36,10 +36,11 @@ describe('InlineDiffView', () => {
it('should render discussions', done => {
const el = component.$el;
component.diffLines[1].discussions = getDiscussionsMockData();
+ component.diffLines[1].discussionsExpanded = true;
Vue.nextTick(() => {
expect(el.querySelectorAll('.notes_holder').length).toEqual(1);
- expect(el.querySelectorAll('.notes_holder .note-discussion li').length).toEqual(5);
+ expect(el.querySelectorAll('.notes_holder .note-discussion li').length).toEqual(6);
expect(el.innerText.indexOf('comment 5')).toBeGreaterThan(-1);
component.$store.dispatch('setInitialNotes', []);
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index f973728cfe1..f8872a3eb13 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -206,6 +206,7 @@ describe('DiffsStoreActions', () => {
position_type: 'text',
},
},
+ hash: 'diff-content-1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a',
},
},
],
diff --git a/spec/javascripts/environments/environment_terminal_button_spec.js b/spec/javascripts/environments/environment_terminal_button_spec.js
index 56e18db59c5..fc98e656efe 100644
--- a/spec/javascripts/environments/environment_terminal_button_spec.js
+++ b/spec/javascripts/environments/environment_terminal_button_spec.js
@@ -12,36 +12,24 @@ describe('Stop Component', () => {
}).$mount();
};
- describe('enabled', () => {
- beforeEach(() => {
- mountWithProps({ terminalPath });
- });
-
- describe('computed', () => {
- it('title', () => {
- expect(component.title).toEqual('Terminal');
- });
- });
-
- it('should render a link to open a web terminal with the provided path', () => {
- expect(component.$el.tagName).toEqual('A');
- expect(component.$el.getAttribute('data-original-title')).toEqual('Terminal');
- expect(component.$el.getAttribute('aria-label')).toEqual('Terminal');
- expect(component.$el.getAttribute('href')).toEqual(terminalPath);
- });
+ beforeEach(() => {
+ mountWithProps({ terminalPath });
+ });
- it('should render a non-disabled button', () => {
- expect(component.$el.classList).not.toContain('disabled');
+ describe('computed', () => {
+ it('title', () => {
+ expect(component.title).toEqual('Terminal');
});
});
- describe('disabled', () => {
- beforeEach(() => {
- mountWithProps({ terminalPath, disabled: true });
- });
+ it('should render a link to open a web terminal with the provided path', () => {
+ expect(component.$el.tagName).toEqual('A');
+ expect(component.$el.getAttribute('data-original-title')).toEqual('Terminal');
+ expect(component.$el.getAttribute('aria-label')).toEqual('Terminal');
+ expect(component.$el.getAttribute('href')).toEqual(terminalPath);
+ });
- it('should render a disabled button', () => {
- expect(component.$el.classList).toContain('disabled');
- });
+ it('should render a non-disabled button', () => {
+ expect(component.$el.classList).not.toContain('disabled');
});
});
diff --git a/spec/javascripts/monitoring/dashboard_state_spec.js b/spec/javascripts/monitoring/dashboard_state_spec.js
deleted file mode 100644
index 6b2be83aa8c..00000000000
--- a/spec/javascripts/monitoring/dashboard_state_spec.js
+++ /dev/null
@@ -1,101 +0,0 @@
-import Vue from 'vue';
-import EmptyState from '~/monitoring/components/empty_state.vue';
-import { statePaths } from './mock_data';
-
-function createComponent(props) {
- const Component = Vue.extend(EmptyState);
-
- return new Component({
- propsData: {
- ...props,
- settingsPath: statePaths.settingsPath,
- clustersPath: statePaths.clustersPath,
- documentationPath: statePaths.documentationPath,
- emptyGettingStartedSvgPath: '/path/to/getting-started.svg',
- emptyLoadingSvgPath: '/path/to/loading.svg',
- emptyNoDataSvgPath: '/path/to/no-data.svg',
- emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
- },
- }).$mount();
-}
-
-function getTextFromNode(component, selector) {
- return component.$el.querySelector(selector).firstChild.nodeValue.trim();
-}
-
-describe('EmptyState', () => {
- describe('Computed props', () => {
- it('currentState', () => {
- const component = createComponent({
- selectedState: 'gettingStarted',
- });
-
- expect(component.currentState).toBe(component.states.gettingStarted);
- });
-
- it('showButtonDescription returns a description with a link for the unableToConnect state', () => {
- const component = createComponent({
- selectedState: 'unableToConnect',
- });
-
- expect(component.showButtonDescription).toEqual(true);
- });
-
- it('showButtonDescription returns the description without a link for any other state', () => {
- const component = createComponent({
- selectedState: 'loading',
- });
-
- expect(component.showButtonDescription).toEqual(false);
- });
- });
-
- it('should show the gettingStarted state', () => {
- const component = createComponent({
- selectedState: 'gettingStarted',
- });
-
- expect(component.$el.querySelector('svg')).toBeDefined();
- expect(getTextFromNode(component, '.state-title')).toEqual(
- component.states.gettingStarted.title,
- );
-
- expect(getTextFromNode(component, '.state-description')).toEqual(
- component.states.gettingStarted.description,
- );
-
- expect(getTextFromNode(component, '.btn-success')).toEqual(
- component.states.gettingStarted.buttonText,
- );
- });
-
- it('should show the loading state', () => {
- const component = createComponent({
- selectedState: 'loading',
- });
-
- expect(component.$el.querySelector('svg')).toBeDefined();
- expect(getTextFromNode(component, '.state-title')).toEqual(component.states.loading.title);
- expect(getTextFromNode(component, '.state-description')).toEqual(
- component.states.loading.description,
- );
-
- expect(getTextFromNode(component, '.btn-success')).toEqual(component.states.loading.buttonText);
- });
-
- it('should show the unableToConnect state', () => {
- const component = createComponent({
- selectedState: 'unableToConnect',
- });
-
- expect(component.$el.querySelector('svg')).toBeDefined();
- expect(getTextFromNode(component, '.state-title')).toEqual(
- component.states.unableToConnect.title,
- );
-
- expect(component.$el.querySelector('.state-description a')).toBeDefined();
- expect(getTextFromNode(component, '.btn-success')).toEqual(
- component.states.unableToConnect.buttonText,
- );
- });
-});
diff --git a/spec/javascripts/monitoring/store/utils_spec.js b/spec/javascripts/monitoring/store/utils_spec.js
new file mode 100644
index 00000000000..73dd370ffb3
--- /dev/null
+++ b/spec/javascripts/monitoring/store/utils_spec.js
@@ -0,0 +1,37 @@
+import { groupQueriesByChartInfo } from '~/monitoring/stores/utils';
+
+describe('groupQueriesByChartInfo', () => {
+ let input;
+ let output;
+
+ it('groups metrics with the same chart title and y_axis label', () => {
+ input = [
+ { title: 'title', y_label: 'MB', queries: [{}] },
+ { title: 'title', y_label: 'MB', queries: [{}] },
+ { title: 'new title', y_label: 'MB', queries: [{}] },
+ ];
+
+ output = [
+ { title: 'title', y_label: 'MB', queries: [{ metricId: null }, { metricId: null }] },
+ { title: 'new title', y_label: 'MB', queries: [{ metricId: null }] },
+ ];
+
+ expect(groupQueriesByChartInfo(input)).toEqual(output);
+ });
+
+ // Functionality associated with the /additional_metrics endpoint
+ it("associates a chart's stringified metric_id with the metric", () => {
+ input = [{ id: 3, title: 'new title', y_label: 'MB', queries: [{}] }];
+ output = [{ id: 3, title: 'new title', y_label: 'MB', queries: [{ metricId: '3' }] }];
+
+ expect(groupQueriesByChartInfo(input)).toEqual(output);
+ });
+
+ // Functionality associated with the /metrics_dashboard endpoint
+ it('aliases a stringified metrics_id on the metric to the metricId key', () => {
+ input = [{ title: 'new title', y_label: 'MB', queries: [{ metric_id: 3 }] }];
+ output = [{ title: 'new title', y_label: 'MB', queries: [{ metricId: '3', metric_id: 3 }] }];
+
+ expect(groupQueriesByChartInfo(input)).toEqual(output);
+ });
+});
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index efa864e7d00..74805ca8c00 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -36,14 +36,20 @@ describe('noteable_discussion component', () => {
});
it('should render user avatar', () => {
+ const discussion = { ...discussionMock };
+ discussion.diff_file = mockDiffFile;
+ discussion.diff_discussion = true;
+
+ wrapper.setProps({ discussion, renderDiffFile: true });
+
expect(wrapper.find('.user-avatar-link').exists()).toBe(true);
});
- it('should not render discussion header for non diff discussions', () => {
+ it('should not render thread header for non diff threads', () => {
expect(wrapper.find('.discussion-header').exists()).toBe(false);
});
- it('should render discussion header', done => {
+ it('should render thread header', done => {
const discussion = { ...discussionMock };
discussion.diff_file = mockDiffFile;
discussion.diff_discussion = true;
@@ -90,16 +96,16 @@ describe('noteable_discussion component', () => {
.catch(done.fail);
});
- it('does not render jump to discussion button', () => {
- expect(
- wrapper.find('*[data-original-title="Jump to next unresolved discussion"]').exists(),
- ).toBe(false);
+ it('does not render jump to thread button', () => {
+ expect(wrapper.find('*[data-original-title="Jump to next unresolved thread"]').exists()).toBe(
+ false,
+ );
});
});
describe('methods', () => {
describe('jumpToNextDiscussion', () => {
- it('expands next unresolved discussion', done => {
+ it('expands next unresolved thread', done => {
const discussion2 = getJSONFixture(discussionWithTwoUnresolvedNotes)[0];
discussion2.resolved = false;
discussion2.active = true;
@@ -114,9 +120,7 @@ describe('noteable_discussion component', () => {
const nextDiscussionId = discussion2.id;
- setFixtures(`
- <div class="discussion" data-discussion-id="${nextDiscussionId}"></div>
- `);
+ setFixtures(`<div class="discussion" data-discussion-id="${nextDiscussionId}"></div>`);
wrapper.vm.jumpToNextDiscussion();
@@ -162,20 +166,20 @@ describe('noteable_discussion component', () => {
.catch(done.fail);
});
- describe('for commit discussions', () => {
- it('should display a monospace started a discussion on commit', () => {
- expect(wrapper.text()).toContain(`started a discussion on commit ${truncatedCommitId}`);
+ describe('for commit threads', () => {
+ it('should display a monospace started a thread on commit', () => {
+ expect(wrapper.text()).toContain(`started a thread on commit ${truncatedCommitId}`);
expect(commitElement.exists()).toBe(true);
expect(commitElement.text()).toContain(truncatedCommitId);
});
});
- describe('for diff discussion with a commit id', () => {
- it('should display started discussion on commit header', done => {
+ describe('for diff thread with a commit id', () => {
+ it('should display started thread on commit header', done => {
wrapper.vm.discussion.for_commit = false;
wrapper.vm.$nextTick(() => {
- expect(wrapper.text()).toContain(`started a discussion on commit ${truncatedCommitId}`);
+ expect(wrapper.text()).toContain(`started a thread on commit ${truncatedCommitId}`);
expect(commitElement).not.toBe(null);
@@ -189,7 +193,7 @@ describe('noteable_discussion component', () => {
wrapper.vm.$nextTick(() => {
expect(wrapper.text()).toContain(
- `started a discussion on an outdated change in commit ${truncatedCommitId}`,
+ `started a thread on an outdated change in commit ${truncatedCommitId}`,
);
expect(commitElement).not.toBe(null);
@@ -199,21 +203,21 @@ describe('noteable_discussion component', () => {
});
});
- describe('for diff discussions without a commit id', () => {
- it('should show started a discussion on the diff text', done => {
+ describe('for diff threads without a commit id', () => {
+ it('should show started a thread on the diff text', done => {
Object.assign(wrapper.vm.discussion, {
for_commit: false,
commit_id: null,
});
wrapper.vm.$nextTick(() => {
- expect(wrapper.text()).toContain('started a discussion on the diff');
+ expect(wrapper.text()).toContain('started a thread on the diff');
done();
});
});
- it('should show discussion on older version text', done => {
+ it('should show thread on older version text', done => {
Object.assign(wrapper.vm.discussion, {
for_commit: false,
commit_id: null,
@@ -221,7 +225,7 @@ describe('noteable_discussion component', () => {
});
wrapper.vm.$nextTick(() => {
- expect(wrapper.text()).toContain('started a discussion on an old version of the diff');
+ expect(wrapper.text()).toContain('started a thread on an old version of the diff');
done();
});
@@ -229,7 +233,7 @@ describe('noteable_discussion component', () => {
});
});
- describe('for resolved discussion', () => {
+ describe('for resolved thread', () => {
beforeEach(() => {
const discussion = getJSONFixture(discussionWithTwoUnresolvedNotes)[0];
wrapper.setProps({ discussion });
@@ -242,7 +246,7 @@ describe('noteable_discussion component', () => {
});
});
- describe('for unresolved discussion', () => {
+ describe('for unresolved thread', () => {
beforeEach(done => {
const discussion = {
...getJSONFixture(discussionWithTwoUnresolvedNotes)[0],
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index 7a9f32ddcff..65f72a135aa 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -373,7 +373,7 @@ describe('Actions Notes Store', () => {
type: 'updateMergeRequestWidget',
},
{
- type: 'updateResolvableDiscussonsCounts',
+ type: 'updateResolvableDiscussionsCounts',
},
],
done,
@@ -400,7 +400,7 @@ describe('Actions Notes Store', () => {
type: 'updateMergeRequestWidget',
},
{
- type: 'updateResolvableDiscussonsCounts',
+ type: 'updateResolvableDiscussionsCounts',
},
{
type: 'diffs/removeDiscussionsFromDiff',
@@ -452,7 +452,7 @@ describe('Actions Notes Store', () => {
type: 'startTaskList',
},
{
- type: 'updateResolvableDiscussonsCounts',
+ type: 'updateResolvableDiscussionsCounts',
},
],
done,
@@ -527,7 +527,7 @@ describe('Actions Notes Store', () => {
],
[
{
- type: 'updateResolvableDiscussonsCounts',
+ type: 'updateResolvableDiscussionsCounts',
},
{
type: 'updateMergeRequestWidget',
@@ -552,7 +552,7 @@ describe('Actions Notes Store', () => {
],
[
{
- type: 'updateResolvableDiscussonsCounts',
+ type: 'updateResolvableDiscussionsCounts',
},
{
type: 'updateMergeRequestWidget',
@@ -587,10 +587,10 @@ describe('Actions Notes Store', () => {
});
});
- describe('updateResolvableDiscussonsCounts', () => {
+ describe('updateResolvableDiscussionsCounts', () => {
it('commits UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS', done => {
testAction(
- actions.updateResolvableDiscussonsCounts,
+ actions.updateResolvableDiscussionsCounts,
null,
{},
[{ type: 'UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS' }],
@@ -712,7 +712,7 @@ describe('Actions Notes Store', () => {
[
{ type: 'updateMergeRequestWidget' },
{ type: 'startTaskList' },
- { type: 'updateResolvableDiscussonsCounts' },
+ { type: 'updateResolvableDiscussionsCounts' },
],
done,
);
diff --git a/spec/javascripts/performance_bar/components/simple_metric_spec.js b/spec/javascripts/performance_bar/components/simple_metric_spec.js
deleted file mode 100644
index 98b843e9711..00000000000
--- a/spec/javascripts/performance_bar/components/simple_metric_spec.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import Vue from 'vue';
-import simpleMetric from '~/performance_bar/components/simple_metric.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-describe('simpleMetric', () => {
- let vm;
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('when the current request has no details', () => {
- beforeEach(() => {
- vm = mountComponent(Vue.extend(simpleMetric), {
- currentRequest: {},
- metric: 'gitaly',
- });
- });
-
- it('does not display details', () => {
- expect(vm.$el.innerText).not.toContain('/');
- });
-
- it('displays the metric name', () => {
- expect(vm.$el.innerText).toContain('gitaly');
- });
- });
-
- describe('when the current request has details', () => {
- beforeEach(() => {
- vm = mountComponent(Vue.extend(simpleMetric), {
- currentRequest: {
- details: { gitaly: { duration: '123ms', calls: '456' } },
- },
- metric: 'gitaly',
- });
- });
-
- it('diplays details', () => {
- expect(vm.$el.innerText.replace(/\s+/g, ' ')).toContain('123ms / 456');
- });
-
- it('displays the metric name', () => {
- expect(vm.$el.innerText).toContain('gitaly');
- });
- });
-});
diff --git a/spec/javascripts/releases/components/release_block_spec.js b/spec/javascripts/releases/components/release_block_spec.js
index e98c665f99d..f761a18e326 100644
--- a/spec/javascripts/releases/components/release_block_spec.js
+++ b/spec/javascripts/releases/components/release_block_spec.js
@@ -14,7 +14,7 @@ describe('Release block', () => {
description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>',
author_name: 'Release bot',
author_email: 'release-bot@example.com',
- created_at: '2012-05-28T05:00:00-07:00',
+ released_at: '2012-05-28T05:00:00-07:00',
author: {
avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png',
id: 482476,
@@ -101,7 +101,7 @@ describe('Release block', () => {
});
it('renders release date', () => {
- expect(vm.$el.textContent).toContain(timeagoMixin.methods.timeFormated(release.created_at));
+ expect(vm.$el.textContent).toContain(timeagoMixin.methods.timeFormated(release.released_at));
});
it('renders number of assets provided', () => {
@@ -152,13 +152,13 @@ describe('Release block', () => {
});
});
- describe('with pre_release flag', () => {
+ describe('with upcoming_release flag', () => {
beforeEach(() => {
- vm = factory(Object.assign({}, release, { pre_release: true }));
+ vm = factory(Object.assign({}, release, { upcoming_release: true }));
});
- it('renders pre-release badge', () => {
- expect(vm.$el.textContent).toContain('Pre-release');
+ it('renders upcoming release badge', () => {
+ expect(vm.$el.textContent).toContain('Upcoming Release');
});
});
});
diff --git a/spec/javascripts/sidebar/todo_spec.js b/spec/javascripts/sidebar/todo_spec.js
index f46ea5a0499..e7abd19c865 100644
--- a/spec/javascripts/sidebar/todo_spec.js
+++ b/spec/javascripts/sidebar/todo_spec.js
@@ -53,14 +53,14 @@ describe('SidebarTodo', () => {
describe('buttonLabel', () => {
it('returns todo button text for marking todo as done when `isTodo` prop is `true`', () => {
- expect(vm.buttonLabel).toBe('Mark todo as done');
+ expect(vm.buttonLabel).toBe('Mark as done');
});
it('returns todo button text for add todo when `isTodo` prop is `false`', done => {
vm.isTodo = false;
Vue.nextTick()
.then(() => {
- expect(vm.buttonLabel).toBe('Add todo');
+ expect(vm.buttonLabel).toBe('Add a To Do');
})
.then(done)
.catch(done.fail);
@@ -131,14 +131,14 @@ describe('SidebarTodo', () => {
});
it('check button label computed property', () => {
- expect(vm.buttonLabel).toEqual('Mark todo as done');
+ expect(vm.buttonLabel).toEqual('Mark as done');
});
it('renders button label element when `collapsed` prop is `false`', () => {
const buttonLabelEl = vm.$el.querySelector('span.issuable-todo-inner');
expect(buttonLabelEl).not.toBeNull();
- expect(buttonLabelEl.innerText.trim()).toBe('Mark todo as done');
+ expect(buttonLabelEl.innerText.trim()).toBe('Mark as done');
});
it('renders button icon when `collapsed` prop is `true`', done => {
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
index bd64d7b2926..5bd1af56bcc 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
@@ -10,7 +10,7 @@ describe('UnresolvedDiscussions', () => {
vm.$destroy();
});
- describe('with discussions path', () => {
+ describe('with threads path', () => {
beforeEach(() => {
vm = mountComponent(Component, {
mr: {
@@ -21,7 +21,7 @@ describe('UnresolvedDiscussions', () => {
it('should have correct elements', () => {
expect(vm.$el.innerText).toContain(
- 'There are unresolved discussions. Please resolve these discussions',
+ 'There are unresolved threads. Please resolve these threads',
);
expect(vm.$el.innerText).toContain('Create an issue to resolve them later');
@@ -29,14 +29,14 @@ describe('UnresolvedDiscussions', () => {
});
});
- describe('without discussions path', () => {
+ describe('without threads path', () => {
beforeEach(() => {
vm = mountComponent(Component, { mr: {} });
});
it('should not show create issue link if user cannot create issue', () => {
expect(vm.$el.innerText).toContain(
- 'There are unresolved discussions. Please resolve these discussions',
+ 'There are unresolved threads. Please resolve these threads',
);
expect(vm.$el.querySelector('.js-create-issue')).toEqual(null);
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index 8ff971114d6..a714fa50f5f 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -83,6 +83,11 @@ describe Banzai::Filter::RelativeLinkFilter do
expect { filter(act) }.not_to raise_error
end
+ it 'does not explode with an escaped null byte' do
+ act = link("/%00")
+ expect { filter(act) }.not_to raise_error
+ end
+
it 'does not raise an exception with a space in the path' do
act = link("/uploads/d18213acd3732630991986120e167e3d/Landscape_8.jpg \nBut here's some more unexpected text :smile:)")
expect { filter(act) }.not_to raise_error
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index 403e0785d1b..127463a57e8 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -144,6 +144,68 @@ describe Feature do
expect(described_class.enabled?(:enabled_feature_flag)).to be_truthy
end
+ it { expect(described_class.l1_cache_backend).to eq(Gitlab::ThreadMemoryCache.cache_backend) }
+ it { expect(described_class.l2_cache_backend).to eq(Rails.cache) }
+
+ it 'caches the status in L1 and L2 caches',
+ :request_store, :use_clean_rails_memory_store_caching do
+ described_class.enable(:enabled_feature_flag)
+ flipper_key = "flipper/v1/feature/enabled_feature_flag"
+
+ expect(described_class.l2_cache_backend)
+ .to receive(:fetch)
+ .once
+ .with(flipper_key, expires_in: 1.hour)
+ .and_call_original
+
+ expect(described_class.l1_cache_backend)
+ .to receive(:fetch)
+ .once
+ .with(flipper_key, expires_in: 1.minute)
+ .and_call_original
+
+ 2.times do
+ expect(described_class.enabled?(:enabled_feature_flag)).to be_truthy
+ end
+ end
+
+ context 'cached feature flag', :request_store do
+ let(:flag) { :some_feature_flag }
+
+ before do
+ described_class.flipper.memoize = false
+ described_class.enabled?(flag)
+ end
+
+ it 'caches the status in L1 cache for the first minute' do
+ expect do
+ expect(described_class.l1_cache_backend).to receive(:fetch).once.and_call_original
+ expect(described_class.l2_cache_backend).not_to receive(:fetch)
+ expect(described_class.enabled?(flag)).to be_truthy
+ end.not_to exceed_query_limit(0)
+ end
+
+ it 'caches the status in L2 cache after 2 minutes' do
+ Timecop.travel 2.minutes do
+ expect do
+ expect(described_class.l1_cache_backend).to receive(:fetch).once.and_call_original
+ expect(described_class.l2_cache_backend).to receive(:fetch).once.and_call_original
+ expect(described_class.enabled?(flag)).to be_truthy
+ end.not_to exceed_query_limit(0)
+ end
+ end
+
+ it 'fetches the status after an hour' do
+ Timecop.travel 61.minutes do
+ expect do
+ expect(described_class.l1_cache_backend).to receive(:fetch).once.and_call_original
+ expect(described_class.l2_cache_backend).to receive(:fetch).once.and_call_original
+ expect(described_class.enabled?(flag)).to be_truthy
+ end.not_to exceed_query_limit(1)
+ end
+ end
+ end
+
context 'with an individual actor' do
CustomActor = Struct.new(:flipper_id)
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 0f933ac5464..8f2434acd26 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -56,7 +56,7 @@ module Gitlab
},
'pre' => {
input: '```mypre"><script>alert(3)</script>',
- output: "<div>\n<div>\n<pre lang=\"mypre\">\"&gt;<code></code></pre>\n</div>\n</div>"
+ output: "<div>\n<div>\n<pre class=\"code highlight js-syntax-highlight plaintext\" lang=\"plaintext\" v-pre=\"true\"><code><span id=\"LC1\" class=\"line\" lang=\"plaintext\">\"&gt;</span></code></pre>\n</div>\n</div>"
}
}
@@ -67,6 +67,72 @@ module Gitlab
end
end
+ context 'with fenced block' do
+ it 'highlights syntax' do
+ input = <<~ADOC
+ ```js
+ console.log('hello world')
+ ```
+ ADOC
+
+ output = <<~HTML
+ <div>
+ <div>
+ <pre class="code highlight js-syntax-highlight javascript" lang="javascript" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span></span></code></pre>
+ </div>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
+ end
+
+ context 'with listing block' do
+ it 'highlights syntax' do
+ input = <<~ADOC
+ [source,c++]
+ .class.cpp
+ ----
+ #include <stdio.h>
+
+ for (int i = 0; i < 5; i++) {
+ std::cout<<"*"<<std::endl;
+ }
+ ----
+ ADOC
+
+ output = <<~HTML
+ <div>
+ <div>class.cpp</div>
+ <div>
+ <pre class="code highlight js-syntax-highlight cpp" lang="cpp" v-pre="true"><code><span id="LC1" class="line" lang="cpp"><span class="cp">#include &lt;stdio.h&gt;</span></span>
+ <span id="LC2" class="line" lang="cpp"></span>
+ <span id="LC3" class="line" lang="cpp"><span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">5</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span></span>
+ <span id="LC4" class="line" lang="cpp"> <span class="n">std</span><span class="o">::</span><span class="n">cout</span><span class="o">&lt;&lt;</span><span class="s">"*"</span><span class="o">&lt;&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span></span>
+ <span id="LC5" class="line" lang="cpp"><span class="p">}</span></span></code></pre>
+ </div>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
+ end
+
+ context 'with stem block' do
+ it 'does not apply syntax highlighting' do
+ input = <<~ADOC
+ [stem]
+ ++++
+ \sqrt{4} = 2
+ ++++
+ ADOC
+
+ output = "<div>\n<div>\n\\$ qrt{4} = 2\\$\n</div>\n</div>"
+
+ expect(render(input, context)).to include(output)
+ end
+ end
+
context 'external links' do
it 'adds the `rel` attribute to the link' do
output = render('link:https://google.com[Google]', context)
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 3b5ca7c950c..d9c73cff01e 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -309,6 +309,15 @@ describe Gitlab::Auth do
.to eq(auth_success)
end
+ it 'succeeds when custom login and token are valid' do
+ deploy_token = create(:deploy_token, username: 'deployer', read_registry: false, projects: [project])
+ auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:download_code])
+
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'deployer')
+ expect(gl_auth.find_for_git_client('deployer', deploy_token.token, project: project, ip: 'ip'))
+ .to eq(auth_success)
+ end
+
it 'fails when login is not valid' do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'random_login')
expect(gl_auth.find_for_git_client('random_login', deploy_token.token, project: project, ip: 'ip'))
diff --git a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb
deleted file mode 100644
index ea8bdd48e72..00000000000
--- a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-require 'spec_helper'
-
-# rubocop:disable RSpec/FactoriesInMigrationSpecs
-describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder, :delete do
- let(:migration) { described_class.new }
-
- before do
- allow(migration).to receive(:logger).and_return(Logger.new(nil))
- end
-
- describe '#perform' do
- it 'renames the path of system-uploads' do
- upload = create(:upload, model: create(:project), path: 'uploads/system/project/avatar.jpg')
-
- migration.perform('uploads/system/', 'uploads/-/system/')
-
- expect(upload.reload.path).to eq('uploads/-/system/project/avatar.jpg')
- end
- end
-end
-# rubocop:enable RSpec/FactoriesInMigrationSpecs
diff --git a/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb
deleted file mode 100644
index 593486fc56c..00000000000
--- a/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-require 'spec_helper'
-
-# rubocop:disable RSpec/FactoriesInMigrationSpecs
-describe Gitlab::BackgroundMigration::MovePersonalSnippetFiles do
- let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'move_snippet_files_test') }
- let(:old_uploads_dir) { File.join('uploads', 'system', 'personal_snippet') }
- let(:new_uploads_dir) { File.join('uploads', '-', 'system', 'personal_snippet') }
- let(:snippet) do
- snippet = create(:personal_snippet)
- create_upload_for_snippet(snippet)
- snippet.update!(description: markdown_linking_file(snippet))
- snippet
- end
-
- let(:migration) { described_class.new }
-
- before do
- allow(migration).to receive(:base_directory) { test_dir }
- end
-
- describe '#perform' do
- it 'moves the file on the disk' do
- expected_path = File.join(test_dir, new_uploads_dir, snippet.id.to_s, "secret#{snippet.id}", 'upload.txt')
-
- migration.perform(old_uploads_dir, new_uploads_dir)
-
- expect(File.exist?(expected_path)).to be_truthy
- end
-
- it 'updates the markdown of the snippet' do
- expected_path = File.join(new_uploads_dir, snippet.id.to_s, "secret#{snippet.id}", 'upload.txt')
- expected_markdown = "[an upload](#{expected_path})"
-
- migration.perform(old_uploads_dir, new_uploads_dir)
-
- expect(snippet.reload.description).to eq(expected_markdown)
- end
-
- it 'updates the markdown of notes' do
- expected_path = File.join(new_uploads_dir, snippet.id.to_s, "secret#{snippet.id}", 'upload.txt')
- expected_markdown = "with [an upload](#{expected_path})"
-
- note = create(:note_on_personal_snippet, noteable: snippet, note: "with #{markdown_linking_file(snippet)}")
-
- migration.perform(old_uploads_dir, new_uploads_dir)
-
- expect(note.reload.note).to eq(expected_markdown)
- end
- end
-
- def create_upload_for_snippet(snippet)
- snippet_path = path_for_file_in_snippet(snippet)
- path = File.join(old_uploads_dir, snippet.id.to_s, snippet_path)
- absolute_path = File.join(test_dir, path)
-
- FileUtils.mkdir_p(File.dirname(absolute_path))
- FileUtils.touch(absolute_path)
-
- create(:upload, model: snippet, path: snippet_path, uploader: PersonalFileUploader)
- end
-
- def path_for_file_in_snippet(snippet)
- secret = "secret#{snippet.id}"
- filename = 'upload.txt'
-
- File.join(secret, filename)
- end
-
- def markdown_linking_file(snippet)
- path = File.join(old_uploads_dir, snippet.id.to_s, path_for_file_in_snippet(snippet))
- "[an upload](#{path})"
- end
-end
-# rubocop:enable RSpec/FactoriesInMigrationSpecs
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index 7f336ee853e..4e8bff3d738 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -90,6 +90,27 @@ describe Gitlab::Ci::Config do
end
end
+ context 'when yml is too big' do
+ let(:yml) do
+ <<~YAML
+ --- &1
+ - hi
+ - *1
+ YAML
+ end
+
+ describe '.new' do
+ it 'raises error' do
+ expect(Gitlab::Sentry).to receive(:track_exception)
+
+ expect { config }.to raise_error(
+ described_class::ConfigError,
+ /The parsed YAML is too big/
+ )
+ end
+ end
+ end
+
context 'when config logic is incorrect' do
let(:yml) { 'before_script: "ls"' }
diff --git a/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb
index 71c61e0345f..36582204cc1 100644
--- a/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb
+++ b/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb
@@ -74,17 +74,11 @@ describe Gitlab::Ci::Reports::TestReportsComparer do
subject { comparer.resolved_count }
context 'when there is a resolved test case in head suites' do
- let(:create_test_case_java_resolved) do
- create_test_case_java_failed.tap do |test_case|
- test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
- end
- end
-
before do
base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
base_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
- head_reports.get_suite('junit').add_test_case(create_test_case_java_resolved)
+ head_reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
it 'returns the correct count' do
diff --git a/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb
index 6ab16e5518d..579b3e6fd24 100644
--- a/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb
+++ b/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb
@@ -10,12 +10,6 @@ describe Gitlab::Ci::Reports::TestSuiteComparer do
let(:test_case_success) { create_test_case_rspec_success }
let(:test_case_failed) { create_test_case_rspec_failed }
- let(:test_case_resolved) do
- create_test_case_rspec_failed.tap do |test_case|
- test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
- end
- end
-
describe '#new_failures' do
subject { comparer.new_failures }
@@ -44,7 +38,7 @@ describe Gitlab::Ci::Reports::TestSuiteComparer do
context 'when head sutie has a success test case which failed in base' do
before do
base_suite.add_test_case(test_case_failed)
- head_suite.add_test_case(test_case_resolved)
+ head_suite.add_test_case(test_case_success)
end
it 'does not return the failed test case' do
@@ -81,7 +75,7 @@ describe Gitlab::Ci::Reports::TestSuiteComparer do
context 'when head sutie has a success test case which failed in base' do
before do
base_suite.add_test_case(test_case_failed)
- head_suite.add_test_case(test_case_resolved)
+ head_suite.add_test_case(test_case_success)
end
it 'does not return the failed test case' do
@@ -126,11 +120,11 @@ describe Gitlab::Ci::Reports::TestSuiteComparer do
context 'when head sutie has a success test case which failed in base' do
before do
base_suite.add_test_case(test_case_failed)
- head_suite.add_test_case(test_case_resolved)
+ head_suite.add_test_case(test_case_success)
end
it 'does not return the resolved test case' do
- is_expected.to eq([test_case_resolved])
+ is_expected.to eq([test_case_success])
end
it 'returns the correct resolved count' do
@@ -156,13 +150,8 @@ describe Gitlab::Ci::Reports::TestSuiteComparer do
context 'when there are a new failure and an existing failure' do
let(:test_case_1_success) { create_test_case_rspec_success }
- let(:test_case_2_failed) { create_test_case_rspec_failed }
-
- let(:test_case_1_failed) do
- create_test_case_rspec_success.tap do |test_case|
- test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_FAILED)
- end
- end
+ let(:test_case_1_failed) { create_test_case_rspec_failed }
+ let(:test_case_2_failed) { create_test_case_rspec_failed('case2') }
before do
base_suite.add_test_case(test_case_1_success)
diff --git a/spec/lib/gitlab/config/loader/yaml_spec.rb b/spec/lib/gitlab/config/loader/yaml_spec.rb
index 44c9a3896a8..0affeb01f6a 100644
--- a/spec/lib/gitlab/config/loader/yaml_spec.rb
+++ b/spec/lib/gitlab/config/loader/yaml_spec.rb
@@ -8,7 +8,7 @@ describe Gitlab::Config::Loader::Yaml do
describe '#valid?' do
it 'returns true' do
- expect(loader.valid?).to be true
+ expect(loader).to be_valid
end
end
@@ -24,7 +24,7 @@ describe Gitlab::Config::Loader::Yaml do
describe '#valid?' do
it 'returns false' do
- expect(loader.valid?).to be false
+ expect(loader).not_to be_valid
end
end
@@ -43,7 +43,10 @@ describe Gitlab::Config::Loader::Yaml do
describe '#initialize' do
it 'raises FormatError' do
- expect { loader }.to raise_error(Gitlab::Config::Loader::FormatError, 'Unknown alias: bad_alias')
+ expect { loader }.to raise_error(
+ Gitlab::Config::Loader::FormatError,
+ 'Unknown alias: bad_alias'
+ )
end
end
end
@@ -53,7 +56,68 @@ describe Gitlab::Config::Loader::Yaml do
describe '#valid?' do
it 'returns false' do
- expect(loader.valid?).to be false
+ expect(loader).not_to be_valid
+ end
+ end
+ end
+
+ # Prevent Billion Laughs attack: https://gitlab.com/gitlab-org/gitlab-ce/issues/56018
+ context 'when yaml size is too large' do
+ let(:yml) do
+ <<~YAML
+ a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]
+ b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
+ c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
+ d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]
+ e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]
+ f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]
+ g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f]
+ h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g]
+ i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h]
+ YAML
+ end
+
+ describe '#valid?' do
+ it 'returns false' do
+ expect(loader).not_to be_valid
+ end
+
+ it 'returns true if "ci_yaml_limit_size" feature flag is disabled' do
+ stub_feature_flags(ci_yaml_limit_size: false)
+
+ expect(loader).to be_valid
+ end
+ end
+
+ describe '#load!' do
+ it 'raises FormatError' do
+ expect { loader.load! }.to raise_error(
+ Gitlab::Config::Loader::FormatError,
+ 'The parsed YAML is too big'
+ )
+ end
+ end
+ end
+
+ # Prevent Billion Laughs attack: https://gitlab.com/gitlab-org/gitlab-ce/issues/56018
+ context 'when yaml has cyclic data structure' do
+ let(:yml) do
+ <<~YAML
+ --- &1
+ - hi
+ - *1
+ YAML
+ end
+
+ describe '#valid?' do
+ it 'returns false' do
+ expect(loader.valid?).to be(false)
+ end
+ end
+
+ describe '#load!' do
+ it 'raises FormatError' do
+ expect { loader.load! }.to raise_error(Gitlab::Config::Loader::FormatError, 'The parsed YAML is too big')
end
end
end
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index c8e65a3e59d..92d90ac2fef 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -87,13 +87,13 @@ describe Gitlab::Danger::Helper do
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] }
+ expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/foo lib/gitlab/database/foo.rb qa/foo ee/changelogs/foo.yml] }
allow(fake_git).to receive(:modified_files) { [] }
allow(fake_git).to receive(:renamed_files) { [] }
expect(helper.changes_by_category).to eq(
backend: %w[foo.rb],
- database: %w[db/foo],
+ database: %w[db/foo lib/gitlab/database/foo.rb],
frontend: %w[foo.js],
none: %w[ee/changelogs/foo.yml foo.md],
qa: %w[qa/foo],
@@ -159,9 +159,22 @@ describe Gitlab::Danger::Helper do
'ee/FOO_VERSION' | :unknown
- 'db/foo' | :database
- 'qa/foo' | :qa
+ 'db/foo' | :database
+ 'ee/db/foo' | :database
+ 'app/models/project_authorization.rb' | :database
+ 'app/services/users/refresh_authorized_projects_service.rb' | :database
+ 'lib/gitlab/background_migration.rb' | :database
+ 'lib/gitlab/background_migration/foo' | :database
+ 'ee/lib/gitlab/background_migration/foo' | :database
+ 'lib/gitlab/database.rb' | :database
+ 'lib/gitlab/database/foo' | :database
+ 'ee/lib/gitlab/database/foo' | :database
+ 'lib/gitlab/github_import.rb' | :database
+ 'lib/gitlab/github_import/foo' | :database
+ 'lib/gitlab/sql/foo' | :database
+ 'rubocop/cop/migration/foo' | :database
+ 'qa/foo' | :qa
'ee/qa/foo' | :qa
'changelogs/foo' | :none
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
index aea02d21048..b755cd1aff0 100644
--- a/spec/lib/gitlab/diff/position_spec.rb
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -610,4 +610,17 @@ describe Gitlab::Diff::Position do
it_behaves_like "diff position json"
end
end
+
+ describe "#file_hash" do
+ subject do
+ described_class.new(
+ old_path: "image.jpg",
+ new_path: "image.jpg"
+ )
+ end
+
+ it "returns SHA1 representation of the file_path" do
+ expect(subject.file_hash).to eq(Digest::SHA1.hexdigest(subject.file_path))
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb b/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb
new file mode 100644
index 00000000000..900816af53a
--- /dev/null
+++ b/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb
@@ -0,0 +1,238 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Diff::PositionTracer::ImageStrategy do
+ include PositionTracerHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:current_user) { project.owner }
+ let(:file_name) { 'test-file' }
+ let(:new_file_name) { "#{file_name}-new" }
+ let(:second_file_name) { "#{file_name}-2" }
+ let(:branch_name) { 'position-tracer-test' }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, position_type: 'image') }
+
+ let(:tracer) do
+ Gitlab::Diff::PositionTracer.new(
+ project: project,
+ old_diff_refs: old_diff_refs,
+ new_diff_refs: new_diff_refs
+ )
+ end
+
+ let(:strategy) { described_class.new(tracer) }
+
+ subject { strategy.trace(old_position) }
+
+ let(:initial_commit) do
+ project.commit(create_branch(branch_name, 'master')[:branch].name)
+ end
+
+ describe '#trace' do
+ describe 'diff scenarios' do
+ let(:create_file_commit) do
+ initial_commit
+
+ create_file(
+ branch_name,
+ file_name,
+ Base64.encode64('content')
+ )
+ end
+
+ let(:update_file_commit) do
+ create_file_commit
+
+ update_file(
+ branch_name,
+ file_name,
+ Base64.encode64('updatedcontent')
+ )
+ end
+
+ let(:update_file_again_commit) do
+ update_file_commit
+
+ update_file(
+ branch_name,
+ file_name,
+ Base64.encode64('updatedcontentagain')
+ )
+ end
+
+ let(:delete_file_commit) do
+ create_file_commit
+ delete_file(branch_name, file_name)
+ end
+
+ let(:rename_file_commit) do
+ delete_file_commit
+
+ create_file(
+ branch_name,
+ new_file_name,
+ Base64.encode64('renamedcontent')
+ )
+ end
+
+ let(:create_second_file_commit) do
+ create_file_commit
+
+ create_file(
+ branch_name,
+ second_file_name,
+ Base64.encode64('morecontent')
+ )
+ end
+
+ let(:create_another_file_commit) do
+ create_file(
+ branch_name,
+ second_file_name,
+ Base64.encode64('morecontent')
+ )
+ end
+
+ let(:update_another_file_commit) do
+ update_file(
+ branch_name,
+ second_file_name,
+ Base64.encode64('updatedmorecontent')
+ )
+ end
+
+ context 'when the file was created in the old diff' do
+ context 'when the file is unchanged between the old and the new diff' do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) }
+
+ it 'returns the new position' do
+ expect_new_position(
+ old_path: file_name,
+ new_path: file_name
+ )
+ end
+ end
+
+ context 'when the file was updated between the old and the new diff' do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, update_file_commit) }
+ let(:change_diff_refs) { diff_refs(create_file_commit, update_file_commit) }
+
+ it 'returns the position of the change' do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name
+ )
+ end
+ end
+
+ context 'when the file was renamed in between the old and the new diff' do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, rename_file_commit) }
+ let(:change_diff_refs) { diff_refs(create_file_commit, rename_file_commit) }
+
+ it 'returns the position of the change' do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name
+ )
+ end
+ end
+
+ context 'when the file was removed in between the old and the new diff' do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, delete_file_commit) }
+ let(:change_diff_refs) { diff_refs(create_file_commit, delete_file_commit) }
+
+ it 'returns the position of the change' do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name
+ )
+ end
+ end
+
+ context 'when the file is unchanged in the new diff' do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(create_another_file_commit, update_another_file_commit) }
+ let(:change_diff_refs) { diff_refs(initial_commit, create_another_file_commit) }
+
+ it 'returns the position of the change' do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name
+ )
+ end
+ end
+ end
+
+ context 'when the file was changed in the old diff' do
+ context 'when the file is unchanged in between the old and the new diff' do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, create_second_file_commit) }
+
+ it 'returns the new position' do
+ expect_new_position(
+ old_path: file_name,
+ new_path: file_name
+ )
+ end
+ end
+
+ context 'when the file was updated in between the old and the new diff' do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) }
+ let(:change_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) }
+
+ it 'returns the position of the change' do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name
+ )
+ end
+ end
+
+ context 'when the file was renamed in between the old and the new diff' do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, rename_file_commit) }
+ let(:change_diff_refs) { diff_refs(update_file_commit, rename_file_commit) }
+
+ it 'returns the position of the change' do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name
+ )
+ end
+ end
+
+ context 'when the file was removed in between the old and the new diff' do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, delete_file_commit) }
+ let(:change_diff_refs) { diff_refs(update_file_commit, delete_file_commit) }
+
+ it 'returns the position of the change' do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name
+ )
+ end
+ end
+
+ context 'when the file is unchanged in the new diff' do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) }
+ let(:new_diff_refs) { diff_refs(create_another_file_commit, update_another_file_commit) }
+ let(:change_diff_refs) { diff_refs(create_file_commit, create_another_file_commit) }
+
+ it 'returns the position of the change' do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb b/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb
new file mode 100644
index 00000000000..7f4902c5b86
--- /dev/null
+++ b/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb
@@ -0,0 +1,1805 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Diff::PositionTracer::LineStrategy do
+ # Douwe's diary New York City, 2016-06-28
+ # --------------------------------------------------------------------------
+ #
+ # Dear diary,
+ #
+ # Ideally, we would have a test for every single diff scenario that can
+ # occur and that the PositionTracer should correctly trace a position
+ # through, across the following variables:
+ #
+ # - Old diff file type: created, changed, renamed, deleted, unchanged (5)
+ # - Old diff line type: added, removed, unchanged (3)
+ # - New diff file type: created, changed, renamed, deleted, unchanged (5)
+ # - New diff line type: added, removed, unchanged (3)
+ # - Old-to-new diff line change: kept, moved, undone (3)
+ #
+ # This adds up to 5 * 3 * 5 * 3 * 3 = 675 different potential scenarios,
+ # and 675 different tests to cover them all. In reality, it would be fewer,
+ # since one cannot have a removed line in a created file diff, for example,
+ # but for the sake of this diary entry, let's be pessimistic.
+ #
+ # Writing these tests is a manual and time consuming process, as every test
+ # requires the manual construction or finding of a combination of diffs that
+ # create the exact diff scenario we are looking for, and can take between
+ # 1 and 10 minutes, depending on the farfetchedness of the scenario and
+ # complexity of creating it.
+ #
+ # This means that writing tests to cover all of these scenarios would end up
+ # taking between 11 and 112 hours in total, which I do not believe is the
+ # best use of my time.
+ #
+ # A better course of action would be to think of scenarios that are likely
+ # to occur, but also potentially tricky to trace correctly, and only cover
+ # those, with a few more obvious scenarios thrown in to cover our bases.
+ #
+ # Unfortunately, I only came to the above realization once I was about
+ # 1/5th of the way through the process of writing ALL THE SPECS, having
+ # already wasted about 3 hours trying to be thorough.
+ #
+ # I did find 2 bugs while writing those though, so that's good.
+ #
+ # In any case, all of this means that the tests below will be extremely
+ # (excessively, unjustifiably) thorough for scenarios where "the file was
+ # created in the old diff" and then drop off to comparatively lackluster
+ # testing of other scenarios.
+ #
+ # I did still try to cover most of the obvious and potentially tricky
+ # scenarios, though.
+
+ include RepoHelpers
+ include PositionTracerHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:current_user) { project.owner }
+ let(:repository) { project.repository }
+ let(:file_name) { "test-file" }
+ let(:new_file_name) { "#{file_name}-new" }
+ let(:second_file_name) { "#{file_name}-2" }
+ let(:branch_name) { "position-tracer-test" }
+
+ let(:old_diff_refs) { raise NotImplementedError }
+ let(:new_diff_refs) { raise NotImplementedError }
+ let(:change_diff_refs) { raise NotImplementedError }
+ let(:old_position) { raise NotImplementedError }
+
+ let(:tracer) do
+ Gitlab::Diff::PositionTracer.new(
+ project: project,
+ old_diff_refs: old_diff_refs,
+ new_diff_refs: new_diff_refs
+ )
+ end
+
+ let(:strategy) { described_class.new(tracer) }
+
+ subject { strategy.trace(old_position) }
+
+ let(:initial_commit) do
+ project.commit(create_branch(branch_name, 'master')[:branch].name)
+ end
+
+ describe "#trace" do
+ describe "diff scenarios" do
+ let(:create_file_commit) do
+ initial_commit
+
+ create_file(
+ branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ A
+ B
+ C
+ CONTENT
+ )
+ end
+
+ let(:create_second_file_commit) do
+ create_file_commit
+
+ create_file(
+ branch_name,
+ second_file_name,
+ <<-CONTENT.strip_heredoc
+ D
+ E
+ CONTENT
+ )
+ end
+
+ let(:update_line_commit) do
+ create_second_file_commit
+
+ update_file(
+ branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ A
+ BB
+ C
+ CONTENT
+ )
+ end
+
+ let(:update_second_file_line_commit) do
+ update_line_commit
+
+ update_file(
+ branch_name,
+ second_file_name,
+ <<-CONTENT.strip_heredoc
+ D
+ EE
+ CONTENT
+ )
+ end
+
+ let(:move_line_commit) do
+ update_second_file_line_commit
+
+ update_file(
+ branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ BB
+ A
+ C
+ CONTENT
+ )
+ end
+
+ let(:add_second_file_line_commit) do
+ move_line_commit
+
+ update_file(
+ branch_name,
+ second_file_name,
+ <<-CONTENT.strip_heredoc
+ D
+ EE
+ F
+ CONTENT
+ )
+ end
+
+ let(:move_second_file_line_commit) do
+ add_second_file_line_commit
+
+ update_file(
+ branch_name,
+ second_file_name,
+ <<-CONTENT.strip_heredoc
+ D
+ F
+ EE
+ CONTENT
+ )
+ end
+
+ let(:delete_line_commit) do
+ move_second_file_line_commit
+
+ update_file(
+ branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ BB
+ A
+ CONTENT
+ )
+ end
+
+ let(:delete_second_file_line_commit) do
+ delete_line_commit
+
+ update_file(
+ branch_name,
+ second_file_name,
+ <<-CONTENT.strip_heredoc
+ D
+ F
+ CONTENT
+ )
+ end
+
+ let(:delete_file_commit) do
+ delete_second_file_line_commit
+
+ delete_file(branch_name, file_name)
+ end
+
+ let(:rename_file_commit) do
+ delete_file_commit
+
+ create_file(
+ branch_name,
+ new_file_name,
+ <<-CONTENT.strip_heredoc
+ BB
+ A
+ CONTENT
+ )
+ end
+
+ let(:update_line_again_commit) do
+ rename_file_commit
+
+ update_file(
+ branch_name,
+ new_file_name,
+ <<-CONTENT.strip_heredoc
+ BB
+ AA
+ CONTENT
+ )
+ end
+
+ let(:move_line_again_commit) do
+ update_line_again_commit
+
+ update_file(
+ branch_name,
+ new_file_name,
+ <<-CONTENT.strip_heredoc
+ AA
+ BB
+ CONTENT
+ )
+ end
+
+ let(:delete_line_again_commit) do
+ move_line_again_commit
+
+ update_file(
+ branch_name,
+ new_file_name,
+ <<-CONTENT.strip_heredoc
+ AA
+ CONTENT
+ )
+ end
+
+ context "when the file was created in the old diff" do
+ context "when the file is created in the new diff" do
+ context "when the position pointed at an added line in the old diff" do
+ context "when the file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when the file's content was changed between the old and the new diff" do
+ context "when that line was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when that line was moved between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+ #
+ # new diff:
+ # 1 + BB
+ # 2 + A
+ # 3 + C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ new_line: 1
+ )
+ end
+ end
+
+ context "when that line was changed between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:change_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: 2,
+ new_line: nil
+ )
+ end
+ end
+
+ context "when that line was deleted between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
+ let(:change_diff_refs) { diff_refs(update_line_commit, delete_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 3) }
+
+ # old diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + BB
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: 3,
+ new_line: nil
+ )
+ end
+ end
+ end
+ end
+ end
+
+ context "when the file is changed in the new diff" do
+ context "when the position pointed at an added line in the old diff" do
+ context "when the file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+ #
+ # new diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when the file's content was changed between the old and the new diff" do
+ context "when that line was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 3) }
+
+ # old diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+ #
+ # new diff:
+ # 1 - A
+ # 2 1 BB
+ # 2 + A
+ # 3 3 C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when that line was moved between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+ #
+ # new diff:
+ # 1 - A
+ # 2 1 BB
+ # 2 + A
+ # 3 3 C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ new_line: 1
+ )
+ end
+ end
+
+ context "when that line was changed between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:change_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+ #
+ # new diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: 2,
+ new_line: nil
+ )
+ end
+ end
+
+ context "when that line was deleted between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) }
+ let(:change_diff_refs) { diff_refs(move_line_commit, delete_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 3) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ # 3 + C
+ #
+ # new diff:
+ # 1 1 BB
+ # 2 2 A
+ # 3 - C
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: 3,
+ new_line: nil
+ )
+ end
+ end
+ end
+ end
+ end
+
+ context "when the file is renamed in the new diff" do
+ context "when the position pointed at an added line in the old diff" do
+ context "when the file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
+ let(:new_diff_refs) { diff_refs(delete_line_commit, rename_file_commit) }
+ let(:change_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ #
+ # new diff:
+ # file_name -> new_file_name
+ # 1 1 BB
+ # 2 2 A
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: nil,
+ new_line: 2
+ )
+ end
+ end
+
+ context "when the file's content was changed between the old and the new diff" do
+ context "when that line was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
+ let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ #
+ # new diff:
+ # file_name -> new_file_name
+ # 1 1 BB
+ # 2 - A
+ # 2 + AA
+
+ it "returns the new position" do
+ expect_new_position(
+ old_path: file_name,
+ new_path: new_file_name,
+ old_line: old_position.new_line,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when that line was moved between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
+ let(:new_diff_refs) { diff_refs(delete_line_commit, move_line_again_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ #
+ # new diff:
+ # file_name -> new_file_name
+ # 1 + AA
+ # 1 2 BB
+ # 2 - A
+
+ it "returns the new position" do
+ expect_new_position(
+ old_path: file_name,
+ new_path: new_file_name,
+ old_line: 1,
+ new_line: 2
+ )
+ end
+ end
+
+ context "when that line was changed between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
+ let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) }
+ let(:change_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ #
+ # new diff:
+ # file_name -> new_file_name
+ # 1 1 BB
+ # 2 - A
+ # 2 + AA
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: new_file_name,
+ old_line: 2,
+ new_line: nil
+ )
+ end
+ end
+ end
+ end
+ end
+
+ context "when the file is deleted in the new diff" do
+ context "when the position pointed at an added line in the old diff" do
+ context "when the file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
+ let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) }
+ let(:change_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ #
+ # new diff:
+ # 1 - BB
+ # 2 - A
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: 2,
+ new_line: nil
+ )
+ end
+ end
+
+ context "when the file's content was changed between the old and the new diff" do
+ context "when that line was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) }
+ let(:change_diff_refs) { diff_refs(move_line_commit, delete_file_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ # 3 + C
+ #
+ # new diff:
+ # 1 - BB
+ # 2 - A
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: 2,
+ new_line: nil
+ )
+ end
+ end
+
+ context "when that line was moved between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(move_line_commit, delete_file_commit) }
+ let(:change_diff_refs) { diff_refs(update_line_commit, delete_file_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+ #
+ # new diff:
+ # 1 - BB
+ # 2 - A
+ # 3 - C
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: 2,
+ new_line: nil
+ )
+ end
+ end
+
+ context "when that line was changed between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(update_line_commit, delete_file_commit) }
+ let(:change_diff_refs) { diff_refs(create_file_commit, delete_file_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+ #
+ # new diff:
+ # 1 - A
+ # 2 - BB
+ # 3 - C
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: 2,
+ new_line: nil
+ )
+ end
+ end
+
+ context "when that line was deleted between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) }
+ let(:change_diff_refs) { diff_refs(move_line_commit, delete_file_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 3) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ # 3 + C
+ #
+ # new diff:
+ # 1 - BB
+ # 2 - A
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: 3,
+ new_line: nil
+ )
+ end
+ end
+ end
+ end
+ end
+
+ context "when the file is unchanged in the new diff" do
+ context "when the position pointed at an added line in the old diff" do
+ context "when the file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, create_second_file_commit) }
+ let(:change_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+ #
+ # new diff:
+ # 1 1 A
+ # 2 2 B
+ # 3 3 C
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: nil,
+ new_line: 2
+ )
+ end
+ end
+
+ context "when the file's content was changed between the old and the new diff" do
+ context "when that line was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) }
+ let(:change_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+ #
+ # new diff:
+ # 1 1 A
+ # 2 2 BB
+ # 3 3 C
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: nil,
+ new_line: 1
+ )
+ end
+ end
+
+ context "when that line was moved between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(move_line_commit, move_second_file_line_commit) }
+ let(:change_diff_refs) { diff_refs(initial_commit, move_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+ #
+ # new diff:
+ # 1 1 BB
+ # 2 2 A
+ # 3 3 C
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: nil,
+ new_line: 1
+ )
+ end
+ end
+
+ context "when that line was changed between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) }
+ let(:change_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+ #
+ # new diff:
+ # 1 1 A
+ # 2 2 BB
+ # 3 3 C
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: 2,
+ new_line: nil
+ )
+ end
+ end
+
+ context "when that line was deleted between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(delete_line_commit, delete_second_file_line_commit) }
+ let(:change_diff_refs) { diff_refs(move_line_commit, delete_second_file_line_commit) }
+ let(:old_position) { position(new_path: file_name, new_line: 3) }
+
+ # old diff:
+ # 1 + BB
+ # 2 + A
+ # 3 + C
+ #
+ # new diff:
+ # 1 1 BB
+ # 2 2 A
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: 3,
+ new_line: nil
+ )
+ end
+ end
+ end
+ end
+ end
+ end
+
+ context "when the file was changed in the old diff" do
+ context "when the file is created in the new diff" do
+ context "when the position pointed at an added line in the old diff" do
+ context "when the file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ old_line: nil,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when the file's content was changed between the old and the new diff" do
+ context "when that line was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + BB
+ # 1 2 A
+ # 2 - B
+ # 3 3 C
+ #
+ # new diff:
+ # 1 + BB
+ # 2 + A
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ old_line: nil,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when that line was moved between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ #
+ # new diff:
+ # 1 + BB
+ # 2 + A
+ # 3 + C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ old_line: nil,
+ new_line: 1
+ )
+ end
+ end
+
+ context "when that line was changed or deleted between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, create_file_commit) }
+ let(:change_diff_refs) { diff_refs(move_line_commit, create_file_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + BB
+ # 1 2 A
+ # 2 - B
+ # 3 3 C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + B
+ # 3 + C
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: 1,
+ new_line: nil
+ )
+ end
+ end
+ end
+ end
+
+ context "when the position pointed at a deleted line in the old diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:change_diff_refs) { diff_refs(create_file_commit, initial_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: 2,
+ new_line: nil
+ )
+ end
+ end
+
+ context "when the position pointed at an unchanged line in the old diff" do
+ context "when the file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 1) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ old_line: nil,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when the file's content was changed between the old and the new diff" do
+ context "when that line was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 2) }
+
+ # old diff:
+ # 1 + BB
+ # 1 2 A
+ # 2 - B
+ # 3 3 C
+ #
+ # new diff:
+ # 1 + BB
+ # 2 + A
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ old_line: nil,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when that line was moved between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(move_line_commit, delete_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2, new_line: 2) }
+
+ # old diff:
+ # 1 1 BB
+ # 2 2 A
+ # 3 - C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + BB
+ # 3 + C
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ old_line: nil,
+ new_line: 1
+ )
+ end
+ end
+
+ context "when that line was changed or deleted between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
+ let(:change_diff_refs) { diff_refs(move_line_commit, delete_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 3, new_line: 3) }
+
+ # old diff:
+ # 1 + BB
+ # 1 2 A
+ # 2 - B
+ # 3 3 C
+ #
+ # new diff:
+ # 1 + A
+ # 2 + B
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: 3,
+ new_line: nil
+ )
+ end
+ end
+ end
+ end
+ end
+
+ context "when the file is changed in the new diff" do
+ context "when the position pointed at an added line in the old diff" do
+ context "when the file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ #
+ # new diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+
+ it "returns the new position" do
+ expect_new_position(
+ old_path: old_position.old_path,
+ new_path: old_position.new_path,
+ old_line: nil,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when the file's content was changed between the old and the new diff" do
+ context "when that line was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + BB
+ # 1 2 A
+ # 2 - B
+ # 3 3 C
+ #
+ # new diff:
+ # 1 1 BB
+ # 2 2 A
+ # 3 - C
+
+ it "returns the new position" do
+ expect_new_position(
+ old_path: old_position.old_path,
+ new_path: old_position.new_path,
+ old_line: 1,
+ new_line: 1
+ )
+ end
+ end
+
+ context "when that line was moved between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ #
+ # new diff:
+ # 1 - A
+ # 2 1 BB
+ # 2 + A
+ # 3 3 C
+
+ it "returns the new position" do
+ expect_new_position(
+ old_path: old_position.old_path,
+ new_path: old_position.new_path,
+ old_line: 2,
+ new_line: 1
+ )
+ end
+ end
+
+ context "when that line was changed or deleted between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:change_diff_refs) { diff_refs(move_line_commit, update_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) }
+
+ # old diff:
+ # 1 + BB
+ # 1 2 A
+ # 2 - B
+ # 3 3 C
+ #
+ # new diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: 1,
+ new_line: nil
+ )
+ end
+ end
+ end
+ end
+
+ context "when the position pointed at a deleted line in the old diff" do
+ context "when the file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ #
+ # new diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+
+ it "returns the new position" do
+ expect_new_position(
+ old_path: old_position.old_path,
+ new_path: old_position.new_path,
+ old_line: old_position.old_line,
+ new_line: nil
+ )
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe "typical use scenarios" do
+ let(:second_branch_name) { "#{branch_name}-2" }
+
+ def expect_new_positions(old_attrs, new_attrs)
+ old_positions = old_attrs.map do |old_attrs|
+ position(old_attrs)
+ end
+
+ new_positions = old_positions.map do |old_position|
+ strategy.trace(old_position)
+ end
+
+ aggregate_failures do
+ new_positions.zip(new_attrs).each do |new_position, new_attrs|
+ if new_attrs&.delete(:change)
+ expect_change_position(new_attrs, new_position)
+ else
+ expect_new_position(new_attrs, new_position)
+ end
+ end
+ end
+ end
+
+ let(:create_file_commit) do
+ initial_commit
+
+ create_file(
+ branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ A
+ B
+ C
+ D
+ E
+ F
+ CONTENT
+ )
+ end
+
+ let(:second_create_file_commit) do
+ create_file_commit
+
+ create_branch(second_branch_name, branch_name)
+
+ update_file(
+ second_branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ Z
+ Z
+ Z
+ A
+ B
+ C
+ D
+ E
+ F
+ CONTENT
+ )
+ end
+
+ let(:update_file_commit) do
+ second_create_file_commit
+
+ update_file(
+ branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ A
+ C
+ DD
+ E
+ F
+ G
+ CONTENT
+ )
+ end
+
+ let(:update_file_again_commit) do
+ update_file_commit
+
+ update_file(
+ branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ A
+ BB
+ C
+ D
+ E
+ FF
+ G
+ CONTENT
+ )
+ end
+
+ describe "simple push of new commit" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) }
+ let(:change_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 3 2 C
+ # 4 - D
+ # 3 + DD
+ # 5 4 E
+ # 6 5 F
+ # 6 + G
+ #
+ # new diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ # 4 4 D
+ # 5 5 E
+ # 6 - F
+ # 6 + FF
+ # 7 + G
+
+ it "returns the new positions" do
+ old_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A
+ { old_path: file_name, old_line: 2 }, # - B
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C
+ { old_path: file_name, old_line: 4 }, # - D
+ { new_path: file_name, new_line: 3 }, # + DD
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E
+ { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F
+ { new_path: file_name, new_line: 6 } # + G
+ ]
+
+ new_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 },
+ { old_path: file_name, old_line: 2 },
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 },
+ { new_path: file_name, new_line: 4, change: true },
+ { new_path: file_name, old_line: 3, change: true },
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 },
+ { new_path: file_name, old_line: 5, change: true },
+ { new_path: file_name, new_line: 7 }
+ ]
+
+ expect_new_positions(old_position_attrs, new_position_attrs)
+ end
+ end
+
+ describe "force push to overwrite last commit" do
+ let(:second_create_file_commit) do
+ create_file_commit
+
+ create_branch(second_branch_name, branch_name)
+
+ update_file(
+ second_branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ A
+ BB
+ C
+ D
+ E
+ FF
+ G
+ CONTENT
+ )
+ end
+
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, second_create_file_commit) }
+ let(:change_diff_refs) { diff_refs(update_file_commit, second_create_file_commit) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 3 2 C
+ # 4 - D
+ # 3 + DD
+ # 5 4 E
+ # 6 5 F
+ # 6 + G
+ #
+ # new diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ # 4 4 D
+ # 5 5 E
+ # 6 - F
+ # 6 + FF
+ # 7 + G
+
+ it "returns the new positions" do
+ old_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A
+ { old_path: file_name, old_line: 2 }, # - B
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C
+ { old_path: file_name, old_line: 4 }, # - D
+ { new_path: file_name, new_line: 3 }, # + DD
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E
+ { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F
+ { new_path: file_name, new_line: 6 } # + G
+ ]
+
+ new_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 },
+ { old_path: file_name, old_line: 2 },
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 },
+ { new_path: file_name, new_line: 4, change: true },
+ { old_path: file_name, old_line: 3, change: true },
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 },
+ { old_path: file_name, old_line: 5, change: true },
+ { new_path: file_name, new_line: 7 }
+ ]
+
+ expect_new_positions(old_position_attrs, new_position_attrs)
+ end
+ end
+
+ describe "force push to delete last commit" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, update_file_commit) }
+ let(:change_diff_refs) { diff_refs(update_file_again_commit, update_file_commit) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ # 4 4 D
+ # 5 5 E
+ # 6 - F
+ # 6 + FF
+ # 7 + G
+ #
+ # new diff:
+ # 1 1 A
+ # 2 - B
+ # 3 2 C
+ # 4 - D
+ # 3 + DD
+ # 5 4 E
+ # 6 5 F
+ # 6 + G
+
+ it "returns the new positions" do
+ old_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A
+ { old_path: file_name, old_line: 2 }, # - B
+ { new_path: file_name, new_line: 2 }, # + BB
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C
+ { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E
+ { old_path: file_name, old_line: 6 }, # - F
+ { new_path: file_name, new_line: 6 }, # + FF
+ { new_path: file_name, new_line: 7 } # + G
+ ]
+
+ new_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 },
+ { old_path: file_name, old_line: 2 },
+ { old_path: file_name, old_line: 2, change: true },
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 },
+ { old_path: file_name, old_line: 4, change: true },
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 },
+ { new_path: file_name, new_line: 5, change: true },
+ { old_path: file_name, old_line: 6, change: true },
+ { new_path: file_name, new_line: 6 }
+ ]
+
+ expect_new_positions(old_position_attrs, new_position_attrs)
+ end
+ end
+
+ describe "rebase on top of target branch" do
+ let(:second_update_file_commit) do
+ update_file_commit
+
+ update_file(
+ second_branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ Z
+ Z
+ Z
+ A
+ C
+ DD
+ E
+ F
+ G
+ CONTENT
+ )
+ end
+
+ let(:update_file_again_commit) do
+ second_update_file_commit
+
+ update_file(
+ branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ A
+ BB
+ C
+ D
+ E
+ FF
+ G
+ CONTENT
+ )
+ end
+
+ let(:overwrite_update_file_again_commit) do
+ update_file_again_commit
+
+ update_file(
+ second_branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ Z
+ Z
+ Z
+ A
+ BB
+ C
+ D
+ E
+ FF
+ G
+ CONTENT
+ )
+ end
+
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, overwrite_update_file_again_commit) }
+ let(:change_diff_refs) { diff_refs(update_file_again_commit, overwrite_update_file_again_commit) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ # 4 4 D
+ # 5 5 E
+ # 6 - F
+ # 6 + FF
+ # 7 + G
+ #
+ # new diff:
+ # 1 + Z
+ # 2 + Z
+ # 3 + Z
+ # 1 4 A
+ # 2 - B
+ # 5 + BB
+ # 3 6 C
+ # 4 7 D
+ # 5 8 E
+ # 6 - F
+ # 9 + FF
+ # 0 + G
+
+ it "returns the new positions" do
+ old_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A
+ { old_path: file_name, old_line: 2 }, # - B
+ { new_path: file_name, new_line: 2 }, # + BB
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C
+ { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E
+ { old_path: file_name, old_line: 6 }, # - F
+ { new_path: file_name, new_line: 6 }, # + FF
+ { new_path: file_name, new_line: 7 } # + G
+ ]
+
+ new_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A
+ { old_path: file_name, old_line: 2 }, # - B
+ { new_path: file_name, new_line: 5 }, # + BB
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C
+ { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E
+ { old_path: file_name, old_line: 6 }, # - F
+ { new_path: file_name, new_line: 9 }, # + FF
+ { new_path: file_name, new_line: 10 } # + G
+ ]
+
+ expect_new_positions(old_position_attrs, new_position_attrs)
+ end
+ end
+
+ describe "merge of target branch" do
+ let(:merge_commit) do
+ second_create_file_commit
+
+ merge_request = create(:merge_request, source_branch: second_branch_name, target_branch: branch_name, source_project: project)
+
+ repository.merge(current_user, merge_request.diff_head_sha, merge_request, "Merge branches")
+
+ project.commit(branch_name)
+ end
+
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) }
+ let(:new_diff_refs) { diff_refs(create_file_commit, merge_commit) }
+ let(:change_diff_refs) { diff_refs(update_file_again_commit, merge_commit) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ # 4 4 D
+ # 5 5 E
+ # 6 - F
+ # 6 + FF
+ # 7 + G
+ #
+ # new diff:
+ # 1 + Z
+ # 2 + Z
+ # 3 + Z
+ # 1 4 A
+ # 2 - B
+ # 5 + BB
+ # 3 6 C
+ # 4 7 D
+ # 5 8 E
+ # 6 - F
+ # 9 + FF
+ # 0 + G
+
+ it "returns the new positions" do
+ old_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A
+ { old_path: file_name, old_line: 2 }, # - B
+ { new_path: file_name, new_line: 2 }, # + BB
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C
+ { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E
+ { old_path: file_name, old_line: 6 }, # - F
+ { new_path: file_name, new_line: 6 }, # + FF
+ { new_path: file_name, new_line: 7 } # + G
+ ]
+
+ new_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A
+ { old_path: file_name, old_line: 2 }, # - B
+ { new_path: file_name, new_line: 5 }, # + BB
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C
+ { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E
+ { old_path: file_name, old_line: 6 }, # - F
+ { new_path: file_name, new_line: 9 }, # + FF
+ { new_path: file_name, new_line: 10 } # + G
+ ]
+
+ expect_new_positions(old_position_attrs, new_position_attrs)
+ end
+ end
+
+ describe "changing target branch" do
+ let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) }
+ let(:new_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) }
+ let(:change_diff_refs) { diff_refs(create_file_commit, update_file_commit) }
+
+ # old diff:
+ # 1 1 A
+ # 2 - B
+ # 2 + BB
+ # 3 3 C
+ # 4 4 D
+ # 5 5 E
+ # 6 - F
+ # 6 + FF
+ # 7 + G
+ #
+ # new diff:
+ # 1 1 A
+ # 2 + BB
+ # 2 3 C
+ # 3 - DD
+ # 4 + D
+ # 4 5 E
+ # 5 - F
+ # 6 + FF
+ # 7 G
+
+ it "returns the new positions" do
+ old_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A
+ { old_path: file_name, old_line: 2 }, # - B
+ { new_path: file_name, new_line: 2 }, # + BB
+ { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C
+ { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D
+ { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E
+ { old_path: file_name, old_line: 6 }, # - F
+ { new_path: file_name, new_line: 6 }, # + FF
+ { new_path: file_name, new_line: 7 } # + G
+ ]
+
+ new_position_attrs = [
+ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 },
+ { old_path: file_name, old_line: 2, change: true },
+ { new_path: file_name, new_line: 2 },
+ { old_path: file_name, new_path: file_name, old_line: 2, new_line: 3 },
+ { new_path: file_name, new_line: 4 },
+ { old_path: file_name, new_path: file_name, old_line: 4, new_line: 5 },
+ { old_path: file_name, old_line: 5 },
+ { new_path: file_name, new_line: 6 },
+ { new_path: file_name, new_line: 7 }
+ ]
+
+ expect_new_positions(old_position_attrs, new_position_attrs)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
index 866550753a8..79b33d4d276 100644
--- a/spec/lib/gitlab/diff/position_tracer_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer_spec.rb
@@ -1,1896 +1,98 @@
require 'spec_helper'
describe Gitlab::Diff::PositionTracer do
- # Douwe's diary New York City, 2016-06-28
- # --------------------------------------------------------------------------
- #
- # Dear diary,
- #
- # Ideally, we would have a test for every single diff scenario that can
- # occur and that the PositionTracer should correctly trace a position
- # through, across the following variables:
- #
- # - Old diff file type: created, changed, renamed, deleted, unchanged (5)
- # - Old diff line type: added, removed, unchanged (3)
- # - New diff file type: created, changed, renamed, deleted, unchanged (5)
- # - New diff line type: added, removed, unchanged (3)
- # - Old-to-new diff line change: kept, moved, undone (3)
- #
- # This adds up to 5 * 3 * 5 * 3 * 3 = 675 different potential scenarios,
- # and 675 different tests to cover them all. In reality, it would be fewer,
- # since one cannot have a removed line in a created file diff, for example,
- # but for the sake of this diary entry, let's be pessimistic.
- #
- # Writing these tests is a manual and time consuming process, as every test
- # requires the manual construction or finding of a combination of diffs that
- # create the exact diff scenario we are looking for, and can take between
- # 1 and 10 minutes, depending on the farfetchedness of the scenario and
- # complexity of creating it.
- #
- # This means that writing tests to cover all of these scenarios would end up
- # taking between 11 and 112 hours in total, which I do not believe is the
- # best use of my time.
- #
- # A better course of action would be to think of scenarios that are likely
- # to occur, but also potentially tricky to trace correctly, and only cover
- # those, with a few more obvious scenarios thrown in to cover our bases.
- #
- # Unfortunately, I only came to the above realization once I was about
- # 1/5th of the way through the process of writing ALL THE SPECS, having
- # already wasted about 3 hours trying to be thorough.
- #
- # I did find 2 bugs while writing those though, so that's good.
- #
- # In any case, all of this means that the tests below will be extremely
- # (excessively, unjustifiably) thorough for scenarios where "the file was
- # created in the old diff" and then drop off to comparatively lackluster
- # testing of other scenarios.
- #
- # I did still try to cover most of the obvious and potentially tricky
- # scenarios, though.
+ include PositionTracerHelpers
- include RepoHelpers
-
- let(:project) { create(:project, :repository) }
- let(:current_user) { project.owner }
- let(:repository) { project.repository }
- let(:file_name) { "test-file" }
- let(:new_file_name) { "#{file_name}-new" }
- let(:second_file_name) { "#{file_name}-2" }
- let(:branch_name) { "position-tracer-test" }
-
- let(:old_diff_refs) { raise NotImplementedError }
- let(:new_diff_refs) { raise NotImplementedError }
- let(:change_diff_refs) { raise NotImplementedError }
- let(:old_position) { raise NotImplementedError }
-
- let(:position_tracer) { described_class.new(project: project, old_diff_refs: old_diff_refs, new_diff_refs: new_diff_refs) }
- subject { position_tracer.trace(old_position) }
-
- def diff_refs(base_commit, head_commit)
- Gitlab::Diff::DiffRefs.new(base_sha: base_commit.id, head_sha: head_commit.id)
- end
-
- def text_position_attrs
- [:old_line, :new_line]
- end
-
- def position(attrs = {})
- attrs.reverse_merge!(
- diff_refs: old_diff_refs
+ subject do
+ described_class.new(
+ project: project,
+ old_diff_refs: old_diff_refs,
+ new_diff_refs: new_diff_refs
)
- Gitlab::Diff::Position.new(attrs)
end
- def expect_new_position(attrs, result = subject)
- aggregate_failures("expect new position #{attrs.inspect}") do
- if attrs.nil?
- expect(result[:outdated]).to be_truthy
- else
- expect(result[:outdated]).to be_falsey
+ describe '#trace' do
+ let(:diff_refs) { double(complete?: true) }
+ let(:project) { double }
+ let(:old_diff_refs) { diff_refs }
+ let(:new_diff_refs) { diff_refs }
+ let(:position) { double(on_text?: on_text?, diff_refs: diff_refs) }
+ let(:tracer) { double }
- new_position = result[:position]
- expect(new_position).not_to be_nil
+ context 'position is on text' do
+ let(:on_text?) { true }
- expect(new_position.diff_refs).to eq(new_diff_refs)
+ it 'calls LineStrategy#trace' do
+ expect(Gitlab::Diff::PositionTracer::LineStrategy)
+ .to receive(:new)
+ .with(subject)
+ .and_return(tracer)
+ expect(tracer).to receive(:trace).with(position)
- attrs.each do |attr, value|
- if text_position_attrs.include?(attr)
- expect(new_position.formatter.send(attr)).to eq(value)
- else
- expect(new_position.send(attr)).to eq(value)
- end
- end
+ subject.trace(position)
end
end
- end
-
- def expect_change_position(attrs, result = subject)
- aggregate_failures("expect change position #{attrs.inspect}") do
- expect(result[:outdated]).to be_truthy
-
- change_position = result[:position]
- if attrs.nil? || attrs.empty?
- expect(change_position).to be_nil
- else
- expect(change_position).not_to be_nil
-
- expect(change_position.diff_refs).to eq(change_diff_refs)
-
- attrs.each do |attr, value|
- if text_position_attrs.include?(attr)
- expect(change_position.formatter.send(attr)).to eq(value)
- else
- expect(change_position.send(attr)).to eq(value)
- end
- end
- end
- end
- end
-
- def create_branch(new_name, branch_name)
- CreateBranchService.new(project, current_user).execute(new_name, branch_name)
- end
-
- def create_file(branch_name, file_name, content)
- Files::CreateService.new(
- project,
- current_user,
- start_branch: branch_name,
- branch_name: branch_name,
- commit_message: "Create file",
- file_path: file_name,
- file_content: content
- ).execute
- project.commit(branch_name)
- end
-
- def update_file(branch_name, file_name, content)
- Files::UpdateService.new(
- project,
- current_user,
- start_branch: branch_name,
- branch_name: branch_name,
- commit_message: "Update file",
- file_path: file_name,
- file_content: content
- ).execute
- project.commit(branch_name)
- end
-
- def delete_file(branch_name, file_name)
- Files::DeleteService.new(
- project,
- current_user,
- start_branch: branch_name,
- branch_name: branch_name,
- commit_message: "Delete file",
- file_path: file_name
- ).execute
- project.commit(branch_name)
- end
-
- let(:initial_commit) do
- create_branch(branch_name, "master")[:branch].name
- project.commit(branch_name)
- end
-
- describe "#trace" do
- describe "diff scenarios" do
- let(:create_file_commit) do
- initial_commit
-
- create_file(
- branch_name,
- file_name,
- <<-CONTENT.strip_heredoc
- A
- B
- C
- CONTENT
- )
- end
-
- let(:create_second_file_commit) do
- create_file_commit
-
- create_file(
- branch_name,
- second_file_name,
- <<-CONTENT.strip_heredoc
- D
- E
- CONTENT
- )
- end
-
- let(:update_line_commit) do
- create_second_file_commit
-
- update_file(
- branch_name,
- file_name,
- <<-CONTENT.strip_heredoc
- A
- BB
- C
- CONTENT
- )
- end
-
- let(:update_second_file_line_commit) do
- update_line_commit
-
- update_file(
- branch_name,
- second_file_name,
- <<-CONTENT.strip_heredoc
- D
- EE
- CONTENT
- )
- end
-
- let(:move_line_commit) do
- update_second_file_line_commit
-
- update_file(
- branch_name,
- file_name,
- <<-CONTENT.strip_heredoc
- BB
- A
- C
- CONTENT
- )
- end
-
- let(:add_second_file_line_commit) do
- move_line_commit
-
- update_file(
- branch_name,
- second_file_name,
- <<-CONTENT.strip_heredoc
- D
- EE
- F
- CONTENT
- )
- end
-
- let(:move_second_file_line_commit) do
- add_second_file_line_commit
-
- update_file(
- branch_name,
- second_file_name,
- <<-CONTENT.strip_heredoc
- D
- F
- EE
- CONTENT
- )
- end
-
- let(:delete_line_commit) do
- move_second_file_line_commit
-
- update_file(
- branch_name,
- file_name,
- <<-CONTENT.strip_heredoc
- BB
- A
- CONTENT
- )
- end
-
- let(:delete_second_file_line_commit) do
- delete_line_commit
-
- update_file(
- branch_name,
- second_file_name,
- <<-CONTENT.strip_heredoc
- D
- F
- CONTENT
- )
- end
-
- let(:delete_file_commit) do
- delete_second_file_line_commit
-
- delete_file(branch_name, file_name)
- end
-
- let(:rename_file_commit) do
- delete_file_commit
-
- create_file(
- branch_name,
- new_file_name,
- <<-CONTENT.strip_heredoc
- BB
- A
- CONTENT
- )
- end
-
- let(:update_line_again_commit) do
- rename_file_commit
-
- update_file(
- branch_name,
- new_file_name,
- <<-CONTENT.strip_heredoc
- BB
- AA
- CONTENT
- )
- end
-
- let(:move_line_again_commit) do
- update_line_again_commit
-
- update_file(
- branch_name,
- new_file_name,
- <<-CONTENT.strip_heredoc
- AA
- BB
- CONTENT
- )
- end
-
- let(:delete_line_again_commit) do
- move_line_again_commit
-
- update_file(
- branch_name,
- new_file_name,
- <<-CONTENT.strip_heredoc
- AA
- CONTENT
- )
- end
-
- context "when the file was created in the old diff" do
- context "when the file is created in the new diff" do
- context "when the position pointed at an added line in the old diff" do
- context "when the file's content was unchanged between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
- let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 2) }
-
- # old diff:
- # 1 + A
- # 2 + B
- # 3 + C
- #
- # new diff:
- # 1 + A
- # 2 + B
- # 3 + C
-
- it "returns the new position" do
- expect_new_position(
- new_path: old_position.new_path,
- new_line: old_position.new_line
- )
- end
- end
-
- context "when the file's content was changed between the old and the new diff" do
- context "when that line was unchanged between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
- let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 1) }
-
- # old diff:
- # 1 + A
- # 2 + B
- # 3 + C
- #
- # new diff:
- # 1 + A
- # 2 + BB
- # 3 + C
-
- it "returns the new position" do
- expect_new_position(
- new_path: old_position.new_path,
- new_line: old_position.new_line
- )
- end
- end
-
- context "when that line was moved between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
- let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 2) }
-
- # old diff:
- # 1 + A
- # 2 + BB
- # 3 + C
- #
- # new diff:
- # 1 + BB
- # 2 + A
- # 3 + C
-
- it "returns the new position" do
- expect_new_position(
- new_path: old_position.new_path,
- new_line: 1
- )
- end
- end
-
- context "when that line was changed between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
- let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
- let(:change_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 2) }
-
- # old diff:
- # 1 + A
- # 2 + B
- # 3 + C
- #
- # new diff:
- # 1 + A
- # 2 + BB
- # 3 + C
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: 2,
- new_line: nil
- )
- end
- end
-
- context "when that line was deleted between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
- let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
- let(:change_diff_refs) { diff_refs(update_line_commit, delete_line_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 3) }
-
- # old diff:
- # 1 + A
- # 2 + BB
- # 3 + C
- #
- # new diff:
- # 1 + A
- # 2 + BB
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: 3,
- new_line: nil
- )
- end
- end
- end
- end
- end
-
- context "when the file is changed in the new diff" do
- context "when the position pointed at an added line in the old diff" do
- context "when the file's content was unchanged between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
- let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 1) }
-
- # old diff:
- # 1 + A
- # 2 + BB
- # 3 + C
- #
- # new diff:
- # 1 1 A
- # 2 - B
- # 2 + BB
- # 3 3 C
-
- it "returns the new position" do
- expect_new_position(
- new_path: old_position.new_path,
- new_line: old_position.new_line
- )
- end
- end
-
- context "when the file's content was changed between the old and the new diff" do
- context "when that line was unchanged between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
- let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 3) }
-
- # old diff:
- # 1 + A
- # 2 + BB
- # 3 + C
- #
- # new diff:
- # 1 - A
- # 2 1 BB
- # 2 + A
- # 3 3 C
-
- it "returns the new position" do
- expect_new_position(
- new_path: old_position.new_path,
- new_line: old_position.new_line
- )
- end
- end
-
- context "when that line was moved between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
- let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 2) }
-
- # old diff:
- # 1 + A
- # 2 + BB
- # 3 + C
- #
- # new diff:
- # 1 - A
- # 2 1 BB
- # 2 + A
- # 3 3 C
-
- it "returns the new position" do
- expect_new_position(
- new_path: old_position.new_path,
- new_line: 1
- )
- end
- end
-
- context "when that line was changed between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
- let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
- let(:change_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 2) }
-
- # old diff:
- # 1 + A
- # 2 + B
- # 3 + C
- #
- # new diff:
- # 1 1 A
- # 2 - B
- # 2 + BB
- # 3 3 C
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: 2,
- new_line: nil
- )
- end
- end
-
- context "when that line was deleted between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) }
- let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) }
- let(:change_diff_refs) { diff_refs(move_line_commit, delete_line_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 3) }
-
- # old diff:
- # 1 + BB
- # 2 + A
- # 3 + C
- #
- # new diff:
- # 1 1 BB
- # 2 2 A
- # 3 - C
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: 3,
- new_line: nil
- )
- end
- end
- end
- end
- end
-
- context "when the file is renamed in the new diff" do
- context "when the position pointed at an added line in the old diff" do
- context "when the file's content was unchanged between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
- let(:new_diff_refs) { diff_refs(delete_line_commit, rename_file_commit) }
- let(:change_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 2) }
-
- # old diff:
- # 1 + BB
- # 2 + A
- #
- # new diff:
- # file_name -> new_file_name
- # 1 1 BB
- # 2 2 A
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: nil,
- new_line: 2
- )
- end
- end
-
- context "when the file's content was changed between the old and the new diff" do
- context "when that line was unchanged between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
- let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 1) }
-
- # old diff:
- # 1 + BB
- # 2 + A
- #
- # new diff:
- # file_name -> new_file_name
- # 1 1 BB
- # 2 - A
- # 2 + AA
-
- it "returns the new position" do
- expect_new_position(
- old_path: file_name,
- new_path: new_file_name,
- old_line: old_position.new_line,
- new_line: old_position.new_line
- )
- end
- end
-
- context "when that line was moved between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
- let(:new_diff_refs) { diff_refs(delete_line_commit, move_line_again_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 1) }
-
- # old diff:
- # 1 + BB
- # 2 + A
- #
- # new diff:
- # file_name -> new_file_name
- # 1 + AA
- # 1 2 BB
- # 2 - A
-
- it "returns the new position" do
- expect_new_position(
- old_path: file_name,
- new_path: new_file_name,
- old_line: 1,
- new_line: 2
- )
- end
- end
-
- context "when that line was changed between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
- let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) }
- let(:change_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 2) }
-
- # old diff:
- # 1 + BB
- # 2 + A
- #
- # new diff:
- # file_name -> new_file_name
- # 1 1 BB
- # 2 - A
- # 2 + AA
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: new_file_name,
- old_line: 2,
- new_line: nil
- )
- end
- end
- end
- end
- end
-
- context "when the file is deleted in the new diff" do
- context "when the position pointed at an added line in the old diff" do
- context "when the file's content was unchanged between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
- let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) }
- let(:change_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 2) }
-
- # old diff:
- # 1 + BB
- # 2 + A
- #
- # new diff:
- # 1 - BB
- # 2 - A
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: 2,
- new_line: nil
- )
- end
- end
-
- context "when the file's content was changed between the old and the new diff" do
- context "when that line was unchanged between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) }
- let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) }
- let(:change_diff_refs) { diff_refs(move_line_commit, delete_file_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 2) }
-
- # old diff:
- # 1 + BB
- # 2 + A
- # 3 + C
- #
- # new diff:
- # 1 - BB
- # 2 - A
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: 2,
- new_line: nil
- )
- end
- end
-
- context "when that line was moved between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
- let(:new_diff_refs) { diff_refs(move_line_commit, delete_file_commit) }
- let(:change_diff_refs) { diff_refs(update_line_commit, delete_file_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 2) }
-
- # old diff:
- # 1 + A
- # 2 + BB
- # 3 + C
- #
- # new diff:
- # 1 - BB
- # 2 - A
- # 3 - C
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: 2,
- new_line: nil
- )
- end
- end
-
- context "when that line was changed between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
- let(:new_diff_refs) { diff_refs(update_line_commit, delete_file_commit) }
- let(:change_diff_refs) { diff_refs(create_file_commit, delete_file_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 2) }
-
- # old diff:
- # 1 + A
- # 2 + B
- # 3 + C
- #
- # new diff:
- # 1 - A
- # 2 - BB
- # 3 - C
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: 2,
- new_line: nil
- )
- end
- end
-
- context "when that line was deleted between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) }
- let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) }
- let(:change_diff_refs) { diff_refs(move_line_commit, delete_file_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 3) }
- # old diff:
- # 1 + BB
- # 2 + A
- # 3 + C
- #
- # new diff:
- # 1 - BB
- # 2 - A
+ context 'position is not on text' do
+ let(:on_text?) { false }
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: 3,
- new_line: nil
- )
- end
- end
- end
- end
- end
+ it 'calls ImageStrategy#trace' do
+ expect(Gitlab::Diff::PositionTracer::ImageStrategy)
+ .to receive(:new)
+ .with(subject)
+ .and_return(tracer)
+ expect(tracer).to receive(:trace).with(position)
- context "when the file is unchanged in the new diff" do
- context "when the position pointed at an added line in the old diff" do
- context "when the file's content was unchanged between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
- let(:new_diff_refs) { diff_refs(create_file_commit, create_second_file_commit) }
- let(:change_diff_refs) { diff_refs(initial_commit, create_file_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 2) }
-
- # old diff:
- # 1 + A
- # 2 + B
- # 3 + C
- #
- # new diff:
- # 1 1 A
- # 2 2 B
- # 3 3 C
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: nil,
- new_line: 2
- )
- end
- end
-
- context "when the file's content was changed between the old and the new diff" do
- context "when that line was unchanged between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
- let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) }
- let(:change_diff_refs) { diff_refs(initial_commit, update_line_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 1) }
-
- # old diff:
- # 1 + A
- # 2 + B
- # 3 + C
- #
- # new diff:
- # 1 1 A
- # 2 2 BB
- # 3 3 C
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: nil,
- new_line: 1
- )
- end
- end
-
- context "when that line was moved between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) }
- let(:new_diff_refs) { diff_refs(move_line_commit, move_second_file_line_commit) }
- let(:change_diff_refs) { diff_refs(initial_commit, move_line_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 2) }
-
- # old diff:
- # 1 + A
- # 2 + BB
- # 3 + C
- #
- # new diff:
- # 1 1 BB
- # 2 2 A
- # 3 3 C
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: nil,
- new_line: 1
- )
- end
- end
-
- context "when that line was changed between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) }
- let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) }
- let(:change_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 2) }
-
- # old diff:
- # 1 + A
- # 2 + B
- # 3 + C
- #
- # new diff:
- # 1 1 A
- # 2 2 BB
- # 3 3 C
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: 2,
- new_line: nil
- )
- end
- end
-
- context "when that line was deleted between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) }
- let(:new_diff_refs) { diff_refs(delete_line_commit, delete_second_file_line_commit) }
- let(:change_diff_refs) { diff_refs(move_line_commit, delete_second_file_line_commit) }
- let(:old_position) { position(new_path: file_name, new_line: 3) }
-
- # old diff:
- # 1 + BB
- # 2 + A
- # 3 + C
- #
- # new diff:
- # 1 1 BB
- # 2 2 A
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: 3,
- new_line: nil
- )
- end
- end
- end
- end
- end
- end
-
- context "when the file was changed in the old diff" do
- context "when the file is created in the new diff" do
- context "when the position pointed at an added line in the old diff" do
- context "when the file's content was unchanged between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
- let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
- let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) }
-
- # old diff:
- # 1 1 A
- # 2 - B
- # 2 + BB
- # 3 3 C
- #
- # new diff:
- # 1 + A
- # 2 + BB
- # 3 + C
-
- it "returns the new position" do
- expect_new_position(
- new_path: old_position.new_path,
- old_line: nil,
- new_line: old_position.new_line
- )
- end
- end
-
- context "when the file's content was changed between the old and the new diff" do
- context "when that line was unchanged between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) }
- let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) }
- let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) }
-
- # old diff:
- # 1 + BB
- # 1 2 A
- # 2 - B
- # 3 3 C
- #
- # new diff:
- # 1 + BB
- # 2 + A
-
- it "returns the new position" do
- expect_new_position(
- new_path: old_position.new_path,
- old_line: nil,
- new_line: old_position.new_line
- )
- end
- end
-
- context "when that line was moved between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
- let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) }
- let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) }
-
- # old diff:
- # 1 1 A
- # 2 - B
- # 2 + BB
- # 3 3 C
- #
- # new diff:
- # 1 + BB
- # 2 + A
- # 3 + C
-
- it "returns the new position" do
- expect_new_position(
- new_path: old_position.new_path,
- old_line: nil,
- new_line: 1
- )
- end
- end
-
- context "when that line was changed or deleted between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) }
- let(:new_diff_refs) { diff_refs(initial_commit, create_file_commit) }
- let(:change_diff_refs) { diff_refs(move_line_commit, create_file_commit) }
- let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) }
-
- # old diff:
- # 1 + BB
- # 1 2 A
- # 2 - B
- # 3 3 C
- #
- # new diff:
- # 1 + A
- # 2 + B
- # 3 + C
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: 1,
- new_line: nil
- )
- end
- end
- end
- end
-
- context "when the position pointed at a deleted line in the old diff" do
- let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
- let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
- let(:change_diff_refs) { diff_refs(create_file_commit, initial_commit) }
- let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) }
-
- # old diff:
- # 1 1 A
- # 2 - B
- # 2 + BB
- # 3 3 C
- #
- # new diff:
- # 1 + A
- # 2 + BB
- # 3 + C
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: 2,
- new_line: nil
- )
- end
- end
-
- context "when the position pointed at an unchanged line in the old diff" do
- context "when the file's content was unchanged between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
- let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
- let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 1) }
-
- # old diff:
- # 1 1 A
- # 2 - B
- # 2 + BB
- # 3 3 C
- #
- # new diff:
- # 1 + A
- # 2 + BB
- # 3 + C
-
- it "returns the new position" do
- expect_new_position(
- new_path: old_position.new_path,
- old_line: nil,
- new_line: old_position.new_line
- )
- end
- end
-
- context "when the file's content was changed between the old and the new diff" do
- context "when that line was unchanged between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) }
- let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) }
- let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 2) }
-
- # old diff:
- # 1 + BB
- # 1 2 A
- # 2 - B
- # 3 3 C
- #
- # new diff:
- # 1 + BB
- # 2 + A
-
- it "returns the new position" do
- expect_new_position(
- new_path: old_position.new_path,
- old_line: nil,
- new_line: old_position.new_line
- )
- end
- end
-
- context "when that line was moved between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(move_line_commit, delete_line_commit) }
- let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
- let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2, new_line: 2) }
-
- # old diff:
- # 1 1 BB
- # 2 2 A
- # 3 - C
- #
- # new diff:
- # 1 + A
- # 2 + BB
- # 3 + C
-
- it "returns the new position" do
- expect_new_position(
- new_path: old_position.new_path,
- old_line: nil,
- new_line: 1
- )
- end
- end
-
- context "when that line was changed or deleted between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) }
- let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) }
- let(:change_diff_refs) { diff_refs(move_line_commit, delete_line_commit) }
- let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 3, new_line: 3) }
-
- # old diff:
- # 1 + BB
- # 1 2 A
- # 2 - B
- # 3 3 C
- #
- # new diff:
- # 1 + A
- # 2 + B
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: 3,
- new_line: nil
- )
- end
- end
- end
- end
- end
-
- context "when the file is changed in the new diff" do
- context "when the position pointed at an added line in the old diff" do
- context "when the file's content was unchanged between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
- let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) }
- let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) }
-
- # old diff:
- # 1 1 A
- # 2 - B
- # 2 + BB
- # 3 3 C
- #
- # new diff:
- # 1 1 A
- # 2 - B
- # 2 + BB
- # 3 3 C
-
- it "returns the new position" do
- expect_new_position(
- old_path: old_position.old_path,
- new_path: old_position.new_path,
- old_line: nil,
- new_line: old_position.new_line
- )
- end
- end
-
- context "when the file's content was changed between the old and the new diff" do
- context "when that line was unchanged between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) }
- let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) }
- let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) }
-
- # old diff:
- # 1 + BB
- # 1 2 A
- # 2 - B
- # 3 3 C
- #
- # new diff:
- # 1 1 BB
- # 2 2 A
- # 3 - C
-
- it "returns the new position" do
- expect_new_position(
- old_path: old_position.old_path,
- new_path: old_position.new_path,
- old_line: 1,
- new_line: 1
- )
- end
- end
-
- context "when that line was moved between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
- let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) }
- let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) }
-
- # old diff:
- # 1 1 A
- # 2 - B
- # 2 + BB
- # 3 3 C
- #
- # new diff:
- # 1 - A
- # 2 1 BB
- # 2 + A
- # 3 3 C
-
- it "returns the new position" do
- expect_new_position(
- old_path: old_position.old_path,
- new_path: old_position.new_path,
- old_line: 2,
- new_line: 1
- )
- end
- end
-
- context "when that line was changed or deleted between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) }
- let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
- let(:change_diff_refs) { diff_refs(move_line_commit, update_line_commit) }
- let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) }
-
- # old diff:
- # 1 + BB
- # 1 2 A
- # 2 - B
- # 3 3 C
- #
- # new diff:
- # 1 1 A
- # 2 - B
- # 2 + BB
- # 3 3 C
-
- it "returns the position of the change" do
- expect_change_position(
- old_path: file_name,
- new_path: file_name,
- old_line: 1,
- new_line: nil
- )
- end
- end
- end
- end
-
- context "when the position pointed at a deleted line in the old diff" do
- context "when the file's content was unchanged between the old and the new diff" do
- let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) }
- let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) }
- let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) }
-
- # old diff:
- # 1 1 A
- # 2 - B
- # 2 + BB
- # 3 3 C
- #
- # new diff:
- # 1 1 A
- # 2 - B
- # 2 + BB
- # 3 3 C
-
- it "returns the new position" do
- expect_new_position(
- old_path: old_position.old_path,
- new_path: old_position.new_path,
- old_line: old_position.old_line,
- new_line: nil
- )
- end
- end
- end
- end
+ subject.trace(position)
end
end
end
- describe "typical use scenarios" do
- let(:second_branch_name) { "#{branch_name}-2" }
-
- def expect_new_positions(old_attrs, new_attrs)
- old_positions = old_attrs.map do |old_attrs|
- position(old_attrs)
- end
+ describe 'diffs methods' do
+ let(:project) { create(:project, :repository) }
+ let(:current_user) { project.owner }
- new_positions = old_positions.map do |old_position|
- position_tracer.trace(old_position)
- end
-
- aggregate_failures do
- new_positions.zip(new_attrs).each do |new_position, new_attrs|
- if new_attrs&.delete(:change)
- expect_change_position(new_attrs, new_position)
- else
- expect_new_position(new_attrs, new_position)
- end
- end
- end
- end
-
- let(:create_file_commit) do
- initial_commit
-
- create_file(
- branch_name,
- file_name,
- <<-CONTENT.strip_heredoc
- A
- B
- C
- D
- E
- F
- CONTENT
- )
- end
-
- let(:second_create_file_commit) do
- create_file_commit
-
- create_branch(second_branch_name, branch_name)
-
- update_file(
- second_branch_name,
- file_name,
- <<-CONTENT.strip_heredoc
- Z
- Z
- Z
- A
- B
- C
- D
- E
- F
- CONTENT
+ let(:old_diff_refs) do
+ diff_refs(
+ project.commit(create_branch('new-branch', 'master')[:branch].name),
+ create_file('new-branch', 'file.md', 'content')
)
end
- let(:update_file_commit) do
- second_create_file_commit
-
- update_file(
- branch_name,
- file_name,
- <<-CONTENT.strip_heredoc
- A
- C
- DD
- E
- F
- G
- CONTENT
- )
- end
-
- let(:update_file_again_commit) do
- update_file_commit
-
- update_file(
- branch_name,
- file_name,
- <<-CONTENT.strip_heredoc
- A
- BB
- C
- D
- E
- FF
- G
- CONTENT
+ let(:new_diff_refs) do
+ diff_refs(
+ create_file('new-branch', 'file.md', 'content'),
+ update_file('new-branch', 'file.md', 'updatedcontent')
)
end
- describe "simple push of new commit" do
- let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) }
- let(:new_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) }
- let(:change_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) }
-
- # old diff:
- # 1 1 A
- # 2 - B
- # 3 2 C
- # 4 - D
- # 3 + DD
- # 5 4 E
- # 6 5 F
- # 6 + G
- #
- # new diff:
- # 1 1 A
- # 2 - B
- # 2 + BB
- # 3 3 C
- # 4 4 D
- # 5 5 E
- # 6 - F
- # 6 + FF
- # 7 + G
-
- it "returns the new positions" do
- old_position_attrs = [
- { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A
- { old_path: file_name, old_line: 2 }, # - B
- { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C
- { old_path: file_name, old_line: 4 }, # - D
- { new_path: file_name, new_line: 3 }, # + DD
- { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E
- { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F
- { new_path: file_name, new_line: 6 }, # + G
- ]
-
- new_position_attrs = [
- { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 },
- { old_path: file_name, old_line: 2 },
- { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 },
- { new_path: file_name, new_line: 4, change: true },
- { new_path: file_name, old_line: 3, change: true },
- { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 },
- { new_path: file_name, old_line: 5, change: true },
- { new_path: file_name, new_line: 7 }
- ]
+ describe '#ac_diffs' do
+ it 'returns the diffs between the base of old and new diff' do
+ diff_refs = subject.ac_diffs.diff_refs
- expect_new_positions(old_position_attrs, new_position_attrs)
+ expect(diff_refs.base_sha).to eq(old_diff_refs.base_sha)
+ expect(diff_refs.start_sha).to eq(old_diff_refs.base_sha)
+ expect(diff_refs.head_sha).to eq(new_diff_refs.base_sha)
end
end
- describe "force push to overwrite last commit" do
- let(:second_create_file_commit) do
- create_file_commit
+ describe '#bd_diffs' do
+ it 'returns the diffs between the HEAD of old and new diff' do
+ diff_refs = subject.bd_diffs.diff_refs
- create_branch(second_branch_name, branch_name)
-
- update_file(
- second_branch_name,
- file_name,
- <<-CONTENT.strip_heredoc
- A
- BB
- C
- D
- E
- FF
- G
- CONTENT
- )
- end
-
- let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) }
- let(:new_diff_refs) { diff_refs(create_file_commit, second_create_file_commit) }
- let(:change_diff_refs) { diff_refs(update_file_commit, second_create_file_commit) }
-
- # old diff:
- # 1 1 A
- # 2 - B
- # 3 2 C
- # 4 - D
- # 3 + DD
- # 5 4 E
- # 6 5 F
- # 6 + G
- #
- # new diff:
- # 1 1 A
- # 2 - B
- # 2 + BB
- # 3 3 C
- # 4 4 D
- # 5 5 E
- # 6 - F
- # 6 + FF
- # 7 + G
-
- it "returns the new positions" do
- old_position_attrs = [
- { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A
- { old_path: file_name, old_line: 2 }, # - B
- { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C
- { old_path: file_name, old_line: 4 }, # - D
- { new_path: file_name, new_line: 3 }, # + DD
- { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E
- { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F
- { new_path: file_name, new_line: 6 }, # + G
- ]
-
- new_position_attrs = [
- { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 },
- { old_path: file_name, old_line: 2 },
- { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 },
- { new_path: file_name, new_line: 4, change: true },
- { old_path: file_name, old_line: 3, change: true },
- { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 },
- { old_path: file_name, old_line: 5, change: true },
- { new_path: file_name, new_line: 7 }
- ]
-
- expect_new_positions(old_position_attrs, new_position_attrs)
+ expect(diff_refs.base_sha).to eq(old_diff_refs.head_sha)
+ expect(diff_refs.start_sha).to eq(old_diff_refs.head_sha)
+ expect(diff_refs.head_sha).to eq(new_diff_refs.head_sha)
end
end
- describe "force push to delete last commit" do
- let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) }
- let(:new_diff_refs) { diff_refs(create_file_commit, update_file_commit) }
- let(:change_diff_refs) { diff_refs(update_file_again_commit, update_file_commit) }
-
- # old diff:
- # 1 1 A
- # 2 - B
- # 2 + BB
- # 3 3 C
- # 4 4 D
- # 5 5 E
- # 6 - F
- # 6 + FF
- # 7 + G
- #
- # new diff:
- # 1 1 A
- # 2 - B
- # 3 2 C
- # 4 - D
- # 3 + DD
- # 5 4 E
- # 6 5 F
- # 6 + G
-
- it "returns the new positions" do
- old_position_attrs = [
- { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A
- { old_path: file_name, old_line: 2 }, # - B
- { new_path: file_name, new_line: 2 }, # + BB
- { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C
- { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D
- { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E
- { old_path: file_name, old_line: 6 }, # - F
- { new_path: file_name, new_line: 6 }, # + FF
- { new_path: file_name, new_line: 7 }, # + G
- ]
-
- new_position_attrs = [
- { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 },
- { old_path: file_name, old_line: 2 },
- { old_path: file_name, old_line: 2, change: true },
- { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 },
- { old_path: file_name, old_line: 4, change: true },
- { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 },
- { new_path: file_name, new_line: 5, change: true },
- { old_path: file_name, old_line: 6, change: true },
- { new_path: file_name, new_line: 6 }
- ]
-
- expect_new_positions(old_position_attrs, new_position_attrs)
- end
- end
-
- describe "rebase on top of target branch" do
- let(:second_update_file_commit) do
- update_file_commit
-
- update_file(
- second_branch_name,
- file_name,
- <<-CONTENT.strip_heredoc
- Z
- Z
- Z
- A
- C
- DD
- E
- F
- G
- CONTENT
- )
- end
-
- let(:update_file_again_commit) do
- second_update_file_commit
-
- update_file(
- branch_name,
- file_name,
- <<-CONTENT.strip_heredoc
- A
- BB
- C
- D
- E
- FF
- G
- CONTENT
- )
- end
-
- let(:overwrite_update_file_again_commit) do
- update_file_again_commit
-
- update_file(
- second_branch_name,
- file_name,
- <<-CONTENT.strip_heredoc
- Z
- Z
- Z
- A
- BB
- C
- D
- E
- FF
- G
- CONTENT
- )
- end
-
- let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) }
- let(:new_diff_refs) { diff_refs(create_file_commit, overwrite_update_file_again_commit) }
- let(:change_diff_refs) { diff_refs(update_file_again_commit, overwrite_update_file_again_commit) }
-
- # old diff:
- # 1 1 A
- # 2 - B
- # 2 + BB
- # 3 3 C
- # 4 4 D
- # 5 5 E
- # 6 - F
- # 6 + FF
- # 7 + G
- #
- # new diff:
- # 1 + Z
- # 2 + Z
- # 3 + Z
- # 1 4 A
- # 2 - B
- # 5 + BB
- # 3 6 C
- # 4 7 D
- # 5 8 E
- # 6 - F
- # 9 + FF
- # 0 + G
-
- it "returns the new positions" do
- old_position_attrs = [
- { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A
- { old_path: file_name, old_line: 2 }, # - B
- { new_path: file_name, new_line: 2 }, # + BB
- { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C
- { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D
- { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E
- { old_path: file_name, old_line: 6 }, # - F
- { new_path: file_name, new_line: 6 }, # + FF
- { new_path: file_name, new_line: 7 }, # + G
- ]
-
- new_position_attrs = [
- { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A
- { old_path: file_name, old_line: 2 }, # - B
- { new_path: file_name, new_line: 5 }, # + BB
- { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C
- { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D
- { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E
- { old_path: file_name, old_line: 6 }, # - F
- { new_path: file_name, new_line: 9 }, # + FF
- { new_path: file_name, new_line: 10 }, # + G
- ]
-
- expect_new_positions(old_position_attrs, new_position_attrs)
- end
- end
-
- describe "merge of target branch" do
- let(:merge_commit) do
- second_create_file_commit
-
- merge_request = create(:merge_request, source_branch: second_branch_name, target_branch: branch_name, source_project: project)
-
- repository.merge(current_user, merge_request.diff_head_sha, merge_request, "Merge branches")
-
- project.commit(branch_name)
- end
-
- let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) }
- let(:new_diff_refs) { diff_refs(create_file_commit, merge_commit) }
- let(:change_diff_refs) { diff_refs(update_file_again_commit, merge_commit) }
-
- # old diff:
- # 1 1 A
- # 2 - B
- # 2 + BB
- # 3 3 C
- # 4 4 D
- # 5 5 E
- # 6 - F
- # 6 + FF
- # 7 + G
- #
- # new diff:
- # 1 + Z
- # 2 + Z
- # 3 + Z
- # 1 4 A
- # 2 - B
- # 5 + BB
- # 3 6 C
- # 4 7 D
- # 5 8 E
- # 6 - F
- # 9 + FF
- # 0 + G
-
- it "returns the new positions" do
- old_position_attrs = [
- { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A
- { old_path: file_name, old_line: 2 }, # - B
- { new_path: file_name, new_line: 2 }, # + BB
- { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C
- { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D
- { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E
- { old_path: file_name, old_line: 6 }, # - F
- { new_path: file_name, new_line: 6 }, # + FF
- { new_path: file_name, new_line: 7 }, # + G
- ]
-
- new_position_attrs = [
- { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A
- { old_path: file_name, old_line: 2 }, # - B
- { new_path: file_name, new_line: 5 }, # + BB
- { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C
- { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D
- { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E
- { old_path: file_name, old_line: 6 }, # - F
- { new_path: file_name, new_line: 9 }, # + FF
- { new_path: file_name, new_line: 10 }, # + G
- ]
-
- expect_new_positions(old_position_attrs, new_position_attrs)
- end
- end
-
- describe "changing target branch" do
- let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) }
- let(:new_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) }
- let(:change_diff_refs) { diff_refs(create_file_commit, update_file_commit) }
-
- # old diff:
- # 1 1 A
- # 2 - B
- # 2 + BB
- # 3 3 C
- # 4 4 D
- # 5 5 E
- # 6 - F
- # 6 + FF
- # 7 + G
- #
- # new diff:
- # 1 1 A
- # 2 + BB
- # 2 3 C
- # 3 - DD
- # 4 + D
- # 4 5 E
- # 5 - F
- # 6 + FF
- # 7 G
-
- it "returns the new positions" do
- old_position_attrs = [
- { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A
- { old_path: file_name, old_line: 2 }, # - B
- { new_path: file_name, new_line: 2 }, # + BB
- { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C
- { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D
- { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E
- { old_path: file_name, old_line: 6 }, # - F
- { new_path: file_name, new_line: 6 }, # + FF
- { new_path: file_name, new_line: 7 }, # + G
- ]
-
- new_position_attrs = [
- { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 },
- { old_path: file_name, old_line: 2, change: true },
- { new_path: file_name, new_line: 2 },
- { old_path: file_name, new_path: file_name, old_line: 2, new_line: 3 },
- { new_path: file_name, new_line: 4 },
- { old_path: file_name, new_path: file_name, old_line: 4, new_line: 5 },
- { old_path: file_name, old_line: 5 },
- { new_path: file_name, new_line: 6 },
- { new_path: file_name, new_line: 7 }
- ]
+ describe '#cd_diffs' do
+ it 'returns the diffs in the new diff' do
+ diff_refs = subject.cd_diffs.diff_refs
- expect_new_positions(old_position_attrs, new_position_attrs)
+ expect(diff_refs.base_sha).to eq(new_diff_refs.base_sha)
+ expect(diff_refs.start_sha).to eq(new_diff_refs.base_sha)
+ expect(diff_refs.head_sha).to eq(new_diff_refs.head_sha)
end
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index cceeae8afe6..a28b95e5bff 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1694,14 +1694,15 @@ describe Gitlab::Git::Repository, :seed_helper do
let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
let(:left_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
let(:right_branch) { 'test-master' }
+ let(:first_parent_ref) { 'refs/heads/test-master' }
let(:target_ref) { 'refs/merge-requests/999/merge' }
before do
- repository.create_branch(right_branch, branch_head) unless repository.branch_exists?(right_branch)
+ repository.create_branch(right_branch, branch_head) unless repository.ref_exists?(first_parent_ref)
end
def merge_to_ref
- repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message')
+ repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message', first_parent_ref)
end
it 'generates a commit in the target_ref' do
@@ -1716,7 +1717,7 @@ describe Gitlab::Git::Repository, :seed_helper do
end
it 'does not change the right branch HEAD' do
- expect { merge_to_ref }.not_to change { repository.find_branch(right_branch).target }
+ expect { merge_to_ref }.not_to change { repository.commit(first_parent_ref).sha }
end
end
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index 18663a72fcd..f38b8d31237 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -79,13 +79,13 @@ describe Gitlab::GitalyClient::OperationService do
end
describe '#user_merge_to_ref' do
- let(:branch) { 'my-branch' }
+ let(:first_parent_ref) { 'refs/heads/my-branch' }
let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
let(:ref) { 'refs/merge-requests/x/merge' }
let(:message) { 'validación' }
let(:response) { Gitaly::UserMergeToRefResponse.new(commit_id: 'new-commit-id') }
- subject { client.user_merge_to_ref(user, source_sha, branch, ref, message) }
+ subject { client.user_merge_to_ref(user, source_sha, nil, ref, message, first_parent_ref) }
it 'sends a user_merge_to_ref message' do
expect_any_instance_of(Gitaly::OperationService::Stub)
diff --git a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
index aec9c4baf0a..d60d1b7559a 100644
--- a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
+++ b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
@@ -7,35 +7,39 @@ require 'spec_helper'
describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
def type(type_authorizations = [])
Class.new(Types::BaseObject) do
- graphql_name "TestType"
+ graphql_name 'TestType'
authorize type_authorizations
end
end
- def type_with_field(field_type, field_authorizations = [], resolved_value = "Resolved value")
+ def type_with_field(field_type, field_authorizations = [], resolved_value = 'Resolved value', **options)
Class.new(Types::BaseObject) do
- graphql_name "TestTypeWithField"
- field :test_field, field_type, null: true, authorize: field_authorizations, resolve: -> (_, _, _) { resolved_value}
+ graphql_name 'TestTypeWithField'
+ options.reverse_merge!(null: true)
+ field :test_field, field_type,
+ authorize: field_authorizations,
+ resolve: -> (_, _, _) { resolved_value },
+ **options
end
end
let(:current_user) { double(:current_user) }
subject(:service) { described_class.new(field) }
- describe "#authorized_resolve" do
- let(:presented_object) { double("presented object") }
- let(:presented_type) { double("parent type", object: presented_object) }
+ describe '#authorized_resolve' do
+ let(:presented_object) { double('presented object') }
+ let(:presented_type) { double('parent type', object: presented_object) }
subject(:resolved) { service.authorized_resolve.call(presented_type, {}, { current_user: current_user }) }
- context "scalar types" do
- shared_examples "checking permissions on the presented object" do
- it "checks the abilities on the object being presented and returns the value" do
+ context 'scalar types' do
+ shared_examples 'checking permissions on the presented object' do
+ it 'checks the abilities on the object being presented and returns the value' do
expected_permissions.each do |permission|
spy_ability_check_for(permission, presented_object, passed: true)
end
- expect(resolved).to eq("Resolved value")
+ expect(resolved).to eq('Resolved value')
end
it "returns nil if the value wasn't authorized" do
@@ -45,61 +49,71 @@ describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
end
end
- context "when the field is a built-in scalar type" do
- let(:field) { type_with_field(GraphQL::STRING_TYPE, :read_field).fields["testField"].to_graphql }
+ context 'when the field is a built-in scalar type' do
+ let(:field) { type_with_field(GraphQL::STRING_TYPE, :read_field).fields['testField'].to_graphql }
let(:expected_permissions) { [:read_field] }
- it_behaves_like "checking permissions on the presented object"
+ it_behaves_like 'checking permissions on the presented object'
end
- context "when the field is a list of scalar types" do
- let(:field) { type_with_field([GraphQL::STRING_TYPE], :read_field).fields["testField"].to_graphql }
+ context 'when the field is a list of scalar types' do
+ let(:field) { type_with_field([GraphQL::STRING_TYPE], :read_field).fields['testField'].to_graphql }
let(:expected_permissions) { [:read_field] }
- it_behaves_like "checking permissions on the presented object"
+ it_behaves_like 'checking permissions on the presented object'
end
- context "when the field is sub-classed scalar type" do
- let(:field) { type_with_field(Types::TimeType, :read_field).fields["testField"].to_graphql }
+ context 'when the field is sub-classed scalar type' do
+ let(:field) { type_with_field(Types::TimeType, :read_field).fields['testField'].to_graphql }
let(:expected_permissions) { [:read_field] }
- it_behaves_like "checking permissions on the presented object"
+ it_behaves_like 'checking permissions on the presented object'
end
- context "when the field is a list of sub-classed scalar types" do
- let(:field) { type_with_field([Types::TimeType], :read_field).fields["testField"].to_graphql }
+ context 'when the field is a list of sub-classed scalar types' do
+ let(:field) { type_with_field([Types::TimeType], :read_field).fields['testField'].to_graphql }
let(:expected_permissions) { [:read_field] }
- it_behaves_like "checking permissions on the presented object"
+ it_behaves_like 'checking permissions on the presented object'
end
end
- context "when the field is a specific type" do
+ context 'when the field is a specific type' do
let(:custom_type) { type(:read_type) }
- let(:object_in_field) { double("presented in field") }
- let(:field) { type_with_field(custom_type, :read_field, object_in_field).fields["testField"].to_graphql }
+ let(:object_in_field) { double('presented in field') }
+ let(:field) { type_with_field(custom_type, :read_field, object_in_field).fields['testField'].to_graphql }
- it "checks both field & type permissions" do
+ it 'checks both field & type permissions' do
spy_ability_check_for(:read_field, object_in_field, passed: true)
spy_ability_check_for(:read_type, object_in_field, passed: true)
expect(resolved).to eq(object_in_field)
end
- it "returns nil if viewing was not allowed" do
+ it 'returns nil if viewing was not allowed' do
spy_ability_check_for(:read_field, object_in_field, passed: false)
spy_ability_check_for(:read_type, object_in_field, passed: true)
expect(resolved).to be_nil
end
- context "when the field is a list" do
- let(:object_1) { double("presented in field 1") }
- let(:object_2) { double("presented in field 2") }
+ context 'when the field is not nullable' do
+ let(:field) { type_with_field(custom_type, [], object_in_field, null: false).fields['testField'].to_graphql }
+
+ it 'returns nil when viewing is not allowed' do
+ spy_ability_check_for(:read_type, object_in_field, passed: false)
+
+ expect(resolved).to be_nil
+ end
+ end
+
+ context 'when the field is a list' do
+ let(:object_1) { double('presented in field 1') }
+ let(:object_2) { double('presented in field 2') }
let(:presented_types) { [double(object: object_1), double(object: object_2)] }
- let(:field) { type_with_field([custom_type], :read_field, presented_types).fields["testField"].to_graphql }
+ let(:field) { type_with_field([custom_type], :read_field, presented_types).fields['testField'].to_graphql }
- it "checks all permissions" do
+ it 'checks all permissions' do
allow(Ability).to receive(:allowed?) { true }
spy_ability_check_for(:read_field, object_1, passed: true)
@@ -110,7 +124,7 @@ describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
expect(resolved).to eq(presented_types)
end
- it "filters out objects that the user cannot see" do
+ it 'filters out objects that the user cannot see' do
allow(Ability).to receive(:allowed?) { true }
spy_ability_check_for(:read_type, object_1, passed: false)
diff --git a/spec/lib/gitlab/graphql/calls_gitaly/instrumentation_spec.rb b/spec/lib/gitlab/graphql/calls_gitaly/instrumentation_spec.rb
new file mode 100644
index 00000000000..d93ce464a92
--- /dev/null
+++ b/spec/lib/gitlab/graphql/calls_gitaly/instrumentation_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::Graphql::CallsGitaly::Instrumentation do
+ subject { described_class.new }
+
+ describe '#calls_gitaly_check' do
+ let(:gitaly_field) { Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true) }
+ let(:no_gitaly_field) { Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: false) }
+
+ context 'if there are no Gitaly calls' do
+ it 'does not raise an error if calls_gitaly is false' do
+ expect { subject.send(:calls_gitaly_check, no_gitaly_field, 0) }.not_to raise_error
+ end
+ end
+
+ context 'if there is at least 1 Gitaly call' do
+ it 'raises an error if calls_gitaly: is false or not defined' do
+ expect { subject.send(:calls_gitaly_check, no_gitaly_field, 1) }.to raise_error(/specify a constant complexity or add `calls_gitaly: true`/)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 2758023fb17..28b187c3676 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -123,6 +123,7 @@ Release:
- project_id
- created_at
- updated_at
+- released_at
Releases::Link:
- id
- release_id
diff --git a/spec/lib/gitlab/issuable_metadata_spec.rb b/spec/lib/gitlab/issuable_metadata_spec.rb
index 916f3876a8e..032467b8b4e 100644
--- a/spec/lib/gitlab/issuable_metadata_spec.rb
+++ b/spec/lib/gitlab/issuable_metadata_spec.rb
@@ -7,11 +7,11 @@ describe Gitlab::IssuableMetadata do
subject { Class.new { include Gitlab::IssuableMetadata }.new }
it 'returns an empty Hash if an empty collection is provided' do
- expect(subject.issuable_meta_data(Issue.none, 'Issue')).to eq({})
+ expect(subject.issuable_meta_data(Issue.none, 'Issue', user)).to eq({})
end
it 'raises an error when given a collection with no limit' do
- expect { subject.issuable_meta_data(Issue.all, 'Issue') }.to raise_error(/must have a limit/)
+ expect { subject.issuable_meta_data(Issue.all, 'Issue', user) }.to raise_error(/must have a limit/)
end
context 'issues' do
@@ -23,7 +23,7 @@ describe Gitlab::IssuableMetadata do
let!(:closing_issues) { create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) }
it 'aggregates stats on issues' do
- data = subject.issuable_meta_data(Issue.all.limit(10), 'Issue')
+ data = subject.issuable_meta_data(Issue.all.limit(10), 'Issue', user)
expect(data.count).to eq(2)
expect(data[issue.id].upvotes).to eq(1)
@@ -46,7 +46,7 @@ describe Gitlab::IssuableMetadata do
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
it 'aggregates stats on merge requests' do
- data = subject.issuable_meta_data(MergeRequest.all.limit(10), 'MergeRequest')
+ data = subject.issuable_meta_data(MergeRequest.all.limit(10), 'MergeRequest', user)
expect(data.count).to eq(2)
expect(data[merge_request.id].upvotes).to eq(1)
diff --git a/spec/lib/gitlab/legacy_github_import/importer_spec.rb b/spec/lib/gitlab/legacy_github_import/importer_spec.rb
index a0c664da185..9163019514b 100644
--- a/spec/lib/gitlab/legacy_github_import/importer_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/importer_spec.rb
@@ -132,6 +132,7 @@ describe Gitlab::LegacyGithubImport::Importer do
body: 'Release v1.0.0',
draft: false,
created_at: created_at,
+ published_at: created_at,
updated_at: updated_at,
url: "#{api_root}/repos/octocat/Hello-World/releases/1"
)
@@ -144,6 +145,7 @@ describe Gitlab::LegacyGithubImport::Importer do
body: nil,
draft: false,
created_at: created_at,
+ published_at: created_at,
updated_at: updated_at,
url: "#{api_root}/repos/octocat/Hello-World/releases/2"
)
diff --git a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
index c57b96fb00d..534cf219520 100644
--- a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
@@ -4,6 +4,7 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do
let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) }
let(:octocat) { double(id: 123456, login: 'octocat') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+ let(:published_at) { DateTime.strptime('2011-01-26T20:00:00Z') }
let(:base_data) do
{
@@ -11,7 +12,7 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do
name: 'First release',
draft: false,
created_at: created_at,
- published_at: created_at,
+ published_at: published_at,
body: 'Release v1.0.0'
}
end
@@ -28,6 +29,7 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do
name: 'First release',
description: 'Release v1.0.0',
created_at: created_at,
+ released_at: published_at,
updated_at: created_at
}
diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
index aaf8c9fa2a0..4d93b70e6e3 100644
--- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
@@ -8,12 +8,19 @@ describe Gitlab::Metrics::Samplers::RubySampler do
allow(Gitlab::Metrics::NullMetric).to receive(:instance).and_return(null_metric)
end
+ describe '#initialize' do
+ it 'sets process_start_time_seconds' do
+ Timecop.freeze do
+ expect(sampler.metrics[:process_start_time_seconds].get).to eq(Time.now.to_i)
+ end
+ end
+ end
+
describe '#sample' do
it 'samples various statistics' do
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)
@@ -44,13 +51,6 @@ describe Gitlab::Metrics::Samplers::RubySampler do
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)
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index da87df15746..3b434a02f63 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -19,12 +19,6 @@ describe Gitlab::Metrics::System 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
@@ -43,12 +37,6 @@ describe Gitlab::Metrics::System 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/namespaced_session_store_spec.rb b/spec/lib/gitlab/namespaced_session_store_spec.rb
index c0af2ede32a..e177c44ad67 100644
--- a/spec/lib/gitlab/namespaced_session_store_spec.rb
+++ b/spec/lib/gitlab/namespaced_session_store_spec.rb
@@ -4,19 +4,33 @@ require 'spec_helper'
describe Gitlab::NamespacedSessionStore do
let(:key) { :some_key }
- subject { described_class.new(key) }
- it 'stores data under the specified key' do
- Gitlab::Session.with_session({}) do
- subject[:new_data] = 123
+ context 'current session' do
+ subject { described_class.new(key) }
- expect(Thread.current[:session_storage][key]).to eq(new_data: 123)
+ it 'stores data under the specified key' do
+ Gitlab::Session.with_session({}) do
+ subject[:new_data] = 123
+
+ expect(Thread.current[:session_storage][key]).to eq(new_data: 123)
+ end
+ end
+
+ it 'retrieves data from the given key' do
+ Thread.current[:session_storage] = { key => { existing_data: 123 } }
+
+ expect(subject[:existing_data]).to eq 123
end
end
- it 'retrieves data from the given key' do
- Thread.current[:session_storage] = { key => { existing_data: 123 } }
+ context 'passed in session' do
+ let(:data) { { 'data' => 42 } }
+ let(:session) { { 'some_key' => data } }
+
+ subject { described_class.new(key, session.with_indifferent_access) }
- expect(subject[:existing_data]).to eq 123
+ it 'retrieves data from the given key' do
+ expect(subject['data']).to eq 42
+ end
end
end
diff --git a/spec/lib/gitlab/performance_bar_spec.rb b/spec/lib/gitlab/performance_bar_spec.rb
index f480376acb4..ee3c571c9c0 100644
--- a/spec/lib/gitlab/performance_bar_spec.rb
+++ b/spec/lib/gitlab/performance_bar_spec.rb
@@ -3,17 +3,42 @@ require 'spec_helper'
describe Gitlab::PerformanceBar do
shared_examples 'allowed user IDs are cached' do
before do
- # Warm the Redis cache
+ # Warm the caches
described_class.enabled?(user)
end
it 'caches the allowed user IDs in cache', :use_clean_rails_memory_store_caching do
expect do
+ expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original
+ expect(described_class.l2_cache_backend).not_to receive(:fetch)
expect(described_class.enabled?(user)).to be_truthy
end.not_to exceed_query_limit(0)
end
+
+ it 'caches the allowed user IDs in L1 cache for 1 minute', :use_clean_rails_memory_store_caching do
+ Timecop.travel 2.minutes do
+ expect do
+ expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original
+ expect(described_class.l2_cache_backend).to receive(:fetch).and_call_original
+ expect(described_class.enabled?(user)).to be_truthy
+ end.not_to exceed_query_limit(0)
+ end
+ end
+
+ it 'caches the allowed user IDs in L2 cache for 5 minutes', :use_clean_rails_memory_store_caching do
+ Timecop.travel 6.minutes do
+ expect do
+ expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original
+ expect(described_class.l2_cache_backend).to receive(:fetch).and_call_original
+ expect(described_class.enabled?(user)).to be_truthy
+ end.not_to exceed_query_limit(2)
+ end
+ end
end
+ it { expect(described_class.l1_cache_backend).to eq(Gitlab::ThreadMemoryCache.cache_backend) }
+ it { expect(described_class.l2_cache_backend).to eq(Rails.cache) }
+
describe '.enabled?' do
let(:user) { create(:user) }
diff --git a/spec/lib/gitlab/user_extractor_spec.rb b/spec/lib/gitlab/user_extractor_spec.rb
deleted file mode 100644
index b86ec5445b8..00000000000
--- a/spec/lib/gitlab/user_extractor_spec.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::UserExtractor do
- let(:text) do
- <<~TXT
- This is a long texth that mentions some users.
- @user-1, @user-2 and user@gitlab.org take a walk in the park.
- There they meet @user-4 that was out with other-user@gitlab.org.
- @user-1 thought it was late, so went home straight away
- TXT
- end
- subject(:extractor) { described_class.new(text) }
-
- describe '#users' do
- it 'returns an empty relation when nil was passed' do
- extractor = described_class.new(nil)
-
- expect(extractor.users).to be_empty
- expect(extractor.users).to be_a(ActiveRecord::Relation)
- end
-
- it 'returns the user case insensitive for usernames' do
- user = create(:user, username: "USER-4")
-
- expect(extractor.users).to include(user)
- end
-
- it 'returns users by primary email' do
- user = create(:user, email: 'user@gitlab.org')
-
- expect(extractor.users).to include(user)
- end
-
- it 'returns users by secondary email' do
- user = create(:email, email: 'other-user@gitlab.org').user
-
- expect(extractor.users).to include(user)
- end
-
- context 'input as array of strings' do
- it 'is treated as one string' do
- extractor = described_class.new(text.lines)
-
- user_1 = create(:user, username: "USER-1")
- user_4 = create(:user, username: "USER-4")
- user_email = create(:user, email: 'user@gitlab.org')
-
- expect(extractor.users).to contain_exactly(user_1, user_4, user_email)
- end
- end
- end
-
- describe '#matches' do
- it 'includes all mentioned email adresses' do
- expect(extractor.matches[:emails]).to contain_exactly('user@gitlab.org', 'other-user@gitlab.org')
- end
-
- it 'includes all mentioned usernames' do
- expect(extractor.matches[:usernames]).to contain_exactly('user-1', 'user-2', 'user-4')
- end
-
- context 'input has no matching e-mail or usernames' do
- it 'returns an empty list of users' do
- extractor = described_class.new('My test')
-
- expect(extractor.users).to be_empty
- end
- end
- end
-
- describe '#references' do
- it 'includes all user-references once' do
- expect(extractor.references).to contain_exactly('user-1', 'user-2', 'user@gitlab.org', 'user-4', 'other-user@gitlab.org')
- end
- end
-end
diff --git a/spec/lib/gitlab/utils/deep_size_spec.rb b/spec/lib/gitlab/utils/deep_size_spec.rb
new file mode 100644
index 00000000000..1e619a15980
--- /dev/null
+++ b/spec/lib/gitlab/utils/deep_size_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Gitlab::Utils::DeepSize do
+ let(:data) do
+ {
+ a: [1, 2, 3],
+ b: {
+ c: [4, 5],
+ d: [
+ { e: [[6], [7]] }
+ ]
+ }
+ }
+ end
+
+ let(:max_size) { 1.kilobyte }
+ let(:max_depth) { 10 }
+ let(:deep_size) { described_class.new(data, max_size: max_size, max_depth: max_depth) }
+
+ describe '#evaluate' do
+ context 'when data within size and depth limits' do
+ it 'returns true' do
+ expect(deep_size).to be_valid
+ end
+ end
+
+ context 'when data not within size limit' do
+ let(:max_size) { 200.bytes }
+
+ it 'returns false' do
+ expect(deep_size).not_to be_valid
+ end
+ end
+
+ context 'when data not within depth limit' do
+ let(:max_depth) { 2 }
+
+ it 'returns false' do
+ expect(deep_size).not_to be_valid
+ end
+ end
+ end
+end
diff --git a/spec/migrations/backfill_and_add_not_null_constraint_to_released_at_column_on_releases_table_spec.rb b/spec/migrations/backfill_and_add_not_null_constraint_to_released_at_column_on_releases_table_spec.rb
new file mode 100644
index 00000000000..9cae1daacea
--- /dev/null
+++ b/spec/migrations/backfill_and_add_not_null_constraint_to_released_at_column_on_releases_table_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20190628185004_backfill_and_add_not_null_constraint_to_released_at_column_on_releases_table.rb')
+
+describe BackfillAndAddNotNullConstraintToReleasedAtColumnOnReleasesTable, :migration do
+ let(:releases) { table(:releases) }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+
+ subject(:migration) { described_class.new }
+
+ it 'fills released_at with the value of created_at' do
+ created_at_a = Time.zone.parse('2019-02-10T08:00:00Z')
+ created_at_b = Time.zone.parse('2019-03-10T18:00:00Z')
+ namespace = namespaces.create(name: 'foo', path: 'foo')
+ project = projects.create!(namespace_id: namespace.id)
+ release_a = releases.create!(project_id: project.id, created_at: created_at_a)
+ release_b = releases.create!(project_id: project.id, created_at: created_at_b)
+
+ disable_migrations_output { migration.up }
+
+ release_a.reload
+ release_b.reload
+ expect(release_a.released_at).to eq(created_at_a)
+ expect(release_b.released_at).to eq(created_at_b)
+ end
+end
diff --git a/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb b/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb
index 34f4a36d63d..65a918d5440 100644
--- a/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb
+++ b/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb
@@ -13,7 +13,7 @@ describe BackfillStoreProjectFullPathInRepo, :migration do
subject(:migration) { described_class.new }
around do |example|
- Sidekiq::Testing.inline! do
+ perform_enqueued_jobs do
example.run
end
end
diff --git a/spec/migrations/remove_project_labels_group_id_spec.rb b/spec/migrations/remove_project_labels_group_id_spec.rb
deleted file mode 100644
index 01b09e71d83..00000000000
--- a/spec/migrations/remove_project_labels_group_id_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# encoding: utf-8
-
-require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20180202111106_remove_project_labels_group_id.rb')
-
-describe RemoveProjectLabelsGroupId, :delete do
- let(:migration) { described_class.new }
- let(:group) { create(:group) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:project_label) { create(:label, group_id: group.id) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:group_label) { create(:group_label) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
-
- describe '#up' do
- it 'updates the project labels group ID' do
- expect { migration.up }.to change { project_label.reload.group_id }.to(nil)
- end
-
- it 'keeps the group labels group ID' do
- expect { migration.up }.not_to change { group_label.reload.group_id }
- end
- end
-end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 55cea48b641..e24bbc39761 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -2998,4 +2998,28 @@ describe Ci::Pipeline, :mailer do
end
end
end
+
+ describe '#error_messages' do
+ subject { pipeline.error_messages }
+
+ before do
+ pipeline.valid?
+ end
+
+ context 'when pipeline has errors' do
+ let(:pipeline) { build(:ci_pipeline, sha: nil, ref: nil) }
+
+ it 'returns the full error messages' do
+ is_expected.to eq("Sha can't be blank and Ref can't be blank")
+ end
+ end
+
+ context 'when pipeline does not have errors' do
+ let(:pipeline) { build(:ci_pipeline) }
+
+ it 'returns empty string' do
+ is_expected.to be_empty
+ end
+ end
+ end
end
diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb
index 2fe82eaa778..8d951ab6f0f 100644
--- a/spec/models/deploy_token_spec.rb
+++ b/spec/models/deploy_token_spec.rb
@@ -8,6 +8,15 @@ describe DeployToken do
it { is_expected.to have_many :project_deploy_tokens }
it { is_expected.to have_many(:projects).through(:project_deploy_tokens) }
+ describe 'validations' do
+ let(:username_format_message) { "can contain only letters, digits, '_', '-', '+', and '.'" }
+
+ it { is_expected.to validate_length_of(:username).is_at_most(255) }
+ it { is_expected.to allow_value('GitLab+deploy_token-3.14').for(:username) }
+ it { is_expected.not_to allow_value('<script>').for(:username).with_message(username_format_message) }
+ it { is_expected.not_to allow_value('').for(:username).with_message(username_format_message) }
+ end
+
describe '#ensure_token' do
it 'ensures a token' do
deploy_token.token = nil
@@ -87,8 +96,30 @@ describe DeployToken do
end
describe '#username' do
- it 'returns a harcoded username' do
- expect(deploy_token.username).to eq("gitlab+deploy-token-#{deploy_token.id}")
+ context 'persisted records' do
+ it 'returns a default username if none is set' do
+ expect(deploy_token.username).to eq("gitlab+deploy-token-#{deploy_token.id}")
+ end
+
+ it 'returns the username provided if one is set' do
+ deploy_token = create(:deploy_token, username: 'deployer')
+
+ expect(deploy_token.username).to eq('deployer')
+ end
+ end
+
+ context 'new records' do
+ it 'returns nil if no username is set' do
+ deploy_token = build(:deploy_token)
+
+ expect(deploy_token.username).to be_nil
+ end
+
+ it 'returns the username provided if one is set' do
+ deploy_token = build(:deploy_token, username: 'deployer')
+
+ expect(deploy_token.username).to eq('deployer')
+ end
end
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 8d0eb0f4a06..713fb647708 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -462,12 +462,6 @@ describe Deployment do
it { is_expected.to be_nil }
end
- context 'project uses the kubernetes service for deployments' do
- let!(:service) { create(:kubernetes_service, project: project) }
-
- it { is_expected.to be_nil }
- end
-
context 'project has a deployment platform' do
let!(:cluster) { create(:cluster, projects: [project]) }
let!(:platform) { create(:cluster_platform_kubernetes, cluster: cluster) }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index a2547755510..fe6d68aff3f 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -7,6 +7,8 @@ describe MergeRequest do
include ProjectForksHelper
include ReactiveCachingHelpers
+ using RSpec::Parameterized::TableSyntax
+
subject { create(:merge_request) }
describe 'associations' do
@@ -1996,6 +1998,47 @@ describe MergeRequest do
end
end
+ describe '#rebase_async' do
+ let(:merge_request) { create(:merge_request) }
+ let(:user_id) { double(:user_id) }
+ let(:rebase_jid) { 'rebase-jid' }
+
+ subject(:execute) { merge_request.rebase_async(user_id) }
+
+ it 'atomically enqueues a RebaseWorker job and updates rebase_jid' do
+ expect(RebaseWorker)
+ .to receive(:perform_async)
+ .with(merge_request.id, user_id)
+ .and_return(rebase_jid)
+
+ expect(merge_request).to receive(:lock!).and_call_original
+
+ execute
+
+ expect(merge_request.rebase_jid).to eq(rebase_jid)
+ end
+
+ it 'refuses to enqueue a job if a rebase is in progress' do
+ merge_request.update_column(:rebase_jid, rebase_jid)
+
+ expect(RebaseWorker).not_to receive(:perform_async)
+ expect(Gitlab::SidekiqStatus)
+ .to receive(:running?)
+ .with(rebase_jid)
+ .and_return(true)
+
+ expect { execute }.to raise_error(ActiveRecord::StaleObjectError)
+ end
+
+ it 'refuses to enqueue a job if the MR is not open' do
+ merge_request.update_column(:state, 'foo')
+
+ expect(RebaseWorker).not_to receive(:perform_async)
+
+ expect { execute }.to raise_error(ActiveRecord::StaleObjectError)
+ end
+ end
+
describe '#mergeable?' do
let(:project) { create(:project) }
@@ -2946,40 +2989,64 @@ describe MergeRequest do
end
describe '#rebase_in_progress?' do
- shared_examples 'checking whether a rebase is in progress' do
- let(:repo_path) do
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- subject.source_project.repository.path
- end
- end
- let(:rebase_path) { File.join(repo_path, "gitlab-worktree", "rebase-#{subject.id}") }
+ where(:rebase_jid, :jid_valid, :result) do
+ 'foo' | true | true
+ 'foo' | false | false
+ '' | true | false
+ nil | true | false
+ end
- before do
- system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{rebase_path} master))
+ with_them do
+ let(:merge_request) { create(:merge_request) }
+
+ subject { merge_request.rebase_in_progress? }
+
+ it do
+ # Stub out the legacy gitaly implementation
+ allow(merge_request).to receive(:gitaly_rebase_in_progress?) { false }
+
+ allow(Gitlab::SidekiqStatus).to receive(:running?).with(rebase_jid) { jid_valid }
+
+ merge_request.rebase_jid = rebase_jid
+
+ is_expected.to eq(result)
end
+ end
+ end
- it 'returns true when there is a current rebase directory' do
- expect(subject.rebase_in_progress?).to be_truthy
+ describe '#gitaly_rebase_in_progress?' do
+ let(:repo_path) do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ subject.source_project.repository.path
end
+ end
+ let(:rebase_path) { File.join(repo_path, "gitlab-worktree", "rebase-#{subject.id}") }
+
+ before do
+ system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{rebase_path} master))
+ end
- it 'returns false when there is no rebase directory' do
- FileUtils.rm_rf(rebase_path)
+ it 'returns true when there is a current rebase directory' do
+ expect(subject.rebase_in_progress?).to be_truthy
+ end
- expect(subject.rebase_in_progress?).to be_falsey
- end
+ it 'returns false when there is no rebase directory' do
+ FileUtils.rm_rf(rebase_path)
- it 'returns false when the rebase directory has expired' do
- time = 20.minutes.ago.to_time
- File.utime(time, time, rebase_path)
+ expect(subject.rebase_in_progress?).to be_falsey
+ end
- expect(subject.rebase_in_progress?).to be_falsey
- end
+ it 'returns false when the rebase directory has expired' do
+ time = 20.minutes.ago.to_time
+ File.utime(time, time, rebase_path)
- it 'returns false when the source project has been removed' do
- allow(subject).to receive(:source_project).and_return(nil)
+ expect(subject.rebase_in_progress?).to be_falsey
+ end
- expect(subject.rebase_in_progress?).to be_falsey
- end
+ it 'returns false when the source project has been removed' do
+ allow(subject).to receive(:source_project).and_return(nil)
+
+ expect(subject.rebase_in_progress?).to be_falsey
end
end
diff --git a/spec/models/namespace/aggregation_schedule_spec.rb b/spec/models/namespace/aggregation_schedule_spec.rb
index 8ed0248e1b2..0f1283717e0 100644
--- a/spec/models/namespace/aggregation_schedule_spec.rb
+++ b/spec/models/namespace/aggregation_schedule_spec.rb
@@ -53,10 +53,39 @@ RSpec.describe Namespace::AggregationSchedule, :clean_gitlab_redis_shared_state,
expect(Namespaces::RootStatisticsWorker)
.to receive(:perform_in).once
- .with(described_class::DEFAULT_LEASE_TIMEOUT, aggregation_schedule.namespace_id )
+ .with(described_class::DEFAULT_LEASE_TIMEOUT, aggregation_schedule.namespace_id)
aggregation_schedule.save!
end
+
+ it 'does not release the lease' do
+ stub_exclusive_lease(lease_key, timeout: described_class::DEFAULT_LEASE_TIMEOUT)
+
+ aggregation_schedule.save!
+
+ exclusive_lease = aggregation_schedule.exclusive_lease
+ expect(exclusive_lease.exists?).to be_truthy
+ end
+
+ it 'only executes the workers once' do
+ # Avoid automatic deletion of Namespace::AggregationSchedule
+ # for testing purposes.
+ expect(Namespaces::RootStatisticsWorker)
+ .to receive(:perform_async).once
+ .and_return(nil)
+
+ expect(Namespaces::RootStatisticsWorker)
+ .to receive(:perform_in).once
+ .with(described_class::DEFAULT_LEASE_TIMEOUT, aggregation_schedule.namespace_id)
+ .and_return(nil)
+
+ # Scheduling workers for the first time
+ aggregation_schedule.schedule_root_storage_statistics
+
+ # Executing again, this time workers should not be scheduled
+ # due to the lease not been released.
+ aggregation_schedule.schedule_root_storage_statistics
+ end
end
context 'with a personalized lease timeout' do
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 5d7d6c34e67..d33bbb0470f 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -142,235 +142,6 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
end
end
- describe '#kubernetes_namespace_for' do
- subject { service.kubernetes_namespace_for(project) }
-
- shared_examples 'a correctly formatted namespace' do
- it 'returns a valid Kubernetes namespace name' do
- expect(subject).to match(Gitlab::Regex.kubernetes_namespace_regex)
- expect(subject).to eq(expected_namespace)
- end
- end
-
- it_behaves_like 'a correctly formatted namespace' do
- let(:expected_namespace) { service.send(:default_namespace) }
- end
-
- context 'when the project path contains forbidden characters' do
- before do
- project.path = '-a_Strange.Path--forSure'
- end
-
- it_behaves_like 'a correctly formatted namespace' do
- let(:expected_namespace) { "a-strange-path--forsure-#{project.id}" }
- end
- end
-
- context 'when namespace is specified' do
- before do
- service.namespace = 'my-namespace'
- end
-
- it_behaves_like 'a correctly formatted namespace' do
- let(:expected_namespace) { 'my-namespace' }
- end
- end
-
- context 'when service is not assigned to project' do
- before do
- service.project = nil
- end
-
- it 'does not return namespace' do
- is_expected.to be_nil
- end
- end
- end
-
- describe '#test' do
- let(:discovery_url) { 'https://kubernetes.example.com/api/v1' }
-
- before do
- stub_kubeclient_discover(service.api_url)
- end
-
- context 'with path prefix in api_url' do
- let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' }
-
- it 'tests with the prefix' do
- service.api_url = 'https://kubernetes.example.com/prefix'
- stub_kubeclient_discover(service.api_url)
-
- expect(service.test[:success]).to be_truthy
- expect(WebMock).to have_requested(:get, discovery_url).once
- end
- end
-
- context 'with custom CA certificate' do
- it 'is added to the certificate store' do
- service.ca_pem = "CA PEM DATA"
-
- cert = double("certificate")
- expect(OpenSSL::X509::Certificate).to receive(:new).with(service.ca_pem).and_return(cert)
- expect_any_instance_of(OpenSSL::X509::Store).to receive(:add_cert).with(cert)
-
- expect(service.test[:success]).to be_truthy
- expect(WebMock).to have_requested(:get, discovery_url).once
- end
- end
-
- context 'success' do
- it 'reads the discovery endpoint' do
- expect(service.test[:success]).to be_truthy
- expect(WebMock).to have_requested(:get, discovery_url).once
- end
- end
-
- context 'failure' do
- it 'fails to read the discovery endpoint' do
- WebMock.stub_request(:get, service.api_url + '/api/v1').to_return(status: 404)
-
- expect(service.test[:success]).to be_falsy
- expect(WebMock).to have_requested(:get, discovery_url).once
- end
- end
- end
-
- describe '#predefined_variable' do
- let(:kubeconfig) do
- config_file = expand_fixture_path('config/kubeconfig.yml')
- config = YAML.load(File.read(config_file))
- config.dig('users', 0, 'user')['token'] = 'token'
- config.dig('contexts', 0, 'context')['namespace'] = namespace
- config.dig('clusters', 0, 'cluster')['certificate-authority-data'] =
- Base64.strict_encode64('CA PEM DATA')
-
- YAML.dump(config)
- end
-
- before do
- subject.api_url = 'https://kube.domain.com'
- subject.token = 'token'
- subject.ca_pem = 'CA PEM DATA'
- subject.project = project
- end
-
- shared_examples 'setting variables' do
- it 'sets the variables' do
- expect(subject.predefined_variables(project: project)).to include(
- { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true },
- { key: 'KUBE_TOKEN', value: 'token', public: false, masked: true },
- { key: 'KUBE_NAMESPACE', value: namespace, public: true },
- { key: 'KUBECONFIG', value: kubeconfig, public: false, file: true },
- { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true },
- { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true }
- )
- end
- end
-
- context 'namespace is provided' do
- let(:namespace) { 'my-project' }
-
- before do
- subject.namespace = namespace
- end
-
- it_behaves_like 'setting variables'
- end
-
- context 'no namespace provided' do
- let(:namespace) { subject.kubernetes_namespace_for(project) }
-
- it_behaves_like 'setting variables'
-
- it 'sets the KUBE_NAMESPACE' do
- kube_namespace = subject.predefined_variables(project: project).find { |h| h[:key] == 'KUBE_NAMESPACE' }
-
- expect(kube_namespace).not_to be_nil
- expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/)
- end
- end
- end
-
- describe '#terminals' do
- let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") }
-
- subject { service.terminals(environment) }
-
- context 'with invalid pods' do
- it 'returns no terminals' do
- stub_reactive_cache(service, pods: [{ "bad" => "pod" }])
-
- is_expected.to be_empty
- end
- end
-
- context 'with valid pods' do
- let(:pod) { kube_pod(environment_slug: environment.slug, namespace: service.kubernetes_namespace_for(project), project_slug: project.full_path_slug) }
- let(:pod_with_no_terminal) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug, status: "Pending") }
- let(:terminals) { kube_terminals(service, pod) }
-
- before do
- stub_reactive_cache(
- service,
- pods: [pod, pod, pod_with_no_terminal, kube_pod(environment_slug: "should-be-filtered-out")]
- )
- end
-
- it 'returns terminals' do
- is_expected.to eq(terminals + terminals)
- end
-
- it 'uses max session time from settings' do
- stub_application_setting(terminal_max_session_time: 600)
-
- times = subject.map { |terminal| terminal[:max_session_time] }
- expect(times).to eq [600, 600, 600, 600]
- end
- end
- end
-
- describe '#calculate_reactive_cache' do
- subject { service.calculate_reactive_cache }
-
- let(:namespace) { service.kubernetes_namespace_for(project) }
-
- context 'when service is inactive' do
- before do
- service.active = false
- end
-
- it { is_expected.to be_nil }
- end
-
- context 'when kubernetes responds with valid pods' do
- before do
- stub_kubeclient_pods(namespace)
- stub_kubeclient_deployments(namespace) # Used by EE
- end
-
- it { is_expected.to include(pods: [kube_pod]) }
- end
-
- context 'when kubernetes responds with 500s' do
- before do
- stub_kubeclient_pods(namespace, status: 500)
- stub_kubeclient_deployments(namespace, status: 500) # Used by EE
- end
-
- it { expect { subject }.to raise_error(Kubeclient::HttpError) }
- end
-
- context 'when kubernetes responds with 404s' do
- before do
- stub_kubeclient_pods(namespace, status: 404)
- stub_kubeclient_deployments(namespace, status: 404) # Used by EE
- end
-
- it { is_expected.to include(pods: []) }
- end
- end
-
describe "#deprecated?" do
let(:kubernetes_service) { create(:kubernetes_service) }
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index 7c106ce6b85..e9d846e7291 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -64,4 +64,14 @@ RSpec.describe Release do
is_expected.to all(be_a(Releases::Source))
end
end
+
+ describe '#upcoming_release?' do
+ context 'during the backfill migration when released_at could be nil' do
+ it 'handles a nil released_at value and returns false' do
+ allow(release).to receive(:released_at).and_return nil
+
+ expect(release.upcoming_release?).to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 13da7bd7407..3d967aa4ab8 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1420,12 +1420,13 @@ describe Repository do
source_project: project)
end
- it 'writes merge of source and target to MR merge_ref_path' do
+ it 'writes merge of source SHA and first parent ref to MR merge_ref_path' do
merge_commit_id = repository.merge_to_ref(user,
merge_request.diff_head_sha,
merge_request,
merge_request.merge_ref_path,
- 'Custom message')
+ 'Custom message',
+ merge_request.target_branch_ref)
merge_commit = repository.commit(merge_commit_id)
diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb
index de1cd9586b6..63fa16c79ca 100644
--- a/spec/requests/api/graphql/namespace/projects_spec.rb
+++ b/spec/requests/api/graphql/namespace/projects_spec.rb
@@ -58,9 +58,7 @@ describe 'getting projects', :nested_groups 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)
+ expect(graphql_data['namespace']).to be_nil
end
end
end
diff --git a/spec/requests/api/graphql/project/repository_spec.rb b/spec/requests/api/graphql/project/repository_spec.rb
index 67af612a4a0..261433a3d6a 100644
--- a/spec/requests/api/graphql/project/repository_spec.rb
+++ b/spec/requests/api/graphql/project/repository_spec.rb
@@ -34,4 +34,28 @@ describe 'getting a repository in a project' do
expect(graphql_data['project']).to be(nil)
end
end
+
+ context 'when the repository is only accessible to members' do
+ let(:project) do
+ create(:project, :public, :repository, repository_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'returns a repository for the owner' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']['repository']).not_to be_nil
+ end
+
+ it 'returns nil for the repository for other users' do
+ post_graphql(query, current_user: create(:user))
+
+ expect(graphql_data['project']['repository']).to be_nil
+ end
+
+ it 'returns nil for the repository for other users' do
+ post_graphql(query, current_user: nil)
+
+ expect(graphql_data['project']['repository']).to be_nil
+ end
+ end
end
diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb
index 656d6f8b50b..67371cb35b6 100644
--- a/spec/requests/api/graphql_spec.rb
+++ b/spec/requests/api/graphql_spec.rb
@@ -131,4 +131,35 @@ describe 'GraphQL' do
end
end
end
+
+ describe 'testing for Gitaly calls' do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ let(:query) do
+ graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id))
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: user)
+ end
+ end
+
+ context 'when Gitaly is called' do
+ before do
+ allow(Gitlab::GitalyClient).to receive(:get_request_count).and_return(1, 2)
+ end
+
+ it "logs a warning that the 'calls_gitaly' field declaration is missing" do
+ expect(Gitlab::Sentry).to receive(:track_exception).once
+
+ post_graphql(query, current_user: user)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb
index 8b02cf56e9f..9a41d790945 100644
--- a/spec/requests/api/issues/get_group_issues_spec.rb
+++ b/spec/requests/api/issues/get_group_issues_spec.rb
@@ -23,7 +23,11 @@ describe API::Issues do
describe 'GET /groups/:id/issues' do
let!(:group) { create(:group) }
- let!(:group_project) { create(:project, :public, creator_id: user.id, namespace: group) }
+ let!(:group_project) { create(:project, :public, :repository, creator_id: user.id, namespace: group) }
+ let!(:private_mrs_project) do
+ create(:project, :public, :repository, creator_id: user.id, namespace: group, merge_requests_access_level: ProjectFeature::PRIVATE)
+ end
+
let!(:group_closed_issue) do
create :closed_issue,
author: user,
@@ -234,6 +238,30 @@ describe API::Issues do
it_behaves_like 'group issues statistics'
end
end
+
+ context "when returns issue merge_requests_count for different access levels" do
+ let!(:merge_request1) do
+ create(:merge_request,
+ :simple,
+ author: user,
+ source_project: private_mrs_project,
+ target_project: private_mrs_project,
+ description: "closes #{group_issue.to_reference(private_mrs_project)}")
+ end
+ let!(:merge_request2) do
+ create(:merge_request,
+ :simple,
+ author: user,
+ source_project: group_project,
+ target_project: group_project,
+ description: "closes #{group_issue.to_reference}")
+ end
+
+ it_behaves_like 'accessible merge requests count' do
+ let(:api_url) { base_url }
+ let(:target_issue) { group_issue }
+ end
+ end
end
end
diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb
index 0b0f754ab57..f7ca6fd1e0a 100644
--- a/spec/requests/api/issues/get_project_issues_spec.rb
+++ b/spec/requests/api/issues/get_project_issues_spec.rb
@@ -4,8 +4,9 @@ require 'spec_helper'
describe API::Issues do
set(:user) { create(:user) }
- set(:project) do
- create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ set(:project) { create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace) }
+ set(:private_mrs_project) do
+ create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace, merge_requests_access_level: ProjectFeature::PRIVATE)
end
let(:user2) { create(:user) }
@@ -60,9 +61,28 @@ describe API::Issues do
let(:no_milestone_title) { 'None' }
let(:any_milestone_title) { 'Any' }
+ let!(:merge_request1) do
+ create(:merge_request,
+ :simple,
+ author: user,
+ source_project: project,
+ target_project: project,
+ description: "closes #{issue.to_reference}")
+ end
+ let!(:merge_request2) do
+ create(:merge_request,
+ :simple,
+ author: user,
+ source_project: private_mrs_project,
+ target_project: private_mrs_project,
+ description: "closes #{issue.to_reference(private_mrs_project)}")
+ end
+
before(:all) do
project.add_reporter(user)
project.add_guest(guest)
+ private_mrs_project.add_reporter(user)
+ private_mrs_project.add_guest(guest)
end
before do
@@ -257,6 +277,11 @@ describe API::Issues do
expect_paginated_array_response(issue.id)
end
+ it_behaves_like 'accessible merge requests count' do
+ let(:api_url) { "/projects/#{project.id}/issues" }
+ let(:target_issue) { issue }
+ end
+
context 'with labeled issues' do
let(:label_b) { create(:label, title: 'foo', project: project) }
let(:label_c) { create(:label, title: 'bar', project: project) }
@@ -636,34 +661,26 @@ describe API::Issues do
expect(json_response['iid']).to eq(confidential_issue.iid)
end
end
- end
-
- describe 'GET :id/issues/:issue_iid/closed_by' do
- let(:merge_request) do
- create(:merge_request,
- :simple,
- author: user,
- source_project: project,
- target_project: project,
- description: "closes #{issue.to_reference}")
- end
- before do
- create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
+ it_behaves_like 'accessible merge requests count' do
+ let(:api_url) { "/projects/#{project.id}/issues/#{issue.iid}" }
+ let(:target_issue) { issue }
end
+ end
+ describe 'GET :id/issues/:issue_iid/closed_by' do
context 'when unauthenticated' do
it 'return public project issues' do
get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by")
- expect_paginated_array_response(merge_request.id)
+ expect_paginated_array_response(merge_request1.id)
end
end
it 'returns merge requests that will close issue on merge' do
get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by", user)
- expect_paginated_array_response(merge_request.id)
+ expect_paginated_array_response(merge_request1.id)
end
context 'when no merge requests will close issue' do
@@ -721,13 +738,6 @@ describe API::Issues do
end
it 'returns merge requests that mentioned a issue' do
- create(:merge_request,
- :simple,
- author: user,
- source_project: project,
- target_project: project,
- description: 'Some description')
-
get_related_merge_requests(project.id, issue.iid, user)
expect_paginated_array_response(related_mr.id)
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index f32ffd1c77b..d195f54be11 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -4,8 +4,9 @@ require 'spec_helper'
describe API::Issues do
set(:user) { create(:user) }
- set(:project) do
- create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ set(:project) { create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace) }
+ set(:private_mrs_project) do
+ create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace, merge_requests_access_level: ProjectFeature::PRIVATE)
end
let(:user2) { create(:user) }
@@ -63,6 +64,8 @@ describe API::Issues do
before(:all) do
project.add_reporter(user)
project.add_guest(guest)
+ private_mrs_project.add_reporter(user)
+ private_mrs_project.add_guest(guest)
end
before do
@@ -725,6 +728,30 @@ describe API::Issues do
end
end
end
+
+ context "when returns issue merge_requests_count for different access levels" do
+ let!(:merge_request1) do
+ create(:merge_request,
+ :simple,
+ author: user,
+ source_project: private_mrs_project,
+ target_project: private_mrs_project,
+ description: "closes #{issue.to_reference(private_mrs_project)}")
+ end
+ let!(:merge_request2) do
+ create(:merge_request,
+ :simple,
+ author: user,
+ source_project: project,
+ target_project: project,
+ description: "closes #{issue.to_reference}")
+ end
+
+ it_behaves_like 'accessible merge requests count' do
+ let(:api_url) { "/issues" }
+ let(:target_issue) { issue }
+ end
+ end
end
describe 'DELETE /projects/:id/issues/:issue_iid' do
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 76d093e0774..ced853caab4 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -834,6 +834,31 @@ describe API::MergeRequests do
end
end
+ context 'head_pipeline' do
+ before do
+ merge_request.update(head_pipeline: create(:ci_pipeline))
+ merge_request.project.project_feature.update(builds_access_level: 10)
+ end
+
+ context 'when user can read the pipeline' do
+ it 'exposes pipeline information' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
+
+ expect(json_response).to include('head_pipeline')
+ end
+ end
+
+ context 'when user can not read the pipeline' do
+ let(:guest) { create(:user) }
+
+ it 'does not expose pipeline information' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", guest)
+
+ expect(json_response).not_to include('head_pipeline')
+ end
+ end
+ end
+
it 'returns the commits behind the target branch when include_diverged_commits_count is present' do
allow_any_instance_of(merge_request.class).to receive(:diverged_commits_count).and_return(1)
@@ -2008,6 +2033,9 @@ describe API::MergeRequests do
expect(response).to have_gitlab_http_status(202)
expect(RebaseWorker.jobs.size).to eq(1)
+
+ expect(merge_request.reload).to be_rebase_in_progress
+ expect(json_response['rebase_in_progress']).to be(true)
end
it 'returns 403 if the user cannot push to the branch' do
@@ -2018,6 +2046,16 @@ describe API::MergeRequests do
expect(response).to have_gitlab_http_status(403)
end
+
+ it 'returns 409 if a rebase is already in progress' do
+ Sidekiq::Testing.fake! do
+ merge_request.rebase_async(user.id)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/rebase", user)
+ end
+
+ expect(response).to have_gitlab_http_status(409)
+ end
end
describe 'Time tracking' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 5f7d2fa6d9c..c67412a44c1 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -2428,7 +2428,7 @@ describe API::Projects do
let(:housekeeping) { Projects::HousekeepingService.new(project) }
before do
- allow(Projects::HousekeepingService).to receive(:new).with(project).and_return(housekeeping)
+ allow(Projects::HousekeepingService).to receive(:new).with(project, :gc).and_return(housekeeping)
end
context 'when authenticated as owner' do
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index 8603fa2a73d..206e898381d 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -24,7 +24,7 @@ describe API::Releases do
project: project,
tag: 'v0.1',
author: maintainer,
- created_at: 2.days.ago)
+ released_at: 2.days.ago)
end
let!(:release_2) do
@@ -32,7 +32,7 @@ describe API::Releases do
project: project,
tag: 'v0.2',
author: maintainer,
- created_at: 1.day.ago)
+ released_at: 1.day.ago)
end
it 'returns 200 HTTP status' do
@@ -41,7 +41,7 @@ describe API::Releases do
expect(response).to have_gitlab_http_status(:ok)
end
- it 'returns releases ordered by created_at' do
+ it 'returns releases ordered by released_at' do
get api("/projects/#{project.id}/releases", maintainer)
expect(json_response.count).to eq(2)
@@ -56,6 +56,26 @@ describe API::Releases do
end
end
+ it 'returns an upcoming_release status for a future release' do
+ tomorrow = Time.now.utc + 1.day
+ create(:release, project: project, tag: 'v0.1', author: maintainer, released_at: tomorrow)
+
+ get api("/projects/#{project.id}/releases", maintainer)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.first['upcoming_release']).to eq(true)
+ end
+
+ it 'returns an upcoming_release status for a past release' do
+ yesterday = Time.now.utc - 1.day
+ create(:release, project: project, tag: 'v0.1', author: maintainer, released_at: yesterday)
+
+ get api("/projects/#{project.id}/releases", maintainer)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.first['upcoming_release']).to eq(false)
+ end
+
context 'when tag does not exist in git repository' do
let!(:release) { create(:release, project: project, tag: 'v1.1.5') }
@@ -316,6 +336,51 @@ describe API::Releases do
expect(project.releases.last.description).to eq('Super nice release')
end
+ it 'sets the released_at to the current time if the released_at parameter is not provided' do
+ now = Time.zone.parse('2015-08-25 06:00:00Z')
+ Timecop.freeze(now) do
+ post api("/projects/#{project.id}/releases", maintainer), params: params
+
+ expect(project.releases.last.released_at).to eq(now)
+ end
+ end
+
+ it 'sets the released_at to the value in the parameters if specified' do
+ params = {
+ name: 'New release',
+ tag_name: 'v0.1',
+ description: 'Super nice release',
+ released_at: '2019-03-20T10:00:00Z'
+ }
+ post api("/projects/#{project.id}/releases", maintainer), params: params
+
+ expect(project.releases.last.released_at).to eq('2019-03-20T10:00:00Z')
+ end
+
+ it 'assumes the utc timezone for released_at if the timezone is not provided' do
+ params = {
+ name: 'New release',
+ tag_name: 'v0.1',
+ description: 'Super nice release',
+ released_at: '2019-03-25 10:00:00'
+ }
+ post api("/projects/#{project.id}/releases", maintainer), params: params
+
+ expect(project.releases.last.released_at).to eq('2019-03-25T10:00:00Z')
+ end
+
+ it 'allows specifying a released_at with a local time zone' do
+ params = {
+ name: 'New release',
+ tag_name: 'v0.1',
+ description: 'Super nice release',
+ released_at: '2019-03-25T10:00:00+09:00'
+ }
+ post api("/projects/#{project.id}/releases", maintainer), params: params
+
+ expect(project.releases.last.released_at).to eq('2019-03-25T01:00:00Z')
+ end
+
context 'when description is empty' do
let(:params) do
{
@@ -540,6 +605,7 @@ describe API::Releases do
project: project,
tag: 'v0.1',
name: 'New release',
+ released_at: '2018-03-01T22:00:00Z',
description: 'Super nice release')
end
@@ -560,6 +626,7 @@ describe API::Releases do
expect(project.releases.last.tag).to eq('v0.1')
expect(project.releases.last.name).to eq('New release')
+ expect(project.releases.last.released_at).to eq('2018-03-01T22:00:00Z')
end
it 'matches response schema' do
@@ -568,6 +635,14 @@ describe API::Releases do
expect(response).to match_response_schema('public_api/v4/release')
end
+ it 'updates released_at' do
+ params = { released_at: '2015-10-10T05:00:00Z' }
+
+ put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
+
+ expect(project.releases.last.released_at).to eq('2015-10-10T05:00:00Z')
+ end
+
context 'when user tries to update sha' do
let(:params) { { sha: 'xxx' } }
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 5548e3fd01a..f5ce3a3570e 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -584,6 +584,34 @@ describe API::Runners do
end
end
+ context 'when valid order_by is provided' do
+ context 'when sort order is not specified' do
+ it 'return jobs in descending order' do
+ get api("/runners/#{project_runner.id}/jobs?order_by=id", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+
+ expect(json_response).to be_an(Array)
+ expect(json_response.length).to eq(2)
+ expect(json_response.first).to include('id' => job_5.id)
+ end
+ end
+
+ context 'when sort order is specified as asc' do
+ it 'return jobs sorted in ascending order' do
+ get api("/runners/#{project_runner.id}/jobs?order_by=id&sort=asc", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+
+ expect(json_response).to be_an(Array)
+ expect(json_response.length).to eq(2)
+ expect(json_response.first).to include('id' => job_4.id)
+ end
+ end
+ end
+
context 'when invalid status is provided' do
it 'return 400' do
get api("/runners/#{project_runner.id}/jobs?status=non-existing", admin)
@@ -591,6 +619,22 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(400)
end
end
+
+ context 'when invalid order_by is provided' do
+ it 'return 400' do
+ get api("/runners/#{project_runner.id}/jobs?order_by=non-existing", admin)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+
+ context 'when invalid sort is provided' do
+ it 'return 400' do
+ get api("/runners/#{project_runner.id}/jobs?sort=non-existing", admin)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
end
context "when runner doesn't exist" do
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 89adbc77a7f..d832963292c 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -102,6 +102,27 @@ describe 'Rack Attack global throttles' do
expect_rejection { get(*get_args) }
end
+
+ it 'logs RackAttack info into structured logs' do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ arguments = {
+ message: 'Rack_Attack',
+ env: :throttle,
+ ip: '127.0.0.1',
+ request_method: 'GET',
+ fullpath: get_args.first,
+ user_id: user.id,
+ username: user.username
+ }
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
+
+ expect_rejection { get(*get_args) }
+ end
end
context 'when the throttle is disabled' do
@@ -189,7 +210,15 @@ describe 'Rack Attack global throttles' do
expect(response).to have_http_status 200
end
- expect(Gitlab::AuthLogger).to receive(:error).once
+ arguments = {
+ message: 'Rack_Attack',
+ env: :throttle,
+ ip: '127.0.0.1',
+ request_method: 'GET',
+ fullpath: '/users/sign_in'
+ }
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(arguments)
get url_that_does_not_require_authentication
end
@@ -345,7 +374,17 @@ describe 'Rack Attack global throttles' do
expect(response).to have_http_status 200
end
- expect(Gitlab::AuthLogger).to receive(:error).once
+ arguments = {
+ message: 'Rack_Attack',
+ env: :throttle,
+ ip: '127.0.0.1',
+ request_method: 'GET',
+ fullpath: '/dashboard/snippets',
+ user_id: user.id,
+ username: user.username
+ }
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
get url_that_requires_authentication
end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 83775b1040e..6dde40d1cb6 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -693,4 +693,24 @@ describe 'project routing' do
it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/settings/repository", "/gitlab/gitlabhq/-/settings/repository"
end
+
+ describe Projects::TemplatesController, 'routing' do
+ describe '#show' do
+ def show_with_template_type(template_type)
+ "/gitlab/gitlabhq/templates/#{template_type}/template_name"
+ end
+
+ it 'routes when :template_type is `merge_request`' do
+ expect(get(show_with_template_type('merge_request'))).to route_to('projects/templates#show', namespace_id: 'gitlab', project_id: 'gitlabhq', template_type: 'merge_request', key: 'template_name', format: 'json')
+ end
+
+ it 'routes when :template_type is `issue`' do
+ expect(get(show_with_template_type('issue'))).to route_to('projects/templates#show', namespace_id: 'gitlab', project_id: 'gitlabhq', template_type: 'issue', key: 'template_name', format: 'json')
+ end
+
+ it 'routes to application#route_not_found when :template_type is unknown' do
+ expect(get(show_with_template_type('invalid'))).to route_to('application#route_not_found', unmatched_route: 'gitlab/gitlabhq/templates/invalid/template_name')
+ end
+ end
+ end
end
diff --git a/spec/routing/uploads_routing_spec.rb b/spec/routing/uploads_routing_spec.rb
index 6a041ffdd6c..42e84774088 100644
--- a/spec/routing/uploads_routing_spec.rb
+++ b/spec/routing/uploads_routing_spec.rb
@@ -12,10 +12,19 @@ describe 'Uploads', 'routing' do
)
end
+ it 'allows creating uploads for users' do
+ expect(post('/uploads/user?id=1')).to route_to(
+ controller: 'uploads',
+ action: 'create',
+ model: 'user',
+ id: '1'
+ )
+ end
+
it 'does not allow creating uploads for other models' do
- UploadsController::MODEL_CLASSES.keys.compact.each do |model|
- next if model == 'personal_snippet'
+ unroutable_models = UploadsController::MODEL_CLASSES.keys.compact - %w(personal_snippet user)
+ unroutable_models.each do |model|
expect(post("/uploads/#{model}?id=1")).not_to be_routable
end
end
diff --git a/spec/rubocop/cop/graphql/authorize_types_spec.rb b/spec/rubocop/cop/graphql/authorize_types_spec.rb
new file mode 100644
index 00000000000..eae3e176d64
--- /dev/null
+++ b/spec/rubocop/cop/graphql/authorize_types_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../../rubocop/cop/graphql/authorize_types'
+
+describe RuboCop::Cop::Graphql::AuthorizeTypes do
+ include RuboCop::RSpec::ExpectOffense
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'when in a type folder' do
+ before do
+ allow(cop).to receive(:in_type?).and_return(true)
+ end
+
+ it 'adds an offense when there is no authorize call' do
+ inspect_source(<<~TYPE)
+ module Types
+ class AType < BaseObject
+ field :a_thing
+ field :another_thing
+ end
+ end
+ TYPE
+
+ expect(cop.offenses.size).to eq 1
+ end
+
+ it 'does not add an offense for classes that have an authorize call' do
+ expect_no_offenses(<<~TYPE.strip)
+ module Types
+ class AType < BaseObject
+ graphql_name 'ATypeName'
+
+ authorize :an_ability, :second_ability
+
+ field :a_thing
+ end
+ end
+ TYPE
+ end
+
+ it 'does not add an offense for classes that only have an authorize call' do
+ expect_no_offenses(<<~TYPE.strip)
+ module Types
+ class AType < SuperClassWithFields
+ authorize :an_ability
+ end
+ end
+ TYPE
+ end
+
+ it 'does not add an offense for base types' do
+ expect_no_offenses(<<~TYPE)
+ module Types
+ class AType < BaseEnum
+ field :a_thing
+ end
+ end
+ TYPE
+ end
+ end
+end
diff --git a/spec/serializers/test_case_entity_spec.rb b/spec/serializers/test_case_entity_spec.rb
index 986c9feb07b..cc5f086ca4e 100644
--- a/spec/serializers/test_case_entity_spec.rb
+++ b/spec/serializers/test_case_entity_spec.rb
@@ -24,7 +24,7 @@ describe TestCaseEntity do
it 'contains correct test case details' do
expect(subject[:status]).to eq('failed')
- expect(subject[:name]).to eq('Test#sum when a is 2 and b is 2 returns summary')
+ expect(subject[:name]).to eq('Test#sum when a is 1 and b is 3 returns summary')
expect(subject[:classname]).to eq('spec.test_spec')
expect(subject[:execution_time]).to eq(2.22)
end
diff --git a/spec/serializers/test_reports_comparer_entity_spec.rb b/spec/serializers/test_reports_comparer_entity_spec.rb
index 59c058fe368..4a951bbbde4 100644
--- a/spec/serializers/test_reports_comparer_entity_spec.rb
+++ b/spec/serializers/test_reports_comparer_entity_spec.rb
@@ -53,13 +53,7 @@ describe TestReportsComparerEntity do
base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
base_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
- head_reports.get_suite('junit').add_test_case(create_test_case_java_resolved)
- end
-
- let(:create_test_case_java_resolved) do
- create_test_case_java_failed.tap do |test_case|
- test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
- end
+ head_reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
it 'contains correct compared test reports details' do
diff --git a/spec/serializers/test_reports_comparer_serializer_spec.rb b/spec/serializers/test_reports_comparer_serializer_spec.rb
index 9ea86c0dd83..62dc6f486c5 100644
--- a/spec/serializers/test_reports_comparer_serializer_spec.rb
+++ b/spec/serializers/test_reports_comparer_serializer_spec.rb
@@ -44,13 +44,7 @@ describe TestReportsComparerSerializer do
base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
base_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
- head_reports.get_suite('junit').add_test_case(create_test_case_java_resolved)
- end
-
- let(:create_test_case_java_resolved) do
- create_test_case_java_failed.tap do |test_case|
- test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
- end
+ head_reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
it 'matches the schema' do
diff --git a/spec/serializers/test_suite_comparer_entity_spec.rb b/spec/serializers/test_suite_comparer_entity_spec.rb
index f61331f53a0..4b2cca2c68c 100644
--- a/spec/serializers/test_suite_comparer_entity_spec.rb
+++ b/spec/serializers/test_suite_comparer_entity_spec.rb
@@ -11,16 +11,10 @@ describe TestSuiteComparerEntity do
let(:test_case_success) { create_test_case_rspec_success }
let(:test_case_failed) { create_test_case_rspec_failed }
- let(:test_case_resolved) do
- create_test_case_rspec_failed.tap do |test_case|
- test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
- end
- end
-
describe '#as_json' do
subject { entity.as_json }
- context 'when head sutie has a newly failed test case which does not exist in base' do
+ context 'when head suite has a newly failed test case which does not exist in base' do
before do
base_suite.add_test_case(test_case_success)
head_suite.add_test_case(test_case_failed)
@@ -41,7 +35,7 @@ describe TestSuiteComparerEntity do
end
end
- context 'when head sutie still has a failed test case which failed in base' do
+ context 'when head suite still has a failed test case which failed in base' do
before do
base_suite.add_test_case(test_case_failed)
head_suite.add_test_case(test_case_failed)
@@ -62,10 +56,10 @@ describe TestSuiteComparerEntity do
end
end
- context 'when head sutie has a success test case which failed in base' do
+ context 'when head suite has a success test case which failed in base' do
before do
base_suite.add_test_case(test_case_failed)
- head_suite.add_test_case(test_case_resolved)
+ head_suite.add_test_case(test_case_success)
end
it 'contains correct compared test suite details' do
@@ -74,13 +68,83 @@ describe TestSuiteComparerEntity do
expect(subject[:summary]).to include(total: 1, resolved: 1, failed: 0)
expect(subject[:new_failures]).to be_empty
subject[:resolved_failures].first.tap do |resolved_failure|
- expect(resolved_failure[:status]).to eq(test_case_resolved.status)
- expect(resolved_failure[:name]).to eq(test_case_resolved.name)
- expect(resolved_failure[:execution_time]).to eq(test_case_resolved.execution_time)
- expect(resolved_failure[:system_output]).to eq(test_case_resolved.system_output)
+ expect(resolved_failure[:status]).to eq(test_case_success.status)
+ expect(resolved_failure[:name]).to eq(test_case_success.name)
+ expect(resolved_failure[:execution_time]).to eq(test_case_success.execution_time)
+ expect(resolved_failure[:system_output]).to eq(test_case_success.system_output)
end
expect(subject[:existing_failures]).to be_empty
end
end
+
+ context 'limits amount of tests returned' do
+ before do
+ stub_const('TestSuiteComparerEntity::DEFAULT_MAX_TESTS', 2)
+ stub_const('TestSuiteComparerEntity::DEFAULT_MIN_TESTS', 1)
+ end
+
+ context 'prefers new over existing and resolved' do
+ before do
+ 3.times { add_new_failure }
+ 3.times { add_existing_failure }
+ 3.times { add_resolved_failure }
+ end
+
+ it 'returns 2 new failures, and 1 of resolved and existing' do
+ expect(subject[:summary]).to include(total: 9, resolved: 3, failed: 6)
+ expect(subject[:new_failures].count).to eq(2)
+ expect(subject[:existing_failures].count).to eq(1)
+ expect(subject[:resolved_failures].count).to eq(1)
+ end
+ end
+
+ context 'prefers existing over resolved' do
+ before do
+ 3.times { add_existing_failure }
+ 3.times { add_resolved_failure }
+ end
+
+ it 'returns 2 existing failures, and 1 resolved' do
+ expect(subject[:summary]).to include(total: 6, resolved: 3, failed: 3)
+ expect(subject[:new_failures].count).to eq(0)
+ expect(subject[:existing_failures].count).to eq(2)
+ expect(subject[:resolved_failures].count).to eq(1)
+ end
+ end
+
+ context 'limits amount of resolved' do
+ before do
+ 3.times { add_resolved_failure }
+ end
+
+ it 'returns 2 resolved failures' do
+ expect(subject[:summary]).to include(total: 3, resolved: 3, failed: 0)
+ expect(subject[:new_failures].count).to eq(0)
+ expect(subject[:existing_failures].count).to eq(0)
+ expect(subject[:resolved_failures].count).to eq(2)
+ end
+ end
+
+ private
+
+ def add_new_failure
+ failed_case = create_test_case_rspec_failed(SecureRandom.hex)
+ head_suite.add_test_case(failed_case)
+ end
+
+ def add_existing_failure
+ failed_case = create_test_case_rspec_failed(SecureRandom.hex)
+ base_suite.add_test_case(failed_case)
+ head_suite.add_test_case(failed_case)
+ end
+
+ def add_resolved_failure
+ case_name = SecureRandom.hex
+ failed_case = create_test_case_rspec_failed(case_name)
+ success_case = create_test_case_rspec_success(case_name)
+ base_suite.add_test_case(failed_case)
+ head_suite.add_test_case(success_case)
+ end
+ end
end
end
diff --git a/spec/services/auto_merge/base_service_spec.rb b/spec/services/auto_merge/base_service_spec.rb
index cd08e0b6f32..a409f21a7e4 100644
--- a/spec/services/auto_merge/base_service_spec.rb
+++ b/spec/services/auto_merge/base_service_spec.rb
@@ -59,6 +59,11 @@ describe AutoMerge::BaseService do
context 'when strategy is merge when pipeline succeeds' do
let(:service) { AutoMerge::MergeWhenPipelineSucceedsService.new(project, user) }
+ before do
+ pipeline = build(:ci_pipeline)
+ allow(merge_request).to receive(:actual_head_pipeline) { pipeline }
+ end
+
it 'sets the auto merge strategy' do
subject
@@ -116,11 +121,7 @@ describe AutoMerge::BaseService do
end
end
- describe '#cancel' do
- subject { service.cancel(merge_request) }
-
- let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
-
+ shared_examples_for 'Canceled or Dropped' do
it 'removes properies from the merge request' do
subject
@@ -168,6 +169,20 @@ describe AutoMerge::BaseService do
it 'does not yield block' do
expect { |b| service.execute(merge_request, &b) }.not_to yield_control
end
+ end
+ end
+
+ describe '#cancel' do
+ subject { service.cancel(merge_request) }
+
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ it_behaves_like 'Canceled or Dropped'
+
+ context 'when failed to save' do
+ before do
+ allow(merge_request).to receive(:save) { false }
+ end
it 'returns error status' do
expect(subject[:status]).to eq(:error)
@@ -175,4 +190,24 @@ describe AutoMerge::BaseService do
end
end
end
+
+ describe '#abort' do
+ subject { service.abort(merge_request, reason) }
+
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+ let(:reason) { 'an error'}
+
+ it_behaves_like 'Canceled or Dropped'
+
+ context 'when failed to save' do
+ before do
+ allow(merge_request).to receive(:save) { false }
+ end
+
+ it 'returns error status' do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to eq("Can't abort the automatic merge")
+ end
+ end
+ end
end
diff --git a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
index a20bf8e17e4..931b52470c4 100644
--- a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
@@ -64,8 +64,11 @@ describe AutoMerge::MergeWhenPipelineSucceedsService do
end
it 'creates a system note' do
+ pipeline = build(:ci_pipeline)
+ allow(merge_request).to receive(:actual_head_pipeline) { pipeline }
+
note = merge_request.notes.last
- expect(note.note).to match %r{enabled an automatic merge when the pipeline for (\w+/\w+@)?\h{8}}
+ expect(note.note).to match "enabled an automatic merge when the pipeline for #{pipeline.sha}"
end
end
@@ -174,6 +177,17 @@ describe AutoMerge::MergeWhenPipelineSucceedsService do
end
end
+ describe "#abort" do
+ before do
+ service.abort(mr_merge_if_green_enabled, 'an error')
+ end
+
+ it 'posts a system note' do
+ note = mr_merge_if_green_enabled.notes.last
+ expect(note.note).to include 'aborted the automatic merge'
+ end
+ end
+
describe 'pipeline integration' do
context 'when there are multiple stages in the pipeline' do
let(:ref) { mr_merge_if_green_enabled.source_branch }
diff --git a/spec/services/auto_merge_service_spec.rb b/spec/services/auto_merge_service_spec.rb
index 93a22e60498..50dfc49a59c 100644
--- a/spec/services/auto_merge_service_spec.rb
+++ b/spec/services/auto_merge_service_spec.rb
@@ -161,4 +161,29 @@ describe AutoMergeService do
end
end
end
+
+ describe '#abort' do
+ subject { service.abort(merge_request, error) }
+
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+ let(:error) { 'an error' }
+
+ it 'delegates to a relevant service instance' do
+ expect_next_instance_of(AutoMerge::MergeWhenPipelineSucceedsService) do |service|
+ expect(service).to receive(:abort).with(merge_request, error)
+ 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 abort 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/branches/diverging_commit_counts_service_spec.rb b/spec/services/branches/diverging_commit_counts_service_spec.rb
index bfdbebdb7c1..370da773ab2 100644
--- a/spec/services/branches/diverging_commit_counts_service_spec.rb
+++ b/spec/services/branches/diverging_commit_counts_service_spec.rb
@@ -19,34 +19,13 @@ describe Branches::DivergingCommitCountsService do
expect(result).to eq(behind: 29, ahead: 2)
end
- context 'when gitaly_count_diverging_commits_no_max is enabled' do
- before do
- stub_feature_flags(gitaly_count_diverging_commits_no_max: true)
- end
-
- it 'calls diverging_commit_count without max count' do
- expect(repository.raw_repository)
- .to receive(:diverging_commit_count)
- .with(root_ref_sha, diverged_branch_sha)
- .and_return([29, 2])
-
- service.call(diverged_branch)
- end
- end
-
- context 'when gitaly_count_diverging_commits_no_max is disabled' do
- before do
- stub_feature_flags(gitaly_count_diverging_commits_no_max: false)
- end
-
- it 'calls diverging_commit_count with max count' do
- expect(repository.raw_repository)
- .to receive(:diverging_commit_count)
- .with(root_ref_sha, diverged_branch_sha, max_count: Repository::MAX_DIVERGING_COUNT)
- .and_return([29, 2])
+ it 'calls diverging_commit_count without max count' do
+ expect(repository.raw_repository)
+ .to receive(:diverging_commit_count)
+ .with(root_ref_sha, diverged_branch_sha)
+ .and_return([29, 2])
- service.call(diverged_branch)
- end
+ service.call(diverged_branch)
end
end
end
diff --git a/spec/services/deploy_tokens/create_service_spec.rb b/spec/services/deploy_tokens/create_service_spec.rb
index 886ffd4593d..fbb66fe4cb7 100644
--- a/spec/services/deploy_tokens/create_service_spec.rb
+++ b/spec/services/deploy_tokens/create_service_spec.rb
@@ -32,6 +32,22 @@ describe DeployTokens::CreateService do
end
end
+ context 'when username is empty string' do
+ let(:deploy_token_params) { attributes_for(:deploy_token, username: '') }
+
+ it 'converts it to nil' do
+ expect(subject.read_attribute(:username)).to be_nil
+ end
+ end
+
+ context 'when username is provided' do
+ let(:deploy_token_params) { attributes_for(:deploy_token, username: 'deployer') }
+
+ it 'keeps the provided username' do
+ expect(subject.read_attribute(:username)).to eq('deployer')
+ end
+ end
+
context 'when the deploy token is invalid' do
let(:deploy_token_params) { attributes_for(:deploy_token, read_repository: false, read_registry: false) }
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 61f99f82a76..e2f201677fa 100644
--- a/spec/services/merge_requests/merge_to_ref_service_spec.rb
+++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb
@@ -22,7 +22,6 @@ describe MergeRequests::MergeToRefService do
shared_examples_for 'successfully merges to ref with merge method' do
it 'writes commit to merge ref' do
repository = project.repository
- target_ref = merge_request.merge_ref_path
expect(repository.ref_exists?(target_ref)).to be(false)
@@ -33,7 +32,7 @@ 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(result[:target_id]).to eq(repository.commit(first_parent_ref).sha)
expect(repository.ref_exists?(target_ref)).to be(true)
expect(ref_head.id).to eq(result[:commit_id])
end
@@ -74,17 +73,22 @@ describe MergeRequests::MergeToRefService do
describe '#execute' do
let(:service) do
- described_class.new(project, user, commit_message: 'Awesome message',
- should_remove_source_branch: true)
+ described_class.new(project, user, **params)
end
+ let(:params) { { commit_message: 'Awesome message', should_remove_source_branch: true } }
+
def process_merge_to_ref
perform_enqueued_jobs do
service.execute(merge_request)
end
end
- it_behaves_like 'successfully merges to ref with merge method'
+ it_behaves_like 'successfully merges to ref with merge method' do
+ let(:first_parent_ref) { 'refs/heads/master' }
+ let(:target_ref) { merge_request.merge_ref_path }
+ end
+
it_behaves_like 'successfully evaluates pre-condition checks'
context 'commit history comparison with regular MergeService' do
@@ -129,14 +133,22 @@ describe MergeRequests::MergeToRefService do
context 'when semi-linear merge method' do
let(:merge_method) { :rebase_merge }
- it_behaves_like 'successfully merges to ref with merge method'
+ it_behaves_like 'successfully merges to ref with merge method' do
+ let(:first_parent_ref) { 'refs/heads/master' }
+ let(:target_ref) { merge_request.merge_ref_path }
+ end
+
it_behaves_like 'successfully evaluates pre-condition checks'
end
context 'when fast-forward merge method' do
let(:merge_method) { :ff }
- it_behaves_like 'successfully merges to ref with merge method'
+ it_behaves_like 'successfully merges to ref with merge method' do
+ let(:first_parent_ref) { 'refs/heads/master' }
+ let(:target_ref) { merge_request.merge_ref_path }
+ end
+
it_behaves_like 'successfully evaluates pre-condition checks'
end
@@ -178,5 +190,34 @@ describe MergeRequests::MergeToRefService do
it { expect(todo).not_to be_done }
end
+
+ describe 'cascading merge refs' do
+ set(:project) { create(:project, :repository) }
+ let(:params) { { commit_message: 'Cascading merge', first_parent_ref: first_parent_ref, target_ref: target_ref } }
+
+ context 'when first merge happens' do
+ let(:merge_request) do
+ create(:merge_request, source_project: project, source_branch: 'feature',
+ target_project: project, target_branch: 'master')
+ end
+
+ it_behaves_like 'successfully merges to ref with merge method' do
+ let(:first_parent_ref) { 'refs/heads/master' }
+ let(:target_ref) { 'refs/merge-requests/1/train' }
+ end
+
+ context 'when second merge happens' do
+ let(:merge_request) do
+ create(:merge_request, source_project: project, source_branch: 'improve/awesome',
+ target_project: project, target_branch: 'master')
+ end
+
+ it_behaves_like 'successfully merges to ref with merge method' do
+ let(:first_parent_ref) { 'refs/merge-requests/1/train' }
+ let(:target_ref) { 'refs/merge-requests/2/train' }
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb
index 7e2f03d1097..ee9caaf2f47 100644
--- a/spec/services/merge_requests/rebase_service_spec.rb
+++ b/spec/services/merge_requests/rebase_service_spec.rb
@@ -6,10 +6,12 @@ describe MergeRequests::RebaseService do
include ProjectForksHelper
let(:user) { create(:user) }
+ let(:rebase_jid) { 'fake-rebase-jid' }
let(:merge_request) do
- create(:merge_request,
+ create :merge_request,
source_branch: 'feature_conflict',
- target_branch: 'master')
+ target_branch: 'master',
+ rebase_jid: rebase_jid
end
let(:project) { merge_request.project }
let(:repository) { project.repository.raw }
@@ -23,11 +25,11 @@ describe MergeRequests::RebaseService do
describe '#execute' do
context 'when another rebase is already in progress' do
before do
- allow(merge_request).to receive(:rebase_in_progress?).and_return(true)
+ allow(merge_request).to receive(:gitaly_rebase_in_progress?).and_return(true)
end
it 'saves the error message' do
- subject.execute(merge_request)
+ service.execute(merge_request)
expect(merge_request.reload.merge_error).to eq 'Rebase task canceled: Another rebase is already in progress'
end
@@ -36,6 +38,13 @@ describe MergeRequests::RebaseService do
expect(service.execute(merge_request)).to match(status: :error,
message: described_class::REBASE_ERROR)
end
+
+ it 'clears rebase_jid' do
+ expect { service.execute(merge_request) }
+ .to change { merge_request.rebase_jid }
+ .from(rebase_jid)
+ .to(nil)
+ end
end
shared_examples 'sequence of failure and success' do
@@ -43,14 +52,19 @@ describe MergeRequests::RebaseService do
allow(repository).to receive(:gitaly_operation_client).and_raise('Something went wrong')
service.execute(merge_request)
+ merge_request.reload
- expect(merge_request.reload.merge_error).to eq described_class::REBASE_ERROR
+ expect(merge_request.reload.merge_error).to eq(described_class::REBASE_ERROR)
+ expect(merge_request.rebase_jid).to eq(nil)
allow(repository).to receive(:gitaly_operation_client).and_call_original
+ merge_request.update!(rebase_jid: rebase_jid)
service.execute(merge_request)
+ merge_request.reload
- expect(merge_request.reload.merge_error).to eq nil
+ expect(merge_request.merge_error).to eq(nil)
+ expect(merge_request.rebase_jid).to eq(nil)
end
end
@@ -72,7 +86,7 @@ describe MergeRequests::RebaseService do
it 'saves a generic error message' do
subject.execute(merge_request)
- expect(merge_request.reload.merge_error).to eq described_class::REBASE_ERROR
+ expect(merge_request.reload.merge_error).to eq(described_class::REBASE_ERROR)
end
it 'returns an error' do
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 97377c2f560..157cfc46e69 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -332,7 +332,7 @@ describe SystemNoteService do
create(:merge_request, source_project: project, target_project: project)
end
- subject { described_class.merge_when_pipeline_succeeds(noteable, project, author, noteable.diff_head_commit) }
+ subject { described_class.merge_when_pipeline_succeeds(noteable, project, author, pipeline.sha) }
it_behaves_like 'a system note' do
let(:action) { 'merge' }
@@ -359,6 +359,22 @@ describe SystemNoteService do
end
end
+ describe '.abort_merge_when_pipeline_succeeds' do
+ let(:noteable) do
+ create(:merge_request, source_project: project, target_project: project)
+ end
+
+ subject { described_class.abort_merge_when_pipeline_succeeds(noteable, project, author, 'merge request was closed') }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'merge' }
+ end
+
+ it "posts the 'merge when pipeline succeeds' system note" do
+ expect(subject.note).to eq "aborted the automatic merge because merge request was closed"
+ end
+ end
+
describe '.change_title' do
let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum') }
@@ -1148,7 +1164,7 @@ describe SystemNoteService do
end
it 'sets the note text' do
- expect(subject.note).to eq 'resolved all discussions'
+ expect(subject.note).to eq 'resolved all threads'
end
end
@@ -1175,16 +1191,30 @@ describe SystemNoteService do
end
it 'links to the diff in the system note' do
- expect(subject.note).to include('version 1')
-
diff_id = merge_request.merge_request_diff.id
line_code = change_position.line_code(project.repository)
- expect(subject.note).to include(diffs_project_merge_request_path(project, merge_request, diff_id: diff_id, anchor: line_code))
+ link = diffs_project_merge_request_path(project, merge_request, diff_id: diff_id, anchor: line_code)
+
+ expect(subject.note).to eq("changed this line in [version 1 of the diff](#{link})")
+ end
+
+ context 'discussion is on an image' do
+ let(:discussion) { create(:image_diff_note_on_merge_request, project: project).to_discussion }
+
+ it 'links to the diff in the system note' do
+ diff_id = merge_request.merge_request_diff.id
+ file_hash = change_position.file_hash
+ link = diffs_project_merge_request_path(project, merge_request, diff_id: diff_id, anchor: file_hash)
+
+ expect(subject.note).to eq("changed this file in [version 1 of the diff](#{link})")
+ end
end
end
- context 'when the change_position is invalid for the discussion' do
- let(:change_position) { project.commit(sample_commit.id) }
+ context 'when the change_position does not point to a valid version' do
+ before do
+ allow(merge_request).to receive(:version_params_for).and_return(nil)
+ end
it 'creates a new note in the discussion' do
# we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded.
diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb
index 0f8af2c5d6d..7c8e57702ae 100644
--- a/spec/support/features/discussion_comments_shared_example.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -1,4 +1,4 @@
-shared_examples 'discussion comments' do |resource_name|
+shared_examples 'thread comments' do |resource_name|
let(:form_selector) { '.js-main-target-form' }
let(:dropdown_selector) { "#{form_selector} .comment-type-dropdown" }
let(:toggle_selector) { "#{dropdown_selector} .dropdown-toggle" }
@@ -48,7 +48,7 @@ shared_examples 'discussion comments' do |resource_name|
find(toggle_selector).click
end
- it 'has a "Comment" item (selected by default) and "Start discussion" item' do
+ it 'has a "Comment" item (selected by default) and "Start thread" item' do
expect(page).to have_selector menu_selector
find("#{menu_selector} li", match: :first)
@@ -59,7 +59,7 @@ shared_examples 'discussion comments' do |resource_name|
expect(items.first).to have_selector '.fa-check'
expect(items.first['class']).to match 'droplab-item-selected'
- expect(items.last).to have_content 'Start discussion'
+ expect(items.last).to have_content 'Start thread'
expect(items.last).to have_content "Discuss a specific suggestion or question#{' that needs to be resolved' if resource_name == 'merge request'}."
expect(items.last).not_to have_selector '.fa-check'
expect(items.last['class']).not_to match 'droplab-item-selected'
@@ -103,7 +103,7 @@ shared_examples 'discussion comments' do |resource_name|
expect(find(dropdown_selector)).to have_content 'Comment'
end
- describe 'when selecting "Start discussion"' do
+ describe 'when selecting "Start thread"' do
before do
find("#{menu_selector} li", match: :first)
all("#{menu_selector} li").last.click
@@ -114,9 +114,9 @@ shared_examples 'discussion comments' do |resource_name|
# on issues page, the submit input is a <button>, on other pages it is <input>
if button.tag_name == 'button'
- expect(find(submit_selector)).to have_content 'Start discussion'
+ expect(find(submit_selector)).to have_content 'Start thread'
else
- expect(find(submit_selector).value).to eq 'Start discussion'
+ expect(find(submit_selector).value).to eq 'Start thread'
end
expect(page).not_to have_selector menu_selector
@@ -124,17 +124,17 @@ shared_examples 'discussion comments' do |resource_name|
if resource_name =~ /(issue|merge request)/
it 'updates the close button text' do
- expect(find(close_selector)).to have_content "Start discussion & close #{resource_name}"
+ expect(find(close_selector)).to have_content "Start thread & close #{resource_name}"
end
it 'typing does not change the close button text' do
find("#{form_selector} .note-textarea").send_keys('b')
- expect(find(close_selector)).to have_content "Start discussion & close #{resource_name}"
+ expect(find(close_selector)).to have_content "Start thread & close #{resource_name}"
end
end
- describe 'creating a discussion' do
+ describe 'creating a thread' do
before do
find(submit_selector).click
wait_for_requests
@@ -150,7 +150,7 @@ shared_examples 'discussion comments' do |resource_name|
wait_for_requests
end
- it 'clicking "Start discussion" will post a discussion' do
+ it 'clicking "Start thread" will post a thread' do
new_comment = all(comments_selector).last
expect(new_comment).to have_content 'a'
@@ -176,10 +176,10 @@ shared_examples 'discussion comments' do |resource_name|
if resource_name == 'merge request'
let(:note_id) { find("#{comments_selector} .note:first-child", match: :first)['data-note-id'] }
- let(:reply_id) { find("#{comments_selector} .note:last-child", match: :first)['data-note-id'] }
+ let(:reply_id) { find("#{comments_selector} .note:last-of-type", match: :first)['data-note-id'] }
it 'can be replied to after resolving' do
- click_button "Resolve discussion"
+ click_button "Resolve thread"
wait_for_requests
refresh
@@ -188,10 +188,10 @@ shared_examples 'discussion comments' do |resource_name|
submit_reply('to reply or not reply')
end
- it 'shows resolved discussion when toggled' do
+ it 'shows resolved thread when toggled' do
submit_reply('a')
- click_button "Resolve discussion"
+ click_button "Resolve thread"
wait_for_requests
expect(page).to have_selector(".note-row-#{note_id}", visible: true)
@@ -205,7 +205,7 @@ shared_examples 'discussion comments' do |resource_name|
end
if resource_name == 'issue'
- it "clicking 'Start discussion & close #{resource_name}' will post a discussion and close the #{resource_name}" do
+ it "clicking 'Start thread & close #{resource_name}' will post a thread and close the #{resource_name}" do
find(close_selector).click
find(comments_selector, match: :first)
@@ -224,7 +224,7 @@ shared_examples 'discussion comments' do |resource_name|
find(toggle_selector).click
end
- it 'has "Start discussion" selected' do
+ it 'has "Start thread" selected' do
find("#{menu_selector} li", match: :first)
items = all("#{menu_selector} li")
@@ -232,7 +232,7 @@ shared_examples 'discussion comments' do |resource_name|
expect(items.first).not_to have_selector '.fa-check'
expect(items.first['class']).not_to match 'droplab-item-selected'
- expect(items.last).to have_content 'Start discussion'
+ expect(items.last).to have_content 'Start thread'
expect(items.last).to have_selector '.fa-check'
expect(items.last['class']).to match 'droplab-item-selected'
end
@@ -277,7 +277,7 @@ shared_examples 'discussion comments' do |resource_name|
expect(items.first).to have_selector '.fa-check'
expect(items.first['class']).to match 'droplab-item-selected'
- expect(items.last).to have_content 'Start discussion'
+ expect(items.last).to have_content 'Start thread'
expect(items.last).not_to have_selector '.fa-check'
expect(items.last['class']).not_to match 'droplab-item-selected'
end
@@ -299,13 +299,13 @@ shared_examples 'discussion comments' do |resource_name|
expect(find("#{form_selector} .js-note-target-reopen")).to have_content "Comment & reopen #{resource_name}"
end
- it "should show a 'Start discussion & reopen #{resource_name}' button when 'Start discussion' is selected" do
+ it "should show a 'Start thread & reopen #{resource_name}' button when 'Start thread' is selected" do
find(toggle_selector).click
find("#{menu_selector} li", match: :first)
all("#{menu_selector} li").last.click
- expect(find("#{form_selector} .js-note-target-reopen")).to have_content "Start discussion & reopen #{resource_name}"
+ expect(find("#{form_selector} .js-note-target-reopen")).to have_content "Start thread & reopen #{resource_name}"
end
end
end
diff --git a/spec/support/features/resolving_discussions_in_issues_shared_examples.rb b/spec/support/features/resolving_discussions_in_issues_shared_examples.rb
index 38e5fb155a4..8d0e03134d0 100644
--- a/spec/support/features/resolving_discussions_in_issues_shared_examples.rb
+++ b/spec/support/features/resolving_discussions_in_issues_shared_examples.rb
@@ -1,4 +1,4 @@
-shared_examples 'creating an issue for a discussion' do
+shared_examples 'creating an issue for a thread' do
it 'shows an issue with the title filled in' do
title_field = page.find_field('issue[title]')
diff --git a/spec/support/helpers/devise_helpers.rb b/spec/support/helpers/devise_helpers.rb
index d32bc2424c0..fb2a110422a 100644
--- a/spec/support/helpers/devise_helpers.rb
+++ b/spec/support/helpers/devise_helpers.rb
@@ -21,4 +21,16 @@ module DeviseHelpers
context.env
end
end
+
+ def with_omniauth_full_host(&block)
+ # The OmniAuth `full_host` parameter doesn't get set correctly (it gets set to something like `http://localhost`
+ # here), and causes integration tests to fail with 404s. We set the `full_host` by removing the request path (and
+ # anything after it) from the request URI.
+ omniauth_config_full_host = OmniAuth.config.full_host
+ OmniAuth.config.full_host = ->(request) { ActionDispatch::Request.new(request).base_url }
+
+ yield
+
+ OmniAuth.config.full_host = omniauth_config_full_host
+ end
end
diff --git a/spec/support/helpers/fake_u2f_device.rb b/spec/support/helpers/fake_u2f_device.rb
index a7605cd483a..22cd8152d77 100644
--- a/spec/support/helpers/fake_u2f_device.rb
+++ b/spec/support/helpers/fake_u2f_device.rb
@@ -32,6 +32,10 @@ class FakeU2fDevice
")
end
+ def fake_u2f_authentication
+ @page.execute_script("window.gl.u2fAuthenticate.renderAuthenticated('abc');")
+ end
+
private
def u2f_device(app_id)
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 0bb2d2510c2..0cb99b4e087 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -87,12 +87,17 @@ module LoginHelpers
click_link "oauth-login-#{provider}"
end
+ def fake_successful_u2f_authentication
+ allow(U2fRegistration).to receive(:authenticate).and_return(true)
+ FakeU2fDevice.new(page, nil).fake_u2f_authentication
+ end
+
def mock_auth_hash_with_saml_xml(provider, uid, email, saml_response)
response_object = { document: saml_xml(saml_response) }
mock_auth_hash(provider, uid, email, response_object: response_object)
end
- def mock_auth_hash(provider, uid, email, response_object: nil)
+ def configure_mock_auth(provider, uid, email, response_object: nil)
# The mock_auth configuration allows you to set per-provider (or default)
# authentication hashes to return during integration testing.
OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({
@@ -118,6 +123,11 @@ module LoginHelpers
response_object: response_object
}
})
+ end
+
+ def mock_auth_hash(provider, uid, email, response_object: nil)
+ configure_mock_auth(provider, uid, email, response_object: response_object)
+
original_env_config_omniauth_auth = Rails.application.env_config['omniauth.auth']
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]
diff --git a/spec/support/helpers/position_tracer_helpers.rb b/spec/support/helpers/position_tracer_helpers.rb
new file mode 100644
index 00000000000..bbf6e06dd40
--- /dev/null
+++ b/spec/support/helpers/position_tracer_helpers.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+module PositionTracerHelpers
+ def diff_refs(base_commit, head_commit)
+ Gitlab::Diff::DiffRefs.new(base_sha: base_commit.id, head_sha: head_commit.id)
+ end
+
+ def position(attrs = {})
+ attrs.reverse_merge!(
+ diff_refs: old_diff_refs
+ )
+ Gitlab::Diff::Position.new(attrs)
+ end
+
+ def expect_new_position(attrs, result = subject)
+ aggregate_failures("expect new position #{attrs.inspect}") do
+ if attrs.nil?
+ expect(result[:outdated]).to be_truthy
+ else
+ new_position = result[:position]
+
+ expect(result[:outdated]).to be_falsey
+ expect(new_position).not_to be_nil
+ expect(new_position.diff_refs).to eq(new_diff_refs)
+
+ attrs.each do |attr, value|
+ expect(new_position.send(attr)).to eq(value)
+ end
+ end
+ end
+ end
+
+ def expect_change_position(attrs, result = subject)
+ aggregate_failures("expect change position #{attrs.inspect}") do
+ change_position = result[:position]
+
+ expect(result[:outdated]).to be_truthy
+
+ if attrs.nil? || attrs.empty?
+ expect(change_position).to be_nil
+ else
+ expect(change_position).not_to be_nil
+ expect(change_position.diff_refs).to eq(change_diff_refs)
+
+ attrs.each do |attr, value|
+ expect(change_position.send(attr)).to eq(value)
+ end
+ end
+ end
+ end
+
+ def create_branch(new_name, branch_name)
+ CreateBranchService.new(project, current_user).execute(new_name, branch_name)
+ end
+
+ def create_file(branch_name, file_name, content)
+ Files::CreateService.new(
+ project,
+ current_user,
+ start_branch: branch_name,
+ branch_name: branch_name,
+ commit_message: "Create file",
+ file_path: file_name,
+ file_content: content
+ ).execute
+ project.commit(branch_name)
+ end
+
+ def update_file(branch_name, file_name, content)
+ Files::UpdateService.new(
+ project,
+ current_user,
+ start_branch: branch_name,
+ branch_name: branch_name,
+ commit_message: "Update file",
+ file_path: file_name,
+ file_content: content
+ ).execute
+ project.commit(branch_name)
+ end
+
+ def delete_file(branch_name, file_name)
+ Files::DeleteService.new(
+ project,
+ current_user,
+ start_branch: branch_name,
+ branch_name: branch_name,
+ commit_message: "Delete file",
+ file_path: file_name
+ ).execute
+ project.commit(branch_name)
+ end
+end
diff --git a/spec/support/matchers/gitaly_matchers.rb b/spec/support/matchers/gitaly_matchers.rb
index ebfabcd8f24..933ed22b5d0 100644
--- a/spec/support/matchers/gitaly_matchers.rb
+++ b/spec/support/matchers/gitaly_matchers.rb
@@ -9,6 +9,6 @@ end
RSpec::Matchers.define :gitaly_request_with_params do |params|
match do |actual|
- params.reduce(true) { |r, (key, val)| r && actual.send(key) == val }
+ params.reduce(true) { |r, (key, val)| r && actual[key.to_s] == val }
end
end
diff --git a/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb b/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb
index 221926aaf7e..2b36955a3c4 100644
--- a/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb
+++ b/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb
@@ -16,7 +16,7 @@ shared_examples 'comment on merge request file' do
visit(merge_request_path(merge_request))
page.within('.notes .discussion') do
- expect(page).to have_content("#{user.name} #{user.to_reference} started a discussion")
+ expect(page).to have_content("#{user.name} #{user.to_reference} started a thread")
expect(page).to have_content(sample_commit.line_code_path)
expect(page).to have_content('Line is wrong')
end
diff --git a/spec/support/shared_examples/requests/api/issues/merge_requests_count_shared_examples.rb b/spec/support/shared_examples/requests/api/issues/merge_requests_count_shared_examples.rb
new file mode 100644
index 00000000000..5f4e178f2e5
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/issues/merge_requests_count_shared_examples.rb
@@ -0,0 +1,37 @@
+def get_issue
+ json_response.is_a?(Array) ? json_response.detect {|issue| issue['id'] == target_issue.id} : json_response
+end
+
+shared_examples 'accessible merge requests count' do
+ it 'returns anonymous accessible merge requests count' do
+ get api(api_url), params: { scope: 'all' }
+
+ issue = get_issue
+ expect(issue).not_to be_nil
+ expect(issue['merge_requests_count']).to eq(1)
+ end
+
+ it 'returns guest accessible merge requests count' do
+ get api(api_url, guest), params: { scope: 'all' }
+
+ issue = get_issue
+ expect(issue).not_to be_nil
+ expect(issue['merge_requests_count']).to eq(1)
+ end
+
+ it 'returns reporter accessible merge requests count' do
+ get api(api_url, user), params: { scope: 'all' }
+
+ issue = get_issue
+ expect(issue).not_to be_nil
+ expect(issue['merge_requests_count']).to eq(2)
+ end
+
+ it 'returns admin accessible merge requests count' do
+ get api(api_url, admin), params: { scope: 'all' }
+
+ issue = get_issue
+ expect(issue).not_to be_nil
+ expect(issue['merge_requests_count']).to eq(2)
+ end
+end
diff --git a/spec/support/test_reports/test_reports_helper.rb b/spec/support/test_reports/test_reports_helper.rb
index 45c6e04dbf3..6840fb9a860 100644
--- a/spec/support/test_reports/test_reports_helper.rb
+++ b/spec/support/test_reports/test_reports_helper.rb
@@ -1,36 +1,36 @@
module TestReportsHelper
- def create_test_case_rspec_success
+ def create_test_case_rspec_success(name = 'test_spec')
Gitlab::Ci::Reports::TestCase.new(
name: 'Test#sum when a is 1 and b is 3 returns summary',
- classname: 'spec.test_spec',
+ classname: "spec.#{name}",
file: './spec/test_spec.rb',
execution_time: 1.11,
status: Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
end
- def create_test_case_rspec_failed
+ def create_test_case_rspec_failed(name = 'test_spec')
Gitlab::Ci::Reports::TestCase.new(
- name: 'Test#sum when a is 2 and b is 2 returns summary',
- classname: 'spec.test_spec',
+ name: 'Test#sum when a is 1 and b is 3 returns summary',
+ classname: "spec.#{name}",
file: './spec/test_spec.rb',
execution_time: 2.22,
system_output: sample_rspec_failed_message,
status: Gitlab::Ci::Reports::TestCase::STATUS_FAILED)
end
- def create_test_case_rspec_skipped
+ def create_test_case_rspec_skipped(name = 'test_spec')
Gitlab::Ci::Reports::TestCase.new(
name: 'Test#sum when a is 3 and b is 3 returns summary',
- classname: 'spec.test_spec',
+ classname: "spec.#{name}",
file: './spec/test_spec.rb',
execution_time: 3.33,
status: Gitlab::Ci::Reports::TestCase::STATUS_SKIPPED)
end
- def create_test_case_rspec_error
+ def create_test_case_rspec_error(name = 'test_spec')
Gitlab::Ci::Reports::TestCase.new(
name: 'Test#sum when a is 4 and b is 4 returns summary',
- classname: 'spec.test_spec',
+ classname: "spec.#{name}",
file: './spec/test_spec.rb',
execution_time: 4.44,
status: Gitlab::Ci::Reports::TestCase::STATUS_ERROR)
@@ -48,34 +48,34 @@ module TestReportsHelper
EOF
end
- def create_test_case_java_success
+ def create_test_case_java_success(name = 'addTest')
Gitlab::Ci::Reports::TestCase.new(
- name: 'addTest',
+ name: name,
classname: 'CalculatorTest',
execution_time: 5.55,
status: Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
end
- def create_test_case_java_failed
+ def create_test_case_java_failed(name = 'addTest')
Gitlab::Ci::Reports::TestCase.new(
- name: 'subtractTest',
+ name: name,
classname: 'CalculatorTest',
execution_time: 6.66,
system_output: sample_java_failed_message,
status: Gitlab::Ci::Reports::TestCase::STATUS_FAILED)
end
- def create_test_case_java_skipped
+ def create_test_case_java_skipped(name = 'addTest')
Gitlab::Ci::Reports::TestCase.new(
- name: 'multiplyTest',
+ name: name,
classname: 'CalculatorTest',
execution_time: 7.77,
status: Gitlab::Ci::Reports::TestCase::STATUS_SKIPPED)
end
- def create_test_case_java_error
+ def create_test_case_java_error(name = 'addTest')
Gitlab::Ci::Reports::TestCase.new(
- name: 'divideTest',
+ name: name,
classname: 'CalculatorTest',
execution_time: 8.88,
status: Gitlab::Ci::Reports::TestCase::STATUS_ERROR)
diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb
index e474a714b10..5ee0a10f38d 100644
--- a/spec/uploaders/file_mover_spec.rb
+++ b/spec/uploaders/file_mover_spec.rb
@@ -3,79 +3,139 @@ require 'spec_helper'
describe FileMover do
include FileMoverHelpers
+ let(:user) { create(:user) }
let(:filename) { 'banana_sample.gif' }
- let(:temp_file_path) { File.join('uploads/-/system/temp', 'secret55', filename) }
+ let(:secret) { 'secret55' }
+ let(:temp_file_path) { File.join("uploads/-/system/user/#{user.id}", secret, filename) }
let(:temp_description) do
"test ![banana_sample](/#{temp_file_path}) "\
"same ![banana_sample](/#{temp_file_path}) "
end
- let(:file_path) { File.join('uploads/-/system/personal_snippet', snippet.id.to_s, 'secret55', filename) }
+ let(:file_path) { File.join('uploads/-/system/personal_snippet', snippet.id.to_s, secret, filename) }
let(:snippet) { create(:personal_snippet, description: temp_description) }
- subject { described_class.new(temp_file_path, snippet).execute }
+ let(:tmp_uploader) do
+ PersonalFileUploader.new(user, secret: secret)
+ end
+
+ let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') }
+ subject { described_class.new(temp_file_path, from_model: user, to_model: snippet).execute }
describe '#execute' do
- before do
- expect(FileUtils).to receive(:mkdir_p).with(a_string_including(File.dirname(file_path)))
- expect(FileUtils).to receive(:move).with(a_string_including(temp_file_path), a_string_including(file_path))
- allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:exists?).and_return(true)
- allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:size).and_return(10)
+ let(:tmp_upload) { tmp_uploader.upload }
- stub_file_mover(temp_file_path)
+ before do
+ tmp_uploader.store!(file)
end
- context 'when move and field update successful' do
- it 'updates the description correctly' do
- subject
+ context 'local storage' do
+ before do
+ allow(FileUtils).to receive(:mkdir_p).with(a_string_including(File.dirname(file_path)))
+ allow(FileUtils).to receive(:move).with(a_string_including(temp_file_path), a_string_including(file_path))
+ allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:exists?).and_return(true)
+ allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:size).and_return(10)
- expect(snippet.reload.description)
- .to eq(
- "test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) "\
- "same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) "
- )
+ stub_file_mover(temp_file_path)
end
- it 'creates a new update record' do
- expect { subject }.to change { Upload.count }.by(1)
+ context 'when move and field update successful' do
+ it 'updates the description correctly' do
+ subject
+
+ expect(snippet.reload.description)
+ .to eq("test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) "\
+ "same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) ")
+ end
+
+ it 'updates existing upload record' do
+ expect { subject }
+ .to change { tmp_upload.reload.attributes.values_at('model_id', 'model_type') }
+ .from([user.id, 'User']).to([snippet.id, 'Snippet'])
+ end
+
+ it 'schedules a background migration' do
+ expect_any_instance_of(PersonalFileUploader).to receive(:schedule_background_upload).once
+
+ subject
+ end
end
- it 'schedules a background migration' do
- expect_any_instance_of(PersonalFileUploader).to receive(:schedule_background_upload).once
+ context 'when update_markdown fails' do
+ before do
+ expect(FileUtils).to receive(:move).with(a_string_including(file_path), a_string_including(temp_file_path))
+ end
- subject
+ subject { described_class.new(file_path, :non_existing_field, from_model: user, to_model: snippet).execute }
+
+ it 'does not update the description' do
+ subject
+
+ expect(snippet.reload.description)
+ .to eq("test ![banana_sample](/uploads/-/system/user/#{user.id}/secret55/banana_sample.gif) "\
+ "same ![banana_sample](/uploads/-/system/user/#{user.id}/secret55/banana_sample.gif) ")
+ end
+
+ it 'does not change the upload record' do
+ expect { subject }
+ .not_to change { tmp_upload.reload.attributes.values_at('model_id', 'model_type') }
+ end
end
end
- context 'when update_markdown fails' do
+ context 'when tmp uploader is not local storage' do
before do
- expect(FileUtils).to receive(:move).with(a_string_including(file_path), a_string_including(temp_file_path))
+ stub_uploads_object_storage(uploader: PersonalFileUploader)
+ allow_any_instance_of(PersonalFileUploader).to receive(:file_storage?) { false }
end
- subject { described_class.new(file_path, snippet, :non_existing_field).execute }
+ after do
+ FileUtils.rm_f(File.join('personal_snippet', snippet.id.to_s, secret, filename))
+ end
- it 'does not update the description' do
- subject
+ context 'when move and field update successful' do
+ it 'updates the description correctly' do
+ subject
- expect(snippet.reload.description)
- .to eq(
- "test ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif) "\
- "same ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif) "
- )
+ expect(snippet.reload.description)
+ .to eq("test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) "\
+ "same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) ")
+ end
+
+ it 'creates new target upload record an delete the old upload' do
+ expect { subject }
+ .to change { Upload.last.attributes.values_at('model_id', 'model_type') }
+ .from([user.id, 'User']).to([snippet.id, 'Snippet'])
+
+ expect(Upload.count).to eq(1)
+ end
end
- it 'does not create a new update record' do
- expect { subject }.not_to change { Upload.count }
+ context 'when update_markdown fails' do
+ subject { described_class.new(file_path, :non_existing_field, from_model: user, to_model: snippet).execute }
+
+ it 'does not update the description' do
+ subject
+
+ expect(snippet.reload.description)
+ .to eq("test ![banana_sample](/uploads/-/system/user/#{user.id}/secret55/banana_sample.gif) "\
+ "same ![banana_sample](/uploads/-/system/user/#{user.id}/secret55/banana_sample.gif) ")
+ end
+
+ it 'does not change the upload record' do
+ expect { subject }
+ .to change { Upload.last.attributes.values_at('model_id', 'model_type') }.from([user.id, 'User'])
+ end
end
end
end
context 'security' do
context 'when relative path is involved' do
- let(:temp_file_path) { File.join('uploads/-/system/temp', '..', 'another_subdir_of_temp') }
+ let(:temp_file_path) { File.join("uploads/-/system/user/#{user.id}", '..', 'another_subdir_of_temp') }
it 'does not trigger move if path is outside designated directory' do
- stub_file_mover('uploads/-/system/another_subdir_of_temp')
+ stub_file_mover("uploads/-/system/user/#{user.id}/another_subdir_of_temp")
expect(FileUtils).not_to receive(:move)
subject
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index 185c62491ce..04206de3dc6 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -184,40 +184,37 @@ describe FileUploader do
end
end
- describe '#cache!' do
- subject do
- uploader.store!(uploaded_file)
- end
+ context 'when remote file is used' do
+ let(:temp_file) { Tempfile.new("test") }
- context 'when remote file is used' do
- let(:temp_file) { Tempfile.new("test") }
+ let!(:fog_connection) do
+ stub_uploads_object_storage(described_class)
+ end
- let!(:fog_connection) do
- stub_uploads_object_storage(described_class)
- end
+ let(:filename) { "my file.txt" }
+ let(:uploaded_file) do
+ UploadedFile.new(temp_file.path, filename: filename, remote_id: "test/123123")
+ end
- let(:uploaded_file) do
- UploadedFile.new(temp_file.path, filename: "my file.txt", remote_id: "test/123123")
- end
+ let!(:fog_file) do
+ fog_connection.directories.new(key: 'uploads').files.create(
+ key: 'tmp/uploads/test/123123',
+ body: 'content'
+ )
+ end
- let!(:fog_file) do
- fog_connection.directories.new(key: 'uploads').files.create(
- key: 'tmp/uploads/test/123123',
- body: 'content'
- )
- end
+ before do
+ FileUtils.touch(temp_file)
- before do
- FileUtils.touch(temp_file)
- end
+ uploader.store!(uploaded_file)
+ end
- after do
- FileUtils.rm_f(temp_file)
- end
+ after do
+ FileUtils.rm_f(temp_file)
+ end
+ describe '#cache!' do
it 'file is stored remotely in permament location with sanitized name' do
- subject
-
expect(uploader).to be_exists
expect(uploader).not_to be_cached
expect(uploader).not_to be_file_storage
@@ -228,5 +225,18 @@ describe FileUploader do
expect(uploader.object_store).to eq(described_class::Store::REMOTE)
end
end
+
+ describe '#to_h' do
+ subject { uploader.to_h }
+
+ let(:filename) { 'my+file.txt' }
+
+ it 'generates URL using original file name instead of filename returned by object storage' do
+ # GCS returns a URL with a `+` instead of `%2B`
+ allow(uploader.file).to receive(:url).and_return('https://storage.googleapis.com/gitlab-test-uploads/@hashed/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b/64c5065e62100b1a12841644256a98be/my+file.txt')
+
+ expect(subject[:url]).to end_with(filename)
+ end
+ end
end
end
diff --git a/spec/validators/color_validator_spec.rb b/spec/validators/color_validator_spec.rb
new file mode 100644
index 00000000000..e5a38ac9372
--- /dev/null
+++ b/spec/validators/color_validator_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ColorValidator do
+ using RSpec::Parameterized::TableSyntax
+
+ subject do
+ Class.new do
+ include ActiveModel::Model
+ include ActiveModel::Validations
+ attr_accessor :color
+ validates :color, color: true
+ end.new
+ end
+
+ where(:color, :is_valid) do
+ '#000abc' | true
+ '#aaa' | true
+ '#BBB' | true
+ '#cCc' | true
+ '#ffff' | false
+ '#000111222' | false
+ 'invalid' | false
+ '000' | false
+ end
+
+ with_them do
+ it 'only accepts valid colors' do
+ subject.color = color
+
+ expect(subject.valid?).to eq(is_valid)
+ end
+ end
+
+ it 'fails fast for long invalid string' do
+ subject.color = '#' + ('0' * 50_000) + 'xxx'
+
+ expect do
+ Timeout.timeout(5.seconds) { subject.valid? }
+ end.not_to raise_error
+ end
+end
diff --git a/spec/workers/namespaces/schedule_aggregation_worker_spec.rb b/spec/workers/namespaces/schedule_aggregation_worker_spec.rb
index 7432ca12f2a..d4a49a3f53a 100644
--- a/spec/workers/namespaces/schedule_aggregation_worker_spec.rb
+++ b/spec/workers/namespaces/schedule_aggregation_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe Namespaces::ScheduleAggregationWorker, '#perform' do
+describe Namespaces::ScheduleAggregationWorker, '#perform', :clean_gitlab_redis_shared_state do
let(:group) { create(:group) }
subject(:worker) { described_class.new }
@@ -10,6 +10,8 @@ describe Namespaces::ScheduleAggregationWorker, '#perform' do
context 'when group is the root ancestor' do
context 'when aggregation schedule exists' do
it 'does not create a new one' do
+ stub_aggregation_schedule_statistics
+
Namespace::AggregationSchedule.safe_find_or_create_by!(namespace_id: group.id)
expect do
@@ -18,26 +20,25 @@ describe Namespaces::ScheduleAggregationWorker, '#perform' do
end
end
- context 'when update_statistics_namespace is off' do
- it 'does not create a new one' do
- stub_feature_flags(update_statistics_namespace: false, namespace: group)
+ context 'when aggregation schedule does not exist' do
+ it 'creates one' do
+ stub_aggregation_schedule_statistics
expect do
worker.perform(group.id)
- end.not_to change(Namespace::AggregationSchedule, :count)
+ end.to change(Namespace::AggregationSchedule, :count).by(1)
+
+ expect(group.aggregation_schedule).to be_present
end
end
- context 'when aggregation schedule does not exist' do
- it 'creates one' do
- allow_any_instance_of(Namespace::AggregationSchedule)
- .to receive(:schedule_root_storage_statistics).and_return(nil)
+ context 'when update_statistics_namespace is off' do
+ it 'does not create a new one' do
+ stub_feature_flags(update_statistics_namespace: false, namespace: group)
expect do
worker.perform(group.id)
- end.to change(Namespace::AggregationSchedule, :count).by(1)
-
- expect(group.aggregation_schedule).to be_present
+ end.not_to change(Namespace::AggregationSchedule, :count)
end
end
end
@@ -47,8 +48,7 @@ describe Namespaces::ScheduleAggregationWorker, '#perform' do
let(:group) { create(:group, parent: parent_group) }
it 'creates an aggregation schedule for the root' do
- allow_any_instance_of(Namespace::AggregationSchedule)
- .to receive(:schedule_root_storage_statistics).and_return(nil)
+ stub_aggregation_schedule_statistics
worker.perform(group.id)
@@ -63,4 +63,15 @@ describe Namespaces::ScheduleAggregationWorker, '#perform' do
worker.perform(12345)
end
end
+
+ def stub_aggregation_schedule_statistics
+ # Namespace::Aggregations are deleted by
+ # Namespace::AggregationSchedule::schedule_root_storage_statistics,
+ # which is executed async. Stubing the service so instances are not deleted
+ # while still running the specs.
+ expect_next_instance_of(Namespace::AggregationSchedule) do |aggregation_schedule|
+ expect(aggregation_schedule)
+ .to receive(:schedule_root_storage_statistics)
+ end
+ end
end
diff --git a/tmp/prometheus_multiproc_dir/puma/.gitkeep b/tmp/prometheus_multiproc_dir/puma/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/tmp/prometheus_multiproc_dir/puma/.gitkeep
diff --git a/tmp/prometheus_multiproc_dir/sidekiq/.gitkeep b/tmp/prometheus_multiproc_dir/sidekiq/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/tmp/prometheus_multiproc_dir/sidekiq/.gitkeep
diff --git a/tmp/prometheus_multiproc_dir/unicorn/.gitkeep b/tmp/prometheus_multiproc_dir/unicorn/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/tmp/prometheus_multiproc_dir/unicorn/.gitkeep
diff --git a/yarn.lock b/yarn.lock
index ed9f31f60a4..b76eba830b1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -705,10 +705,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.66.0.tgz#3c02da455421ea241f32e915671842435df027ff"
integrity sha512-nxOoQPnofMs3BjRr3SVzQcclM0G6QFrLM8L4nnUCN+8Gxq2u8ukfSU5FCrkivXz+FP9Qo/FYilWV7CY8kDkt6A==
-"@gitlab/ui@^5.1.0":
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.1.0.tgz#b8c8f266edc68f616e92c0ba3a18a692002393e4"
- integrity sha512-IPgk5W7mSXcbni+zNuJeVU89Co72jSQAXTxU7AtmItt5XT6nI9US2ZAWNUl8XCiOOw81jzYv0PLp4bMiXdLkww==
+"@gitlab/ui@^5.3.2":
+ version "5.3.2"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.3.2.tgz#8ee906cf0586834de0077e165f25764c0bf8a9e9"
+ integrity sha512-VgxlDXqG2q+u72Km+/Ljdvjh0DzvljvsztiXTxnOO+Eb+/I26JBWfdboqFr3E02JzT8W4s4rRinhRttLWfcM/A==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.2.1"
@@ -1244,7 +1244,7 @@ abab@^2.0.0:
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f"
integrity sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==
-abbrev@1, abbrev@1.0.x:
+abbrev@1:
version "1.0.9"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
integrity sha1-kbR5JYinc4wl813W9jdSovh3YTU=
@@ -1630,7 +1630,7 @@ async-limiter@~1.0.0:
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==
-async@1.x, async@^1.5.2:
+async@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=
@@ -3993,18 +3993,6 @@ escaper@^2.5.3:
resolved "https://registry.yarnpkg.com/escaper/-/escaper-2.5.3.tgz#8b8fe90ba364054151ab7eff18b4ce43b1e13ab5"
integrity sha512-QGb9sFxBVpbzMggrKTX0ry1oiI4CSDAl9vIL702hzl1jGW8VZs7qfqTRX7WDOjoNDoEVGcEtu1ZOQgReSfT2kQ==
-escodegen@1.8.x:
- version "1.8.1"
- resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018"
- integrity sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=
- dependencies:
- esprima "^2.7.1"
- estraverse "^1.9.1"
- esutils "^2.0.2"
- optionator "^0.8.1"
- optionalDependencies:
- source-map "~0.2.0"
-
escodegen@^1.9.1:
version "1.11.0"
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589"
@@ -4213,11 +4201,6 @@ espree@^4.0.0, espree@^4.1.0:
acorn-jsx "^5.0.0"
eslint-visitor-keys "^1.0.0"
-esprima@2.7.x, esprima@^2.7.1:
- version "2.7.3"
- resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
- integrity sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=
-
esprima@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
@@ -4242,11 +4225,6 @@ esrecurse@^4.1.0:
dependencies:
estraverse "^4.1.0"
-estraverse@^1.9.1:
- version "1.9.3"
- resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44"
- integrity sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=
-
estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
@@ -4953,17 +4931,6 @@ glob-to-regexp@^0.3.0:
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^5.0.15:
- version "5.0.15"
- resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
- integrity sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=
- dependencies:
- inflight "^1.0.4"
- inherits "2"
- minimatch "2 || 3"
- once "^1.3.0"
- path-is-absolute "^1.0.0"
-
global-dirs@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445"
@@ -5163,7 +5130,7 @@ handle-thing@^2.0.0:
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754"
integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==
-handlebars@^4.0.1, handlebars@^4.1.2:
+handlebars@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67"
integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==
@@ -5206,11 +5173,6 @@ has-cors@1.1.0:
resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=
-has-flag@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
- integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=
-
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -6104,26 +6066,6 @@ istanbul-reports@^2.1.1:
dependencies:
handlebars "^4.1.2"
-istanbul@^0.4.5:
- version "0.4.5"
- resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.5.tgz#65c7d73d4c4da84d4f3ac310b918fb0b8033733b"
- integrity sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=
- dependencies:
- abbrev "1.0.x"
- async "1.x"
- escodegen "1.8.x"
- esprima "2.7.x"
- glob "^5.0.15"
- handlebars "^4.0.1"
- js-yaml "3.x"
- mkdirp "0.5.x"
- nopt "3.x"
- once "1.x"
- resolve "1.1.x"
- supports-color "^3.1.0"
- which "^1.1.1"
- wordwrap "^1.0.0"
-
istextorbinary@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.2.1.tgz#a5231a08ef6dd22b268d0895084cf8d58b5bec53"
@@ -6593,7 +6535,7 @@ js-tokens@^3.0.2:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
-js-yaml@3.x, js-yaml@^3.12.0, js-yaml@^3.9.0:
+js-yaml@^3.12.0, js-yaml@^3.9.0:
version "3.13.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
@@ -7472,7 +7414,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
-"minimatch@2 || 3", minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2:
+minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
@@ -7818,7 +7760,7 @@ nodemon@^1.18.9:
undefsafe "^2.0.2"
update-notifier "^2.5.0"
-"nopt@2 || 3", nopt@3.x:
+"nopt@2 || 3":
version "3.0.6"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k=
@@ -8019,7 +7961,7 @@ on-headers@~1.0.1:
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
integrity sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=
-once@1.x, once@^1.3.0, once@^1.3.1, once@^1.4.0:
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
@@ -9490,7 +9432,7 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
-resolve@1.1.7, resolve@1.1.x:
+resolve@1.1.7:
version "1.1.7"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
@@ -10048,13 +9990,6 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
-source-map@~0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d"
- integrity sha1-2rc/vPwrqBm03gO9b26qSBZLP50=
- dependencies:
- amdefine ">=0.0.4"
-
spdx-correct@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
@@ -10456,13 +10391,6 @@ supports-color@^2.0.0:
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
-supports-color@^3.1.0:
- version "3.2.3"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
- integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=
- dependencies:
- has-flag "^1.0.0"
-
supports-color@^5.1.0, supports-color@^5.2.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@@ -11635,7 +11563,7 @@ which-module@^2.0.0:
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
-which@1, which@^1.1.1, which@^1.2.1, which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1:
+which@1, which@^1.2.1, which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
@@ -11656,16 +11584,16 @@ widest-line@^2.0.0:
dependencies:
string-width "^2.1.1"
-wordwrap@^1.0.0, wordwrap@~1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
- integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
-
wordwrap@~0.0.2:
version "0.0.3"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc=
+wordwrap@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+ integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
+
worker-farm@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.2.tgz#32b312e5dc3d5d45d79ef44acc2587491cd729ae"