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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.rubocop.yml4
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile5
-rw-r--r--Gemfile.lock6
-rw-r--r--Gemfile.rails5.lock6
-rw-r--r--app/assets/javascripts/boards/components/board.js3
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue2
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue11
-rw-r--r--app/assets/javascripts/jobs/components/stages_dropdown.vue1
-rw-r--r--app/assets/javascripts/jobs/store/mutations.js2
-rw-r--r--app/assets/javascripts/jobs/store/state.js4
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js12
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js17
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue8
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue5
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue25
-rw-r--r--app/assets/javascripts/notes/stores/collapse_utils.js5
-rw-r--r--app/assets/javascripts/notes/stores/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js2
-rw-r--r--app/assets/javascripts/pages/admin/admin.js15
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/account_and_limits.js8
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js36
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue65
-rw-r--r--app/assets/javascripts/pages/admin/projects/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue139
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue187
-rw-r--r--app/assets/javascripts/pages/admin/users/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/users/new/index.js6
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js15
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue169
-rw-r--r--app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js16
-rw-r--r--app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js16
-rw-r--r--app/assets/javascripts/pages/profiles/index.js7
-rw-r--r--app/assets/javascripts/pages/profiles/two_factor_auths/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/branches/new/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/graphs/charts/index.js22
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js91
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js221
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js64
-rw-r--r--app/assets/javascripts/pages/projects/init_blob.js3
-rw-r--r--app/assets/javascripts/pages/projects/init_form.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js2
-rw-r--r--app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue124
-rw-r--r--app/assets/javascripts/pages/projects/labels/index/index.js16
-rw-r--r--app/assets/javascripts/pages/projects/network/network.js10
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js22
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue102
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue46
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js3
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js4
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/charts/index.js59
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/index/index.js66
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/init_pipelines.js7
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue114
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue36
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/constants.js6
-rw-r--r--app/assets/javascripts/pages/projects/shared/project_avatar.js5
-rw-r--r--app/assets/javascripts/pages/projects/wikis/index.js3
-rw-r--r--app/assets/javascripts/pages/search/show/search.js25
-rw-r--r--app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js2
-rw-r--r--app/assets/javascripts/pages/sessions/new/username_validator.js10
-rw-r--r--app/assets/javascripts/pages/users/index.js6
-rw-r--r--app/assets/javascripts/performance_bar/components/simple_metric.vue46
-rw-r--r--app/assets/javascripts/performance_bar/index.js3
-rw-r--r--app/assets/javascripts/performance_bar/services/performance_bar_service.js11
-rw-r--r--app/assets/javascripts/performance_bar/stores/performance_bar_store.js4
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue126
-rw-r--r--app/assets/javascripts/profile/gl_crop.js47
-rw-r--r--app/assets/javascripts/profile/profile.js6
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js8
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js59
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js4
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.js37
-rw-r--r--app/assets/javascripts/registry/components/app.vue65
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue99
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue102
-rw-r--r--app/assets/javascripts/registry/index.js39
-rw-r--r--app/assets/javascripts/registry/stores/mutations.js1
-rw-r--r--app/assets/javascripts/reports/components/grouped_test_reports_app.vue127
-rw-r--r--app/assets/javascripts/reports/components/issue_status_icon.vue6
-rw-r--r--app/assets/javascripts/reports/components/issues_list.vue6
-rw-r--r--app/assets/javascripts/reports/components/modal.vue40
-rw-r--r--app/assets/javascripts/reports/components/test_issue_body.vue42
-rw-r--r--app/assets/javascripts/reports/store/actions.js8
-rw-r--r--app/assets/javascripts/reports/store/index.js13
-rw-r--r--app/assets/javascripts/reports/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/reports/store/mutations.js3
-rw-r--r--app/assets/javascripts/reports/store/state.js1
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue20
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue132
-rw-r--r--app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue34
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/help_state.vue14
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue6
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js20
-rw-r--r--app/assets/javascripts/sidebar/mount_milestone_sidebar.js19
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js39
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js47
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue64
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_countdown.vue49
-rw-r--r--app/assets/javascripts/vue_shared/directives/tooltip.js8
-rw-r--r--app/assets/stylesheets/framework/typography.scss2
-rw-r--r--app/controllers/concerns/boards_responses.rb5
-rw-r--r--app/controllers/groups/boards_controller.rb18
-rw-r--r--app/controllers/projects/boards_controller.rb20
-rw-r--r--app/controllers/projects/git_http_controller.rb5
-rw-r--r--app/finders/issuable_finder.rb35
-rw-r--r--app/finders/issues_finder.rb8
-rw-r--r--app/models/board_group_recent_visit.rb25
-rw-r--r--app/models/board_project_recent_visit.rb25
-rw-r--r--app/models/commit_status.rb5
-rw-r--r--app/models/environment.rb2
-rw-r--r--app/presenters/commit_status_presenter.rb3
-rw-r--r--app/services/boards/visits/create_service.rb17
-rw-r--r--app/services/boards/visits/latest_service.rb17
-rw-r--r--app/services/resource_events/merge_into_notes_service.rb2
-rw-r--r--app/views/projects/tree/_tree_commit_column.html.haml2
-rw-r--r--app/views/shared/boards/components/_board.html.haml15
-rw-r--r--app/views/shared/notifications/_button.html.haml4
-rw-r--r--app/workers/post_receive.rb13
-rwxr-xr-xbin/web5
-rwxr-xr-xbin/web_puma63
-rw-r--r--changelogs/unreleased/27231-add-license-data-to-projects-endpoint.yml5
-rw-r--r--changelogs/unreleased/40372-prometheus-dashboard-broken-on-firefox.yml5
-rw-r--r--changelogs/unreleased/42790-improve-feedback-for-internal-git-access-checks-timeouts.yml5
-rw-r--r--changelogs/unreleased/45068-no-longer-require-a-deploy-to-start-prometheus-monitoring.yml5
-rw-r--r--changelogs/unreleased/51306-fix-inaccessible-dropdown-for-codeless-projects.yml5
-rw-r--r--changelogs/unreleased/51335-fail-early-when-user-cannot-be-identified.yml5
-rw-r--r--changelogs/unreleased/52115-Link-button-in-markdown-editor-should-recognize-URLs.yml5
-rw-r--r--changelogs/unreleased/52384-api-filter-assignee-none-any.yml5
-rw-r--r--changelogs/unreleased/52545-guest-create-issue-in-group-board.yml5
-rw-r--r--changelogs/unreleased/52993-ldap-rename_provider-rake-task-broken.yml5
-rw-r--r--changelogs/unreleased/53133-jobs-list.yml5
-rw-r--r--changelogs/unreleased/add-failure-reason-for-execution-timeout.yml5
-rw-r--r--changelogs/unreleased/an-multithreading.yml5
-rw-r--r--changelogs/unreleased/avoid-lock-when-introduce-new-failure-reason.yml5
-rw-r--r--changelogs/unreleased/bvl-preload-user-status-for-events.yml5
-rw-r--r--changelogs/unreleased/bw-automatically-navigate-to-last-board-visited.yml5
-rw-r--r--changelogs/unreleased/jramsay-42673-commit-tooltip.yml5
-rw-r--r--changelogs/unreleased/pl-uprade-prometheus-alertmanager.yml5
-rw-r--r--config/initializers/7_prometheus_metrics.rb22
-rw-r--r--config/initializers/8_metrics.rb4
-rw-r--r--config/initializers/active_record_lifecycle.rb23
-rw-r--r--config/initializers/macos.rb13
-rw-r--r--config/initializers/rbtrace.rb9
-rw-r--r--config/initializers/routing_draw.rb8
-rw-r--r--config/initializers/sidekiq.rb2
-rw-r--r--config/puma.example.development.rb77
-rw-r--r--config/routes.rb2
-rw-r--r--config/routes/admin.rb2
-rw-r--r--config/routes/group.rb3
-rw-r--r--config/unicorn.rb.example37
-rw-r--r--config/unicorn.rb.example.development67
-rw-r--r--db/migrate/20181010235606_create_board_project_recent_visits.rb19
-rw-r--r--db/migrate/20181016152238_create_board_group_recent_visits.rb20
-rw-r--r--db/schema.rb34
-rw-r--r--doc/README.md1
-rw-r--r--doc/administration/high_availability/redis.md8
-rw-r--r--doc/administration/high_availability/redis_source.md4
-rw-r--r--doc/api/issues.md6
-rw-r--r--doc/api/merge_requests.md6
-rw-r--r--doc/api/projects.md49
-rw-r--r--doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md6
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/development/ee_features.md27
-rw-r--r--doc/development/testing_guide/review_apps.md15
-rw-r--r--doc/topics/authentication/index.md2
-rw-r--r--doc/topics/autodevops/index.md13
-rw-r--r--doc/university/README.md2
-rw-r--r--doc/university/glossary/README.md2
-rw-r--r--doc/update/11.4-to-11.5.md404
-rw-r--r--doc/user/project/clusters/runbooks/index.md49
-rw-r--r--doc/user/project/issue_board.md3
-rw-r--r--doc/user/project/pages/getting_started_part_three.md2
-rw-r--r--lib/api/entities.rb23
-rw-r--r--lib/api/helpers/custom_validators.rb13
-rw-r--r--lib/api/internal.rb2
-rw-r--r--lib/api/issues.rb3
-rw-r--r--lib/api/merge_requests.rb3
-rw-r--r--lib/api/projects.rb9
-rw-r--r--lib/api/runner.rb3
-rw-r--r--lib/gitlab/checks/change_access.rb110
-rw-r--r--lib/gitlab/checks/lfs_integrity.rb5
-rw-r--r--lib/gitlab/checks/timed_logger.rb83
-rw-r--r--lib/gitlab/ci/status/build/failed.rb3
-rw-r--r--lib/gitlab/cluster/lifecycle_events.rb99
-rw-r--r--lib/gitlab/cluster/puma_worker_killer_initializer.rb32
-rw-r--r--lib/gitlab/git/lfs_changes.rb4
-rw-r--r--lib/gitlab/git_access.rb22
-rw-r--r--lib/gitlab/git_post_receive.rb4
-rw-r--r--lib/gitlab/gitaly_client/blob_service.rb17
-rw-r--r--lib/gitlab/identifier.rb21
-rw-r--r--lib/gitlab/patch/draw_route.rb38
-rw-r--r--lib/tasks/gitlab/ldap.rake2
-rw-r--r--qa/qa.rb4
-rw-r--r--qa/qa/factory/README.md369
-rw-r--r--qa/qa/factory/base.rb79
-rw-r--r--qa/qa/factory/dependency.rb28
-rw-r--r--qa/qa/factory/product.rb33
-rw-r--r--qa/qa/factory/repository/project_push.rb11
-rw-r--r--qa/qa/factory/repository/wiki_push.rb10
-rw-r--r--qa/qa/factory/resource/branch.rb10
-rw-r--r--qa/qa/factory/resource/deploy_key.rb22
-rw-r--r--qa/qa/factory/resource/deploy_token.rb20
-rw-r--r--qa/qa/factory/resource/file.rb8
-rw-r--r--qa/qa/factory/resource/fork.rb25
-rw-r--r--qa/qa/factory/resource/group.rb8
-rw-r--r--qa/qa/factory/resource/issue.rb17
-rw-r--r--qa/qa/factory/resource/kubernetes_cluster.rb19
-rw-r--r--qa/qa/factory/resource/label.rb16
-rw-r--r--qa/qa/factory/resource/merge_request.rb42
-rw-r--r--qa/qa/factory/resource/merge_request_from_fork.rb21
-rw-r--r--qa/qa/factory/resource/personal_access_token.rb8
-rw-r--r--qa/qa/factory/resource/project.rb29
-rw-r--r--qa/qa/factory/resource/project_imported_from_github.rb8
-rw-r--r--qa/qa/factory/resource/project_milestone.rb15
-rw-r--r--qa/qa/factory/resource/runner.rb10
-rw-r--r--qa/qa/factory/resource/sandbox.rb8
-rw-r--r--qa/qa/factory/resource/secret_variable.rb10
-rw-r--r--qa/qa/factory/resource/ssh_key.rb12
-rw-r--r--qa/qa/factory/resource/user.rb11
-rw-r--r--qa/qa/factory/resource/wiki.rb8
-rw-r--r--qa/qa/factory/settings/hashed_storage.rb8
-rw-r--r--qa/qa/git/repository.rb8
-rw-r--r--qa/qa/runtime/logger.rb6
-rw-r--r--qa/qa/scenario/test/integration/ldap_no_tls.rb13
-rw-r--r--qa/qa/scenario/test/integration/ldap_tls.rb (renamed from qa/qa/scenario/test/integration/ldap.rb)6
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb4
-rw-r--r--qa/spec/factory/base_spec.rb143
-rw-r--r--qa/spec/factory/dependency_spec.rb79
-rw-r--r--qa/spec/factory/product_spec.rb66
-rw-r--r--qa/spec/page/logging_spec.rb6
-rw-r--r--qa/spec/runtime/logger_spec.rb6
-rw-r--r--qa/spec/scenario/test/integration/ldap_spec.rb12
-rw-r--r--spec/controllers/boards/issues_controller_spec.rb19
-rw-r--r--spec/controllers/groups/boards_controller_spec.rb58
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb56
-rw-r--r--spec/factories/board_group_recent_visit.rb9
-rw-r--r--spec/factories/board_project_recent_visit.rb9
-rw-r--r--spec/fast_spec_helper.rb1
-rw-r--r--spec/features/boards/new_issue_spec.rb10
-rw-r--r--spec/features/issues/resource_label_events_spec.rb7
-rw-r--r--spec/finders/issues_finder_spec.rb30
-rw-r--r--spec/javascripts/jobs/components/sidebar_spec.js5
-rw-r--r--spec/javascripts/jobs/store/mutations_spec.js4
-rw-r--r--spec/javascripts/lib/utils/datetime_utility_spec.js (renamed from spec/javascripts/datetime_utility_spec.js)18
-rw-r--r--spec/javascripts/lib/utils/text_markdown_spec.js27
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js16
-rw-r--r--spec/javascripts/vue_shared/components/gl_countdown_spec.js77
-rw-r--r--spec/javascripts/vue_shared/directives/tooltip_spec.js39
-rw-r--r--spec/lib/api/helpers/custom_validators_spec.rb64
-rw-r--r--spec/lib/gitlab/checks/change_access_spec.rb18
-rw-r--r--spec/lib/gitlab/checks/lfs_integrity_spec.rb3
-rw-r--r--spec/lib/gitlab/checks/timed_logger_spec.rb63
-rw-r--r--spec/lib/gitlab/git/lfs_changes_spec.rb4
-rw-r--r--spec/lib/gitlab/git_access_spec.rb10
-rw-r--r--spec/lib/gitlab/identifier_spec.rb49
-rw-r--r--spec/lib/gitlab/patch/draw_route_spec.rb30
-rw-r--r--spec/models/board_group_recent_visit_spec.rb64
-rw-r--r--spec/models/board_project_recent_visit_spec.rb64
-rw-r--r--spec/models/environment_spec.rb41
-rw-r--r--spec/rack_servers/configs/config.ru12
-rw-r--r--spec/rack_servers/configs/puma.rb32
-rw-r--r--spec/rack_servers/puma_spec.rb84
-rw-r--r--spec/rack_servers/unicorn_spec.rb (renamed from spec/unicorn/unicorn_spec.rb)33
-rw-r--r--spec/requests/api/internal_spec.rb18
-rw-r--r--spec/requests/api/issues_spec.rb18
-rw-r--r--spec/requests/api/merge_requests_spec.rb17
-rw-r--r--spec/requests/api/projects_spec.rb38
-rw-r--r--spec/requests/api/runner_spec.rb18
-rw-r--r--spec/services/boards/visits/create_service_spec.rb53
-rw-r--r--spec/services/boards/visits/latest_service_spec.rb47
-rw-r--r--spec/services/resource_events/merge_into_notes_service_spec.rb9
-rw-r--r--spec/workers/post_receive_spec.rb133
-rw-r--r--vendor/prometheus/values.yaml4
294 files changed, 5123 insertions, 2553 deletions
diff --git a/.gitignore b/.gitignore
index 82b3d08f7a8..aecaae95b8c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,6 +40,7 @@ eslint-report.html
/config/redis.queues.yml
/config/redis.shared_state.yml
/config/unicorn.rb
+/config/puma.rb
/config/secrets.yml
/config/sidekiq.yml
/config/registry.key
diff --git a/.rubocop.yml b/.rubocop.yml
index 0f4018326a1..a95ded8af1c 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -76,10 +76,14 @@ Naming/FileName:
- 'qa/qa/specs/**/*'
- 'qa/bin/*'
- 'config/**/*'
+ - 'ee/config/**/*'
- 'lib/generators/**/*'
- 'locale/unfound_translations.rb'
- 'ee/locale/unfound_translations.rb'
- 'ee/lib/generators/**/*'
+ - 'qa/qa/scenario/test/integration/ldap_no_tls.rb'
+ - 'qa/qa/scenario/test/integration/ldap_tls.rb'
+
IgnoreExecutableScripts: true
AllowedAcronyms:
- EE
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 9da0a092a0d..6da4de57dc6 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-8.4.0 \ No newline at end of file
+8.4.1
diff --git a/Gemfile b/Gemfile
index c442ed9065e..d8c996cf6c4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -153,6 +153,11 @@ group :unicorn do
gem 'unicorn-worker-killer', '~> 0.4.4'
end
+group :puma do
+ gem 'puma', '~> 3.12', require: false
+ gem 'puma_worker_killer', require: false
+end
+
# State machine
gem 'state_machines-activerecord', '~> 0.5.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index 240fcd4e551..e533b564d15 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -598,6 +598,10 @@ GEM
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (3.0.3)
+ puma (3.12.0)
+ puma_worker_killer (0.1.0)
+ get_process_mem (~> 0.2)
+ puma (>= 2.7, < 4)
pyu-ruby-sasl (0.0.3.3)
rack (1.6.10)
rack-accept (0.4.5)
@@ -1076,6 +1080,8 @@ DEPENDENCIES
prometheus-client-mmap (~> 0.9.4)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
+ puma (~> 3.12)
+ puma_worker_killer
rack-attack (~> 4.4.1)
rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.2.1)
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index 22c783c2d3b..b24911f3bb2 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -602,6 +602,10 @@ GEM
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (3.0.3)
+ puma (3.12.0)
+ puma_worker_killer (0.1.0)
+ get_process_mem (~> 0.2)
+ puma (>= 2.7, < 4)
pyu-ruby-sasl (0.0.3.3)
rack (2.0.5)
rack-accept (0.4.5)
@@ -1085,6 +1089,8 @@ DEPENDENCIES
prometheus-client-mmap (~> 0.9.4)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
+ puma (~> 3.12)
+ puma_worker_killer
rack-attack (~> 4.4.1)
rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.2.1)
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index 75477ebb3b3..623cda5679a 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -53,6 +53,9 @@ export default Vue.extend({
const { issuesSize } = this.list;
return `${n__('%d issue', '%d issues', issuesSize)}`;
},
+ isNewIssueShown() {
+ return this.list.type === 'backlog' || (!this.disabled && this.list.type !== 'closed');
+ }
},
watch: {
filter: {
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 4e8fe16160a..427a0868b0c 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -46,7 +46,7 @@ export default {
selectable: true,
data: (term, callback) => {
this.loading = true;
- return Api.groupProjects(this.groupId, term, {}, projects => {
+ return Api.groupProjects(this.groupId, term, {with_issues_enabled: true}, projects => {
this.loading = false;
callback(projects);
});
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
index 906769ee6a2..28a02230d89 100644
--- a/app/assets/javascripts/jobs/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -31,7 +31,7 @@ export default {
},
},
computed: {
- ...mapState(['job', 'stages', 'jobs', 'selectedStage']),
+ ...mapState(['job', 'stages', 'jobs', 'selectedStage', 'isLoadingStages']),
coverage() {
return `${this.job.coverage}%`;
},
@@ -59,10 +59,10 @@ export default {
return '';
}
- let t = this.job.metadata.timeout_human_readable;
- if (this.job.metadata.timeout_source !== '') {
- t += ` (from ${this.job.metadata.timeout_source})`;
- }
+ let t = this.job.metadata.timeout_human_readable;
+ if (this.job.metadata.timeout_source !== '') {
+ t += ` (from ${this.job.metadata.timeout_source})`;
+ }
return t;
},
@@ -270,6 +270,7 @@ export default {
/>
<stages-dropdown
+ v-if="!isLoadingStages"
:stages="stages"
:pipeline="job.pipeline"
:selected-stage="selectedStage"
diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue
index e5e1d56e287..dc26b246d71 100644
--- a/app/assets/javascripts/jobs/components/stages_dropdown.vue
+++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue
@@ -22,7 +22,6 @@ export default {
required: true,
},
},
-
computed: {
hasRef() {
return !_.isEmpty(this.pipeline.ref);
diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js
index 4195d787f12..cd440d21c1f 100644
--- a/app/assets/javascripts/jobs/store/mutations.js
+++ b/app/assets/javascripts/jobs/store/mutations.js
@@ -71,7 +71,7 @@ export default {
* after the first request,
* and we do not want to hijack that
*/
- if (state.selectedStage === 'More' && job.stage) {
+ if (state.selectedStage === '' && job.stage) {
state.selectedStage = job.stage;
}
},
diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js
index 0eb269ca38f..04825187c99 100644
--- a/app/assets/javascripts/jobs/store/state.js
+++ b/app/assets/javascripts/jobs/store/state.js
@@ -1,5 +1,3 @@
-import { __ } from '~/locale';
-
export default () => ({
jobEndpoint: null,
traceEndpoint: null,
@@ -29,7 +27,7 @@ export default () => ({
// sidebar dropdown & list of jobs
isLoadingStages: false,
isLoadingJobs: false,
- selectedStage: __('More'),
+ selectedStage: '',
stages: [],
jobs: [],
});
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 1bdf98d0c97..46740308f17 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -473,3 +473,15 @@ export const stringifyTime = timeObject => {
*/
export const abbreviateTime = timeStr =>
timeStr.split(' ').filter(unitStr => unitStr.charAt(0) !== '0')[0];
+
+/**
+ * Calculates the milliseconds between now and a given date string.
+ * The result cannot become negative.
+ *
+ * @param endDate date string that the time difference is calculated for
+ * @return {number} number of milliseconds remaining until the given date
+ */
+export const calculateRemainingMilliseconds = endDate => {
+ const remainingMilliseconds = new Date(endDate).getTime() - Date.now();
+ return Math.max(remainingMilliseconds, 0);
+};
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index e26a6b986be..c52cfb806a2 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -2,6 +2,8 @@
import $ from 'jquery';
import { insertText } from '~/lib/utils/common_utils';
+const LINK_TAG_PATTERN = '[{text}](url)';
+
function selectedText(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
}
@@ -76,6 +78,21 @@ export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wr
removedFirstNewLine = false;
currentLineEmpty = false;
+ // check for link pattern and selected text is an URL
+ // if so fill in the url part instead of the text part of the pattern.
+ if (tag === LINK_TAG_PATTERN) {
+ if (URL) {
+ try {
+ const ignoredUrl = new URL(selected);
+ // valid url
+ tag = '[text]({text})';
+ select = 'text';
+ } catch (e) {
+ // ignore - no valid url
+ }
+ }
+ }
+
// Remove the first newline
if (selected.indexOf('\n') === 0) {
removedFirstNewLine = true;
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 67338aa96c3..98182d92c2f 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -149,7 +149,7 @@ export default {
.catch(() => Flash(s__('Metrics|There was an error getting deployment information.'))),
this.service
.getEnvironmentsData()
- .then((data) => this.store.storeEnvironmentsData(data))
+ .then(data => this.store.storeEnvironmentsData(data))
.catch(() => Flash(s__('Metrics|There was an error getting environments information.'))),
])
.then(() => {
@@ -157,6 +157,7 @@ export default {
this.state = 'noData';
return;
}
+
this.showEmptyState = false;
})
.then(this.resize)
@@ -195,7 +196,10 @@ export default {
name="chevron-down"
/>
</button>
- <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
+ <div
+ v-if="store.environmentsData.length > 0"
+ class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"
+ >
<ul>
<li
v-for="environment in store.environmentsData"
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index ed5c8b15945..5c6e2e09e46 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -121,6 +121,7 @@ export default {
draw() {
const breakpointSize = bp.getBreakpointSize();
const query = this.graphData.queries[0];
+ const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width;
this.margin = measurements.large.margin;
if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
this.graphHeight = 300;
@@ -130,13 +131,13 @@ export default {
this.unitOfDisplay = query.unit || '';
this.yAxisLabel = this.graphData.y_label || 'Values';
this.legendTitle = query.label || 'Average';
- this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right;
+ this.graphWidth = svgWidth - this.margin.left - this.margin.right;
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
this.baseGraphHeight = this.graphHeight - 50;
this.baseGraphWidth = this.graphWidth;
// pixel offsets inside the svg and outside are not 1:1
- this.realPixelRatio = this.$refs.baseSvg.clientWidth / this.baseGraphWidth;
+ this.realPixelRatio = svgWidth / this.baseGraphWidth;
this.renderAxesPaths();
this.formatDeployments();
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index c68860d98ae..e707f44bf5a 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -95,29 +95,20 @@ export default {
return awardList.filter(award => award.user.id === this.getUserData.id).length;
},
awardTitle(awardsList) {
- const hasReactionByCurrentUser = this.hasReactionByCurrentUser(
- awardsList,
- );
+ const hasReactionByCurrentUser = this.hasReactionByCurrentUser(awardsList);
const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10;
let awardList = awardsList;
// Filter myself from list if I am awarded.
if (hasReactionByCurrentUser) {
- awardList = awardList.filter(
- award => award.user.id !== this.getUserData.id,
- );
+ awardList = awardList.filter(award => award.user.id !== this.getUserData.id);
}
// Get only 9-10 usernames to show in tooltip text.
- const namesToShow = awardList
- .slice(0, TOOLTIP_NAME_COUNT)
- .map(award => award.user.name);
+ const namesToShow = awardList.slice(0, TOOLTIP_NAME_COUNT).map(award => award.user.name);
// Get the remaining list to use in `and x more` text.
- const remainingAwardList = awardList.slice(
- TOOLTIP_NAME_COUNT,
- awardList.length,
- );
+ const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length);
// Add myself to the begining of the list so title will start with You.
if (hasReactionByCurrentUser) {
@@ -128,9 +119,7 @@ export default {
// 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 = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`;
} 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(', ');
@@ -170,9 +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.'));
},
},
};
diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js
index fa4a1c56b20..4532226aa07 100644
--- a/app/assets/javascripts/notes/stores/collapse_utils.js
+++ b/app/assets/javascripts/notes/stores/collapse_utils.js
@@ -68,10 +68,7 @@ export const collapseSystemNotes = notes => {
lastDescriptionSystemNote = note;
lastDescriptionSystemNoteIndex = acc.length;
} else if (lastDescriptionSystemNote) {
- const timeDifferenceMinutes = getTimeDifferenceMinutes(
- lastDescriptionSystemNote,
- note,
- );
+ const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note);
// are they less than 10 minutes appart?
if (timeDifferenceMinutes > 10) {
diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js
index f105b7d0d11..d41b02b4a4b 100644
--- a/app/assets/javascripts/notes/stores/index.js
+++ b/app/assets/javascripts/notes/stores/index.js
@@ -4,5 +4,4 @@ import notesModule from './modules';
Vue.use(Vuex);
-export default () =>
- new Vuex.Store(notesModule());
+export default () => new Vuex.Store(notesModule());
diff --git a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js
index 15e737fff05..d9cf62db3f7 100644
--- a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js
+++ b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js
@@ -31,7 +31,7 @@ export default class AbuseReports {
$messageCellElement.text(originalMessage);
} else {
$messageCellElement.data('messageTruncated', 'true');
- $messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`);
+ $messageCellElement.text(`${originalMessage.substr(0, MAX_MESSAGE_LENGTH - 3)}...`);
}
}
}
diff --git a/app/assets/javascripts/pages/admin/admin.js b/app/assets/javascripts/pages/admin/admin.js
index ff4d6ab15f9..4616a075729 100644
--- a/app/assets/javascripts/pages/admin/admin.js
+++ b/app/assets/javascripts/pages/admin/admin.js
@@ -23,7 +23,7 @@ export default function adminInit() {
}
});
- $('body').on('click', '.js-toggle-colors-link', (e) => {
+ $('body').on('click', '.js-toggle-colors-link', e => {
e.preventDefault();
$('.js-toggle-colors-container').toggleClass('hide');
});
@@ -33,12 +33,15 @@ export default function adminInit() {
$(this).tab('show');
});
- $('.log-bottom').on('click', (e) => {
+ $('.log-bottom').on('click', e => {
e.preventDefault();
const $visibleLog = $('.file-content:visible');
- $visibleLog.animate({
- scrollTop: $visibleLog.find('ol').height(),
- }, 'fast');
+ $visibleLog.animate(
+ {
+ scrollTop: $visibleLog.find('ol').height(),
+ },
+ 'fast',
+ );
});
$('.change-owner-link').on('click', function changeOwnerLinkClick(e) {
@@ -47,7 +50,7 @@ export default function adminInit() {
modal.show();
});
- $('.change-owner-cancel-link').on('click', (e) => {
+ $('.change-owner-cancel-link').on('click', e => {
e.preventDefault();
modal.hide();
$('.change-owner-link').show();
diff --git a/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js b/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js
index 7281f907ec7..455c637a6b3 100644
--- a/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js
+++ b/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js
@@ -1,10 +1,14 @@
import { __ } from '~/locale';
export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE = __('Regex pattern');
-export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE = __('To define internal users, first enable new users set to external');
+export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE = __(
+ 'To define internal users, first enable new users set to external',
+);
function setUserInternalRegexPlaceholder(checkbox) {
- const userInternalRegex = document.getElementById('application_setting_user_default_internal_regex');
+ const userInternalRegex = document.getElementById(
+ 'application_setting_user_default_internal_regex',
+ );
if (checkbox && userInternalRegex) {
if (checkbox.checked) {
userInternalRegex.readOnly = false;
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
index e7ceccb6f47..d5ded3f9a79 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
@@ -17,20 +17,24 @@ export default () => {
const previewPath = $('textarea#broadcast_message_message').data('previewPath');
- $('textarea#broadcast_message_message').on('input', _.debounce(function onMessageInput() {
- const message = $(this).val();
- if (message === '') {
- $('.js-broadcast-message-preview').text('Your message here');
- } else {
- axios.post(previewPath, {
- broadcast_message: {
- message,
- },
- })
- .then(({ data }) => {
- $('.js-broadcast-message-preview').html(data.message);
- })
- .catch(() => flash(__('An error occurred while rendering preview broadcast message')));
- }
- }, 250));
+ $('textarea#broadcast_message_message').on(
+ 'input',
+ _.debounce(function onMessageInput() {
+ const message = $(this).val();
+ if (message === '') {
+ $('.js-broadcast-message-preview').text('Your message here');
+ } else {
+ axios
+ .post(previewPath, {
+ broadcast_message: {
+ message,
+ },
+ })
+ .then(({ data }) => {
+ $('.js-broadcast-message-preview').html(data.message);
+ })
+ .catch(() => flash(__('An error occurred while rendering preview broadcast message')));
+ }
+ }, 250),
+ );
};
diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
index bc84666779e..e2fec3c7172 100644
--- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
+++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
@@ -1,39 +1,42 @@
<script>
- import axios from '~/lib/utils/axios_utils';
- import createFlash from '~/flash';
- import GlModal from '~/vue_shared/components/gl_modal.vue';
- import { redirectTo } from '~/lib/utils/url_utility';
- import { s__ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
+import GlModal from '~/vue_shared/components/gl_modal.vue';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
- export default {
- components: {
- GlModal,
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ url: {
+ type: String,
+ required: true,
},
- props: {
- url: {
- type: String,
- required: true,
- },
+ },
+ computed: {
+ text() {
+ return s__(
+ 'AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running.',
+ );
},
- computed: {
- text() {
- return s__('AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running.');
- },
+ },
+ methods: {
+ onSubmit() {
+ return axios
+ .post(this.url)
+ .then(response => {
+ // follow the rediect to refresh the page
+ redirectTo(response.request.responseURL);
+ })
+ .catch(error => {
+ createFlash(s__('AdminArea|Stopping jobs failed'));
+ throw error;
+ });
},
- methods: {
- onSubmit() {
- return axios.post(this.url)
- .then((response) => {
- // follow the rediect to refresh the page
- redirectTo(response.request.responseURL);
- })
- .catch((error) => {
- createFlash(s__('AdminArea|Stopping jobs failed'));
- throw error;
- });
- },
- },
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js
index 31c96eb87af..d6b1e747aec 100644
--- a/app/assets/javascripts/pages/admin/projects/index.js
+++ b/app/assets/javascripts/pages/admin/projects/index.js
@@ -4,6 +4,7 @@ import NamespaceSelect from '../../../namespace_select';
document.addEventListener('DOMContentLoaded', () => {
new ProjectsList(); // eslint-disable-line no-new
- document.querySelectorAll('.js-namespace-select')
+ document
+ .querySelectorAll('.js-namespace-select')
.forEach(dropdown => new NamespaceSelect({ dropdown }));
});
diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
index ff66d3a8ac4..3c383735f4a 100644
--- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
+++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
@@ -1,81 +1,84 @@
<script>
- import _ from 'underscore';
- import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
- import { s__, sprintf } from '~/locale';
+import _ from 'underscore';
+import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+import { s__, sprintf } from '~/locale';
- export default {
- components: {
- DeprecatedModal,
+export default {
+ components: {
+ DeprecatedModal,
+ },
+ props: {
+ deleteProjectUrl: {
+ type: String,
+ required: false,
+ default: '',
},
- props: {
- deleteProjectUrl: {
- type: String,
- required: false,
- default: '',
- },
- projectName: {
- type: String,
- required: false,
- default: '',
- },
- csrfToken: {
- type: String,
- required: false,
- default: '',
- },
+ projectName: {
+ type: String,
+ required: false,
+ default: '',
},
- data() {
- return {
- enteredProjectName: '',
- };
+ csrfToken: {
+ type: String,
+ required: false,
+ default: '',
},
- computed: {
- title() {
- return sprintf(s__('AdminProjects|Delete Project %{projectName}?'),
- {
- projectName: `'${_.escape(this.projectName)}'`,
- },
- false,
- );
- },
- text() {
- return sprintf(s__(`AdminProjects|
+ },
+ data() {
+ return {
+ enteredProjectName: '',
+ };
+ },
+ computed: {
+ title() {
+ return sprintf(
+ s__('AdminProjects|Delete Project %{projectName}?'),
+ {
+ projectName: `'${_.escape(this.projectName)}'`,
+ },
+ false,
+ );
+ },
+ text() {
+ return sprintf(
+ s__(`AdminProjects|
You’re about to permanently delete the project %{projectName}, its repository,
and all related resources including issues, merge requests, etc.. Once you confirm and press
%{strong_start}Delete project%{strong_end}, it cannot be undone or recovered.`),
- {
- projectName: `<strong>${_.escape(this.projectName)}</strong>`,
- strong_start: '<strong>',
- strong_end: '</strong>',
- },
- false,
- );
- },
- confirmationTextLabel() {
- return sprintf(s__('AdminUsers|To confirm, type %{projectName}'),
- {
- projectName: `<code>${_.escape(this.projectName)}</code>`,
- },
- false,
- );
- },
- primaryButtonLabel() {
- return s__('AdminProjects|Delete project');
- },
- canSubmit() {
- return this.enteredProjectName === this.projectName;
- },
+ {
+ projectName: `<strong>${_.escape(this.projectName)}</strong>`,
+ strong_start: '<strong>',
+ strong_end: '</strong>',
+ },
+ false,
+ );
+ },
+ confirmationTextLabel() {
+ return sprintf(
+ s__('AdminUsers|To confirm, type %{projectName}'),
+ {
+ projectName: `<code>${_.escape(this.projectName)}</code>`,
+ },
+ false,
+ );
+ },
+ primaryButtonLabel() {
+ return s__('AdminProjects|Delete project');
+ },
+ canSubmit() {
+ return this.enteredProjectName === this.projectName;
+ },
+ },
+ methods: {
+ onCancel() {
+ this.enteredProjectName = '';
},
- methods: {
- onCancel() {
- this.enteredProjectName = '';
- },
- onSubmit() {
- this.$refs.form.submit();
- this.enteredProjectName = '';
- },
+ onSubmit() {
+ this.$refs.form.submit();
+ this.enteredProjectName = '';
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/pages/admin/projects/index/index.js b/app/assets/javascripts/pages/admin/projects/index/index.js
index ddbefec87b6..6fa8760545d 100644
--- a/app/assets/javascripts/pages/admin/projects/index/index.js
+++ b/app/assets/javascripts/pages/admin/projects/index/index.js
@@ -28,7 +28,7 @@ document.addEventListener('DOMContentLoaded', () => {
},
});
- $(document).on('shown.bs.modal', (event) => {
+ $(document).on('shown.bs.modal', event => {
if (event.relatedTarget.classList.contains('delete-project-button')) {
const buttonProps = event.relatedTarget.dataset;
deleteModal.deleteProjectUrl = buttonProps.deleteProjectUrl;
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index 8d5efcdcd96..4b33fcc759a 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -1,114 +1,119 @@
<script>
- import _ from 'underscore';
- import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
- import { s__, sprintf } from '~/locale';
+import _ from 'underscore';
+import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+import { s__, sprintf } from '~/locale';
- export default {
- components: {
- DeprecatedModal,
+export default {
+ components: {
+ DeprecatedModal,
+ },
+ props: {
+ deleteUserUrl: {
+ type: String,
+ required: false,
+ default: '',
},
- props: {
- deleteUserUrl: {
- type: String,
- required: false,
- default: '',
- },
- blockUserUrl: {
- type: String,
- required: false,
- default: '',
- },
- deleteContributions: {
- type: Boolean,
- required: false,
- default: false,
- },
- username: {
- type: String,
- required: false,
- default: '',
- },
- csrfToken: {
- type: String,
- required: false,
- default: '',
- },
+ blockUserUrl: {
+ type: String,
+ required: false,
+ default: '',
},
- data() {
- return {
- enteredUsername: '',
- };
+ deleteContributions: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- computed: {
- title() {
- const keepContributionsTitle = s__('AdminUsers|Delete User %{username}?');
- const deleteContributionsTitle = s__('AdminUsers|Delete User %{username} and contributions?');
+ username: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ csrfToken: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ enteredUsername: '',
+ };
+ },
+ computed: {
+ title() {
+ const keepContributionsTitle = s__('AdminUsers|Delete User %{username}?');
+ const deleteContributionsTitle = s__('AdminUsers|Delete User %{username} and contributions?');
- return sprintf(
- this.deleteContributions ? deleteContributionsTitle : keepContributionsTitle, {
- username: `'${_.escape(this.username)}'`,
- }, false);
- },
- text() {
- const keepContributionsText = s__(`AdminArea|
+ return sprintf(
+ this.deleteContributions ? deleteContributionsTitle : keepContributionsTitle,
+ {
+ username: `'${_.escape(this.username)}'`,
+ },
+ false,
+ );
+ },
+ text() {
+ const keepContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}.
Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user".
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
- const deleteContributionsText = s__(`AdminArea|
+ const deleteContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}.
This will delete all of the issues, merge requests, and groups linked to them.
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
- return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText,
- {
- username: `<strong>${_.escape(this.username)}</strong>`,
- strong_start: '<strong>',
- strong_end: '</strong>',
- },
- false,
- );
- },
- confirmationTextLabel() {
- return sprintf(s__('AdminUsers|To confirm, type %{username}'),
- {
- username: `<code>${_.escape(this.username)}</code>`,
- },
- false,
- );
- },
- primaryButtonLabel() {
- const keepContributionsLabel = s__('AdminUsers|Delete user');
- const deleteContributionsLabel = s__('AdminUsers|Delete user and contributions');
+ return sprintf(
+ this.deleteContributions ? deleteContributionsText : keepContributionsText,
+ {
+ username: `<strong>${_.escape(this.username)}</strong>`,
+ strong_start: '<strong>',
+ strong_end: '</strong>',
+ },
+ false,
+ );
+ },
+ confirmationTextLabel() {
+ return sprintf(
+ s__('AdminUsers|To confirm, type %{username}'),
+ {
+ username: `<code>${_.escape(this.username)}</code>`,
+ },
+ false,
+ );
+ },
+ primaryButtonLabel() {
+ const keepContributionsLabel = s__('AdminUsers|Delete user');
+ const deleteContributionsLabel = s__('AdminUsers|Delete user and contributions');
- return this.deleteContributions ? deleteContributionsLabel : keepContributionsLabel;
- },
- secondaryButtonLabel() {
- return s__('AdminUsers|Block user');
- },
- canSubmit() {
- return this.enteredUsername === this.username;
- },
+ return this.deleteContributions ? deleteContributionsLabel : keepContributionsLabel;
},
- methods: {
- onCancel() {
- this.enteredUsername = '';
- },
- onSecondaryAction() {
- const { form } = this.$refs;
+ secondaryButtonLabel() {
+ return s__('AdminUsers|Block user');
+ },
+ canSubmit() {
+ return this.enteredUsername === this.username;
+ },
+ },
+ methods: {
+ onCancel() {
+ this.enteredUsername = '';
+ },
+ onSecondaryAction() {
+ const { form } = this.$refs;
- form.action = this.blockUserUrl;
- this.$refs.method.value = 'put';
+ form.action = this.blockUserUrl;
+ this.$refs.method.value = 'put';
- form.submit();
- },
- onSubmit() {
- this.$refs.form.submit();
- this.enteredUsername = '';
- },
+ form.submit();
+ },
+ onSubmit() {
+ this.$refs.form.submit();
+ this.enteredUsername = '';
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/pages/admin/users/index.js b/app/assets/javascripts/pages/admin/users/index.js
index 06599c3fd5f..45046688b57 100644
--- a/app/assets/javascripts/pages/admin/users/index.js
+++ b/app/assets/javascripts/pages/admin/users/index.js
@@ -32,12 +32,14 @@ document.addEventListener('DOMContentLoaded', () => {
},
});
- $(document).on('shown.bs.modal', (event) => {
+ $(document).on('shown.bs.modal', event => {
if (event.relatedTarget.classList.contains('delete-user-button')) {
const buttonProps = event.relatedTarget.dataset;
deleteModal.deleteUserUrl = buttonProps.deleteUserUrl;
deleteModal.blockUserUrl = buttonProps.blockUserUrl;
- deleteModal.deleteContributions = event.relatedTarget.hasAttribute('data-delete-contributions');
+ deleteModal.deleteContributions = event.relatedTarget.hasAttribute(
+ 'data-delete-contributions',
+ );
deleteModal.username = buttonProps.username;
}
});
diff --git a/app/assets/javascripts/pages/admin/users/new/index.js b/app/assets/javascripts/pages/admin/users/new/index.js
index 58bfa8d64e7..3e6a090cb0e 100644
--- a/app/assets/javascripts/pages/admin/users/new/index.js
+++ b/app/assets/javascripts/pages/admin/users/new/index.js
@@ -4,7 +4,9 @@ export default class UserInternalRegexHandler {
constructor() {
this.regexPattern = $('[data-user-internal-regex-pattern]').data('user-internal-regex-pattern');
if (this.regexPattern && this.regexPattern !== '') {
- this.regexOptions = $('[data-user-internal-regex-options]').data('user-internal-regex-options');
+ this.regexOptions = $('[data-user-internal-regex-options]').data(
+ 'user-internal-regex-options',
+ );
this.external = $('#user_external');
this.warningMessage = $('#warning_external_automatically_set');
this.addListenerToEmailField();
@@ -13,7 +15,7 @@ export default class UserInternalRegexHandler {
}
addListenerToEmailField() {
- $('#user_email').on('input', (event) => {
+ $('#user_email').on('input', event => {
this.setExternalCheckbox(event.currentTarget.value);
});
}
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index 72f3f70b98f..1b56b97f751 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -79,7 +79,8 @@ export default class Todos {
.then(({ data }) => {
this.updateRowState(target);
this.updateBadges(data);
- }).catch(() => {
+ })
+ .catch(() => {
this.updateRowState(target, true);
return flash(__('Error updating todo status.'));
});
@@ -118,10 +119,12 @@ export default class Todos {
axios[target.dataset.method](target.dataset.href, {
ids: this.todo_ids,
- }).then(({ data }) => {
- this.updateAllState(target, data);
- this.updateBadges(data);
- }).catch(() => flash(__('Error updating status for all todos.')));
+ })
+ .then(({ data }) => {
+ this.updateAllState(target, data);
+ this.updateBadges(data);
+ })
+ .catch(() => flash(__('Error updating status for all todos.')));
}
updateAllState(target, data) {
@@ -133,7 +136,7 @@ export default class Todos {
target.removeAttribute('disabled');
target.classList.remove('disabled');
- this.todo_ids = (target === markAllDoneBtn) ? data.updated_ids : [];
+ this.todo_ids = target === markAllDoneBtn ? data.updated_ids : [];
undoAllBtn.classList.toggle('hidden');
markAllDoneBtn.classList.toggle('hidden');
todoListContainer.classList.toggle('hidden');
diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
index 48668562f09..a4778077bc4 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
@@ -1,94 +1,117 @@
<script>
- import axios from '~/lib/utils/axios_utils';
+import axios from '~/lib/utils/axios_utils';
- import Flash from '~/flash';
- import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
- import { n__, s__, sprintf } from '~/locale';
- import { redirectTo } from '~/lib/utils/url_utility';
- import eventHub from '../event_hub';
+import Flash from '~/flash';
+import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+import { n__, s__, sprintf } from '~/locale';
+import { redirectTo } from '~/lib/utils/url_utility';
+import eventHub from '../event_hub';
- export default {
- components: {
- DeprecatedModal,
+export default {
+ components: {
+ DeprecatedModal,
+ },
+ props: {
+ issueCount: {
+ type: Number,
+ required: true,
},
- props: {
- issueCount: {
- type: Number,
- required: true,
- },
- mergeRequestCount: {
- type: Number,
- required: true,
- },
- milestoneId: {
- type: Number,
- required: true,
- },
- milestoneTitle: {
- type: String,
- required: true,
- },
- milestoneUrl: {
- type: String,
- required: true,
- },
+ mergeRequestCount: {
+ type: Number,
+ required: true,
},
- computed: {
- text() {
- const milestoneTitle = sprintf('<strong>%{milestoneTitle}</strong>', { milestoneTitle: this.milestoneTitle });
-
- if (this.issueCount === 0 && this.mergeRequestCount === 0) {
- return sprintf(
- s__(`Milestones|
-You’re about to permanently delete the milestone %{milestoneTitle}.
-This milestone is not currently used in any issues or merge requests.`),
- {
- milestoneTitle,
- },
- false,
- );
- }
+ milestoneId: {
+ type: Number,
+ required: true,
+ },
+ milestoneTitle: {
+ type: String,
+ required: true,
+ },
+ milestoneUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ text() {
+ const milestoneTitle = sprintf('<strong>%{milestoneTitle}</strong>', {
+ milestoneTitle: this.milestoneTitle,
+ });
+ if (this.issueCount === 0 && this.mergeRequestCount === 0) {
return sprintf(
s__(`Milestones|
-You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}.
-Once deleted, it cannot be undone or recovered.`),
+You’re about to permanently delete the milestone %{milestoneTitle}.
+This milestone is not currently used in any issues or merge requests.`),
{
milestoneTitle,
- issuesWithCount: n__('%d issue', '%d issues', this.issueCount),
- mergeRequestsWithCount: n__('%d merge request', '%d merge requests', this.mergeRequestCount),
},
false,
);
- },
- title() {
- return sprintf(s__('Milestones|Delete milestone %{milestoneTitle}?'), { milestoneTitle: this.milestoneTitle });
- },
- },
- methods: {
- onSubmit() {
- eventHub.$emit('deleteMilestoneModal.requestStarted', this.milestoneUrl);
+ }
- return axios.delete(this.milestoneUrl)
- .then((response) => {
- eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: true });
+ return sprintf(
+ s__(`Milestones|
+You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}.
+Once deleted, it cannot be undone or recovered.`),
+ {
+ milestoneTitle,
+ issuesWithCount: n__('%d issue', '%d issues', this.issueCount),
+ mergeRequestsWithCount: n__(
+ '%d merge request',
+ '%d merge requests',
+ this.mergeRequestCount,
+ ),
+ },
+ false,
+ );
+ },
+ title() {
+ return sprintf(s__('Milestones|Delete milestone %{milestoneTitle}?'), {
+ milestoneTitle: this.milestoneTitle,
+ });
+ },
+ },
+ methods: {
+ onSubmit() {
+ eventHub.$emit('deleteMilestoneModal.requestStarted', this.milestoneUrl);
- // follow the rediect to milestones overview page
- redirectTo(response.request.responseURL);
- })
- .catch((error) => {
- eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: false });
+ return axios
+ .delete(this.milestoneUrl)
+ .then(response => {
+ eventHub.$emit('deleteMilestoneModal.requestFinished', {
+ milestoneUrl: this.milestoneUrl,
+ successful: true,
+ });
- if (error.response && error.response.status === 404) {
- Flash(sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), { milestoneTitle: this.milestoneTitle }));
- } else {
- Flash(sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), { milestoneTitle: this.milestoneTitle }));
- }
- throw error;
+ // follow the rediect to milestones overview page
+ redirectTo(response.request.responseURL);
+ })
+ .catch(error => {
+ eventHub.$emit('deleteMilestoneModal.requestFinished', {
+ milestoneUrl: this.milestoneUrl,
+ successful: false,
});
- },
+
+ if (error.response && error.response.status === 404) {
+ Flash(
+ sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), {
+ milestoneTitle: this.milestoneTitle,
+ }),
+ );
+ } else {
+ Flash(
+ sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), {
+ milestoneTitle: this.milestoneTitle,
+ }),
+ );
+ }
+ throw error;
+ });
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js
index d51b5c221e3..1d559dc6e41 100644
--- a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js
+++ b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js
@@ -7,7 +7,9 @@ export default () => {
Vue.use(Translate);
const onRequestFinished = ({ milestoneUrl, successful }) => {
- const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
+ const button = document.querySelector(
+ `.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`,
+ );
if (!successful) {
button.removeAttribute('disabled');
@@ -16,14 +18,16 @@ export default () => {
button.querySelector('.js-loading-icon').classList.add('hidden');
};
- const onRequestStarted = (milestoneUrl) => {
- const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
+ const onRequestStarted = milestoneUrl => {
+ const button = document.querySelector(
+ `.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`,
+ );
button.setAttribute('disabled', '');
button.querySelector('.js-loading-icon').classList.remove('hidden');
eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished);
};
- const onDeleteButtonClick = (event) => {
+ const onDeleteButtonClick = event => {
const button = event.currentTarget;
const modalProps = {
milestoneId: parseInt(button.dataset.milestoneId, 10),
@@ -37,12 +41,12 @@ export default () => {
};
const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button');
- deleteMilestoneButtons.forEach((button) => {
+ deleteMilestoneButtons.forEach(button => {
button.addEventListener('click', onDeleteButtonClick);
});
eventHub.$once('deleteMilestoneModal.mounted', () => {
- deleteMilestoneButtons.forEach((button) => {
+ deleteMilestoneButtons.forEach(button => {
button.removeAttribute('disabled');
});
});
diff --git a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
index 8e79341e96a..fcc62a2b2af 100644
--- a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
+++ b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
@@ -7,20 +7,24 @@ Vue.use(Translate);
export default () => {
const onRequestFinished = ({ milestoneUrl, successful }) => {
- const button = document.querySelector(`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`);
+ const button = document.querySelector(
+ `.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`,
+ );
if (!successful) {
button.removeAttribute('disabled');
}
};
- const onRequestStarted = (milestoneUrl) => {
- const button = document.querySelector(`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`);
+ const onRequestStarted = milestoneUrl => {
+ const button = document.querySelector(
+ `.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`,
+ );
button.setAttribute('disabled', '');
eventHub.$once('promoteMilestoneModal.requestFinished', onRequestFinished);
};
- const onDeleteButtonClick = (event) => {
+ const onDeleteButtonClick = event => {
const button = event.currentTarget;
const modalProps = {
milestoneTitle: button.dataset.milestoneTitle,
@@ -32,12 +36,12 @@ export default () => {
};
const promoteMilestoneButtons = document.querySelectorAll('.js-promote-project-milestone-button');
- promoteMilestoneButtons.forEach((button) => {
+ promoteMilestoneButtons.forEach(button => {
button.addEventListener('click', onDeleteButtonClick);
});
eventHub.$once('promoteMilestoneModal.mounted', () => {
- promoteMilestoneButtons.forEach((button) => {
+ promoteMilestoneButtons.forEach(button => {
button.removeAttribute('disabled');
});
});
diff --git a/app/assets/javascripts/pages/profiles/index.js b/app/assets/javascripts/pages/profiles/index.js
index 04e50963699..883be18b336 100644
--- a/app/assets/javascripts/pages/profiles/index.js
+++ b/app/assets/javascripts/pages/profiles/index.js
@@ -3,9 +3,12 @@ import '~/profile/gl_crop';
import Profile from '~/profile/profile';
document.addEventListener('DOMContentLoaded', () => {
- $(document).on('input.ssh_key', '#key_key', function () { // eslint-disable-line func-names
+ // eslint-disable-next-line func-names
+ $(document).on('input.ssh_key', '#key_key', function() {
const $title = $('#key_title');
- const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
+ const comment = $(this)
+ .val()
+ .match(/^\S+ \S+ (.+)\n?$/);
// Extract the SSH Key title from its comment
if (comment && comment.length > 1) {
diff --git a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
index 8e8f47c21d8..417935e2ad0 100644
--- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
+++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
@@ -5,7 +5,9 @@ document.addEventListener('DOMContentLoaded', () => {
const twoFactorNode = document.querySelector('.js-two-factor-auth');
const skippable = twoFactorNode.dataset.twoFactorSkippable === 'true';
if (skippable) {
- const button = `<a class="btn btn-sm btn-warning float-right" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`;
+ const button = `<a class="btn btn-sm btn-warning float-right" data-method="patch" href="${
+ twoFactorNode.dataset.two_factor_skip_url
+ }">Configure it later</a>`;
const flashAlert = document.querySelector('.flash-alert .container-fluid');
if (flashAlert) flashAlert.insertAdjacentHTML('beforeend', button);
}
diff --git a/app/assets/javascripts/pages/projects/branches/new/index.js b/app/assets/javascripts/pages/projects/branches/new/index.js
index a9658fd1eb4..13ff47d53c2 100644
--- a/app/assets/javascripts/pages/projects/branches/new/index.js
+++ b/app/assets/javascripts/pages/projects/branches/new/index.js
@@ -1,6 +1,11 @@
import $ from 'jquery';
import NewBranchForm from '~/new_branch_form';
-document.addEventListener('DOMContentLoaded', () => (
- new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML))
-));
+document.addEventListener(
+ 'DOMContentLoaded',
+ () =>
+ new NewBranchForm(
+ $('.js-create-branch-form'),
+ JSON.parse(document.getElementById('availableRefs').innerHTML),
+ ),
+);
diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js
index 80159a82bd4..3ccad513c05 100644
--- a/app/assets/javascripts/pages/projects/graphs/charts/index.js
+++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js
@@ -31,14 +31,16 @@ document.addEventListener('DOMContentLoaded', () => {
const chartData = data => ({
labels: Object.keys(data),
- datasets: [{
- fillColor: 'rgba(220,220,220,0.5)',
- strokeColor: 'rgba(220,220,220,1)',
- barStrokeWidth: 1,
- barValueSpacing: 1,
- barDatasetSpacing: 1,
- data: _.values(data),
- }],
+ datasets: [
+ {
+ fillColor: 'rgba(220,220,220,0.5)',
+ strokeColor: 'rgba(220,220,220,1)',
+ barStrokeWidth: 1,
+ barValueSpacing: 1,
+ barDatasetSpacing: 1,
+ data: _.values(data),
+ },
+ ],
});
const hourData = chartData(projectChartData.hour);
@@ -51,7 +53,9 @@ document.addEventListener('DOMContentLoaded', () => {
responsiveChart($('#month-chart'), monthData);
const data = projectChartData.languages;
- const ctx = $('#languages-chart').get(0).getContext('2d');
+ const ctx = $('#languages-chart')
+ .get(0)
+ .getContext('2d');
const options = {
scaleOverlay: true,
responsive: true,
diff --git a/app/assets/javascripts/pages/projects/graphs/show/index.js b/app/assets/javascripts/pages/projects/graphs/show/index.js
index 71f629fbc13..f79c386b59e 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/index.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/index.js
@@ -7,7 +7,8 @@ import ContributorsStatGraph from './stat_graph_contributors';
document.addEventListener('DOMContentLoaded', () => {
const url = document.querySelector('.js-graphs-show').dataset.projectGraphPath;
- axios.get(url)
+ axios
+ .get(url)
.then(({ data }) => {
const graph = new ContributorsStatGraph();
graph.init(data);
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
index 58bb8c5b0c8..76613394af6 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
@@ -3,7 +3,11 @@
import $ from 'jquery';
import _ from 'underscore';
import { n__, s__, createDateTimeFormat, sprintf } from '~/locale';
-import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph';
+import {
+ ContributorsGraph,
+ ContributorsAuthorGraph,
+ ContributorsMasterGraph,
+} from './stat_graph_contributors_graph';
import ContributorsStatGraphUtil from './stat_graph_contributors_util';
export default (function() {
@@ -14,7 +18,7 @@ export default (function() {
ContributorsStatGraph.prototype.init = function(log) {
var author_commits, total_commits;
this.parsed_log = ContributorsStatGraphUtil.parse_log(log);
- this.set_current_field("commits");
+ this.set_current_field('commits');
total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field);
this.add_master_graph(total_commits);
@@ -31,23 +35,26 @@ export default (function() {
var limited_author_data;
this.authors = [];
limited_author_data = author_data.slice(0, 100);
- return _.each(limited_author_data, (function(_this) {
- return function(d) {
- var author_graph, author_header;
- author_header = _this.create_author_header(d);
- $(".contributors-list").append(author_header);
-
- author_graph = new ContributorsAuthorGraph(d.dates);
- _this.authors[d.author_name] = author_graph;
- return author_graph.draw();
- };
- })(this));
+ return _.each(
+ limited_author_data,
+ (function(_this) {
+ return function(d) {
+ var author_graph, author_header;
+ author_header = _this.create_author_header(d);
+ $('.contributors-list').append(author_header);
+
+ author_graph = new ContributorsAuthorGraph(d.dates);
+ _this.authors[d.author_name] = author_graph;
+ return author_graph.draw();
+ };
+ })(this),
+ );
};
ContributorsStatGraph.prototype.format_author_commit_info = function(author) {
var commits;
commits = $('<span/>', {
- "class": 'graph-author-commits-count'
+ class: 'graph-author-commits-count',
});
commits.text(n__('%d commit', '%d commits', author.commits));
return $('<span/>').append(commits);
@@ -56,13 +63,13 @@ export default (function() {
ContributorsStatGraph.prototype.create_author_header = function(author) {
var author_commit_info, author_commit_info_span, author_email, author_name, list_item;
list_item = $('<li/>', {
- "class": 'person',
- style: 'display: block;'
+ class: 'person',
+ style: 'display: block;',
});
author_name = $('<h4>' + author.author_name + '</h4>');
author_email = $('<p class="graph-author-email">' + author.author_email + '</p>');
author_commit_info_span = $('<span/>', {
- "class": 'commits'
+ class: 'commits',
});
author_commit_info = this.format_author_commit_info(author);
author_commit_info_span.html(author_commit_info);
@@ -80,37 +87,41 @@ export default (function() {
};
ContributorsStatGraph.prototype.redraw_authors = function() {
- $("ol").html("");
+ $('ol').html('');
const { x_domain } = ContributorsGraph.prototype;
- const author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain);
-
- return _.each(author_commits, (function(_this) {
- return function(d) {
- _this.redraw_author_commit_info(d);
- if (_this.authors[d.author_name] != null) {
- $(_this.authors[d.author_name].list_item).appendTo("ol");
- _this.authors[d.author_name].set_data(d.dates);
- return _this.authors[d.author_name].redraw();
- }
- return '';
- };
- })(this));
+ const author_commits = ContributorsStatGraphUtil.get_author_data(
+ this.parsed_log,
+ this.field,
+ x_domain,
+ );
+
+ return _.each(
+ author_commits,
+ (function(_this) {
+ return function(d) {
+ _this.redraw_author_commit_info(d);
+ if (_this.authors[d.author_name] != null) {
+ $(_this.authors[d.author_name].list_item).appendTo('ol');
+ _this.authors[d.author_name].set_data(d.dates);
+ return _this.authors[d.author_name].redraw();
+ }
+ return '';
+ };
+ })(this),
+ );
};
ContributorsStatGraph.prototype.set_current_field = function(field) {
- return this.field = field;
+ return (this.field = field);
};
ContributorsStatGraph.prototype.change_date_header = function() {
const { x_domain } = ContributorsGraph.prototype;
- const formattedDateRange = sprintf(
- s__('ContributorsPage|%{startDate} – %{endDate}'),
- {
- startDate: this.dateFormat.format(new Date(x_domain[0])),
- endDate: this.dateFormat.format(new Date(x_domain[1])),
- },
- );
+ const formattedDateRange = sprintf(s__('ContributorsPage|%{startDate} – %{endDate}'), {
+ startDate: this.dateFormat.format(new Date(x_domain[0])),
+ endDate: this.dateFormat.format(new Date(x_domain[1])),
+ });
return $('#date_header').text(formattedDateRange);
};
@@ -120,7 +131,7 @@ export default (function() {
if ($author != null) {
author_list_item = $(this.authors[author.author_name].list_item);
author_commit_info = this.format_author_commit_info(author);
- return author_list_item.find("span").html(author_commit_info);
+ return author_list_item.find('span').html(author_commit_info);
}
return '';
};
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
index 5f91686347a..377dce6c746 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
@@ -11,10 +11,32 @@ import { brushX } from 'd3-brush';
import { timeParse } from 'd3-time-format';
import { dateTickFormat } from '~/lib/utils/tick_formats';
-const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse };
+const d3 = {
+ extent,
+ max,
+ select,
+ scaleTime,
+ scaleLinear,
+ axisLeft,
+ axisBottom,
+ area,
+ brushX,
+ timeParse,
+};
const hasProp = {}.hasOwnProperty;
-const extend = function(child, parent) { for (const key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
+const extend = function(child, parent) {
+ for (const key in parent) {
+ if (hasProp.call(parent, key)) child[key] = parent[key];
+ }
+ function ctor() {
+ this.constructor = child;
+ }
+ ctor.prototype = parent.prototype;
+ child.prototype = new ctor();
+ child.__super__ = parent.prototype;
+ return child;
+};
export const ContributorsGraph = (function() {
function ContributorsGraph() {}
@@ -23,7 +45,7 @@ export const ContributorsGraph = (function() {
top: 20,
right: 10,
bottom: 30,
- left: 40
+ left: 40,
};
ContributorsGraph.prototype.x_domain = null;
@@ -33,35 +55,39 @@ export const ContributorsGraph = (function() {
ContributorsGraph.prototype.dates = [];
ContributorsGraph.prototype.determine_width = function(baseWidth, $parentElement) {
- const parentPaddingWidth = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right'));
+ const parentPaddingWidth =
+ parseFloat($parentElement.css('padding-left')) +
+ parseFloat($parentElement.css('padding-right'));
const marginWidth = this.MARGIN.left + this.MARGIN.right;
return baseWidth - parentPaddingWidth - marginWidth;
};
ContributorsGraph.set_x_domain = function(data) {
- return ContributorsGraph.prototype.x_domain = data;
+ return (ContributorsGraph.prototype.x_domain = data);
};
ContributorsGraph.set_y_domain = function(data) {
- return ContributorsGraph.prototype.y_domain = [
- 0, d3.max(data, function(d) {
- return d.commits = d.commits || d.additions || d.deletions;
- })
- ];
+ return (ContributorsGraph.prototype.y_domain = [
+ 0,
+ d3.max(data, function(d) {
+ return (d.commits = d.commits || d.additions || d.deletions);
+ }),
+ ]);
};
ContributorsGraph.init_x_domain = function(data) {
- return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) {
+ return (ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) {
return d.date;
- });
+ }));
};
ContributorsGraph.init_y_domain = function(data) {
- return ContributorsGraph.prototype.y_domain = [
- 0, d3.max(data, function(d) {
- return d.commits = d.commits || d.additions || d.deletions;
- })
- ];
+ return (ContributorsGraph.prototype.y_domain = [
+ 0,
+ d3.max(data, function(d) {
+ return (d.commits = d.commits || d.additions || d.deletions);
+ }),
+ ]);
};
ContributorsGraph.init_domain = function(data) {
@@ -70,7 +96,7 @@ export const ContributorsGraph = (function() {
};
ContributorsGraph.set_dates = function(data) {
- return ContributorsGraph.prototype.dates = data;
+ return (ContributorsGraph.prototype.dates = data);
};
ContributorsGraph.prototype.set_x_domain = function() {
@@ -87,20 +113,33 @@ export const ContributorsGraph = (function() {
};
ContributorsGraph.prototype.create_scale = function(width, height) {
- this.x = d3.scaleTime().range([0, width]).clamp(true);
- return this.y = d3.scaleLinear().range([height, 0]).nice();
+ this.x = d3
+ .scaleTime()
+ .range([0, width])
+ .clamp(true);
+ return (this.y = d3
+ .scaleLinear()
+ .range([height, 0])
+ .nice());
};
ContributorsGraph.prototype.draw_x_axis = function() {
- return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis);
+ return this.svg
+ .append('g')
+ .attr('class', 'x axis')
+ .attr('transform', 'translate(0, ' + this.height + ')')
+ .call(this.x_axis);
};
ContributorsGraph.prototype.draw_y_axis = function() {
- return this.svg.append("g").attr("class", "y axis").call(this.y_axis);
+ return this.svg
+ .append('g')
+ .attr('class', 'y axis')
+ .call(this.y_axis);
};
ContributorsGraph.prototype.set_data = function(data) {
- return this.data = data;
+ return (this.data = data);
};
return ContributorsGraph;
@@ -137,9 +176,9 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.parse_dates = function(data) {
- const parseDate = d3.timeParse("%Y-%m-%d");
+ const parseDate = d3.timeParse('%Y-%m-%d');
return data.forEach(function(d) {
- return d.date = parseDate(d.date);
+ return (d.date = parseDate(d.date));
});
};
@@ -148,42 +187,63 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.create_axes = function() {
- this.x_axis = d3.axisBottom()
+ this.x_axis = d3
+ .axisBottom()
.scale(this.x)
.tickFormat(dateTickFormat);
- return this.y_axis = d3.axisLeft().scale(this.y).ticks(5);
+ return (this.y_axis = d3
+ .axisLeft()
+ .scale(this.y)
+ .ticks(5));
};
ContributorsMasterGraph.prototype.create_svg = function() {
- this.svg = d3.select("#contributors-master")
- .append("svg")
- .attr("width", this.width + this.MARGIN.left + this.MARGIN.right)
- .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom)
- .attr("class", "tint-box")
- .append("g")
- .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+ this.svg = d3
+ .select('#contributors-master')
+ .append('svg')
+ .attr('width', this.width + this.MARGIN.left + this.MARGIN.right)
+ .attr('height', this.height + this.MARGIN.top + this.MARGIN.bottom)
+ .attr('class', 'tint-box')
+ .append('g')
+ .attr('transform', 'translate(' + this.MARGIN.left + ',' + this.MARGIN.top + ')');
return this.svg;
};
ContributorsMasterGraph.prototype.create_area = function(x, y) {
- return this.area = d3.area().x(function(d) {
- return x(d.date);
- }).y0(this.height).y1(function(d) {
- d.commits = d.commits || d.additions || d.deletions;
- return y(d.commits);
- });
+ return (this.area = d3
+ .area()
+ .x(function(d) {
+ return x(d.date);
+ })
+ .y0(this.height)
+ .y1(function(d) {
+ d.commits = d.commits || d.additions || d.deletions;
+ return y(d.commits);
+ }));
};
ContributorsMasterGraph.prototype.create_brush = function() {
- return this.brush = d3.brushX(this.x).extent([[this.x.range()[0], 0], [this.x.range()[1], this.height]]).on("end", this.update_content);
+ return (this.brush = d3
+ .brushX(this.x)
+ .extent([[this.x.range()[0], 0], [this.x.range()[1], this.height]])
+ .on('end', this.update_content));
};
ContributorsMasterGraph.prototype.draw_path = function(data) {
- return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area);
+ return this.svg
+ .append('path')
+ .datum(data)
+ .attr('class', 'area')
+ .attr('d', this.area);
};
ContributorsMasterGraph.prototype.add_brush = function() {
- return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height);
+ return this.svg
+ .append('g')
+ .attr('class', 'selection')
+ .call(this.brush)
+ .selectAll('rect')
+ .attr('height', this.height);
};
ContributorsMasterGraph.prototype.update_content = function() {
@@ -193,7 +253,7 @@ export const ContributorsMasterGraph = (function(superClass) {
} else {
ContributorsGraph.set_x_domain(this.x_max_domain);
}
- return $("#brush_change").trigger('change');
+ return $('#brush_change').trigger('change');
};
ContributorsMasterGraph.prototype.draw = function() {
@@ -216,9 +276,9 @@ export const ContributorsMasterGraph = (function(superClass) {
this.process_dates(this.data);
ContributorsGraph.set_y_domain(this.data);
this.set_y_domain();
- this.svg.select("path").datum(this.data);
- this.svg.select("path").attr("d", this.area);
- return this.svg.select(".y.axis").call(this.y_axis);
+ this.svg.select('path').datum(this.data);
+ this.svg.select('path').attr('d', this.area);
+ return this.svg.select('.y.axis').call(this.y_axis);
};
return ContributorsMasterGraph;
@@ -252,43 +312,58 @@ export const ContributorsAuthorGraph = (function(superClass) {
};
ContributorsAuthorGraph.prototype.create_axes = function() {
- this.x_axis = d3.axisBottom()
+ this.x_axis = d3
+ .axisBottom()
.scale(this.x)
.ticks(8)
.tickFormat(dateTickFormat);
- return this.y_axis = d3.axisLeft().scale(this.y).ticks(5);
+ return (this.y_axis = d3
+ .axisLeft()
+ .scale(this.y)
+ .ticks(5));
};
ContributorsAuthorGraph.prototype.create_area = function(x, y) {
- return this.area = d3.area().x(function(d) {
- const parseDate = d3.timeParse("%Y-%m-%d");
- return x(parseDate(d));
- }).y0(this.height).y1((function(_this) {
- return function(d) {
- if (_this.data[d] != null) {
- return y(_this.data[d]);
- } else {
- return y(0);
- }
- };
- })(this));
+ return (this.area = d3
+ .area()
+ .x(function(d) {
+ const parseDate = d3.timeParse('%Y-%m-%d');
+ return x(parseDate(d));
+ })
+ .y0(this.height)
+ .y1(
+ (function(_this) {
+ return function(d) {
+ if (_this.data[d] != null) {
+ return y(_this.data[d]);
+ } else {
+ return y(0);
+ }
+ };
+ })(this),
+ ));
};
ContributorsAuthorGraph.prototype.create_svg = function() {
const persons = document.querySelectorAll('.person');
this.list_item = persons[persons.length - 1];
- this.svg = d3.select(this.list_item)
- .append("svg")
- .attr("width", this.width + this.MARGIN.left + this.MARGIN.right)
- .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom)
- .attr("class", "spark")
- .append("g")
- .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+ this.svg = d3
+ .select(this.list_item)
+ .append('svg')
+ .attr('width', this.width + this.MARGIN.left + this.MARGIN.right)
+ .attr('height', this.height + this.MARGIN.top + this.MARGIN.bottom)
+ .attr('class', 'spark')
+ .append('g')
+ .attr('transform', 'translate(' + this.MARGIN.left + ',' + this.MARGIN.top + ')');
return this.svg;
};
ContributorsAuthorGraph.prototype.draw_path = function(data) {
- return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area);
+ return this.svg
+ .append('path')
+ .datum(data)
+ .attr('class', 'area-contributor')
+ .attr('d', this.area);
};
ContributorsAuthorGraph.prototype.draw = function() {
@@ -304,10 +379,10 @@ export const ContributorsAuthorGraph = (function(superClass) {
ContributorsAuthorGraph.prototype.redraw = function() {
this.set_domain();
- this.svg.select("path").datum(this.dates);
- this.svg.select("path").attr("d", this.area);
- this.svg.select(".x.axis").call(this.x_axis);
- return this.svg.select(".y.axis").call(this.y_axis);
+ this.svg.select('path').datum(this.dates);
+ this.svg.select('path').attr('d', this.area);
+ this.svg.select('.x.axis').call(this.x_axis);
+ return this.svg.select('.y.axis').call(this.y_axis);
};
return ContributorsAuthorGraph;
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js
index cd0e2bc023c..988ae164955 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js
@@ -26,12 +26,12 @@ export default {
by_author = _.toArray(by_author);
return {
total: total,
- by_author: by_author
+ by_author: by_author,
};
},
add_date: function(date, collection) {
collection[date] = {};
- return collection[date].date = date;
+ return (collection[date].date = date);
},
add_author: function(author, by_author, by_email) {
var data, normalized_email;
@@ -49,28 +49,28 @@ export default {
return this.store_deletions(entry, total, by_author);
},
store_commits: function(total, by_author) {
- this.add(total, "commits", 1);
- return this.add(by_author, "commits", 1);
+ this.add(total, 'commits', 1);
+ return this.add(by_author, 'commits', 1);
},
add: function(collection, field, value) {
if (collection[field] == null) {
collection[field] = 0;
}
- return collection[field] += value;
+ return (collection[field] += value);
},
store_additions: function(entry, total, by_author) {
if (entry.additions == null) {
entry.additions = 0;
}
- this.add(total, "additions", entry.additions);
- return this.add(by_author, "additions", entry.additions);
+ this.add(total, 'additions', entry.additions);
+ return this.add(by_author, 'additions', entry.additions);
},
store_deletions: function(entry, total, by_author) {
if (entry.deletions == null) {
entry.deletions = 0;
}
- this.add(total, "deletions", entry.deletions);
- return this.add(by_author, "deletions", entry.deletions);
+ this.add(total, 'deletions', entry.deletions);
+ return this.add(by_author, 'deletions', entry.deletions);
},
get_total_data: function(parsed_log, field) {
var log, total_data;
@@ -95,15 +95,18 @@ export default {
}
log = parsed_log.by_author;
author_data = [];
- _.each(log, (function(_this) {
- return function(log_entry) {
- var parsed_log_entry;
- parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range);
- if (!_.isEmpty(parsed_log_entry.dates)) {
- return author_data.push(parsed_log_entry);
- }
- };
- })(this));
+ _.each(
+ log,
+ (function(_this) {
+ return function(log_entry) {
+ var parsed_log_entry;
+ parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range);
+ if (!_.isEmpty(parsed_log_entry.dates)) {
+ return author_data.push(parsed_log_entry);
+ }
+ };
+ })(this),
+ );
return _.sortBy(author_data, function(d) {
return d[field];
}).reverse();
@@ -120,16 +123,19 @@ export default {
parsed_entry.additions = 0;
parsed_entry.deletions = 0;
- _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) {
- return function(value, key) {
- if (_this.in_range(value.date, date_range)) {
- parsed_entry.dates[value.date] = value[field];
- parsed_entry.commits += value.commits;
- parsed_entry.additions += value.additions;
- return parsed_entry.deletions += value.deletions;
- }
- };
- })(this));
+ _.each(
+ _.omit(log_entry, 'author_name', 'author_email'),
+ (function(_this) {
+ return function(value, key) {
+ if (_this.in_range(value.date, date_range)) {
+ parsed_entry.dates[value.date] = value[field];
+ parsed_entry.commits += value.commits;
+ parsed_entry.additions += value.additions;
+ return (parsed_entry.deletions += value.deletions);
+ }
+ };
+ })(this),
+ );
return parsed_entry;
},
in_range: function(date, date_range) {
@@ -139,5 +145,5 @@ export default {
} else {
return false;
}
- }
+ },
};
diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js
index bc08ccf3584..bd8afa2d5ba 100644
--- a/app/assets/javascripts/pages/projects/init_blob.js
+++ b/app/assets/javascripts/pages/projects/init_blob.js
@@ -16,7 +16,8 @@ export default () => {
);
const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
- const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
+ const fileBlobPermalinkUrl =
+ fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/init_form.js b/app/assets/javascripts/pages/projects/init_form.js
index 9f20a3e4e46..019efe077f7 100644
--- a/app/assets/javascripts/pages/projects/init_form.js
+++ b/app/assets/javascripts/pages/projects/init_form.js
@@ -1,7 +1,7 @@
import ZenMode from '~/zen_mode';
import GLForm from '~/gl_form';
-export default function ($formEl) {
+export default function($formEl) {
new ZenMode(); // eslint-disable-line no-new
new GLForm($formEl); // eslint-disable-line no-new
}
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index ef65196872c..8987c8e3f47 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -5,7 +5,7 @@ import ZenMode from '~/zen_mode';
import '~/notes/index';
import initIssueableApp from '~/issue_show';
-export default function () {
+export default function() {
initIssueableApp();
new Issue(); // eslint-disable-line no-new
new ShortcutsIssuable(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
index 5d2247f6c6d..e8b646f3f6e 100644
--- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
@@ -1,72 +1,86 @@
<script>
- import _ from 'underscore';
- import axios from '~/lib/utils/axios_utils';
- import createFlash from '~/flash';
- import GlModal from '~/vue_shared/components/gl_modal.vue';
- import { s__, sprintf } from '~/locale';
- import { visitUrl } from '~/lib/utils/url_utility';
- import eventHub from '../event_hub';
+import _ from 'underscore';
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
+import GlModal from '~/vue_shared/components/gl_modal.vue';
+import { s__, sprintf } from '~/locale';
+import { visitUrl } from '~/lib/utils/url_utility';
+import eventHub from '../event_hub';
- export default {
- components: {
- GlModal,
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ url: {
+ type: String,
+ required: true,
},
- props: {
- url: {
- type: String,
- required: true,
- },
- labelTitle: {
- type: String,
- required: true,
- },
- labelColor: {
- type: String,
- required: true,
- },
- labelTextColor: {
- type: String,
- required: true,
- },
- groupName: {
- type: String,
- required: true,
- },
+ labelTitle: {
+ type: String,
+ required: true,
},
- computed: {
- text() {
- return sprintf(s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}.
- Existing project labels with the same title will be merged. This action cannot be reversed.`), {
+ labelColor: {
+ type: String,
+ required: true,
+ },
+ labelTextColor: {
+ type: String,
+ required: true,
+ },
+ groupName: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ text() {
+ return sprintf(
+ s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}.
+ Existing project labels with the same title will be merged. This action cannot be reversed.`),
+ {
labelTitle: this.labelTitle,
groupName: this.groupName,
- });
- },
- title() {
- const label = `<span
+ },
+ );
+ },
+ title() {
+ const label = `<span
class="label color-label"
style="background-color: ${this.labelColor}; color: ${this.labelTextColor};"
>${_.escape(this.labelTitle)}</span>`;
- return sprintf(s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'), {
+ return sprintf(
+ s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'),
+ {
labelTitle: label,
- }, false);
- },
+ },
+ false,
+ );
},
- methods: {
- onSubmit() {
- eventHub.$emit('promoteLabelModal.requestStarted', this.url);
- return axios.post(this.url, { params: { format: 'json' } })
- .then((response) => {
- eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: true });
- visitUrl(response.data.url);
- })
- .catch((error) => {
- eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: false });
- createFlash(error);
+ },
+ methods: {
+ onSubmit() {
+ eventHub.$emit('promoteLabelModal.requestStarted', this.url);
+ return axios
+ .post(this.url, { params: { format: 'json' } })
+ .then(response => {
+ eventHub.$emit('promoteLabelModal.requestFinished', {
+ labelUrl: this.url,
+ successful: true,
+ });
+ visitUrl(response.data.url);
+ })
+ .catch(error => {
+ eventHub.$emit('promoteLabelModal.requestFinished', {
+ labelUrl: this.url,
+ successful: false,
});
- },
+ createFlash(error);
+ });
},
- };
+ },
+};
</script>
<template>
<gl-modal
diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js
index 03cfef61311..36cf485f33d 100644
--- a/app/assets/javascripts/pages/projects/labels/index/index.js
+++ b/app/assets/javascripts/pages/projects/labels/index/index.js
@@ -10,20 +10,24 @@ const initLabelIndex = () => {
initLabels();
const onRequestFinished = ({ labelUrl, successful }) => {
- const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`);
+ const button = document.querySelector(
+ `.js-promote-project-label-button[data-url="${labelUrl}"]`,
+ );
if (!successful) {
button.removeAttribute('disabled');
}
};
- const onRequestStarted = (labelUrl) => {
- const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`);
+ const onRequestStarted = labelUrl => {
+ const button = document.querySelector(
+ `.js-promote-project-label-button[data-url="${labelUrl}"]`,
+ );
button.setAttribute('disabled', '');
eventHub.$once('promoteLabelModal.requestFinished', onRequestFinished);
};
- const onDeleteButtonClick = (event) => {
+ const onDeleteButtonClick = event => {
const button = event.currentTarget;
const modalProps = {
labelTitle: button.dataset.labelTitle,
@@ -37,12 +41,12 @@ const initLabelIndex = () => {
};
const promoteLabelButtons = document.querySelectorAll('.js-promote-project-label-button');
- promoteLabelButtons.forEach((button) => {
+ promoteLabelButtons.forEach(button => {
button.addEventListener('click', onDeleteButtonClick);
});
eventHub.$once('promoteLabelModal.mounted', () => {
- promoteLabelButtons.forEach((button) => {
+ promoteLabelButtons.forEach(button => {
button.removeAttribute('disabled');
});
});
diff --git a/app/assets/javascripts/pages/projects/network/network.js b/app/assets/javascripts/pages/projects/network/network.js
index 70fbb3f301c..226d63f05c4 100644
--- a/app/assets/javascripts/pages/projects/network/network.js
+++ b/app/assets/javascripts/pages/projects/network/network.js
@@ -6,13 +6,15 @@ import BranchGraph from '../../../network/branch_graph';
export default (function() {
function Network(opts) {
var vph;
- $("#filter_ref").click(function() {
- return $(this).closest('form').submit();
+ $('#filter_ref').click(function() {
+ return $(this)
+ .closest('form')
+ .submit();
});
- this.branch_graph = new BranchGraph($(".network-graph"), opts);
+ this.branch_graph = new BranchGraph($('.network-graph'), opts);
vph = $(window).height() - 250;
$('.network-graph').css({
- 'height': vph + 'px'
+ height: vph + 'px',
});
}
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
index 544360dcd51..6197dc8a9db 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
@@ -1,12 +1,16 @@
import Vue from 'vue';
import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue';
-document.addEventListener('DOMContentLoaded', () => new Vue({
- el: '#pipeline-schedules-callout',
- components: {
- 'pipeline-schedules-callout': PipelineSchedulesCallout,
- },
- render(createElement) {
- return createElement('pipeline-schedules-callout');
- },
-}));
+document.addEventListener(
+ 'DOMContentLoaded',
+ () =>
+ new Vue({
+ el: '#pipeline-schedules-callout',
+ components: {
+ 'pipeline-schedules-callout': PipelineSchedulesCallout,
+ },
+ render(createElement) {
+ return createElement('pipeline-schedules-callout');
+ },
+ }),
+);
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index ef53d67e7cb..ab6f42d928c 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -1,63 +1,63 @@
<script>
- import _ from 'underscore';
+import _ from 'underscore';
- export default {
- props: {
- initialCronInterval: {
- type: String,
- required: false,
- default: '',
- },
- },
- data() {
- return {
- inputNameAttribute: 'schedule[cron]',
- cronInterval: this.initialCronInterval,
- cronIntervalPresets: {
- everyDay: '0 4 * * *',
- everyWeek: '0 4 * * 0',
- everyMonth: '0 4 1 * *',
- },
- cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron',
- customInputEnabled: false,
- };
+export default {
+ props: {
+ initialCronInterval: {
+ type: String,
+ required: false,
+ default: '',
},
- computed: {
- intervalIsPreset() {
- return _.contains(this.cronIntervalPresets, this.cronInterval);
- },
- // The text input is editable when there's a custom interval, or when it's
- // a preset interval and the user clicks the 'custom' radio button
- isEditable() {
- return !!(this.customInputEnabled || !this.intervalIsPreset);
+ },
+ data() {
+ return {
+ inputNameAttribute: 'schedule[cron]',
+ cronInterval: this.initialCronInterval,
+ cronIntervalPresets: {
+ everyDay: '0 4 * * *',
+ everyWeek: '0 4 * * 0',
+ everyMonth: '0 4 1 * *',
},
+ cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron',
+ customInputEnabled: false,
+ };
+ },
+ computed: {
+ intervalIsPreset() {
+ return _.contains(this.cronIntervalPresets, this.cronInterval);
},
- watch: {
- cronInterval() {
- // updates field validation state when model changes, as
- // glFieldError only updates on input.
- this.$nextTick(() => {
- gl.pipelineScheduleFieldErrors.updateFormValidityState();
- });
- },
+ // The text input is editable when there's a custom interval, or when it's
+ // a preset interval and the user clicks the 'custom' radio button
+ isEditable() {
+ return !!(this.customInputEnabled || !this.intervalIsPreset);
},
- created() {
- if (this.intervalIsPreset) {
- this.enableCustomInput = false;
- }
+ },
+ watch: {
+ cronInterval() {
+ // updates field validation state when model changes, as
+ // glFieldError only updates on input.
+ this.$nextTick(() => {
+ gl.pipelineScheduleFieldErrors.updateFormValidityState();
+ });
},
- methods: {
- toggleCustomInput(shouldEnable) {
- this.customInputEnabled = shouldEnable;
+ },
+ created() {
+ if (this.intervalIsPreset) {
+ this.enableCustomInput = false;
+ }
+ },
+ methods: {
+ toggleCustomInput(shouldEnable) {
+ this.customInputEnabled = shouldEnable;
- if (shouldEnable) {
- // We need to change the value so other radios don't remain selected
- // because the model (cronInterval) hasn't changed. The server trims it.
- this.cronInterval = `${this.cronInterval} `;
- }
- },
+ if (shouldEnable) {
+ // We need to change the value so other radios don't remain selected
+ // because the model (cronInterval) hasn't changed. The server trims it.
+ this.cronInterval = `${this.cronInterval} `;
+ }
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
index 77508e62cef..33fc2420e4d 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
@@ -1,31 +1,31 @@
<script>
- import Vue from 'vue';
- import Cookies from 'js-cookie';
- import Translate from '../../../../../vue_shared/translate';
- import illustrationSvg from '../icons/intro_illustration.svg';
+import Vue from 'vue';
+import Cookies from 'js-cookie';
+import Translate from '../../../../../vue_shared/translate';
+import illustrationSvg from '../icons/intro_illustration.svg';
- Vue.use(Translate);
+Vue.use(Translate);
- const cookieKey = 'pipeline_schedules_callout_dismissed';
+const cookieKey = 'pipeline_schedules_callout_dismissed';
- export default {
- name: 'PipelineSchedulesCallout',
- data() {
- return {
- docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
- calloutDismissed: Cookies.get(cookieKey) === 'true',
- };
+export default {
+ name: 'PipelineSchedulesCallout',
+ data() {
+ return {
+ docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
+ calloutDismissed: Cookies.get(cookieKey) === 'true',
+ };
+ },
+ created() {
+ this.illustrationSvg = illustrationSvg;
+ },
+ methods: {
+ dismissCallout() {
+ this.calloutDismissed = true;
+ Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
},
- created() {
- this.illustrationSvg = illustrationSvg;
- },
- methods: {
- dismissCallout() {
- this.calloutDismissed = true;
- Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
- },
- },
- };
+ },
+};
</script>
<template>
<div
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js
index 4ef0d11dd36..0057700c1b3 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js
@@ -26,8 +26,7 @@ export default class TargetBranchDropdown {
}
formatBranchesList() {
- return this.$dropdown.data('data')
- .map(val => ({ name: val }));
+ return this.$dropdown.data('data').map(val => ({ name: val }));
}
setDropdownToggle() {
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
index c3ac54733a3..4d494efef6c 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
@@ -11,7 +11,9 @@ Vue.use(Translate);
function initIntervalPatternInput() {
const intervalPatternMount = document.getElementById('interval-pattern-input');
- const initialCronInterval = intervalPatternMount ? intervalPatternMount.dataset.initialInterval : '';
+ const initialCronInterval = intervalPatternMount
+ ? intervalPatternMount.dataset.initialInterval
+ : '';
return new Vue({
el: intervalPatternMount,
diff --git a/app/assets/javascripts/pages/projects/pipelines/charts/index.js b/app/assets/javascripts/pages/projects/pipelines/charts/index.js
index 07b6992eba1..48353f3b4ef 100644
--- a/app/assets/javascripts/pages/projects/pipelines/charts/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/charts/index.js
@@ -7,26 +7,29 @@ const options = {
maintainAspectRatio: false,
};
-const buildChart = (chartScope) => {
+const buildChart = chartScope => {
const data = {
labels: chartScope.labels,
- datasets: [{
- fillColor: '#707070',
- strokeColor: '#707070',
- pointColor: '#707070',
- pointStrokeColor: '#EEE',
- data: chartScope.totalValues,
- },
- {
- fillColor: '#1aaa55',
- strokeColor: '#1aaa55',
- pointColor: '#1aaa55',
- pointStrokeColor: '#fff',
- data: chartScope.successValues,
- },
+ datasets: [
+ {
+ fillColor: '#707070',
+ strokeColor: '#707070',
+ pointColor: '#707070',
+ pointStrokeColor: '#EEE',
+ data: chartScope.totalValues,
+ },
+ {
+ fillColor: '#1aaa55',
+ strokeColor: '#1aaa55',
+ pointColor: '#1aaa55',
+ pointStrokeColor: '#fff',
+ data: chartScope.successValues,
+ },
],
};
- const ctx = $(`#${chartScope.scope}Chart`).get(0).getContext('2d');
+ const ctx = $(`#${chartScope.scope}Chart`)
+ .get(0)
+ .getContext('2d');
new Chart(ctx).Line(data, options);
};
@@ -36,14 +39,16 @@ document.addEventListener('DOMContentLoaded', () => {
const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML);
const data = {
labels: chartTimesData.labels,
- datasets: [{
- fillColor: 'rgba(220,220,220,0.5)',
- strokeColor: 'rgba(220,220,220,1)',
- barStrokeWidth: 1,
- barValueSpacing: 1,
- barDatasetSpacing: 1,
- data: chartTimesData.values,
- }],
+ datasets: [
+ {
+ fillColor: 'rgba(220,220,220,0.5)',
+ strokeColor: 'rgba(220,220,220,1)',
+ barStrokeWidth: 1,
+ barValueSpacing: 1,
+ barDatasetSpacing: 1,
+ data: chartTimesData.values,
+ },
+ ],
};
if (window.innerWidth < 768) {
@@ -51,7 +56,11 @@ document.addEventListener('DOMContentLoaded', () => {
options.scaleFontSize = 8;
}
- new Chart($('#build_timesChart').get(0).getContext('2d')).Bar(data, options);
+ new Chart(
+ $('#build_timesChart')
+ .get(0)
+ .getContext('2d'),
+ ).Bar(data, options);
chartsData.forEach(scope => buildChart(scope));
});
diff --git a/app/assets/javascripts/pages/projects/pipelines/index/index.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js
index a84e2790680..fc337a7609b 100644
--- a/app/assets/javascripts/pages/projects/pipelines/index/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js
@@ -6,35 +6,39 @@ import { convertPermissionToBoolean } from '../../../../lib/utils/common_utils';
Vue.use(Translate);
-document.addEventListener('DOMContentLoaded', () => new Vue({
- el: '#pipelines-list-vue',
- components: {
- pipelinesComponent,
- },
- data() {
- return {
- store: new PipelinesStore(),
- };
- },
- created() {
- this.dataset = document.querySelector(this.$options.el).dataset;
- },
- render(createElement) {
- return createElement('pipelines-component', {
- props: {
- store: this.store,
- endpoint: this.dataset.endpoint,
- helpPagePath: this.dataset.helpPagePath,
- emptyStateSvgPath: this.dataset.emptyStateSvgPath,
- errorStateSvgPath: this.dataset.errorStateSvgPath,
- noPipelinesSvgPath: this.dataset.noPipelinesSvgPath,
- autoDevopsPath: this.dataset.helpAutoDevopsPath,
- newPipelinePath: this.dataset.newPipelinePath,
- canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline),
- hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi),
- ciLintPath: this.dataset.ciLintPath,
- resetCachePath: this.dataset.resetCachePath,
+document.addEventListener(
+ 'DOMContentLoaded',
+ () =>
+ new Vue({
+ el: '#pipelines-list-vue',
+ components: {
+ pipelinesComponent,
},
- });
- },
-}));
+ data() {
+ return {
+ store: new PipelinesStore(),
+ };
+ },
+ created() {
+ this.dataset = document.querySelector(this.$options.el).dataset;
+ },
+ render(createElement) {
+ return createElement('pipelines-component', {
+ props: {
+ store: this.store,
+ endpoint: this.dataset.endpoint,
+ helpPagePath: this.dataset.helpPagePath,
+ emptyStateSvgPath: this.dataset.emptyStateSvgPath,
+ errorStateSvgPath: this.dataset.errorStateSvgPath,
+ noPipelinesSvgPath: this.dataset.noPipelinesSvgPath,
+ autoDevopsPath: this.dataset.helpAutoDevopsPath,
+ newPipelinePath: this.dataset.newPipelinePath,
+ canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline),
+ hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi),
+ ciLintPath: this.dataset.ciLintPath,
+ resetCachePath: this.dataset.resetCachePath,
+ },
+ });
+ },
+ }),
+);
diff --git a/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js b/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js
index 94dfeb96e8c..ba4ae04ab3d 100644
--- a/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js
+++ b/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js
@@ -2,9 +2,12 @@ import Pipelines from '~/pipelines';
export default () => {
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
- const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`;
+ const pipelineStatusUrl = `${document
+ .querySelector('.js-pipeline-tab-link a')
+ .getAttribute('href')}/status.json`;
- new Pipelines({ // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new Pipelines({
initTabs: true,
pipelineStatusUrl,
tabsOptions: {
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
index 06101290f6c..dced839c883 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
@@ -1,73 +1,71 @@
<script>
- import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue';
+import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue';
- export default {
- components: {
- projectFeatureToggle,
- },
+export default {
+ components: {
+ projectFeatureToggle,
+ },
- model: {
- prop: 'value',
- event: 'change',
- },
+ model: {
+ prop: 'value',
+ event: 'change',
+ },
- props: {
- name: {
- type: String,
- required: false,
- default: '',
- },
- options: {
- type: Array,
- required: false,
- default: () => [],
- },
- value: {
- type: Number,
- required: false,
- default: 0,
- },
- disabledInput: {
- type: Boolean,
- required: false,
- default: false,
- },
+ props: {
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ options: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ value: {
+ type: Number,
+ required: false,
+ default: 0,
},
+ disabledInput: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
- computed: {
- featureEnabled() {
- return this.value !== 0;
- },
+ computed: {
+ featureEnabled() {
+ return this.value !== 0;
+ },
- displayOptions() {
- if (this.featureEnabled) {
- return this.options;
- }
- return [
- [0, 'Enable feature to choose access level'],
- ];
- },
+ displayOptions() {
+ if (this.featureEnabled) {
+ return this.options;
+ }
+ return [[0, 'Enable feature to choose access level']];
+ },
- displaySelectInput() {
- return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2;
- },
+ displaySelectInput() {
+ return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2;
},
+ },
- methods: {
- toggleFeature(featureEnabled) {
- if (featureEnabled === false || this.options.length < 1) {
- this.$emit('change', 0);
- } else {
- const [firstOptionValue] = this.options[this.options.length - 1];
- this.$emit('change', firstOptionValue);
- }
- },
+ methods: {
+ toggleFeature(featureEnabled) {
+ if (featureEnabled === false || this.options.length < 1) {
+ this.$emit('change', 0);
+ } else {
+ const [firstOptionValue] = this.options[this.options.length - 1];
+ this.$emit('change', firstOptionValue);
+ }
+ },
- selectOption(e) {
- this.$emit('change', Number(e.target.value));
- },
+ selectOption(e) {
+ this.$emit('change', Number(e.target.value));
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
index 83437363af5..898d605463f 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
@@ -1,23 +1,23 @@
<script>
- export default {
- props: {
- label: {
- type: String,
- required: false,
- default: null,
- },
- helpPath: {
- type: String,
- required: false,
- default: null,
- },
- helpText: {
- type: String,
- required: false,
- default: null,
- },
+export default {
+ props: {
+ label: {
+ type: String,
+ required: false,
+ default: null,
},
- };
+ helpPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ helpText: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/constants.js b/app/assets/javascripts/pages/projects/shared/permissions/constants.js
index ce47562f259..bc5c29d12b5 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/constants.js
+++ b/app/assets/javascripts/pages/projects/shared/permissions/constants.js
@@ -5,7 +5,9 @@ export const visibilityOptions = {
};
export const visibilityLevelDescriptions = {
- [visibilityOptions.PRIVATE]: 'The project is accessible only by members of the project. Access must be granted explicitly to each user.',
+ [visibilityOptions.PRIVATE]:
+ 'The project is accessible only by members of the project. Access must be granted explicitly to each user.',
[visibilityOptions.INTERNAL]: 'The project can be accessed by any user who is logged in.',
- [visibilityOptions.PUBLIC]: 'The project can be accessed by anyone, regardless of authentication.',
+ [visibilityOptions.PUBLIC]:
+ 'The project can be accessed by anyone, regardless of authentication.',
};
diff --git a/app/assets/javascripts/pages/projects/shared/project_avatar.js b/app/assets/javascripts/pages/projects/shared/project_avatar.js
index 447877752fe..1e69ecb481d 100644
--- a/app/assets/javascripts/pages/projects/shared/project_avatar.js
+++ b/app/assets/javascripts/pages/projects/shared/project_avatar.js
@@ -8,8 +8,9 @@ export default function projectAvatar() {
$('.js-project-avatar-input').bind('change', function onClickAvatarInput() {
const form = $(this).closest('form');
- // eslint-disable-next-line no-useless-escape
- const filename = $(this).val().replace(/^.*[\\\/]/, '');
+ const filename = $(this)
+ .val()
+ .replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape
return form.find('.js-avatar-filename').text(filename);
});
}
diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js
index c2629090f01..f5fd84d69ac 100644
--- a/app/assets/javascripts/pages/projects/wikis/index.js
+++ b/app/assets/javascripts/pages/projects/wikis/index.js
@@ -21,7 +21,8 @@ document.addEventListener('DOMContentLoaded', () => {
const { deleteWikiUrl, pageTitle } = deleteWikiModalWrapperEl.dataset;
- new Vue({ // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new Vue({
el: deleteWikiModalWrapperEl,
data: {
deleteWikiUrl: '',
diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js
index e3e0ab91993..0c896c8599e 100644
--- a/app/assets/javascripts/pages/search/show/search.js
+++ b/app/assets/javascripts/pages/search/show/search.js
@@ -22,7 +22,7 @@ export default class Search {
fields: ['full_name'],
},
data(term, callback) {
- return Api.groups(term, {}, (data) => {
+ return Api.groups(term, {}, data => {
data.unshift({
full_name: 'Any',
});
@@ -37,7 +37,7 @@ export default class Search {
return obj.full_name;
},
toggleLabel(obj) {
- return `${($groupDropdown.data('defaultLabel'))} ${obj.full_name}`;
+ return `${$groupDropdown.data('defaultLabel')} ${obj.full_name}`;
},
clicked: () => Search.submitSearch(),
});
@@ -52,7 +52,7 @@ export default class Search {
},
data: (term, callback) => {
this.getProjectsData(term)
- .then((data) => {
+ .then(data => {
data.unshift({
name_with_namespace: 'Any',
});
@@ -70,7 +70,7 @@ export default class Search {
return obj.name_with_namespace;
},
toggleLabel(obj) {
- return `${($projectDropdown.data('defaultLabel'))} ${obj.name_with_namespace}`;
+ return `${$projectDropdown.data('defaultLabel')} ${obj.name_with_namespace}`;
},
clicked: () => Search.submitSearch(),
});
@@ -99,17 +99,24 @@ export default class Search {
}
clearSearchField() {
- return $(this.searchInput).val('').trigger('keyup').focus();
+ return $(this.searchInput)
+ .val('')
+ .trigger('keyup')
+ .focus();
}
getProjectsData(term) {
- return new Promise((resolve) => {
+ return new Promise(resolve => {
if (this.groupId) {
Api.groupProjects(this.groupId, term, {}, resolve);
} else {
- Api.projects(term, {
- order_by: 'id',
- }, resolve);
+ Api.projects(
+ term,
+ {
+ order_by: 'id',
+ },
+ resolve,
+ );
}
});
}
diff --git a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
index 1e7c29aefaa..2b8f1e8b0ef 100644
--- a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
+++ b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
@@ -20,7 +20,7 @@ export default class SigninTabsMemoizer {
bootstrap() {
const tabs = document.querySelectorAll(this.tabSelector);
if (tabs.length > 0) {
- tabs[0].addEventListener('click', (e) => {
+ tabs[0].addEventListener('click', e => {
if (e.target && e.target.nodeName === 'A') {
const anchorName = e.target.getAttribute('href');
this.saveData(anchorName);
diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js
index d621f988d86..7a41805bada 100644
--- a/app/assets/javascripts/pages/sessions/new/username_validator.js
+++ b/app/assets/javascripts/pages/sessions/new/username_validator.js
@@ -22,10 +22,10 @@ export default class UsernameValidator {
available: false,
valid: false,
pending: false,
- empty: true
+ empty: true,
};
- const debounceTimeout = _.debounce((username) => {
+ const debounceTimeout = _.debounce(username => {
this.validateUsername(username);
}, debounceTimeoutDuration);
@@ -81,7 +81,8 @@ export default class UsernameValidator {
this.state.pending = true;
this.state.available = false;
this.renderState();
- axios.get(`${gon.relative_url_root}/users/${username}/exists`)
+ axios
+ .get(`${gon.relative_url_root}/users/${username}/exists`)
.then(({ data }) => this.setAvailabilityState(data.exists))
.catch(() => flash(__('An error occurred while validating username')));
}
@@ -100,8 +101,7 @@ export default class UsernameValidator {
clearFieldValidationState() {
this.inputElement.siblings('p').hide();
- this.inputElement.removeClass(invalidInputClass)
- .removeClass(successInputClass);
+ this.inputElement.removeClass(invalidInputClass).removeClass(successInputClass);
}
setUnavailableState() {
diff --git a/app/assets/javascripts/pages/users/index.js b/app/assets/javascripts/pages/users/index.js
index 6b1626b0161..a191df00dfa 100644
--- a/app/assets/javascripts/pages/users/index.js
+++ b/app/assets/javascripts/pages/users/index.js
@@ -13,10 +13,12 @@ function initUserProfile(action) {
new UserTabs({ parentEl: '.user-profile', action });
// hide project limit message
- $('.hide-project-limit-message').on('click', (e) => {
+ $('.hide-project-limit-message').on('click', e => {
e.preventDefault();
Cookies.set('hide_project_limit_message', 'false');
- $(this).parents('.project-limit-message').remove();
+ $(this)
+ .parents('.project-limit-message')
+ .remove();
});
}
diff --git a/app/assets/javascripts/performance_bar/components/simple_metric.vue b/app/assets/javascripts/performance_bar/components/simple_metric.vue
index 760ea8fe1e6..7a558558c4d 100644
--- a/app/assets/javascripts/performance_bar/components/simple_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/simple_metric.vue
@@ -1,29 +1,29 @@
<script>
- export default {
- props: {
- currentRequest: {
- type: Object,
- required: true,
- },
- metric: {
- type: String,
- required: true,
- },
+export default {
+ props: {
+ currentRequest: {
+ type: Object,
+ 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
- );
- },
+ 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
diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js
index 6e5ef0ac0b2..29bfb7ee5df 100644
--- a/app/assets/javascripts/performance_bar/index.js
+++ b/app/assets/javascripts/performance_bar/index.js
@@ -9,8 +9,7 @@ export default ({ container }) =>
performanceBarApp: () => import('./components/performance_bar_app.vue'),
},
data() {
- const performanceBarData = document.querySelector(this.$options.el)
- .dataset;
+ const performanceBarData = document.querySelector(this.$options.el).dataset;
const store = new PerformanceBarStore();
return {
diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
index 60d9ba62570..3a496fa2ed8 100644
--- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js
+++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
@@ -11,8 +11,10 @@ export default class PerformanceBarService {
static registerInterceptor(peekUrl, callback) {
const interceptor = response => {
- const [fireCallback, requestId, requestUrl] =
- PerformanceBarService.callbackParams(response, peekUrl);
+ const [fireCallback, requestId, requestUrl] = PerformanceBarService.callbackParams(
+ response,
+ peekUrl,
+ );
if (fireCallback) {
callback(requestId, requestUrl);
@@ -30,10 +32,7 @@ export default class PerformanceBarService {
static removeInterceptor(interceptor) {
axios.interceptors.response.eject(interceptor);
- Vue.http.interceptors = _.without(
- Vue.http.interceptors,
- vueResourceInterceptor,
- );
+ Vue.http.interceptors = _.without(Vue.http.interceptors, vueResourceInterceptor);
}
static callbackParams(response, peekUrl) {
diff --git a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
index c6b2f55243c..031e774d533 100644
--- a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
+++ b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
@@ -32,8 +32,6 @@ export default class PerformanceBarStore {
}
canTrackRequest(requestUrl) {
- return (
- this.requests.filter(request => request.url === requestUrl).length < 2
- );
+ return this.requests.filter(request => request.url === requestUrl).length < 2;
}
}
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
index 974629fa2af..99b57f4c9d5 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -1,78 +1,78 @@
<script>
- import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
- import { __, s__, sprintf } from '~/locale';
- import csrf from '~/lib/utils/csrf';
+import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+import { __, s__, sprintf } from '~/locale';
+import csrf from '~/lib/utils/csrf';
- export default {
- components: {
- DeprecatedModal,
+export default {
+ components: {
+ DeprecatedModal,
+ },
+ props: {
+ actionUrl: {
+ type: String,
+ required: true,
},
- props: {
- actionUrl: {
- type: String,
- required: true,
- },
- confirmWithPassword: {
- type: Boolean,
- required: true,
- },
- username: {
- type: String,
- required: true,
- },
+ confirmWithPassword: {
+ type: Boolean,
+ required: true,
},
- data() {
- return {
- enteredPassword: '',
- enteredUsername: '',
- };
+ username: {
+ type: String,
+ required: true,
},
- computed: {
- csrfToken() {
- return csrf.token;
- },
- inputLabel() {
- let confirmationValue;
- if (this.confirmWithPassword) {
- confirmationValue = __('password');
- } else {
- confirmationValue = __('username');
- }
+ },
+ data() {
+ return {
+ enteredPassword: '',
+ enteredUsername: '',
+ };
+ },
+ computed: {
+ csrfToken() {
+ return csrf.token;
+ },
+ inputLabel() {
+ let confirmationValue;
+ if (this.confirmWithPassword) {
+ confirmationValue = __('password');
+ } else {
+ confirmationValue = __('username');
+ }
- confirmationValue = `<code>${confirmationValue}</code>`;
+ confirmationValue = `<code>${confirmationValue}</code>`;
- return sprintf(
- s__('Profiles|Type your %{confirmationValue} to confirm:'),
- { confirmationValue },
- false,
- );
- },
- text() {
- return sprintf(
- s__(`Profiles|
+ return sprintf(
+ s__('Profiles|Type your %{confirmationValue} to confirm:'),
+ { confirmationValue },
+ false,
+ );
+ },
+ text() {
+ return sprintf(
+ s__(`Profiles|
You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account.
Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
- {
- yourAccount: `<strong>${s__('Profiles|your account')}</strong>`,
- deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`,
- },
- false,
- );
- },
+ {
+ yourAccount: `<strong>${s__('Profiles|your account')}</strong>`,
+ deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`,
+ },
+ false,
+ );
},
- methods: {
- canSubmit() {
- if (this.confirmWithPassword) {
- return this.enteredPassword !== '';
- }
+ },
+ methods: {
+ canSubmit() {
+ if (this.confirmWithPassword) {
+ return this.enteredPassword !== '';
+ }
- return this.enteredUsername === this.username;
- },
- onSubmit() {
- this.$refs.form.submit();
- },
+ return this.enteredUsername === this.username;
+ },
+ onSubmit() {
+ this.$refs.form.submit();
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js
index af134881f31..befe91c332f 100644
--- a/app/assets/javascripts/profile/gl_crop.js
+++ b/app/assets/javascripts/profile/gl_crop.js
@@ -4,20 +4,35 @@ import $ from 'jquery';
import 'cropper';
import _ from 'underscore';
-((global) => {
+(global => {
// Matches everything but the file name
const FILENAMEREGEX = /^.*[\\\/]/;
class GitLabCrop {
- constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg,
- exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) {
+ constructor(
+ input,
+ {
+ filename,
+ previewImage,
+ modalCrop,
+ pickImageEl,
+ uploadImageBtn,
+ modalCropImg,
+ exportWidth = 200,
+ exportHeight = 200,
+ cropBoxWidth = 200,
+ cropBoxHeight = 200,
+ } = {},
+ ) {
this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this);
this.onModalHide = this.onModalHide.bind(this);
this.onModalShow = this.onModalShow.bind(this);
this.onPickImageClick = this.onPickImageClick.bind(this);
this.fileInput = $(input);
this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
- this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `${this.fileInput.attr('id')}-trigger`);
+ this.fileInput
+ .attr('name', `${this.fileInput.attr('name')}-trigger`)
+ .attr('id', `${this.fileInput.attr('id')}-trigger`);
this.exportWidth = exportWidth;
this.exportHeight = exportHeight;
this.cropBoxWidth = cropBoxWidth;
@@ -59,7 +74,7 @@ import _ from 'underscore';
btn = this;
return _this.onActionBtnClick(btn);
});
- return this.croppedImageBlob = null;
+ return (this.croppedImageBlob = null);
}
onPickImageClick() {
@@ -94,9 +109,9 @@ import _ from 'underscore';
width: cropBoxWidth,
height: cropBoxHeight,
left: (container.width - cropBoxWidth) / 2,
- top: (container.height - cropBoxHeight) / 2
+ top: (container.height - cropBoxHeight) / 2,
});
- }
+ },
});
}
@@ -116,7 +131,7 @@ import _ from 'underscore';
var data, result;
data = $(btn).data();
if (this.modalCropImg.data('cropper') && data.method) {
- return result = this.modalCropImg.cropper(data.method, data.option);
+ return (result = this.modalCropImg.cropper(data.method, data.option));
}
}
@@ -127,7 +142,7 @@ import _ from 'underscore';
readFile(input) {
var _this, reader;
_this = this;
- reader = new FileReader;
+ reader = new FileReader();
reader.onload = () => {
_this.modalCropImg.attr('src', reader.result);
return _this.modalCrop.modal('show');
@@ -145,7 +160,7 @@ import _ from 'underscore';
array.push(binary.charCodeAt(i));
}
return new Blob([new Uint8Array(array)], {
- type: 'image/png'
+ type: 'image/png',
});
}
@@ -157,11 +172,13 @@ import _ from 'underscore';
}
setBlob() {
- this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', {
- width: 200,
- height: 200
- }).toDataURL('image/png');
- return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL);
+ this.dataURL = this.modalCropImg
+ .cropper('getCroppedCanvas', {
+ width: 200,
+ height: 200,
+ })
+ .toDataURL('image/png');
+ return (this.croppedImageBlob = this.dataURLtoBlob(this.dataURL));
}
getBlob() {
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index e49c67ffb5c..8704a655b28 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -26,11 +26,7 @@ export default class Profile {
}
bindEvents() {
- $('.js-preferences-form').on(
- 'change.preference',
- 'input[type=radio]',
- this.submitForm,
- );
+ $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
$('#user_notification_email').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
this.form.on('submit', this.onSubmitForm);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index b601b19e7be..48343c8ba0a 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -46,8 +46,12 @@ export default class ProtectedBranchCreate {
onSelect() {
// Enable submit button
const $branchInput = this.$form.find('input[name="protected_branch[name]"]');
- const $allowedToMergeInput = this.$form.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
- const $allowedToPushInput = this.$form.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
+ const $allowedToMergeInput = this.$form.find(
+ 'input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]',
+ );
+ const $allowedToPushInput = this.$form.find(
+ 'input[name="protected_branch[push_access_levels_attributes][0][access_level]"]',
+ );
const completedForm = !(
$branchInput.val() &&
$allowedToMergeInput.length &&
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index 54560d08ad7..5bc08f60d16 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -29,8 +29,12 @@ export default class ProtectedBranchEdit {
}
onSelect() {
- const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`);
- const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`);
+ const $allowedToMergeInput = this.$wrap.find(
+ `input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`,
+ );
+ const $allowedToPushInput = this.$wrap.find(
+ `input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`,
+ );
// Do not update if one dropdown has not selected any option
if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
@@ -38,25 +42,36 @@ export default class ProtectedBranchEdit {
this.$allowedToMergeDropdown.disable();
this.$allowedToPushDropdown.disable();
- axios.patch(this.$wrap.data('url'), {
- protected_branch: {
- merge_access_levels_attributes: [{
- id: this.$allowedToMergeDropdown.data('accessLevelId'),
- access_level: $allowedToMergeInput.val(),
- }],
- push_access_levels_attributes: [{
- id: this.$allowedToPushDropdown.data('accessLevelId'),
- access_level: $allowedToPushInput.val(),
- }],
- },
- }).then(() => {
- this.$allowedToMergeDropdown.enable();
- this.$allowedToPushDropdown.enable();
- }).catch(() => {
- this.$allowedToMergeDropdown.enable();
- this.$allowedToPushDropdown.enable();
-
- flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list'));
- });
+ axios
+ .patch(this.$wrap.data('url'), {
+ protected_branch: {
+ merge_access_levels_attributes: [
+ {
+ id: this.$allowedToMergeDropdown.data('accessLevelId'),
+ access_level: $allowedToMergeInput.val(),
+ },
+ ],
+ push_access_levels_attributes: [
+ {
+ id: this.$allowedToPushDropdown.data('accessLevelId'),
+ access_level: $allowedToPushInput.val(),
+ },
+ ],
+ },
+ })
+ .then(() => {
+ this.$allowedToMergeDropdown.enable();
+ this.$allowedToPushDropdown.enable();
+ })
+ .catch(() => {
+ this.$allowedToMergeDropdown.enable();
+ this.$allowedToPushDropdown.enable();
+
+ flash(
+ 'Failed to update branch!',
+ 'alert',
+ document.querySelector('.js-protected-branches-list'),
+ );
+ });
}
}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index 2f8116df0d2..fddf2674cbb 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -40,7 +40,9 @@ export default class ProtectedTagCreate {
const $tagInput = this.$form.find('input[name="protected_tag[name]"]');
const $allowedToCreateInput = this.$form.find('#create_access_levels_attributes');
- this.$form.find('input[type="submit"]').prop('disabled', !($tagInput.val() && $allowedToCreateInput.length));
+ this.$form
+ .find('input[type="submit"]')
+ .prop('disabled', !($tagInput.val() && $allowedToCreateInput.length));
}
static getProtectedTags(term, callback) {
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js
index 8687b2a4044..c52497e62f2 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js
@@ -21,26 +21,33 @@ export default class ProtectedTagEdit {
}
onSelect() {
- const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`);
+ const $allowedToCreateInput = this.$wrap.find(
+ `input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`,
+ );
// Do not update if one dropdown has not selected any option
if (!$allowedToCreateInput.length) return;
this.$allowedToCreateDropdownButton.disable();
- axios.patch(this.$wrap.data('url'), {
- protected_tag: {
- create_access_levels_attributes: [{
- id: this.$allowedToCreateDropdownButton.data('accessLevelId'),
- access_level: $allowedToCreateInput.val(),
- }],
- },
- }).then(() => {
- this.$allowedToCreateDropdownButton.enable();
- }).catch(() => {
- this.$allowedToCreateDropdownButton.enable();
-
- flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list'));
- });
+ axios
+ .patch(this.$wrap.data('url'), {
+ protected_tag: {
+ create_access_levels_attributes: [
+ {
+ id: this.$allowedToCreateDropdownButton.data('accessLevelId'),
+ access_level: $allowedToCreateInput.val(),
+ },
+ ],
+ },
+ })
+ .then(() => {
+ this.$allowedToCreateDropdownButton.enable();
+ })
+ .catch(() => {
+ this.$allowedToCreateDropdownButton.enable();
+
+ flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list'));
+ });
}
}
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue
index 7e2287ac4db..9dd1c87a87d 100644
--- a/app/assets/javascripts/registry/components/app.vue
+++ b/app/assets/javascripts/registry/components/app.vue
@@ -1,42 +1,35 @@
<script>
- import { mapGetters, mapActions } from 'vuex';
- import Flash from '../../flash';
- import store from '../stores';
- import collapsibleContainer from './collapsible_container.vue';
- import { errorMessages, errorMessagesTypes } from '../constants';
+import { mapGetters, mapActions } from 'vuex';
+import Flash from '../../flash';
+import store from '../stores';
+import collapsibleContainer from './collapsible_container.vue';
+import { errorMessages, errorMessagesTypes } from '../constants';
- export default {
- name: 'RegistryListApp',
- components: {
- collapsibleContainer,
+export default {
+ name: 'RegistryListApp',
+ components: {
+ collapsibleContainer,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
},
- props: {
- endpoint: {
- type: String,
- required: true,
- },
- },
- store,
- computed: {
- ...mapGetters([
- 'isLoading',
- 'repos',
- ]),
- },
- created() {
- this.setMainEndpoint(this.endpoint);
- },
- mounted() {
- this.fetchRepos()
- .catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS]));
- },
- methods: {
- ...mapActions([
- 'setMainEndpoint',
- 'fetchRepos',
- ]),
- },
- };
+ },
+ store,
+ computed: {
+ ...mapGetters(['isLoading', 'repos']),
+ },
+ created() {
+ this.setMainEndpoint(this.endpoint);
+ },
+ mounted() {
+ this.fetchRepos().catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS]));
+ },
+ methods: {
+ ...mapActions(['setMainEndpoint', 'fetchRepos']),
+ },
+};
</script>
<template>
<div>
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index d9bf41924d1..501b2625ae5 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -1,62 +1,59 @@
<script>
- import { mapActions } from 'vuex';
- import Flash from '../../flash';
- import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
- import tableRegistry from './table_registry.vue';
- import { errorMessages, errorMessagesTypes } from '../constants';
- import { __ } from '../../locale';
+import { mapActions } from 'vuex';
+import Flash from '../../flash';
+import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
+import tableRegistry from './table_registry.vue';
+import { errorMessages, errorMessagesTypes } from '../constants';
+import { __ } from '../../locale';
- export default {
- name: 'CollapsibeContainerRegisty',
- components: {
- clipboardButton,
- tableRegistry,
+export default {
+ name: 'CollapsibeContainerRegisty',
+ components: {
+ clipboardButton,
+ tableRegistry,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ repo: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
- },
- props: {
- repo: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- isOpen: false,
- };
- },
- methods: {
- ...mapActions([
- 'fetchRepos',
- 'fetchList',
- 'deleteRepo',
- ]),
+ },
+ data() {
+ return {
+ isOpen: false,
+ };
+ },
+ methods: {
+ ...mapActions(['fetchRepos', 'fetchList', 'deleteRepo']),
- toggleRepo() {
- this.isOpen = !this.isOpen;
+ toggleRepo() {
+ this.isOpen = !this.isOpen;
- if (this.isOpen) {
- this.fetchList({ repo: this.repo })
- .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
- }
- },
+ if (this.isOpen) {
+ this.fetchList({ repo: this.repo }).catch(() =>
+ this.showError(errorMessagesTypes.FETCH_REGISTRY),
+ );
+ }
+ },
- handleDeleteRepository() {
- this.deleteRepo(this.repo)
- .then(() => {
- Flash(__('This container registry has been scheduled for deletion.'), 'notice');
- this.fetchRepos();
- })
- .catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
- },
+ handleDeleteRepository() {
+ this.deleteRepo(this.repo)
+ .then(() => {
+ Flash(__('This container registry has been scheduled for deletion.'), 'notice');
+ this.fetchRepos();
+ })
+ .catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
+ },
- showError(message) {
- Flash(errorMessages[message]);
- },
+ showError(message) {
+ Flash(errorMessages[message]);
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index fafb35bd69a..bb6c977fc63 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -1,66 +1,62 @@
<script>
- import { mapActions } from 'vuex';
- import { n__ } from '../../locale';
- import Flash from '../../flash';
- import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
- import tablePagination from '../../vue_shared/components/table_pagination.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
- import timeagoMixin from '../../vue_shared/mixins/timeago';
- import { errorMessages, errorMessagesTypes } from '../constants';
- import { numberToHumanSize } from '../../lib/utils/number_utils';
+import { mapActions } from 'vuex';
+import { n__ } from '../../locale';
+import Flash from '../../flash';
+import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import tablePagination from '../../vue_shared/components/table_pagination.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
+import timeagoMixin from '../../vue_shared/mixins/timeago';
+import { errorMessages, errorMessagesTypes } from '../constants';
+import { numberToHumanSize } from '../../lib/utils/number_utils';
- export default {
- components: {
- clipboardButton,
- tablePagination,
+export default {
+ components: {
+ clipboardButton,
+ tablePagination,
+ },
+ directives: {
+ tooltip,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ repo: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ },
+ computed: {
+ shouldRenderPagination() {
+ return this.repo.pagination.total > this.repo.pagination.perPage;
},
- mixins: [
- timeagoMixin,
- ],
- props: {
- repo: {
- type: Object,
- required: true,
- },
- },
- computed: {
- shouldRenderPagination() {
- return this.repo.pagination.total > this.repo.pagination.perPage;
- },
- },
- methods: {
- ...mapActions([
- 'fetchList',
- 'deleteRegistry',
- ]),
+ },
+ methods: {
+ ...mapActions(['fetchList', 'deleteRegistry']),
- layers(item) {
- return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
- },
+ layers(item) {
+ return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
+ },
- formatSize(size) {
- return numberToHumanSize(size);
- },
+ formatSize(size) {
+ return numberToHumanSize(size);
+ },
- handleDeleteRegistry(registry) {
- this.deleteRegistry(registry)
- .then(() => this.fetchList({ repo: this.repo }))
- .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
- },
+ handleDeleteRegistry(registry) {
+ this.deleteRegistry(registry)
+ .then(() => this.fetchList({ repo: this.repo }))
+ .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
+ },
- onPageChange(pageNumber) {
- this.fetchList({ repo: this.repo, page: pageNumber })
- .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
- },
+ onPageChange(pageNumber) {
+ this.fetchList({ repo: this.repo, page: pageNumber }).catch(() =>
+ this.showError(errorMessagesTypes.FETCH_REGISTRY),
+ );
+ },
- showError(message) {
- Flash(errorMessages[message]);
- },
+ showError(message) {
+ Flash(errorMessages[message]);
},
- };
+ },
+};
</script>
<template>
<div>
diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js
index e15cd94a915..025afefe7f0 100644
--- a/app/assets/javascripts/registry/index.js
+++ b/app/assets/javascripts/registry/index.js
@@ -4,22 +4,23 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate);
-export default () => new Vue({
- el: '#js-vue-registry-images',
- components: {
- registryApp,
- },
- data() {
- const { dataset } = document.querySelector(this.$options.el);
- return {
- endpoint: dataset.endpoint,
- };
- },
- render(createElement) {
- return createElement('registry-app', {
- props: {
- endpoint: this.endpoint,
- },
- });
- },
-});
+export default () =>
+ new Vue({
+ el: '#js-vue-registry-images',
+ components: {
+ registryApp,
+ },
+ data() {
+ const { dataset } = document.querySelector(this.$options.el);
+ return {
+ endpoint: dataset.endpoint,
+ };
+ },
+ render(createElement) {
+ return createElement('registry-app', {
+ props: {
+ endpoint: this.endpoint,
+ },
+ });
+ },
+ });
diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js
index 208c3c39866..69c051cd2d6 100644
--- a/app/assets/javascripts/registry/stores/mutations.js
+++ b/app/assets/javascripts/registry/stores/mutations.js
@@ -2,7 +2,6 @@ import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils';
export default {
-
[types.SET_MAIN_ENDPOINT](state, endpoint) {
Object.assign(state, { endpoint });
},
diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
index b373d83a44b..bd204503cc7 100644
--- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
+++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
@@ -1,79 +1,72 @@
<script>
- import { mapActions, mapGetters, mapState } from 'vuex';
- import { s__ } from '~/locale';
- import { componentNames } from './issue_body';
- import ReportSection from './report_section.vue';
- import SummaryRow from './summary_row.vue';
- import IssuesList from './issues_list.vue';
- import Modal from './modal.vue';
- import createStore from '../store';
- import { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils';
+import { mapActions, mapGetters, mapState } from 'vuex';
+import { s__ } from '~/locale';
+import { componentNames } from './issue_body';
+import ReportSection from './report_section.vue';
+import SummaryRow from './summary_row.vue';
+import IssuesList from './issues_list.vue';
+import Modal from './modal.vue';
+import createStore from '../store';
+import { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils';
- export default {
- name: 'GroupedTestReportsApp',
- store: createStore(),
- components: {
- ReportSection,
- SummaryRow,
- IssuesList,
- Modal,
+export default {
+ name: 'GroupedTestReportsApp',
+ store: createStore(),
+ components: {
+ ReportSection,
+ SummaryRow,
+ IssuesList,
+ Modal,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
},
- props: {
- endpoint: {
- type: String,
- required: true,
- },
- },
- componentNames,
- computed: {
- ...mapState([
- 'reports',
- 'isLoading',
- 'hasError',
- 'summary',
- ]),
- ...mapState({
- modalTitle: state => state.modal.title || '',
- modalData: state => state.modal.data || {},
- }),
- ...mapGetters([
- 'summaryStatus',
- ]),
- groupedSummaryText() {
- if (this.isLoading) {
- return s__('Reports|Test summary results are being parsed');
- }
+ },
+ componentNames,
+ computed: {
+ ...mapState(['reports', 'isLoading', 'hasError', 'summary']),
+ ...mapState({
+ modalTitle: state => state.modal.title || '',
+ modalData: state => state.modal.data || {},
+ }),
+ ...mapGetters(['summaryStatus']),
+ groupedSummaryText() {
+ if (this.isLoading) {
+ return s__('Reports|Test summary results are being parsed');
+ }
- if (this.hasError) {
- return s__('Reports|Test summary failed loading results');
- }
+ if (this.hasError) {
+ return s__('Reports|Test summary failed loading results');
+ }
- return summaryTextBuilder(s__('Reports|Test summary'), this.summary);
- },
+ return summaryTextBuilder(s__('Reports|Test summary'), this.summary);
},
- created() {
- this.setEndpoint(this.endpoint);
+ },
+ created() {
+ this.setEndpoint(this.endpoint);
- this.fetchReports();
+ this.fetchReports();
+ },
+ methods: {
+ ...mapActions(['setEndpoint', 'fetchReports']),
+ reportText(report) {
+ const summary = report.summary || {};
+ return reportTextBuilder(report.name, summary);
+ },
+ getReportIcon(report) {
+ return statusIcon(report.status);
},
- methods: {
- ...mapActions(['setEndpoint', 'fetchReports']),
- reportText(report) {
- const summary = report.summary || {};
- return reportTextBuilder(report.name, summary);
- },
- getReportIcon(report) {
- return statusIcon(report.status);
- },
- shouldRenderIssuesList(report) {
- return (
- report.existing_failures.length > 0 ||
- report.new_failures.length > 0 ||
- report.resolved_failures.length > 0
- );
- },
+ shouldRenderIssuesList(report) {
+ return (
+ report.existing_failures.length > 0 ||
+ report.new_failures.length > 0 ||
+ report.resolved_failures.length > 0
+ );
},
- };
+ },
+};
</script>
<template>
<report-section
diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue
index 85811698a37..6e143c4f98c 100644
--- a/app/assets/javascripts/reports/components/issue_status_icon.vue
+++ b/app/assets/javascripts/reports/components/issue_status_icon.vue
@@ -1,10 +1,6 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
-import {
- STATUS_FAILED,
- STATUS_NEUTRAL,
- STATUS_SUCCESS,
-} from '../constants';
+import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '../constants';
export default {
name: 'IssueStatusIcon',
diff --git a/app/assets/javascripts/reports/components/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue
index df42201b5de..3b425ee2fed 100644
--- a/app/assets/javascripts/reports/components/issues_list.vue
+++ b/app/assets/javascripts/reports/components/issues_list.vue
@@ -1,10 +1,6 @@
<script>
import IssuesBlock from '~/reports/components/report_issues.vue';
-import {
- STATUS_SUCCESS,
- STATUS_FAILED,
- STATUS_NEUTRAL,
-} from '~/reports/constants';
+import { STATUS_SUCCESS, STATUS_FAILED, STATUS_NEUTRAL } from '~/reports/constants';
/**
* Renders block of issues
diff --git a/app/assets/javascripts/reports/components/modal.vue b/app/assets/javascripts/reports/components/modal.vue
index acc5c6d85e2..5f9e4072b2d 100644
--- a/app/assets/javascripts/reports/components/modal.vue
+++ b/app/assets/javascripts/reports/components/modal.vue
@@ -1,27 +1,27 @@
<script>
- import Modal from '~/vue_shared/components/gl_modal.vue';
- import LoadingButton from '~/vue_shared/components/loading_button.vue';
- import CodeBlock from '~/vue_shared/components/code_block.vue';
- import { fieldTypes } from '../constants';
+import Modal from '~/vue_shared/components/gl_modal.vue';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import CodeBlock from '~/vue_shared/components/code_block.vue';
+import { fieldTypes } from '../constants';
- export default {
- components: {
- Modal,
- LoadingButton,
- CodeBlock,
+export default {
+ components: {
+ Modal,
+ LoadingButton,
+ CodeBlock,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
},
- props: {
- title: {
- type: String,
- required: true,
- },
- modalData: {
- type: Object,
- required: true,
- },
+ modalData: {
+ type: Object,
+ required: true,
},
- fieldTypes,
- };
+ },
+ fieldTypes,
+};
</script>
<template>
<modal
diff --git a/app/assets/javascripts/reports/components/test_issue_body.vue b/app/assets/javascripts/reports/components/test_issue_body.vue
index cd443a49b52..1a87822fcc3 100644
--- a/app/assets/javascripts/reports/components/test_issue_body.vue
+++ b/app/assets/javascripts/reports/components/test_issue_body.vue
@@ -1,28 +1,28 @@
<script>
- import { mapActions } from 'vuex';
+import { mapActions } from 'vuex';
- export default {
- name: 'TestIssueBody',
- props: {
- issue: {
- type: Object,
- required: true,
- },
- // failed || success
- status: {
- type: String,
- required: true,
- },
- isNew: {
- type: Boolean,
- required: false,
- default: false,
- },
+export default {
+ name: 'TestIssueBody',
+ props: {
+ issue: {
+ type: Object,
+ required: true,
},
- methods: {
- ...mapActions(['openModal']),
+ // failed || success
+ status: {
+ type: String,
+ required: true,
},
- };
+ isNew: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ methods: {
+ ...mapActions(['openModal']),
+ },
+};
</script>
<template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
diff --git a/app/assets/javascripts/reports/store/actions.js b/app/assets/javascripts/reports/store/actions.js
index acabcc1d193..db8ab5ccb80 100644
--- a/app/assets/javascripts/reports/store/actions.js
+++ b/app/assets/javascripts/reports/store/actions.js
@@ -43,9 +43,11 @@ export const fetchReports = ({ state, dispatch }) => {
},
data: state.endpoint,
method: 'getReports',
- successCallback: ({ data, status }) => dispatch('receiveReportsSuccess', {
- data, status,
- }),
+ successCallback: ({ data, status }) =>
+ dispatch('receiveReportsSuccess', {
+ data,
+ status,
+ }),
errorCallback: () => dispatch('receiveReportsError'),
});
diff --git a/app/assets/javascripts/reports/store/index.js b/app/assets/javascripts/reports/store/index.js
index 9d8f7dc3b74..467c692b438 100644
--- a/app/assets/javascripts/reports/store/index.js
+++ b/app/assets/javascripts/reports/store/index.js
@@ -7,9 +7,10 @@ import state from './state';
Vue.use(Vuex);
-export default () => new Vuex.Store({
- actions,
- mutations,
- getters,
- state: state(),
-});
+export default () =>
+ new Vuex.Store({
+ actions,
+ mutations,
+ getters,
+ state: state(),
+ });
diff --git a/app/assets/javascripts/reports/store/mutation_types.js b/app/assets/javascripts/reports/store/mutation_types.js
index 82bda31df5d..599d4862dfe 100644
--- a/app/assets/javascripts/reports/store/mutation_types.js
+++ b/app/assets/javascripts/reports/store/mutation_types.js
@@ -4,4 +4,3 @@ export const REQUEST_REPORTS = 'REQUEST_REPORTS';
export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS';
export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR';
export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA';
-
diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js
index b88bff97075..2a37f5b74fa 100644
--- a/app/assets/javascripts/reports/store/mutations.js
+++ b/app/assets/javascripts/reports/store/mutations.js
@@ -19,7 +19,6 @@ export default {
state.status = response.status;
state.reports = response.suites;
-
},
[types.RECEIVE_REPORTS_ERROR](state) {
state.isLoading = false;
@@ -36,7 +35,7 @@ export default {
[types.SET_ISSUE_MODAL_DATA](state, payload) {
state.modal.title = payload.issue.name;
- Object.keys(payload.issue).forEach((key) => {
+ Object.keys(payload.issue).forEach(key => {
if (Object.prototype.hasOwnProperty.call(state.modal.data, key)) {
state.modal.data[key] = {
...state.modal.data[key],
diff --git a/app/assets/javascripts/reports/store/state.js b/app/assets/javascripts/reports/store/state.js
index 4cab2e27a16..5484900276c 100644
--- a/app/assets/javascripts/reports/store/state.js
+++ b/app/assets/javascripts/reports/store/state.js
@@ -57,5 +57,4 @@ export default () => ({
},
},
},
-
});
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 123c92aff64..cfa7029b388 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -69,7 +69,8 @@ export default {
this.loading = false;
}
- this.mediator.saveAssignees(this.field)
+ this.mediator
+ .saveAssignees(this.field)
.then(setLoadingFalse.bind(this))
.catch(() => {
setLoadingFalse();
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 2b8d6207dea..439e8a69df0 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -56,11 +56,7 @@ export default {
.update('issue', { confidential })
.then(() => window.location.reload())
.catch(() => {
- Flash(
- __(
- 'Something went wrong trying to change the confidentiality of this issue',
- ),
- );
+ Flash(__('Something went wrong trying to change the confidentiality of this issue'));
});
},
},
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index cdff4105335..48a2b9194aa 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -34,11 +34,7 @@ export default {
required: true,
type: Object,
validator(mediatorObject) {
- return (
- mediatorObject.service &&
- mediatorObject.service.update &&
- mediatorObject.store
- );
+ return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
},
},
},
@@ -67,8 +63,7 @@ export default {
methods: {
toggleForm() {
- this.mediator.store.isLockDialogOpen = !this.mediator.store
- .isLockDialogOpen;
+ this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
},
updateLockedAttribute(locked) {
@@ -79,9 +74,14 @@ export default {
.then(() => window.location.reload())
.catch(() =>
Flash(
- sprintf(__('Something went wrong trying to change the locked state of this %{issuableDisplayName}'), {
- issuableDisplayName: this.issuableDisplayName,
- }),
+ sprintf(
+ __(
+ 'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
+ ),
+ {
+ issuableDisplayName: this.issuableDisplayName,
+ },
+ ),
),
);
},
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 286a16f7bbf..11b5dbe5f8e 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -1,78 +1,78 @@
<script>
- import { __, n__, sprintf } from '~/locale';
- import tooltip from '~/vue_shared/directives/tooltip';
- import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
+import { __, n__, sprintf } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ userAvatarImage,
+ },
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- components: {
- userAvatarImage,
+ participants: {
+ type: Array,
+ required: false,
+ default: () => [],
},
- props: {
- loading: {
- type: Boolean,
- required: false,
- default: false,
- },
- participants: {
- type: Array,
- required: false,
- default: () => [],
- },
- numberOfLessParticipants: {
- type: Number,
- required: false,
- default: 7,
- },
+ numberOfLessParticipants: {
+ type: Number,
+ required: false,
+ default: 7,
},
- data() {
- return {
- isShowingMoreParticipants: false,
- };
+ },
+ data() {
+ return {
+ isShowingMoreParticipants: false,
+ };
+ },
+ computed: {
+ lessParticipants() {
+ return this.participants.slice(0, this.numberOfLessParticipants);
},
- computed: {
- lessParticipants() {
- return this.participants.slice(0, this.numberOfLessParticipants);
- },
- visibleParticipants() {
- return this.isShowingMoreParticipants ? this.participants : this.lessParticipants;
- },
- hasMoreParticipants() {
- return this.participants.length > this.numberOfLessParticipants;
- },
- toggleLabel() {
- let label = '';
- if (this.isShowingMoreParticipants) {
- label = __('- show less');
- } else {
- label = sprintf(__('+ %{moreCount} more'), {
- moreCount: this.participants.length - this.numberOfLessParticipants,
- });
- }
+ visibleParticipants() {
+ return this.isShowingMoreParticipants ? this.participants : this.lessParticipants;
+ },
+ hasMoreParticipants() {
+ return this.participants.length > this.numberOfLessParticipants;
+ },
+ toggleLabel() {
+ let label = '';
+ if (this.isShowingMoreParticipants) {
+ label = __('- show less');
+ } else {
+ label = sprintf(__('+ %{moreCount} more'), {
+ moreCount: this.participants.length - this.numberOfLessParticipants,
+ });
+ }
- return label;
- },
- participantLabel() {
- return sprintf(
- n__('%{count} participant', '%{count} participants', this.participants.length),
- { count: this.loading ? '' : this.participantCount },
- );
- },
- participantCount() {
- return this.participants.length;
- },
+ return label;
+ },
+ participantLabel() {
+ return sprintf(
+ n__('%{count} participant', '%{count} participants', this.participants.length),
+ { count: this.loading ? '' : this.participantCount },
+ );
+ },
+ participantCount() {
+ return this.participants.length;
+ },
+ },
+ methods: {
+ toggleMoreParticipants() {
+ this.isShowingMoreParticipants = !this.isShowingMoreParticipants;
},
- methods: {
- toggleMoreParticipants() {
- this.isShowingMoreParticipants = !this.isShowingMoreParticipants;
- },
- onClickCollapsedIcon() {
- this.$emit('toggleSidebar');
- },
+ onClickCollapsedIcon() {
+ this.$emit('toggleSidebar');
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
index 5c1ead1a8ac..4ac515e552a 100644
--- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
@@ -1,23 +1,23 @@
<script>
- import Store from '../../stores/sidebar_store';
- import participants from './participants.vue';
+import Store from '../../stores/sidebar_store';
+import participants from './participants.vue';
- export default {
- components: {
- participants,
+export default {
+ components: {
+ participants,
+ },
+ props: {
+ mediator: {
+ type: Object,
+ required: true,
},
- props: {
- mediator: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- store: new Store(),
- };
- },
- };
+ },
+ data() {
+ return {
+ store: new Store(),
+ };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
index 385717e7c1e..95a2c8cce6e 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
@@ -21,10 +21,9 @@ export default {
},
methods: {
onToggleSubscription() {
- this.mediator.toggleSubscription()
- .catch(() => {
- Flash(__('Error occurred when toggling the notification subscription'));
- });
+ this.mediator.toggleSubscription().catch(() => {
+ Flash(__('Error occurred when toggling the notification subscription'));
+ });
},
},
};
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
index 19ec0f05a26..91909cd49b8 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
@@ -15,16 +15,22 @@ export default {
},
estimateText() {
return sprintf(
- s__('estimateCommand|%{slash_command} will update the estimated time with the latest command.'), {
+ s__(
+ 'estimateCommand|%{slash_command} will update the estimated time with the latest command.',
+ ),
+ {
slash_command: '<code>/estimate</code>',
- }, false,
+ },
+ false,
);
},
spendText() {
return sprintf(
- s__('spendCommand|%{slash_command} will update the sum of the time spent.'), {
+ s__('spendCommand|%{slash_command} will update the sum of the time spent.'),
+ {
slash_command: '<code>/spend</code>',
- }, false,
+ },
+ false,
);
},
},
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
index 8660b0546cf..8e8b9f19b6e 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
@@ -26,7 +26,7 @@ export default {
methods: {
listenForQuickActions() {
$(document).on('ajax:success', '.gfm-form', this.quickActionListened);
- eventHub.$on('timeTrackingUpdated', (data) => {
+ eventHub.$on('timeTrackingUpdated', data => {
this.quickActionListened(null, data);
});
},
@@ -34,9 +34,7 @@ export default {
const subscribedCommands = ['spend_time', 'time_estimate'];
let changedCommands;
if (data !== undefined) {
- changedCommands = data.commands_changes
- ? Object.keys(data.commands_changes)
- : [];
+ changedCommands = data.commands_changes ? Object.keys(data.commands_changes) : [];
} else {
changedCommands = [];
}
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
index a6b3a674952..bc59774f0a8 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -41,9 +41,9 @@ export default {
},
computed: {
buttonClasses() {
- return this.collapsed ?
- 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state' :
- 'btn btn-default btn-todo issuable-header-btn float-right';
+ return this.collapsed
+ ? 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state'
+ : 'btn btn-default btn-todo issuable-header-btn float-right';
},
buttonLabel() {
return this.isTodo ? MARK_TEXT : TODO_TEXT;
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
index b267422cd97..225ebb61195 100644
--- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -37,7 +37,8 @@ class SidebarMoveIssue {
// Keep the dropdown open after selecting an option
shouldPropagate: false,
data: (searchTerm, callback) => {
- this.mediator.fetchAutocompleteProjects(searchTerm)
+ this.mediator
+ .fetchAutocompleteProjects(searchTerm)
.then(callback)
.catch(() => new window.Flash('An error occurred while fetching projects autocomplete.'));
},
@@ -48,7 +49,7 @@ class SidebarMoveIssue {
</a>
</li>
`,
- clicked: (options) => {
+ clicked: options => {
const project = options.selectedObj;
const selectedProjectId = options.isMarking ? project.id : 0;
this.mediator.setMoveToProjectId(selectedProjectId);
@@ -68,17 +69,12 @@ class SidebarMoveIssue {
onConfirmClicked() {
if (isValidProjectId(this.mediator.store.moveToProjectId)) {
- this.$confirmButton
- .disable()
- .addClass('is-loading');
+ this.$confirmButton.disable().addClass('is-loading');
- this.mediator.moveIssue()
- .catch(() => {
- window.Flash('An error occurred while moving the issue.');
- this.$confirmButton
- .enable()
- .removeClass('is-loading');
- });
+ this.mediator.moveIssue().catch(() => {
+ window.Flash('An error occurred while moving the issue.');
+ this.$confirmButton.enable().removeClass('is-loading');
+ });
}
}
}
diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
index 87da65a1b1f..1ebdbec7bc9 100644
--- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
@@ -15,15 +15,16 @@ export default class SidebarMilestone {
components: {
timeTracker,
},
- render: createElement => createElement('timeTracker', {
- props: {
- timeEstimate: parseInt(timeEstimate, 10),
- timeSpent: parseInt(timeSpent, 10),
- humanTimeEstimate,
- humanTimeSpent,
- rootPath: '/',
- },
- }),
+ render: createElement =>
+ createElement('timeTracker', {
+ props: {
+ timeEstimate: parseInt(timeEstimate, 10),
+ timeSpent: parseInt(timeSpent, 10),
+ humanTimeEstimate,
+ humanTimeSpent,
+ rootPath: '/',
+ },
+ }),
});
}
}
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 655bf9198b7..6f8214b18ee 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -22,14 +22,15 @@ function mountAssigneesComponent(mediator) {
components: {
SidebarAssignees,
},
- render: createElement => createElement('sidebar-assignees', {
- props: {
- mediator,
- field: el.dataset.field,
- signedIn: el.hasAttribute('data-signed-in'),
- issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
- },
- }),
+ render: createElement =>
+ createElement('sidebar-assignees', {
+ props: {
+ mediator,
+ field: el.dataset.field,
+ signedIn: el.hasAttribute('data-signed-in'),
+ issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
+ },
+ }),
});
}
@@ -83,11 +84,12 @@ function mountParticipantsComponent(mediator) {
components: {
sidebarParticipants,
},
- render: createElement => createElement('sidebar-participants', {
- props: {
- mediator,
- },
- }),
+ render: createElement =>
+ createElement('sidebar-participants', {
+ props: {
+ mediator,
+ },
+ }),
});
}
@@ -102,11 +104,12 @@ function mountSubscriptionsComponent(mediator) {
components: {
sidebarSubscriptions,
},
- render: createElement => createElement('sidebar-subscriptions', {
- props: {
- mediator,
- },
- }),
+ render: createElement =>
+ createElement('sidebar-subscriptions', {
+ props: {
+ mediator,
+ },
+ }),
});
}
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index 37c97225bfd..cbe20f761ff 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -22,11 +22,15 @@ export default class SidebarService {
}
update(key, data) {
- return Vue.http.put(this.endpoint, {
- [key]: data,
- }, {
- emulateJSON: true,
- });
+ return Vue.http.put(
+ this.endpoint,
+ {
+ [key]: data,
+ },
+ {
+ emulateJSON: true,
+ },
+ );
}
getProjectsAutocomplete(searchTerm) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
deleted file mode 100644
index a23496c6bf5..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * This file is the centerpiece of an attempt to reduce potential conflicts
- * between the CE and EE versions of the MR widget. EE additions to the MR widget should
- * be contained in the ee/vue_merge_request_widget directory, and should **extend**
- * rather than mutate CE MR Widget code.
- *
- * This file should be the only source of conflicts between EE and CE. EE-only components should
- * imported directly where they are needed, and import paths for EE extensions of CE components
- * should overwrite import paths **without** changing the order of dependencies listed here.
- */
-
-export { default as Vue } from 'vue';
-export { default as SmartInterval } from '~/smart_interval';
-export { default as WidgetHeader } from './components/mr_widget_header.vue';
-export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue';
-export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
-export { default as Deployment } from './components/deployment.vue';
-export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue';
-export { default as MergedState } from './components/states/mr_widget_merged.vue';
-export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue';
-export { default as ClosedState } from './components/states/mr_widget_closed.vue';
-export { default as MergingState } from './components/states/mr_widget_merging.vue';
-export { default as WorkInProgressState } from './components/states/work_in_progress.vue';
-export { default as ArchivedState } from './components/states/mr_widget_archived.vue';
-export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue';
-export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue';
-export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue';
-export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue';
-export { default as ReadyToMergeState } from './components/states/ready_to_merge.vue';
-export { default as ShaMismatchState } from './components/states/sha_mismatch.vue';
-export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue';
-export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue';
-export { default as PipelineFailedState } from './components/states/pipeline_failed.vue';
-export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
-export { default as RebaseState } from './components/states/mr_widget_rebase.vue';
-export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue';
-export { default as CheckingState } from './components/states/mr_widget_checking.vue';
-export { default as MRWidgetStore } from './stores/mr_widget_store';
-export { default as MRWidgetService } from './services/mr_widget_service';
-export { default as eventHub } from './event_hub';
-export { default as getStateKey } from './stores/get_state_key';
-export { default as stateMaps } from './stores/state_maps';
-export { default as SquashBeforeMerge } from './components/states/squash_before_merge.vue';
-export { default as notify } from '../lib/utils/notify';
-export { default as SourceBranchRemovalStatus } from './components/source_branch_removal_status.vue';
-
-export { default as mrWidgetOptions } from './mr_widget_options.vue';
diff --git a/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js
new file mode 100644
index 00000000000..8780aa4bd1c
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js
@@ -0,0 +1,3 @@
+import MRWidgetOptions from './mr_widget_options.vue';
+
+export default MRWidgetOptions;
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index cc6e620f365..60cebbfc2b2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -1,4 +1,5 @@
-import { Vue, mrWidgetOptions } from './dependencies';
+import Vue from 'vue';
+import MrWidgetOptions from './ee_switch_mr_widget_options';
import Translate from '../vue_shared/translate';
Vue.use(Translate);
@@ -6,7 +7,7 @@ Vue.use(Translate);
export default () => {
gl.mrWidgetData.gitlabLogo = gon.gitlab_logo;
- const vm = new Vue(mrWidgetOptions);
+ const vm = new Vue(MrWidgetOptions);
window.gl.mrWidget = {
checkStatus: vm.checkStatus,
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 8180f13a7cb..5d9f7cebcf2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -2,39 +2,37 @@
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
import createFlash from '../flash';
-import {
- WidgetHeader,
- WidgetMergeHelp,
- WidgetPipeline,
- Deployment,
- WidgetRelatedLinks,
- MergedState,
- ClosedState,
- MergingState,
- RebaseState,
- WorkInProgressState,
- ArchivedState,
- ConflictsState,
- NothingToMergeState,
- MissingBranchState,
- NotAllowedState,
- ReadyToMergeState,
- ShaMismatchState,
- UnresolvedDiscussionsState,
- PipelineBlockedState,
- PipelineFailedState,
- FailedToMerge,
- MergeWhenPipelineSucceedsState,
- AutoMergeFailed,
- CheckingState,
- MRWidgetStore,
- MRWidgetService,
- eventHub,
- stateMaps,
- SquashBeforeMerge,
- notify,
- SourceBranchRemovalStatus,
-} from './dependencies';
+import WidgetHeader from './components/mr_widget_header.vue';
+import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
+import WidgetPipeline from './components/mr_widget_pipeline.vue';
+import Deployment from './components/deployment.vue';
+import WidgetRelatedLinks from './components/mr_widget_related_links.vue';
+import MergedState from './components/states/mr_widget_merged.vue';
+import ClosedState from './components/states/mr_widget_closed.vue';
+import MergingState from './components/states/mr_widget_merging.vue';
+import RebaseState from './components/states/mr_widget_rebase.vue';
+import WorkInProgressState from './components/states/work_in_progress.vue';
+import ArchivedState from './components/states/mr_widget_archived.vue';
+import ConflictsState from './components/states/mr_widget_conflicts.vue';
+import NothingToMergeState from './components/states/nothing_to_merge.vue';
+import MissingBranchState from './components/states/mr_widget_missing_branch.vue';
+import NotAllowedState from './components/states/mr_widget_not_allowed.vue';
+import ReadyToMergeState from './components/states/ready_to_merge.vue';
+import ShaMismatchState from './components/states/sha_mismatch.vue';
+import UnresolvedDiscussionsState from './components/states/unresolved_discussions.vue';
+import PipelineBlockedState from './components/states/mr_widget_pipeline_blocked.vue';
+import PipelineFailedState from './components/states/pipeline_failed.vue';
+import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue';
+import MergeWhenPipelineSucceedsState from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
+import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue';
+import CheckingState from './components/states/mr_widget_checking.vue';
+import MRWidgetStore from './stores/ee_switch_mr_widget_store';
+import MRWidgetService from './services/ee_switch_mr_widget_service';
+import eventHub from './event_hub';
+import stateMaps from './stores/ee_switch_state_maps';
+import SquashBeforeMerge from './components/states/squash_before_merge.vue';
+import notify from '~/lib/utils/notify';
+import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue';
import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue';
import { setFaviconOverlay } from '../lib/utils/common_utils';
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js
new file mode 100644
index 00000000000..ea2aabb78fe
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js
@@ -0,0 +1,3 @@
+import MRWidgetService from './mr_widget_service';
+
+export default MRWidgetService;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js
new file mode 100644
index 00000000000..ebef30e3eab
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js
@@ -0,0 +1,3 @@
+import getStateKey from './get_state_key';
+
+export default getStateKey;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js
new file mode 100644
index 00000000000..92a07c53f2d
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js
@@ -0,0 +1,3 @@
+import MergeRequestStore from './mr_widget_store';
+
+export default MergeRequestStore;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js
new file mode 100644
index 00000000000..50cf9503ea7
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js
@@ -0,0 +1,3 @@
+import stateMaps from './state_maps';
+
+export default stateMaps;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 672e5280b5e..e6655914700 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -1,5 +1,5 @@
import Timeago from 'timeago.js';
-import { getStateKey } from '../dependencies';
+import getStateKey from './ee_switch_get_state_key';
import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility';
diff --git a/app/assets/javascripts/vue_shared/components/gl_countdown.vue b/app/assets/javascripts/vue_shared/components/gl_countdown.vue
new file mode 100644
index 00000000000..9327a2a4a6c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/gl_countdown.vue
@@ -0,0 +1,49 @@
+<script>
+import { calculateRemainingMilliseconds, formatTime } from '~/lib/utils/datetime_utility';
+
+/**
+ * Counts down to a given end date.
+ */
+export default {
+ props: {
+ endDateString: {
+ type: String,
+ required: true,
+ validator(value) {
+ return !Number.isNaN(new Date(value).getTime());
+ },
+ },
+ },
+
+ data() {
+ return {
+ remainingTime: formatTime(0),
+ countdownUpdateIntervalId: null,
+ };
+ },
+
+ mounted() {
+ const updateRemainingTime = () => {
+ const remainingMilliseconds = calculateRemainingMilliseconds(this.endDateString);
+ this.remainingTime = formatTime(remainingMilliseconds);
+ };
+
+ updateRemainingTime();
+ this.countdownUpdateIntervalId = window.setInterval(updateRemainingTime, 1000);
+ },
+
+ beforeDestroy() {
+ window.clearInterval(this.countdownUpdateIntervalId);
+ },
+};
+</script>
+
+<template>
+ <time
+ v-gl-tooltip
+ :datetime="endDateString"
+ :title="endDateString"
+ >
+ {{ remainingTime }}
+ </time>
+</template>
diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js
index 4f2412ce520..549d27e96d9 100644
--- a/app/assets/javascripts/vue_shared/directives/tooltip.js
+++ b/app/assets/javascripts/vue_shared/directives/tooltip.js
@@ -9,6 +9,14 @@ export default {
componentUpdated(el) {
$(el).tooltip('_fixTitle');
+
+ // update visible tooltips
+ const tooltipInstance = $(el).data('bs.tooltip');
+ const tip = tooltipInstance.getTipElement();
+ tooltipInstance.setElementContent(
+ $(tip.querySelectorAll('.tooltip-inner')),
+ tooltipInstance.getTitle(),
+ );
},
unbind(el) {
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 6d891e21556..e261bd7c0ca 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -34,7 +34,7 @@
margin-bottom: 0;
}
- *:first-child:not(.katex-display) {
+ *:first-child {
margin-top: 0;
}
diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb
index b7e4f9b81f1..3cdf4ddf8bb 100644
--- a/app/controllers/concerns/boards_responses.rb
+++ b/app/controllers/concerns/boards_responses.rb
@@ -50,7 +50,10 @@ module BoardsResponses
end
def authorize_create_issue
- authorize_action_for!(project, :admin_issue)
+ list = List.find(issue_params[:list_id])
+ action = list.backlog? ? :create_issue : :admin_issue
+
+ authorize_action_for!(project, action)
end
def authorize_admin_list
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index 8d259b4052e..cdc6f53df8e 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -5,6 +5,7 @@ class Groups::BoardsController < Groups::ApplicationController
before_action :assign_endpoint_vars
before_action :boards, only: :index
+ before_action :redirect_to_recent_board, only: :index
def index
respond_with_boards
@@ -13,6 +14,9 @@ class Groups::BoardsController < Groups::ApplicationController
def show
@board = boards.find(params[:id])
+ # add/update the board in the recent visited table
+ Boards::Visits::CreateService.new(@board.group, current_user).execute(@board) if request.format.html?
+
respond_with_board
end
@@ -31,4 +35,18 @@ class Groups::BoardsController < Groups::ApplicationController
def serialize_as_json(resource)
resource.as_json(only: [:id])
end
+
+ def includes_board?(board_id)
+ boards.any? { |board| board.id == board_id }
+ end
+
+ def redirect_to_recent_board
+ return if request.format.json?
+
+ recently_visited = Boards::Visits::LatestService.new(group, current_user).execute
+
+ if recently_visited && includes_board?(recently_visited.board_id)
+ redirect_to(group_board_path(id: recently_visited.board_id), status: :found)
+ end
+ end
end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 77b818347c7..8189b5d182a 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -8,6 +8,7 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :authorize_read_board!, only: [:index, :show]
before_action :boards, only: :index
before_action :assign_endpoint_vars
+ before_action :redirect_to_recent_board, only: :index
def index
respond_with_boards
@@ -16,6 +17,9 @@ class Projects::BoardsController < Projects::ApplicationController
def show
@board = boards.find(params[:id])
+ # add/update the board in the recent visited table
+ Boards::Visits::CreateService.new(@board.project, current_user).execute(@board) if request.format.html?
+
respond_with_board
end
@@ -33,10 +37,24 @@ class Projects::BoardsController < Projects::ApplicationController
end
def authorize_read_board!
- return access_denied! unless can?(current_user, :read_board, project)
+ access_denied! unless can?(current_user, :read_board, project)
end
def serialize_as_json(resource)
resource.as_json(only: [:id])
end
+
+ def includes_board?(board_id)
+ boards.any? { |board| board.id == board_id }
+ end
+
+ def redirect_to_recent_board
+ return if request.format.json?
+
+ recently_visited = Boards::Visits::LatestService.new(project, current_user).execute
+
+ if recently_visited && includes_board?(recently_visited.board_id)
+ redirect_to(namespace_project_board_path(id: recently_visited.board_id), status: :found)
+ end
+ end
end
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index be708835e30..c0aa39d87c6 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -8,6 +8,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403
rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404
rescue_from Gitlab::GitAccess::ProjectCreationError, with: :render_422
+ rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
@@ -62,6 +63,10 @@ class Projects::GitHttpController < Projects::GitHttpClientController
render plain: exception.message, status: :unprocessable_entity
end
+ def render_503(exception)
+ render plain: exception.message, status: :service_unavailable
+ end
+
def access
@access ||= access_klass.new(access_actor, project,
'http', authentication_abilities: authentication_abilities,
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 8abfe0c4c17..eb3d2498830 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -14,7 +14,7 @@
# project_id: integer
# milestone_title: string
# author_id: integer
-# assignee_id: integer
+# assignee_id: integer or 'None' or 'Any'
# search: string
# label_name: string
# sort: string
@@ -34,6 +34,11 @@ class IssuableFinder
requires_cross_project_access unless: -> { project? }
+ # This is used as a common filter for None / Any
+ FILTER_NONE = 'none'.freeze
+ FILTER_ANY = 'any'.freeze
+
+ # This is accepted as a deprecated filter and is also used in unassigning users
NONE = '0'.freeze
attr_accessor :current_user, :params
@@ -236,16 +241,20 @@ class IssuableFinder
# rubocop: enable CodeReuse/ActiveRecord
def assignee_id?
- params[:assignee_id].present? && params[:assignee_id].to_s != NONE
+ params[:assignee_id].present?
end
def assignee_username?
- params[:assignee_username].present? && params[:assignee_username].to_s != NONE
+ params[:assignee_username].present?
end
- def no_assignee?
+ def filter_by_no_assignee?
# Assignee_id takes precedence over assignee_username
- params[:assignee_id].to_s == NONE || params[:assignee_username].to_s == NONE
+ [NONE, FILTER_NONE].include?(params[:assignee_id].to_s.downcase) || params[:assignee_username].to_s == NONE
+ end
+
+ def filter_by_any_assignee?
+ params[:assignee_id].to_s.downcase == FILTER_ANY
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -399,15 +408,17 @@ class IssuableFinder
# rubocop: disable CodeReuse/ActiveRecord
def by_assignee(items)
- if assignee
- items = items.where(assignee_id: assignee.id)
- elsif no_assignee?
- items = items.where(assignee_id: nil)
+ if filter_by_no_assignee?
+ items.where(assignee_id: nil)
+ elsif filter_by_any_assignee?
+ items.where('assignee_id IS NOT NULL')
+ elsif assignee
+ items.where(assignee_id: assignee.id)
elsif assignee_id? || assignee_username? # assignee not found
- items = items.none
+ items.none
+ else
+ items
end
-
- items
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index abdc47b9866..cee57a83df4 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -137,10 +137,12 @@ class IssuesFinder < IssuableFinder
# rubocop: disable CodeReuse/ActiveRecord
def by_assignee(items)
- if assignee
- items.assigned_to(assignee)
- elsif no_assignee?
+ if filter_by_no_assignee?
items.unassigned
+ elsif filter_by_any_assignee?
+ items.assigned
+ elsif assignee
+ items.assigned_to(assignee)
elsif assignee_id? || assignee_username? # assignee not found
items.none
else
diff --git a/app/models/board_group_recent_visit.rb b/app/models/board_group_recent_visit.rb
new file mode 100644
index 00000000000..92abbb67222
--- /dev/null
+++ b/app/models/board_group_recent_visit.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# Tracks which boards in a specific group a user has visited
+class BoardGroupRecentVisit < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :group
+ belongs_to :board
+
+ validates :user, presence: true
+ validates :group, presence: true
+ validates :board, presence: true
+
+ scope :by_user_group, -> (user, group) { where(user: user, group: group).order(:updated_at) }
+
+ def self.visited!(user, board)
+ visit = find_or_create_by(user: user, group: board.group, board: board)
+ visit.touch if visit.updated_at < Time.now
+ rescue ActiveRecord::RecordNotUnique
+ retry
+ end
+
+ def self.latest(user, group)
+ by_user_group(user, group).last
+ end
+end
diff --git a/app/models/board_project_recent_visit.rb b/app/models/board_project_recent_visit.rb
new file mode 100644
index 00000000000..7cffff906d8
--- /dev/null
+++ b/app/models/board_project_recent_visit.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# Tracks which boards in a specific project a user has visited
+class BoardProjectRecentVisit < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :project
+ belongs_to :board
+
+ validates :user, presence: true
+ validates :project, presence: true
+ validates :board, presence: true
+
+ scope :by_user_project, -> (user, project) { where(user: user, project: project).order(:updated_at) }
+
+ def self.visited!(user, board)
+ visit = find_or_create_by(user: user, project: board.project, board: board)
+ visit.touch if visit.updated_at < Time.now
+ rescue ActiveRecord::RecordNotUnique
+ retry
+ end
+
+ def self.latest(user, project)
+ by_user_project(user, project).last
+ end
+end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 06507345fe8..7d36f9982ab 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -50,7 +50,8 @@ class CommitStatus < ActiveRecord::Base
runner_system_failure: 4,
missing_dependency_failure: 5,
runner_unsupported: 6,
- stale_schedule: 7
+ stale_schedule: 7,
+ job_execution_timeout: 8
}
##
@@ -109,7 +110,7 @@ class CommitStatus < ActiveRecord::Base
before_transition any => :failed do |commit_status, transition|
failure_reason = transition.args.first
- commit_status.failure_reason = failure_reason
+ commit_status.failure_reason = CommitStatus.failure_reasons[failure_reason]
end
after_transition do |commit_status, transition|
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 0816c395185..1c31c01eb9f 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -149,7 +149,7 @@ class Environment < ActiveRecord::Base
end
def has_metrics?
- prometheus_adapter&.can_query? && available? && last_deployment.present?
+ prometheus_adapter&.can_query? && available?
end
def metrics
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
index 29eaad759bb..a866e76df5a 100644
--- a/app/presenters/commit_status_presenter.rb
+++ b/app/presenters/commit_status_presenter.rb
@@ -9,7 +9,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
runner_system_failure: 'There has been a runner system failure, please try again',
missing_dependency_failure: 'There has been a missing dependency failure',
runner_unsupported: 'Your runner is outdated, please upgrade your runner',
- stale_schedule: 'Delayed job could not be executed by some reason, please try again'
+ stale_schedule: 'Delayed job could not be executed by some reason, please try again',
+ job_execution_timeout: 'The script exceeded the maximum execution time set for the job'
}.freeze
private_constant :CALLOUT_FAILURE_MESSAGES
diff --git a/app/services/boards/visits/create_service.rb b/app/services/boards/visits/create_service.rb
new file mode 100644
index 00000000000..e2adf755511
--- /dev/null
+++ b/app/services/boards/visits/create_service.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Boards
+ module Visits
+ class CreateService < Boards::BaseService
+ def execute(board)
+ return unless current_user && Gitlab::Database.read_write?
+
+ if parent.is_a?(Group)
+ BoardGroupRecentVisit.visited!(current_user, board)
+ else
+ BoardProjectRecentVisit.visited!(current_user, board)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/boards/visits/latest_service.rb b/app/services/boards/visits/latest_service.rb
new file mode 100644
index 00000000000..9e4c77a6317
--- /dev/null
+++ b/app/services/boards/visits/latest_service.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Boards
+ module Visits
+ class LatestService < Boards::BaseService
+ def execute
+ return nil unless current_user
+
+ if parent.is_a?(Group)
+ BoardGroupRecentVisit.latest(current_user, parent)
+ else
+ BoardProjectRecentVisit.latest(current_user, parent)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/resource_events/merge_into_notes_service.rb b/app/services/resource_events/merge_into_notes_service.rb
index 596c0105ea0..7504773a002 100644
--- a/app/services/resource_events/merge_into_notes_service.rb
+++ b/app/services/resource_events/merge_into_notes_service.rb
@@ -34,7 +34,7 @@ module ResourceEvents
def label_events_by_discussion_id
return [] unless resource.respond_to?(:resource_label_events)
- events = resource.resource_label_events.includes(:label, :user)
+ events = resource.resource_label_events.includes(:label, user: :status)
events = since_fetch_at(events)
events.group_by { |event| event.discussion_id }
diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml
index 406dccb74fb..e37fd7624be 100644
--- a/app/views/projects/tree/_tree_commit_column.html.haml
+++ b/app/views/projects/tree/_tree_commit_column.html.haml
@@ -1,2 +1,2 @@
%span.str-truncated
- = link_to_html commit.redacted_full_title_html, project_commit_path(@project, commit.id), class: 'tree-commit-link'
+ = link_to_html commit.redacted_full_title_html, project_commit_path(@project, commit.id), title: commit.redacted_full_title_html, class: 'tree-commit-link'
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index e26f5260e5b..c6c5cadc3f5 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -39,14 +39,13 @@
{{ list.issuesSize }}
= render_if_exists "shared/boards/components/list_weight"
- - if can?(current_user, :admin_list, current_board_parent)
- %button.issue-count-badge-add-button.btn.btn-sm.btn-default.ml-1.has-tooltip.js-no-trigger-collapse{ type: "button",
- "@click" => "showNewIssueForm",
- "v-if" => 'list.type !== "closed"',
- "aria-label" => _("New issue"),
- "title" => _("New issue"),
- data: { placement: "top", container: "body" } }
- = icon("plus", class: "js-no-trigger-collapse")
+ %button.issue-count-badge-add-button.btn.btn-sm.btn-default.ml-1.has-tooltip.js-no-trigger-collapse{ type: "button",
+ "@click" => "showNewIssueForm",
+ "v-if" => "isNewIssueShown",
+ "aria-label" => _("New issue"),
+ "title" => _("New issue"),
+ data: { placement: "top", container: "body" } }
+ = icon("plus", class: "js-no-trigger-collapse")
%board-list{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"',
":list" => "list",
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index 09ddf732ada..f6c7ca70ebd 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -9,11 +9,11 @@
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
- %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
+ %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= icon('caret-down')
.sr-only Toggle dropdown
- else
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), display: 'static' } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
= icon("caret-down")
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 09a594cdb4e..72a1733a2a8 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -29,15 +29,14 @@ class PostReceive
def process_project_changes(post_received)
changes = []
refs = Set.new
+ @user = post_received.identify
- post_received.changes_refs do |oldrev, newrev, ref|
- @user ||= post_received.identify(newrev)
-
- unless @user
- log("Triggered hook for non-existing user \"#{post_received.identifier}\"")
- return false # rubocop:disable Cop/AvoidReturnFromBlocks
- end
+ unless @user
+ log("Triggered hook for non-existing user \"#{post_received.identifier}\"")
+ return false
+ end
+ post_received.changes_refs do |oldrev, newrev, ref|
if Gitlab::Git.tag_ref?(ref)
GitTagPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
elsif Gitlab::Git.branch_ref?(ref)
diff --git a/bin/web b/bin/web
index ecd0bbd10b0..06ff7c39296 100755
--- a/bin/web
+++ b/bin/web
@@ -3,6 +3,11 @@
cd $(dirname $0)/..
app_root=$(pwd)
+# Switch to experimental PUMA configuration
+if [ -n "${EXPERIMENTAL_PUMA}" ]; then
+ exec bin/web_puma "$@"
+fi
+
unicorn_pidfile="$app_root/tmp/pids/unicorn.pid"
unicorn_config="$app_root/config/unicorn.rb"
unicorn_cmd="bundle exec unicorn_rails -c $unicorn_config -E $RAILS_ENV"
diff --git a/bin/web_puma b/bin/web_puma
new file mode 100755
index 00000000000..178fe84800d
--- /dev/null
+++ b/bin/web_puma
@@ -0,0 +1,63 @@
+#!/bin/sh
+
+set -e
+
+cd $(dirname $0)/..
+app_root=$(pwd)
+
+puma_pidfile="$app_root/tmp/pids/puma.pid"
+puma_config="$app_root/config/puma.rb"
+
+spawn_puma()
+{
+ exec bundle exec puma --config "${puma_config}" "$@"
+}
+
+get_puma_pid()
+{
+ pid=$(cat "${puma_pidfile}")
+ if [ -z "$pid" ] ; then
+ echo "Could not find a PID in $puma_pidfile"
+ exit 1
+ fi
+ echo "${pid}"
+}
+
+start()
+{
+ spawn_puma -d
+}
+
+start_foreground()
+{
+ spawn_puma
+}
+
+stop()
+{
+ get_puma_pid
+ kill -QUIT "$(get_puma_pid)"
+}
+
+reload()
+{
+ kill -USR2 "$(get_puma_pid)"
+}
+
+case "$1" in
+ start)
+ start
+ ;;
+ start_foreground)
+ start_foreground
+ ;;
+ stop)
+ stop
+ ;;
+ reload)
+ reload
+ ;;
+ *)
+ echo "Usage: RAILS_ENV=your_env $0 {start|stop|reload}"
+ ;;
+esac
diff --git a/changelogs/unreleased/27231-add-license-data-to-projects-endpoint.yml b/changelogs/unreleased/27231-add-license-data-to-projects-endpoint.yml
new file mode 100644
index 00000000000..f5ed6ccf6df
--- /dev/null
+++ b/changelogs/unreleased/27231-add-license-data-to-projects-endpoint.yml
@@ -0,0 +1,5 @@
+---
+title: Add license data to projects endpoint
+merge_request: 21606
+author: J.D. Bean (@jdbean)
+type: added
diff --git a/changelogs/unreleased/40372-prometheus-dashboard-broken-on-firefox.yml b/changelogs/unreleased/40372-prometheus-dashboard-broken-on-firefox.yml
new file mode 100644
index 00000000000..8376fac7abf
--- /dev/null
+++ b/changelogs/unreleased/40372-prometheus-dashboard-broken-on-firefox.yml
@@ -0,0 +1,5 @@
+---
+title: Fix prometheus graphs in firefox
+merge_request: 22400
+author:
+type: fixed
diff --git a/changelogs/unreleased/42790-improve-feedback-for-internal-git-access-checks-timeouts.yml b/changelogs/unreleased/42790-improve-feedback-for-internal-git-access-checks-timeouts.yml
new file mode 100644
index 00000000000..d58d8da3a0e
--- /dev/null
+++ b/changelogs/unreleased/42790-improve-feedback-for-internal-git-access-checks-timeouts.yml
@@ -0,0 +1,5 @@
+---
+title: Adds trace of each access check when git push times out
+merge_request: 22265
+author:
+type: added
diff --git a/changelogs/unreleased/45068-no-longer-require-a-deploy-to-start-prometheus-monitoring.yml b/changelogs/unreleased/45068-no-longer-require-a-deploy-to-start-prometheus-monitoring.yml
new file mode 100644
index 00000000000..6a305099dde
--- /dev/null
+++ b/changelogs/unreleased/45068-no-longer-require-a-deploy-to-start-prometheus-monitoring.yml
@@ -0,0 +1,5 @@
+---
+title: No longer require a deploy to start Prometheus monitoring
+merge_request: 22401
+author:
+type: changed
diff --git a/changelogs/unreleased/51306-fix-inaccessible-dropdown-for-codeless-projects.yml b/changelogs/unreleased/51306-fix-inaccessible-dropdown-for-codeless-projects.yml
new file mode 100644
index 00000000000..13e3bb66430
--- /dev/null
+++ b/changelogs/unreleased/51306-fix-inaccessible-dropdown-for-codeless-projects.yml
@@ -0,0 +1,5 @@
+---
+title: Fix inaccessible dropdown for code-less projects
+merge_request: 22137
+author:
+type: other
diff --git a/changelogs/unreleased/51335-fail-early-when-user-cannot-be-identified.yml b/changelogs/unreleased/51335-fail-early-when-user-cannot-be-identified.yml
new file mode 100644
index 00000000000..a042debc28f
--- /dev/null
+++ b/changelogs/unreleased/51335-fail-early-when-user-cannot-be-identified.yml
@@ -0,0 +1,5 @@
+---
+title: If user was not found, service hooks won't run on post receive background job
+merge_request: 22519
+author:
+type: fixed
diff --git a/changelogs/unreleased/52115-Link-button-in-markdown-editor-should-recognize-URLs.yml b/changelogs/unreleased/52115-Link-button-in-markdown-editor-should-recognize-URLs.yml
new file mode 100644
index 00000000000..8521335c2ea
--- /dev/null
+++ b/changelogs/unreleased/52115-Link-button-in-markdown-editor-should-recognize-URLs.yml
@@ -0,0 +1,5 @@
+---
+title: Link button in markdown editor recognize URLs
+merge_request: 1983
+author: Johann Hubert Sonntagbauer
+type: changed
diff --git a/changelogs/unreleased/52384-api-filter-assignee-none-any.yml b/changelogs/unreleased/52384-api-filter-assignee-none-any.yml
new file mode 100644
index 00000000000..9acec04d946
--- /dev/null
+++ b/changelogs/unreleased/52384-api-filter-assignee-none-any.yml
@@ -0,0 +1,5 @@
+---
+title: Add None/Any option for assignee_id in Issues and Merge Requests API
+merge_request: 22598
+author: Heinrich Lee Yu
+type: added
diff --git a/changelogs/unreleased/52545-guest-create-issue-in-group-board.yml b/changelogs/unreleased/52545-guest-create-issue-in-group-board.yml
new file mode 100644
index 00000000000..5701e44eb32
--- /dev/null
+++ b/changelogs/unreleased/52545-guest-create-issue-in-group-board.yml
@@ -0,0 +1,5 @@
+---
+title: Always show new issue button in boards' Open list
+merge_request: 22557
+author: Heinrich Lee Yu
+type: fixed
diff --git a/changelogs/unreleased/52993-ldap-rename_provider-rake-task-broken.yml b/changelogs/unreleased/52993-ldap-rename_provider-rake-task-broken.yml
new file mode 100644
index 00000000000..ca78f9a392e
--- /dev/null
+++ b/changelogs/unreleased/52993-ldap-rename_provider-rake-task-broken.yml
@@ -0,0 +1,5 @@
+---
+title: Use gitlab_environment for ldap rake task
+merge_request: 22582
+author:
+type: fixed
diff --git a/changelogs/unreleased/53133-jobs-list.yml b/changelogs/unreleased/53133-jobs-list.yml
new file mode 100644
index 00000000000..2e13edc0e76
--- /dev/null
+++ b/changelogs/unreleased/53133-jobs-list.yml
@@ -0,0 +1,5 @@
+---
+title: Fix stage dropdown not rendering in different languages
+merge_request: 22604
+author:
+type: other
diff --git a/changelogs/unreleased/add-failure-reason-for-execution-timeout.yml b/changelogs/unreleased/add-failure-reason-for-execution-timeout.yml
new file mode 100644
index 00000000000..c8488cbf200
--- /dev/null
+++ b/changelogs/unreleased/add-failure-reason-for-execution-timeout.yml
@@ -0,0 +1,5 @@
+---
+title: Add failure reason for execution timeout
+merge_request: 22224
+author:
+type: changed
diff --git a/changelogs/unreleased/an-multithreading.yml b/changelogs/unreleased/an-multithreading.yml
new file mode 100644
index 00000000000..fca847e6ea4
--- /dev/null
+++ b/changelogs/unreleased/an-multithreading.yml
@@ -0,0 +1,5 @@
+---
+title: Experimental support for running Puma multithreaded web-server
+merge_request: 22372
+author:
+type: performance
diff --git a/changelogs/unreleased/avoid-lock-when-introduce-new-failure-reason.yml b/changelogs/unreleased/avoid-lock-when-introduce-new-failure-reason.yml
new file mode 100644
index 00000000000..30b9ae032d4
--- /dev/null
+++ b/changelogs/unreleased/avoid-lock-when-introduce-new-failure-reason.yml
@@ -0,0 +1,5 @@
+---
+title: Support backward compatibility when introduce new failure reason
+merge_request: 22566
+author:
+type: changed
diff --git a/changelogs/unreleased/bvl-preload-user-status-for-events.yml b/changelogs/unreleased/bvl-preload-user-status-for-events.yml
new file mode 100644
index 00000000000..e13b19b19c1
--- /dev/null
+++ b/changelogs/unreleased/bvl-preload-user-status-for-events.yml
@@ -0,0 +1,5 @@
+---
+title: Show user status for label events in system notes
+merge_request: 22609
+author:
+type: fixed
diff --git a/changelogs/unreleased/bw-automatically-navigate-to-last-board-visited.yml b/changelogs/unreleased/bw-automatically-navigate-to-last-board-visited.yml
new file mode 100644
index 00000000000..836b9aa21c5
--- /dev/null
+++ b/changelogs/unreleased/bw-automatically-navigate-to-last-board-visited.yml
@@ -0,0 +1,5 @@
+---
+title: Automatically navigate to last board visited
+merge_request: 22430
+author:
+type: changed
diff --git a/changelogs/unreleased/jramsay-42673-commit-tooltip.yml b/changelogs/unreleased/jramsay-42673-commit-tooltip.yml
new file mode 100644
index 00000000000..083cd1a54a0
--- /dev/null
+++ b/changelogs/unreleased/jramsay-42673-commit-tooltip.yml
@@ -0,0 +1,5 @@
+---
+title: Add commit message to commit tree anchor title
+merge_request: 22585
+author:
+type: fixed
diff --git a/changelogs/unreleased/pl-uprade-prometheus-alertmanager.yml b/changelogs/unreleased/pl-uprade-prometheus-alertmanager.yml
new file mode 100644
index 00000000000..d0c8ed8001d
--- /dev/null
+++ b/changelogs/unreleased/pl-uprade-prometheus-alertmanager.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade Prometheus to 2.4.3 and Alertmanager to 0.15.2
+merge_request: 22600
+author:
+type: other
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index 146c4b1e024..8052880cc3d 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -26,9 +26,25 @@ Sidekiq.configure_server do |config|
end
if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled?
- unless Sidekiq.server?
- Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start
+ Gitlab::Cluster::LifecycleEvents.on_worker_start do
+ defined?(::Prometheus::Client.reinitialize_on_pid_change) && Prometheus::Client.reinitialize_on_pid_change
+
+ unless Sidekiq.server?
+ Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start
+ end
+
+ Gitlab::Metrics::Samplers::RubySampler.initialize_instance(Settings.monitoring.ruby_sampler_interval).start
end
+end
- Gitlab::Metrics::Samplers::RubySampler.initialize_instance(Settings.monitoring.ruby_sampler_interval).start
+Gitlab::Cluster::LifecycleEvents.on_master_restart do
+ # The following is necessary to ensure stale Prometheus metrics don't
+ # accumulate over time. It needs to be done in this hook as opposed to
+ # inside an init script to ensure metrics files aren't deleted after new
+ # unicorn workers start after a SIGUSR2 is received.
+ prometheus_multiproc_dir = ENV['prometheus_multiproc_dir']
+ if prometheus_multiproc_dir
+ old_metrics = Dir[File.join(prometheus_multiproc_dir, '*.db')]
+ FileUtils.rm_rf(old_metrics)
+ end
end
diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb
index eccf82ab8dc..c8d261d415e 100644
--- a/config/initializers/8_metrics.rb
+++ b/config/initializers/8_metrics.rb
@@ -158,7 +158,9 @@ if Gitlab::Metrics.enabled? && !Rails.env.test?
GC::Profiler.enable
- Gitlab::Metrics::Samplers::InfluxSampler.initialize_instance.start
+ Gitlab::Cluster::LifecycleEvents.on_worker_start do
+ Gitlab::Metrics::Samplers::InfluxSampler.initialize_instance.start
+ end
module TrackNewRedisConnections
def connect(*args)
diff --git a/config/initializers/active_record_lifecycle.rb b/config/initializers/active_record_lifecycle.rb
new file mode 100644
index 00000000000..7fa37121efc
--- /dev/null
+++ b/config/initializers/active_record_lifecycle.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# Don't handle sidekiq configuration as it
+# has its own special active record configuration here
+if defined?(ActiveRecord::Base) && !Sidekiq.server?
+ Gitlab::Cluster::LifecycleEvents.on_worker_start do
+ ActiveSupport.on_load(:active_record) do
+ ActiveRecord::Base.establish_connection
+
+ Rails.logger.debug("ActiveRecord connection established")
+ end
+ end
+end
+
+if defined?(ActiveRecord::Base)
+ Gitlab::Cluster::LifecycleEvents.on_before_fork do
+ # the following is highly recommended for Rails + "preload_app true"
+ # as there's no need for the master process to hold a connection
+ ActiveRecord::Base.connection.disconnect!
+
+ Rails.logger.debug("ActiveRecord connection disconnected")
+ end
+end
diff --git a/config/initializers/macos.rb b/config/initializers/macos.rb
new file mode 100644
index 00000000000..f410af6ed47
--- /dev/null
+++ b/config/initializers/macos.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+if /darwin/ =~ RUBY_PLATFORM
+ Gitlab::Cluster::LifecycleEvents.on_before_fork do
+ require 'fiddle'
+
+ # Dynamically load Foundation.framework, ~implicitly~ initialising
+ # the Objective-C runtime before any forking happens in Unicorn
+ #
+ # From https://bugs.ruby-lang.org/issues/14009
+ Fiddle.dlopen '/System/Library/Frameworks/Foundation.framework/Foundation'
+ end
+end
diff --git a/config/initializers/rbtrace.rb b/config/initializers/rbtrace.rb
new file mode 100644
index 00000000000..6a1b71bf4bd
--- /dev/null
+++ b/config/initializers/rbtrace.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+if ENV['ENABLE_RBTRACE']
+ Gitlab::Cluster::LifecycleEvents.on_worker_start do
+ # Unicorn clears out signals before it forks, so rbtrace won't work
+ # unless it is enabled after the fork.
+ require 'rbtrace'
+ end
+end
diff --git a/config/initializers/routing_draw.rb b/config/initializers/routing_draw.rb
index 25003cf0239..f0f74954eef 100644
--- a/config/initializers/routing_draw.rb
+++ b/config/initializers/routing_draw.rb
@@ -1,7 +1,3 @@
# Adds draw method into Rails routing
-# It allows us to keep routing splitted into files
-class ActionDispatch::Routing::Mapper
- def draw(routes_name)
- instance_eval(File.read(Rails.root.join("config/routes/#{routes_name}.rb")))
- end
-end
+# It allows us to keep routing split into files
+ActionDispatch::Routing::Mapper.prepend Gitlab::Patch::DrawRoute
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index bc6b7aed6aa..565efc858d1 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -14,8 +14,6 @@ Sidekiq.default_worker_options = { retry: 3 }
enable_json_logs = Gitlab.config.sidekiq.log_format == 'json'
Sidekiq.configure_server do |config|
- require 'rbtrace' if ENV['ENABLE_RBTRACE']
-
config.redis = queues_config_hash
config.server_middleware do |chain|
diff --git a/config/puma.example.development.rb b/config/puma.example.development.rb
new file mode 100644
index 00000000000..490c940077a
--- /dev/null
+++ b/config/puma.example.development.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+# -----------------------------------------------------------------------
+# This file is used by the GDK to generate a default config/puma.rb file
+# Note that `/home/git` will be substituted for the actual GDK root
+# directory when this file is generated
+# -----------------------------------------------------------------------
+
+# Load "path" as a rackup file.
+#
+# The default is "config.ru".
+#
+rackup 'config.ru'
+pidfile '/home/git/gitlab/tmp/pids/puma.pid'
+state_path '/home/git/gitlab/tmp/pids/puma.state'
+
+stdout_redirect '/home/git/gitlab/log/puma.stdout.log',
+ '/home/git/gitlab/log/puma.stderr.log',
+ true
+
+# Configure "min" to be the minimum number of threads to use to answer
+# requests and "max" the maximum.
+#
+# The default is "0, 16".
+#
+threads 1, 4
+
+# By default, workers accept all requests and queue them to pass to handlers.
+# When false, workers accept the number of simultaneous requests configured.
+#
+# Queueing requests generally improves performance, but can cause deadlocks if
+# the app is waiting on a request to itself. See https://github.com/puma/puma/issues/612
+#
+# When set to false this may require a reverse proxy to handle slow clients and
+# queue requests before they reach puma. This is due to disabling HTTP keepalive
+queue_requests false
+
+# Bind the server to "url". "tcp://", "unix://" and "ssl://" are the only
+# accepted protocols.
+bind 'unix:///home/git/gitlab.socket'
+
+workers 2
+
+require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"
+require_relative "/home/git/gitlab/lib/gitlab/cluster/puma_worker_killer_initializer"
+
+on_restart do
+ # Signal application hooks that we're about to restart
+ Gitlab::Cluster::LifecycleEvents.do_master_restart
+end
+
+before_fork do
+ # Signal to the puma killer
+ Gitlab::Cluster::PumaWorkerKillerInitializer.start @config.options unless ENV['DISABLE_PUMA_WORKER_KILLER']
+
+ # Signal application hooks that we're about to fork
+ Gitlab::Cluster::LifecycleEvents.do_before_fork
+end
+
+Gitlab::Cluster::LifecycleEvents.set_puma_options @config.options
+on_worker_boot do
+ # Signal application hooks of worker start
+ Gitlab::Cluster::LifecycleEvents.do_worker_start
+end
+
+# Preload the application before starting the workers; this conflicts with
+# phased restart feature. (off by default)
+
+preload_app!
+
+tag 'gitlab-puma-worker'
+
+# Verifies that all workers have checked in to the master process within
+# the given timeout. If not the worker process will be restarted. Default
+# value is 60 seconds.
+#
+worker_timeout 60
diff --git a/config/routes.rb b/config/routes.rb
index c081ca9672a..8723a928cc3 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -34,6 +34,8 @@ Rails.application.routes.draw do
match '*all', via: [:get, :post], to: proc { [404, {}, ['']] }
end
+ draw :oauth
+
use_doorkeeper_openid_connect
# Autocomplete
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index fb29c4748c1..af333bdc748 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -71,6 +71,7 @@ namespace :admin do
resource :logs, only: [:show]
resource :health_check, controller: 'health_check', only: [:show]
resource :background_jobs, controller: 'background_jobs', only: [:show]
+
resource :system_info, controller: 'system_info', only: [:show]
resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ }
@@ -104,6 +105,7 @@ namespace :admin do
resource :application_settings, only: [:show, :update] do
resources :services, only: [:index, :edit, :update]
+
get :usage_data
put :reset_registration_token
put :reset_health_check_token
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 602bbe837cf..2328b50b760 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
resources :groups, only: [:index, :new, :create] do
post :preview_markdown
end
@@ -63,7 +65,6 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
end
end
- # On CE only index and show actions are needed
resources :boards, only: [:index, :show]
resources :runners, only: [:index, :edit, :update, :destroy, :show] do
diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example
index e06cce3e97a..4637eb8bc6e 100644
--- a/config/unicorn.rb.example
+++ b/config/unicorn.rb.example
@@ -81,22 +81,16 @@ preload_app true
# fast LAN.
check_client_connection false
+require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"
+
before_exec do |server|
- # The following is necessary to ensure stale Prometheus metrics don't
- # accumulate over time. It needs to be done in this hook as opposed to
- # inside an init script to ensure metrics files aren't deleted after new
- # unicorn workers start after a SIGUSR2 is received.
- if ENV['prometheus_multiproc_dir']
- old_metrics = Dir[File.join(ENV['prometheus_multiproc_dir'], '*.db')]
- FileUtils.rm_rf(old_metrics)
- end
+ # Signal application hooks that we're about to restart
+ Gitlab::Cluster::LifecycleEvents.do_master_restart
end
before_fork do |server, worker|
- # the following is highly recommended for Rails + "preload_app true"
- # as there's no need for the master process to hold a connection
- defined?(ActiveRecord::Base) &&
- ActiveRecord::Base.connection.disconnect!
+ # Signal application hooks that we're about to fork
+ Gitlab::Cluster::LifecycleEvents.do_before_fork
# The following is only recommended for memory/DB-constrained
# installations. It is not needed if your system can house
@@ -124,25 +118,10 @@ before_fork do |server, worker|
end
after_fork do |server, worker|
- # Unicorn clears out signals before it forks, so rbtrace won't work
- # unless it is enabled after the fork.
- require 'rbtrace' if ENV['ENABLE_RBTRACE']
+ # Signal application hooks of worker start
+ Gitlab::Cluster::LifecycleEvents.do_worker_start
# per-process listener ports for debugging/admin/migrations
# addr = "127.0.0.1:#{9293 + worker.nr}"
# server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true)
-
- # the following is *required* for Rails + "preload_app true",
- defined?(ActiveRecord::Base) &&
- ActiveRecord::Base.establish_connection
-
- # reset prometheus client, this will cause any opened metrics files to be closed
- defined?(::Prometheus::Client.reinitialize_on_pid_change) &&
- Prometheus::Client.reinitialize_on_pid_change
-
- # if preload_app is true, then you may also want to check and
- # restart any other shared sockets/descriptors such as Memcached,
- # and Redis. TokyoCabinet file handles are safe to reuse
- # between any number of forked children (assuming your kernel
- # correctly implements pread()/pwrite() system calls)
end
diff --git a/config/unicorn.rb.example.development b/config/unicorn.rb.example.development
index f31df66015a..f7541bb9d55 100644
--- a/config/unicorn.rb.example.development
+++ b/config/unicorn.rb.example.development
@@ -1,32 +1,61 @@
+# frozen_string_literal: true
+
+# -------------------------------------------------------------------------
+# This file is used by the GDK to generate a default config/unicorn.rb file
+# Note that `/home/git` will be substituted for the actual GDK root
+# directory when this file is generated
+# -------------------------------------------------------------------------
+
worker_processes 2
timeout 60
+listen '/home/git/gitlab.socket'
+
preload_app true
check_client_connection false
+require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"
+
+before_exec do |server|
+ # Signal application hooks that we're about to restart
+ Gitlab::Cluster::LifecycleEvents.do_master_restart
+end
+
before_fork do |server, worker|
- # the following is highly recommended for Rails + "preload_app true"
- # as there's no need for the master process to hold a connection
- defined?(ActiveRecord::Base) &&
- ActiveRecord::Base.connection.disconnect!
-
- if /darwin/ =~ RUBY_PLATFORM
- require 'fiddle'
-
- # Dynamically load Foundation.framework, ~implicitly~ initialising
- # the Objective-C runtime before any forking happens in Unicorn
- #
- # From https://bugs.ruby-lang.org/issues/14009
- Fiddle.dlopen '/System/Library/Frameworks/Foundation.framework/Foundation'
+ # Signal application hooks that we're about to fork
+ Gitlab::Cluster::LifecycleEvents.do_before_fork
+
+ # The following is only recommended for memory/DB-constrained
+ # installations. It is not needed if your system can house
+ # twice as many worker_processes as you have configured.
+ #
+ # This allows a new master process to incrementally
+ # phase out the old master process with SIGTTOU to avoid a
+ # thundering herd (especially in the "preload_app false" case)
+ # when doing a transparent upgrade. The last worker spawned
+ # will then kill off the old master process with a SIGQUIT.
+ old_pid = "#{server.config[:pid]}.oldbin"
+ if old_pid != server.pid
+ begin
+ sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
+ Process.kill(sig, File.read(old_pid).to_i)
+ rescue Errno::ENOENT, Errno::ESRCH
+ end
end
+ #
+ # Throttle the master from forking too quickly by sleeping. Due
+ # to the implementation of standard Unix signal handlers, this
+ # helps (but does not completely) prevent identical, repeated signals
+ # from being lost when the receiving process is busy.
+ # sleep 1
end
after_fork do |server, worker|
- # Unicorn clears out signals before it forks, so rbtrace won't work
- # unless it is enabled after the fork.
- require 'rbtrace' if ENV['ENABLE_RBTRACE']
+ # Signal application hooks of worker start
+ Gitlab::Cluster::LifecycleEvents.do_worker_start
- # the following is *required* for Rails + "preload_app true",
- defined?(ActiveRecord::Base) &&
- ActiveRecord::Base.establish_connection
+ # per-process listener ports for debugging/admin/migrations
+ # addr = "127.0.0.1:#{9293 + worker.nr}"
+ # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true)
end
+
diff --git a/db/migrate/20181010235606_create_board_project_recent_visits.rb b/db/migrate/20181010235606_create_board_project_recent_visits.rb
new file mode 100644
index 00000000000..426f41e202a
--- /dev/null
+++ b/db/migrate/20181010235606_create_board_project_recent_visits.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class CreateBoardProjectRecentVisits < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :board_project_recent_visits, id: :bigserial do |t|
+ t.timestamps_with_timezone null: false
+
+ t.references :user, index: true, foreign_key: { on_delete: :cascade }
+ t.references :project, index: true, foreign_key: { on_delete: :cascade }
+ t.references :board, index: true, foreign_key: { on_delete: :cascade }
+ end
+
+ add_index :board_project_recent_visits, [:user_id, :project_id, :board_id], unique: true, name: 'index_board_project_recent_visits_on_user_project_and_board'
+ end
+end
diff --git a/db/migrate/20181016152238_create_board_group_recent_visits.rb b/db/migrate/20181016152238_create_board_group_recent_visits.rb
new file mode 100644
index 00000000000..1e55dc8658e
--- /dev/null
+++ b/db/migrate/20181016152238_create_board_group_recent_visits.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class CreateBoardGroupRecentVisits < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :board_group_recent_visits, id: :bigserial do |t|
+ t.timestamps_with_timezone null: false
+
+ t.references :user, index: true, foreign_key: { on_delete: :cascade }
+ t.references :board, index: true, foreign_key: { on_delete: :cascade }
+ t.references :group, references: :namespace, column: :group_id, index: true
+ t.foreign_key :namespaces, column: :group_id, on_delete: :cascade
+ end
+
+ add_index :board_group_recent_visits, [:user_id, :group_id, :board_id], unique: true, name: 'index_board_group_recent_visits_on_user_group_and_board'
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ddfccbba678..7a75aafd7b0 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20181013005024) do
+ActiveRecord::Schema.define(version: 20181016152238) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -204,6 +204,32 @@ ActiveRecord::Schema.define(version: 20181013005024) do
add_index "badges", ["group_id"], name: "index_badges_on_group_id", using: :btree
add_index "badges", ["project_id"], name: "index_badges_on_project_id", using: :btree
+ create_table "board_group_recent_visits", id: :bigserial, force: :cascade do |t|
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "user_id"
+ t.integer "board_id"
+ t.integer "group_id"
+ end
+
+ add_index "board_group_recent_visits", ["board_id"], name: "index_board_group_recent_visits_on_board_id", using: :btree
+ add_index "board_group_recent_visits", ["group_id"], name: "index_board_group_recent_visits_on_group_id", using: :btree
+ add_index "board_group_recent_visits", ["user_id", "group_id", "board_id"], name: "index_board_group_recent_visits_on_user_group_and_board", unique: true, using: :btree
+ add_index "board_group_recent_visits", ["user_id"], name: "index_board_group_recent_visits_on_user_id", using: :btree
+
+ create_table "board_project_recent_visits", id: :bigserial, force: :cascade do |t|
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "user_id"
+ t.integer "project_id"
+ t.integer "board_id"
+ end
+
+ add_index "board_project_recent_visits", ["board_id"], name: "index_board_project_recent_visits_on_board_id", using: :btree
+ add_index "board_project_recent_visits", ["project_id"], name: "index_board_project_recent_visits_on_project_id", using: :btree
+ add_index "board_project_recent_visits", ["user_id", "project_id", "board_id"], name: "index_board_project_recent_visits_on_user_project_and_board", unique: true, using: :btree
+ add_index "board_project_recent_visits", ["user_id"], name: "index_board_project_recent_visits_on_user_id", using: :btree
+
create_table "boards", force: :cascade do |t|
t.integer "project_id"
t.datetime "created_at", null: false
@@ -2306,6 +2332,12 @@ ActiveRecord::Schema.define(version: 20181013005024) do
add_foreign_key "application_settings", "users", column: "usage_stats_set_by_user_id", name: "fk_964370041d", on_delete: :nullify
add_foreign_key "badges", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "badges", "projects", on_delete: :cascade
+ add_foreign_key "board_group_recent_visits", "boards", on_delete: :cascade
+ add_foreign_key "board_group_recent_visits", "namespaces", column: "group_id", on_delete: :cascade
+ add_foreign_key "board_group_recent_visits", "users", on_delete: :cascade
+ add_foreign_key "board_project_recent_visits", "boards", on_delete: :cascade
+ add_foreign_key "board_project_recent_visits", "projects", on_delete: :cascade
+ add_foreign_key "board_project_recent_visits", "users", on_delete: :cascade
add_foreign_key "boards", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "boards", "projects", name: "fk_f15266b5f9", on_delete: :cascade
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index 03371226041..20fcd2e1724 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -165,6 +165,7 @@ configuration. Then customize everything from buildpacks to CI/CD.
- [Deployment of Helm, Ingress, and Prometheus on Kubernetes](user/project/clusters/index.md#installing-applications)
- [Protected variables](ci/variables/README.md#protected-variables)
- [Easy creation of Kubernetes clusters on GKE](user/project/clusters/index.md#adding-and-creating-a-new-gke-cluster-via-gitlab)
+- [Executable Runbooks](user/project/clusters/runbooks/index.md)
### Monitor
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index b5d1ff698c6..dcee57def74 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -15,7 +15,7 @@ Omnibus GitLab packages.
> **Notes:**
> - Redis requires authentication for High Availability. See
-> [Redis Security](http://redis.io/topics/security) documentation for more
+> [Redis Security](https://redis.io/topics/security) documentation for more
> information. We recommend using a combination of a Redis password and tight
> firewall rules to secure your Redis service.
> - You are highly encouraged to read the [Redis Sentinel][sentinel] documentation
@@ -82,7 +82,7 @@ When a **Master** fails to respond, it's the application's responsibility
for a new **Master**).
To get a better understanding on how to correctly set up Sentinel, please read
-the [Redis Sentinel documentation](http://redis.io/topics/sentinel) first, as
+the [Redis Sentinel documentation](https://redis.io/topics/sentinel) first, as
failing to configure it correctly can lead to data loss or can bring your
whole cluster down, invalidating the failover effort.
@@ -885,8 +885,8 @@ Read more on High Availability:
[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[gh-531]: https://github.com/redis/redis-rb/issues/531
[gh-534]: https://github.com/redis/redis-rb/issues/534
-[redis]: http://redis.io/
-[sentinel]: http://redis.io/topics/sentinel
+[redis]: https://redis.io/
+[sentinel]: https://redis.io/topics/sentinel
[omnifile]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/libraries/gitlab_rails.rb
[source]: ../../install/installation.md
[ce]: https://about.gitlab.com/downloads
diff --git a/doc/administration/high_availability/redis_source.md b/doc/administration/high_availability/redis_source.md
index 5823c575251..2101d36d2b6 100644
--- a/doc/administration/high_availability/redis_source.md
+++ b/doc/administration/high_availability/redis_source.md
@@ -107,7 +107,7 @@ starting with `sentinel` prefix.
Assuming that the Redis Sentinel is installed on the same instance as Redis
master with IP `10.0.0.1` (some settings might overlap with the master):
-1. [Install Redis Sentinel](http://redis.io/topics/sentinel)
+1. [Install Redis Sentinel](https://redis.io/topics/sentinel)
1. Edit `/etc/redis/sentinel.conf`:
```conf
@@ -363,7 +363,7 @@ production:
port: 26379 # point to sentinel, not to redis port
```
-When in doubt, please read [Redis Sentinel documentation](http://redis.io/topics/sentinel).
+When in doubt, please read [Redis Sentinel documentation](https://redis.io/topics/sentinel).
[gh-531]: https://github.com/redis/redis-rb/issues/531
[downloads]: https://about.gitlab.com/downloads
diff --git a/doc/api/issues.md b/doc/api/issues.md
index cc1d6834a20..57e861bc62e 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -40,7 +40,7 @@ GET /issues?my_reaction_emoji=star
| `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no milestone. `Any+Milestone` 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`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
| `iids[]` | Array[integer] | 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` |
@@ -154,7 +154,7 @@ GET /groups/:id/issues?my_reaction_emoji=star
| `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no 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` _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
| `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` |
@@ -268,7 +268,7 @@ GET /projects/:id/issues?my_reaction_emoji=star
| `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no 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` _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
| `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` |
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 862ee398a84..0291b7e00c2 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -42,7 +42,7 @@ Parameters:
| `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` |
+| `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` _([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 |
@@ -166,7 +166,7 @@ Parameters:
| `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`.<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` _([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)_ |
| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([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 |
@@ -279,7 +279,7 @@ Parameters:
| `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`.<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` _([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)_ |
| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([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 |
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 947e7db9c52..961241f31e1 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -451,6 +451,7 @@ GET /projects/:id
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `statistics` | boolean | no | Include project statistics |
+| `license` | boolean | no | Include project license data |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
```json
@@ -508,6 +509,14 @@ GET /projects/:id
},
"archived": false,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
+ "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE",
+ "license": {
+ "key": "lgpl-3.0",
+ "name": "GNU Lesser General Public License v3.0",
+ "nickname": "GNU LGPLv3",
+ "html_url": "http://choosealicense.com/licenses/lgpl-3.0/",
+ "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt"
+ },
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 0,
@@ -572,6 +581,14 @@ If the project is a fork, and you provide a valid token to authenticate, the
"http_url_to_repo":"https://gitlab.com/gitlab-org/gitlab-ce.git",
"web_url":"https://gitlab.com/gitlab-org/gitlab-ce",
"avatar_url":"https://assets.gitlab-static.net/uploads/-/system/project/avatar/13083/logo-extra-whitespace.png",
+ "license_url": "https://gitlab.com/gitlab-org/gitlab-ce/blob/master/LICENSE",
+ "license": {
+ "key": "mit",
+ "name": "MIT License",
+ "nickname": null,
+ "html_url": "http://choosealicense.com/licenses/mit/",
+ "source_url": "https://opensource.org/licenses/MIT",
+ },
"star_count":3812,
"forks_count":3561,
"last_activity_at":"2018-01-02T11:40:26.570Z",
@@ -905,6 +922,14 @@ Example response:
"import_status": "none",
"archived": true,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
+ "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE",
+ "license": {
+ "key": "lgpl-3.0",
+ "name": "GNU Lesser General Public License v3.0",
+ "nickname": "GNU LGPLv3",
+ "html_url": "http://choosealicense.com/licenses/lgpl-3.0/",
+ "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt"
+ },
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 1,
@@ -983,6 +1008,14 @@ Example response:
"import_status": "none",
"archived": true,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
+ "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE",
+ "license": {
+ "key": "lgpl-3.0",
+ "name": "GNU Lesser General Public License v3.0",
+ "nickname": "GNU LGPLv3",
+ "html_url": "http://choosealicense.com/licenses/lgpl-3.0/",
+ "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt"
+ },
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 0,
@@ -1101,6 +1134,14 @@ Example response:
},
"archived": true,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
+ "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE",
+ "license": {
+ "key": "lgpl-3.0",
+ "name": "GNU Lesser General Public License v3.0",
+ "nickname": "GNU LGPLv3",
+ "html_url": "http://choosealicense.com/licenses/lgpl-3.0/",
+ "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt"
+ },
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 0,
@@ -1197,6 +1238,14 @@ Example response:
},
"archived": false,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
+ "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE",
+ "license": {
+ "key": "lgpl-3.0",
+ "name": "GNU Lesser General Public License v3.0",
+ "nickname": "GNU LGPLv3",
+ "html_url": "http://choosealicense.com/licenses/lgpl-3.0/",
+ "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt"
+ },
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 0,
diff --git a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
index b2c73caae2e..c0346d78141 100644
--- a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
+++ b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
@@ -398,10 +398,10 @@ other reasons][ci-reasons] to keep using GitLab CI/CD. The benefits to our teams
- [Using Docker images documentation][using-docker]
- [Example project: Hello GitLab CI/CD on GitLab][hello-gitlab]
-[phoenix-site]: http://phoenixframework.org/ "Phoenix Framework"
+[phoenix-site]: https://phoenixframework.org/ "Phoenix Framework"
[phoenix-learning-guide]: https://hexdocs.pm/phoenix/learning.html "Phoenix Learning Guide"
-[phoenix-install]: http://www.phoenixframework.org/docs/installation "Phoenix Installation"
-[phoenix-mysql]: http://www.phoenixframework.org/docs/using-mysql "Phoenix with MySQL"
+[phoenix-install]: https://hexdocs.pm/phoenix/installation.html "Phoenix Installation"
+[phoenix-mysql]: https://hexdocs.pm/phoenix/ecto.html#using-mysql "Phoenix with MySQL"
[elixir-site]: http://elixir-lang.org/ "Elixir"
[elixir-mix]: http://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html "Introduction to mix"
[elixir-docs]: http://elixir-lang.org/getting-started/introduction.html "Elixir Documentation"
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 4b2a6ccc7e4..981aa101dd3 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -474,6 +474,7 @@ docker build:
changes:
- Dockerfile
- docker/scripts/*
+ - dockerfiles/**/*
```
In the scenario above, if you are pushing multiple commits to GitLab to an
@@ -482,6 +483,7 @@ one of the commits contains changes to either:
- The `Dockerfile` file.
- Any of the files inside `docker/scripts/` directory.
+- Any of the files and subfolders inside `dockerfiles` directory.
CAUTION: **Warning:**
There are some caveats when using this feature with new branches and tags. See
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index f9e6efa2c30..b6f053ff0e9 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -171,7 +171,7 @@ There are a few gotchas with it:
class Base
def execute
return unless enabled?
-
+
# ...
# ...
end
@@ -185,12 +185,12 @@ There are a few gotchas with it:
class Base
def execute
return unless enabled?
-
+
do_something
end
-
+
private
-
+
def do_something
# ...
# ...
@@ -204,14 +204,14 @@ There are a few gotchas with it:
```ruby
module EE::Base
extend ::Gitlab::Utils::Override
-
+
override :do_something
def do_something
# Follow the above pattern to call super and extend it
end
end
```
-
+
This would require updating CE first, or make sure this is back ported to CE.
When prepending, place them in the `ee/` specific sub-directory, and
@@ -332,6 +332,21 @@ full implementation details.
[ce-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12373
[ee-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2199
+### Code in `config/routes`
+
+When we add `draw :admin` in `config/routes.rb`, the application will try to
+load the file located in `config/routes/admin.rb`, and also try to load the
+file located in `ee/config/routes/admin.rb`.
+
+In EE, it should at least load one file, at most two files. If it cannot find
+any files, an error will be raised. In CE, since we don't know if there will
+be an EE route, it will not raise any errors even if it cannot find anything.
+
+This means if we want to extend a particular CE route file, just add the same
+file located in `ee/config/routes`. If we want to add an EE only route, we
+could still put `draw :ee_only` in both CE and EE, and add
+`ee/config/routes/ee_only.rb` in EE, similar to `render_if_exists`.
+
### Code in `app/controllers/`
In controllers, the most common type of conflict is with `before_action` that
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index 25c6371f3d7..4292a17bfa5 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -1,15 +1,13 @@
# Review apps
-We currently have review apps available as a manual job in EE pipelines. Here is
-[the first implementation](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6259).
-
-That said, [the Quality team is working](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6665)
-on making Review Apps automatically deployed by each pipeline, both in CE and EE.
+Review Apps are automatically deployed by each pipeline, both in
+[CE](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22010) and
+[EE](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6665).
## How does it work?
-1. On every EE [pipeline][gitlab-pipeline] during the `test` stage, you can
- start the [`review` job][review-job]
+1. On every [pipeline][gitlab-pipeline] during the `test` stage, the
+ [`review` job][review-job] is automatically started.
1. The `review` job [triggers a pipeline][cng-pipeline] in the
[`CNG-mirror`][cng-mirror] [^1] project
1. The `CNG-mirror` pipeline creates the Docker images of each component (e.g. `gitlab-rails-ee`,
@@ -39,6 +37,9 @@ on making Review Apps automatically deployed by each pipeline, both in CE and EE
review app manually, and is also started by GitLab once a branch is deleted
- [TBD] Review apps are cleaned up regularly using a pipeline schedule that runs
the [`scripts/review_apps/automated_cleanup.rb`][automated_cleanup.rb] script
+- If you're unable to log in using the `root` username and password the
+ deployment may have failed. Stop the review app via the `stop_review`
+ manual job and then retry the `review` job to redeploy the review app.
[^1]: We use the `CNG-mirror` project so that the `CNG`, (**C**loud **N**ative **G**itLab), project's registry is
not overloaded with a lot of transient Docker images.
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index 9546f43eea8..73301394e9f 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -43,7 +43,7 @@ This page gathers all the resources for the topic **Authentication** within GitL
## Third-party resources
- [Kanboard Plugin GitLab Authentication](https://github.com/kanboard/plugin-gitlab-auth)
-- [Jenkins GitLab OAuth Plugin](https://wiki.jenkins-ci.org/display/JENKINS/GitLab+OAuth+Plugin)
+- [Jenkins GitLab OAuth Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+OAuth+Plugin)
- [Set up Gitlab CE with Active Directory authentication](https://www.caseylabs.com/setup-gitlab-ce-with-active-directory-authentication/)
- [How to customize GitLab to support OpenID authentication](http://eric.van-der-vlist.com/blog/2013/11/23/how-to-customize-gitlab-to-support-openid-authentication/)
- [Openshift - Configuring Authentication and User Agent](https://docs.openshift.org/latest/install_config/configuring_authentication.html#GitLab)
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 4d4832184e2..96e788666a1 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -225,10 +225,11 @@ Auto DevOps at the project level.
### Enabling/disabling Auto DevOps at the project-level
-1. Check that your project doesn't have a `.gitlab-ci.yml`, or if one exists, remove it.
+If enabling, check that your project doesn't have a `.gitlab-ci.yml`, or if one exists, remove it.
+
1. Go to your project's **Settings > CI/CD > Auto DevOps**.
-1. Check the **Default to Auto DevOps pipeline** checkbox.
-1. Optionally, but recommended, add in the [base domain](#auto-devops-base-domain)
+1. Toggle the **Default to Auto DevOps pipeline** checkbox (checked to enable, unchecked to disable)
+1. When enabling, it's optional but recommended to add in the [base domain](#auto-devops-base-domain)
that will be used by Auto DevOps to [deploy your application](#auto-deploy)
and choose the [deployment strategy](#deployment-strategy).
1. Click **Save changes** for the changes to take effect.
@@ -246,12 +247,6 @@ There is also a feature flag to enable Auto DevOps to a percentage of projects
which can be enabled from the console with
`Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(10)`.
-### Disable Auto DevOps at the project level
-
-1. Go to your project's **Settings > CI/CD > Auto DevOps**.
-1. Uncheck the **Default to Auto DevOps pipeline** checkbox.
-1. Click **Save changes** for the changes to take effect.
-
### Deployment strategy
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0.
diff --git a/doc/university/README.md b/doc/university/README.md
index 5edf92b3b09..f19b1ffd3d9 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -205,7 +205,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
### 4. External Articles
-1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](http://www.wsj.com/articles/SB10001424053111903480904576512250915629460)
+1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](https://www.wsj.com/articles/SB10001424053111903480904576512250915629460)
1. [2014 Blog post by Chris Dixon - Software eats software development](http://cdixon.org/2014/04/13/software-eats-software-development/)
1. [2015 Venture Beat article - Actually, Open Source is Eating the World](http://venturebeat.com/2015/12/06/its-actually-open-source-software-thats-eating-the-world/)
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index 6ff27e495fb..6e0f71017c6 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -303,7 +303,7 @@ A [tool](https://docs.gitlab.com/ee/integration/external-issue-tracker.html) use
### Jenkins
-An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins-ci.org/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular. Related [documentation](https://docs.gitlab.com/ee/integration/jenkins.html).
+An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins.io/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular. Related [documentation](https://docs.gitlab.com/ee/integration/jenkins.html).
### Jira
diff --git a/doc/update/11.4-to-11.5.md b/doc/update/11.4-to-11.5.md
new file mode 100644
index 00000000000..e64ab2acae2
--- /dev/null
+++ b/doc/update/11.4-to-11.5.md
@@ -0,0 +1,404 @@
+---
+comments: false
+---
+
+# From 11.4 to 11.5
+
+Make sure you view this update guide from the branch (version) of GitLab you would
+like to install (e.g., `11-5-stable`. You can select the branch in the version
+dropdown at the top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+NOTE: If you installed GitLab from source, make sure `rsync` is installed.
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 11.0 and higher only support Ruby 2.4.x and dropped support for Ruby 2.3.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download Ruby and compile it:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.5.tar.gz
+echo '4d650f302f1ec00256450b112bb023644b6ab6dd ruby-2.4.5.tar.gz' | shasum -c - && tar xzf ruby-2.4.5.tar.gz
+cd ruby-2.4.5
+
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab utilizes [webpack](http://webpack.js.org) to compile frontend assets.
+This requires a minimum version of node v6.0.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v6.0.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+GitLab also requires the use of yarn `>= v1.2.0` to manage JavaScript
+dependencies.
+
+```bash
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Update Go
+
+NOTE: GitLab 11.4 and higher only supports Go 1.10.x and newer, and dropped support for Go
+1.9.x. Be sure to upgrade your installation if necessary.
+
+You can check which version you are running with `go version`.
+
+Download and install Go:
+
+```bash
+# Remove former Go installation folder
+sudo rm -rf /usr/local/go
+
+curl --remote-name --progress https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz
+echo 'fa1b0e45d3b647c252f51f5e1204aba049cde4af177ef9f2181f43004f901035 go1.10.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.10.3.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.10.3.linux-amd64.tar.gz
+```
+
+### 6. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all --prune
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+sudo -u git -H git checkout -- locale
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 11-5-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 11-5-stable-ee
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
+sudo -u git -H bin/compile
+```
+
+### 8. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. GitLab-Workhorse uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-workhorse
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
+sudo -u git -H make
+```
+
+### 9. Update Gitaly
+
+#### New Gitaly configuration options required
+
+In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`.
+
+```shell
+echo '
+[gitaly-ruby]
+dir = "/home/git/gitaly/ruby"
+
+[gitlab-shell]
+dir = "/home/git/gitlab-shell"
+' | sudo -u git tee -a /home/git/gitaly/config.toml
+```
+
+#### Check Gitaly configuration
+
+Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
+configuration file may contain syntax errors. The block name
+`[[storages]]`, which may occur more than once in your `config.toml`
+file, should be `[[storage]]` instead.
+
+```shell
+sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml
+```
+
+#### Compile Gitaly
+
+```shell
+cd /home/git/gitaly
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
+sudo -u git -H make
+```
+
+### 10. Update gitlab-pages
+
+#### Only needed if you use GitLab Pages.
+
+Install and compile gitlab-pages. GitLab-Pages uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-pages
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
+sudo -u git -H make
+```
+
+### 11. Update MySQL permissions
+
+If you are using MySQL you need to grant the GitLab user the necessary
+permissions on the database:
+
+```bash
+mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';"
+```
+
+If you use MySQL with replication, or just have MySQL configured with binary logging,
+you will need to also run the following on all of your MySQL servers:
+
+```bash
+mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;"
+```
+
+You can make this setting permanent by adding it to your `my.cnf`:
+
+```
+log_bin_trust_function_creators=1
+```
+
+### 12. Update configuration files
+
+#### New `unicorn.rb` configuration
+
+Note: we have made [changes](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22372) to `unicorn.rb` to allow GitLab run with both Unicorn and Puma in future.
+
+- Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/config/unicorn.rb.example but with your settings.
+ - In particular, make sure that `require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"` line exists and the `before_exec`, `before_fork`, and `after_fork` handlers are configured as shown below:
+
+```ruby
+require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"
+
+before_exec do |server|
+ # Signal application hooks that we're about to restart
+ Gitlab::Cluster::LifecycleEvents.do_master_restart
+end
+
+before_fork do |server, worker|
+ # Signal application hooks that we're about to fork
+ Gitlab::Cluster::LifecycleEvents.do_before_fork
+end
+
+after_fork do |server, worker|
+ # Signal application hooks of worker start
+ Gitlab::Cluster::LifecycleEvents.do_worker_start
+end
+```
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/11-4-stable:config/gitlab.yml.example origin/11-5-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/11-1-stable:lib/support/nginx/gitlab-ssl origin/11-5-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/11-1-stable:lib/support/nginx/gitlab origin/11-5-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/11-1-stable:lib/support/init.d/gitlab.default.example origin/11-5-stable:lib/support/init.d/gitlab.default.example
+```
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 13. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Compile GetText PO files
+
+sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
+
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 14. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 15. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (11.4)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 11.3 to 11.4](11.3-to-11.4.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/user/project/clusters/runbooks/index.md b/doc/user/project/clusters/runbooks/index.md
new file mode 100644
index 00000000000..3b81e439119
--- /dev/null
+++ b/doc/user/project/clusters/runbooks/index.md
@@ -0,0 +1,49 @@
+# Runbooks
+
+Runbooks are a collection of documented procedures that explain how to
+carry out a particular process, be it starting, stopping, debugging,
+or troubleshooting a particular system.
+
+## Overview
+
+Historically, runbooks took the form of a decision tree or a detailed
+step-by-step guide depending on the condition or system.
+
+Modern implementations have introduced the concept of an "executable
+runbooks", where along with a well define process, operators can execute
+code blocks or database queries against a given environment.
+
+## Nurtch Executable Runbooks
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/45912) in GitLab 11.4.
+
+The JupyterHub app offered via GitLab’s Kubernetes integration now ships
+with Nurtch’s Rubix library, providing a simple way to create DevOps
+runbooks. A sample runbook is provided, showcasing common operations.
+
+**<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+Watch this [video](https://www.youtube.com/watch?v=Q_OqHIIUPjE)
+for an overview of how this is acomplished in GitLab!**
+
+## Requirements
+
+To create an executable runbook, you will need:
+
+1. **Kubernetes** - A Kubernetes cluster is required to deploy the rest of the applications.
+ The simplest way to get started is to add a cluster using [GitLab's GKE integration](https://docs.gitlab.com/ee/user/project/clusters/#adding-and-creating-a-new-gke-cluster-via-gitlab).
+1. **Helm Tiller** - Helm is a package manager for Kubernetes and is required to install
+ all the other applications. It is installed in its own pod inside the cluster which
+ can run the helm CLI in a safe environment.
+1. **Ingress** - Ingress can provide load balancing, SSL termination, and name-based
+ virtual hosting. It acts as a web proxy for your applications.
+1. **JupyterHub** - JupyterHub is a multi-user service for managing notebooks across
+ a team. Jupyter Notebooks provide a web-based interactive programming environment
+ used for data analysis, visualization, and machine learning.
+
+## Nurtch
+
+Nurtch is the company behind the [Rubix library](https://github.com/Nurtch/rubix). Rubix is
+an open-source python library that makes it easy to perform common DevOps tasks inside Jupyter Notebooks.
+Tasks such as plotting Cloudwatch metrics and rolling your ECS/Kubernetes app are simplified
+down to a couple of lines of code. Check the [Nurtch Documentation](http://docs.nurtch.com/en/latest)
+for more information.
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index 464fa5987c1..f5ea350a58f 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -176,6 +176,9 @@ Clicking on the current board name in the upper left corner will reveal a
menu from where you can create another Issue Board and rename or delete the
existing one.
+Clicking on the main issue board link will take you to the last board
+you visited.
+
NOTE: **Note:**
The Multiple Issue Boards feature is available for
**projects in GitLab Starter Edition** and for **groups in GitLab Premium Edition**.
diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md
index e1eede8bbed..89b9621b8b9 100644
--- a/doc/user/project/pages/getting_started_part_three.md
+++ b/doc/user/project/pages/getting_started_part_three.md
@@ -212,7 +212,7 @@ security measure, necessary just for big companies, like banks and shoppings sit
with financial transactions.
Now we have a different picture. [According to Josh Aas](https://letsencrypt.org/2015/10/29/phishing-and-malware.html), Executive Director at [ISRG](https://en.wikipedia.org/wiki/Internet_Security_Research_Group):
-> _We’ve since come to realize that HTTPS is important for almost all websites. It’s important for any website that allows people to log in with a password, any website that [tracks its users](https://www.washingtonpost.com/news/the-switch/wp/2013/12/10/nsa-uses-google-cookies-to-pinpoint-targets-for-hacking/) in any way, any website that [doesn’t want its content altered](http://arstechnica.com/tech-policy/2014/09/why-comcasts-javascript-ad-injections-threaten-security-net-neutrality/), and for any site that offers content people might not want others to know they are consuming. We’ve also learned that any site not secured by HTTPS [can be used to attack other sites](http://krebsonsecurity.com/2015/04/dont-be-fodder-for-chinas-great-cannon/)._
+> _We’ve since come to realize that HTTPS is important for almost all websites. It’s important for any website that allows people to log in with a password, any website that [tracks its users](https://www.washingtonpost.com/news/the-switch/wp/2013/12/10/nsa-uses-google-cookies-to-pinpoint-targets-for-hacking/) in any way, any website that [doesn’t want its content altered](http://arstechnica.com/tech-policy/2014/09/why-comcasts-javascript-ad-injections-threaten-security-net-neutrality/), and for any site that offers content people might not want others to know they are consuming. We’ve also learned that any site not secured by HTTPS [can be used to attack other sites](https://krebsonsecurity.com/2015/04/dont-be-fodder-for-chinas-great-cannon/)._
Therefore, the reason why certificates are so important is that they encrypt
the connection between the **client** (you, me, your visitors)
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 18c30723d73..9f7be27b047 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -160,13 +160,27 @@ module API
# (fixed in https://github.com/rails/rails/pull/25976).
project.tags.map(&:name).sort
end
+
expose :ssh_url_to_repo, :http_url_to_repo, :web_url, :readme_url
+
+ expose :license_url, if: :license do |project|
+ license = project.repository.license_blob
+
+ if license
+ Gitlab::Routing.url_helpers.project_blob_url(project, File.join(project.default_branch, license.path))
+ end
+ end
+
+ expose :license, with: 'API::Entities::LicenseBasic', if: :license do |project|
+ project.repository.license
+ end
+
expose :avatar_url do |project, options|
project.avatar_url(only_path: false)
end
+
expose :star_count, :forks_count
expose :last_activity_at
-
expose :namespace, using: 'API::Entities::NamespaceBasic'
expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes
@@ -1208,11 +1222,14 @@ module API
expose :deployable, using: Entities::Job
end
- class License < Grape::Entity
+ class LicenseBasic < Grape::Entity
expose :key, :name, :nickname
- expose :popular?, as: :popular
expose :url, as: :html_url
expose(:source_url) { |license| license.meta['source'] }
+ end
+
+ class License < LicenseBasic
+ expose :popular?, as: :popular
expose(:description) { |license| license.meta['description'] }
expose(:conditions) { |license| license.meta['conditions'] }
expose(:permissions) { |license| license.meta['permissions'] }
diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb
index 23b1cd1ad45..1058f4e8a5e 100644
--- a/lib/api/helpers/custom_validators.rb
+++ b/lib/api/helpers/custom_validators.rb
@@ -10,8 +10,21 @@ module API
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:absence)
end
end
+
+ class IntegerNoneAny < Grape::Validations::Base
+ def validate_param!(attr_name, params)
+ value = params[attr_name]
+
+ return if value.is_a?(Integer) ||
+ [IssuableFinder::FILTER_NONE, IssuableFinder::FILTER_ANY].include?(value.to_s.downcase)
+
+ raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)],
+ message: "should be an integer, 'None' or 'Any'"
+ end
+ end
end
end
end
Grape::Validations.register_validator(:absence, ::API::Helpers::CustomValidators::Absence)
+Grape::Validations.register_validator(:integer_none_any, ::API::Helpers::CustomValidators::IntegerNoneAny)
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 4dd6b19e353..ae40b5f7557 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -65,6 +65,8 @@ module API
result
rescue Gitlab::GitAccess::UnauthorizedError => e
break response_with_status(code: 401, success: false, message: e.message)
+ rescue Gitlab::GitAccess::TimeoutError => e
+ break response_with_status(code: 503, success: false, message: e.message)
rescue Gitlab::GitAccess::NotFoundError => e
break response_with_status(code: 404, success: false, message: e.message)
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 25d78053c88..405fc30a2ed 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -40,7 +40,8 @@ module API
optional :updated_after, type: DateTime, desc: 'Return issues updated after the specified time'
optional :updated_before, type: DateTime, desc: 'Return issues updated before the specified time'
optional :author_id, type: Integer, desc: 'Return issues which are authored by the user with the given ID'
- optional :assignee_id, type: Integer, desc: 'Return issues which are assigned to the user with the given ID'
+ optional :assignee_id, types: [Integer, String], integer_none_any: true,
+ desc: 'Return issues which are assigned to the user with the given ID'
optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all],
desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`'
optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 440d94ae186..a617efaaa4c 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -89,7 +89,8 @@ module API
optional :updated_before, type: DateTime, desc: 'Return merge requests updated before the specified time'
optional :view, type: String, values: %w[simple], desc: 'If simple, returns the `iid`, URL, title, description, and basic state of merge request'
optional :author_id, type: Integer, desc: 'Return merge requests which are authored by the user with the given ID'
- optional :assignee_id, type: Integer, desc: 'Return merge requests which are assigned to the user with the given ID'
+ optional :assignee_id, types: [Integer, String], integer_none_any: true,
+ desc: 'Return merge requests which are assigned to the user with the given ID'
optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all],
desc: 'Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`'
optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index ae2d327e45b..0a914f9012e 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -114,7 +114,8 @@ module API
options = options.reverse_merge(
with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails,
statistics: params[:statistics],
- current_user: current_user
+ current_user: current_user,
+ license: false
)
options[:with] = Entities::BasicProjectDetails if params[:simple]
@@ -230,13 +231,17 @@ module API
params do
use :statistics_params
use :with_custom_attributes
+
+ optional :license, type: Boolean, default: false,
+ desc: 'Include project license data'
end
get ":id" do
options = {
with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails,
current_user: current_user,
user_can_admin_project: can?(current_user, :admin_project, user_project),
- statistics: params[:statistics]
+ statistics: params[:statistics],
+ license: params[:license]
}
project, options = with_custom_attributes(user_project, options)
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index d8768a54986..2f15f3a7d76 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -142,8 +142,7 @@ module API
requires :id, type: Integer, desc: %q(Job's ID)
optional :trace, type: String, desc: %q(Job's full trace)
optional :state, type: String, desc: %q(Job's status: success, failed)
- optional :failure_reason, type: String, values: CommitStatus.failure_reasons.keys,
- desc: %q(Job's failure_reason)
+ optional :failure_reason, type: String, desc: %q(Job's failure_reason)
end
put '/:id' do
job = authenticate_job!
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index 49e7f7e1fd7..074afe9c412 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -18,11 +18,24 @@ module Gitlab
lfs_objects_missing: 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'
}.freeze
- attr_reader :user_access, :project, :skip_authorization, :skip_lfs_integrity_check, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name
+ LOG_MESSAGES = {
+ push_checks: "Checking if you are allowed to push...",
+ delete_default_branch_check: "Checking if default branch is being deleted...",
+ protected_branch_checks: "Checking if you are force pushing to a protected branch...",
+ protected_branch_push_checks: "Checking if you are allowed to push to the protected branch...",
+ protected_branch_deletion_checks: "Checking if you are allowed to delete the protected branch...",
+ tag_checks: "Checking if you are allowed to change existing tags...",
+ protected_tag_checks: "Checking if you are creating, updating or deleting a protected tag...",
+ lfs_objects_exist_check: "Scanning repository for blobs stored in LFS and verifying their files have been uploaded to GitLab...",
+ commits_check_file_paths_validation: "Validating commits' file paths...",
+ commits_check: "Validating commit contents..."
+ }.freeze
+
+ attr_reader :user_access, :project, :skip_authorization, :skip_lfs_integrity_check, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name, :logger
def initialize(
change, user_access:, project:, skip_authorization: false,
- skip_lfs_integrity_check: false, protocol:
+ skip_lfs_integrity_check: false, protocol:, logger:
)
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref)
@@ -32,6 +45,9 @@ module Gitlab
@skip_authorization = skip_authorization
@skip_lfs_integrity_check = skip_lfs_integrity_check
@protocol = protocol
+
+ @logger = logger
+ @logger.append_message("Running checks for ref: #{@branch_name || @tag_name}")
end
def exec(skip_commits_check: false)
@@ -49,26 +65,32 @@ module Gitlab
protected
def push_checks
- unless can_push?
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code]
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ unless can_push?
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code]
+ end
end
end
def branch_checks
return unless branch_name
- if deletion? && branch_name == project.default_branch
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch]
+ logger.log_timed(LOG_MESSAGES[:delete_default_branch_check]) do
+ if deletion? && branch_name == project.default_branch
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch]
+ end
end
protected_branch_checks
end
def protected_branch_checks
- return unless ProtectedBranch.protected?(project, branch_name)
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ return unless ProtectedBranch.protected?(project, branch_name) # rubocop:disable Cop/AvoidReturnFromBlocks
- if forced_push?
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch]
+ if forced_push?
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch]
+ end
end
if deletion?
@@ -79,23 +101,27 @@ module Gitlab
end
def protected_branch_deletion_checks
- unless user_access.can_delete_branch?(branch_name)
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch]
- end
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ unless user_access.can_delete_branch?(branch_name)
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch]
+ end
- unless updated_from_web?
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch]
+ unless updated_from_web?
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch]
+ end
end
end
def protected_branch_push_checks
- if matching_merge_request?
- unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name)
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch]
- end
- else
- unless user_access.can_push_to_branch?(branch_name)
- raise GitAccess::UnauthorizedError, push_to_protected_branch_rejected_message
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ if matching_merge_request?
+ unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name)
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch]
+ end
+ else
+ unless user_access.can_push_to_branch?(branch_name)
+ raise GitAccess::UnauthorizedError, push_to_protected_branch_rejected_message
+ end
end
end
end
@@ -103,21 +129,25 @@ module Gitlab
def tag_checks
return unless tag_name
- if tag_exists? && user_access.cannot_do_action?(:admin_project)
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags]
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ if tag_exists? && user_access.cannot_do_action?(:admin_project)
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags]
+ end
end
protected_tag_checks
end
def protected_tag_checks
- return unless ProtectedTag.protected?(project, tag_name)
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks
- raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update?
- raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion?
+ raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update?
+ raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion?
- unless user_access.can_create_tag?(tag_name)
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag]
+ unless user_access.can_create_tag?(tag_name)
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag]
+ end
end
end
@@ -125,14 +155,20 @@ module Gitlab
return if deletion? || newrev.nil?
return unless should_run_commit_validations?
- # n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593
- ::Gitlab::GitalyClient.allow_n_plus_1_calls do
- commits.each do |commit|
- commit_check.validate(commit, validations_for_commit(commit))
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593
+ ::Gitlab::GitalyClient.allow_n_plus_1_calls do
+ commits.each do |commit|
+ logger.check_timeout_reached
+
+ commit_check.validate(commit, validations_for_commit(commit))
+ end
end
end
- commit_check.validate_file_paths
+ logger.log_timed(LOG_MESSAGES[:commits_check_file_paths_validation]) do
+ commit_check.validate_file_paths
+ end
end
# Method overwritten in EE to inject custom validations
@@ -194,10 +230,12 @@ module Gitlab
end
def lfs_objects_exist_check
- lfs_check = Checks::LfsIntegrity.new(project, newrev)
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ lfs_check = Checks::LfsIntegrity.new(project, newrev, logger.time_left)
- if lfs_check.objects_missing?
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:lfs_objects_missing]
+ if lfs_check.objects_missing?
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:lfs_objects_missing]
+ end
end
end
diff --git a/lib/gitlab/checks/lfs_integrity.rb b/lib/gitlab/checks/lfs_integrity.rb
index fa3dc1808df..1652d5a30a4 100644
--- a/lib/gitlab/checks/lfs_integrity.rb
+++ b/lib/gitlab/checks/lfs_integrity.rb
@@ -3,9 +3,10 @@
module Gitlab
module Checks
class LfsIntegrity
- def initialize(project, newrev)
+ def initialize(project, newrev, time_left)
@project = project
@newrev = newrev
+ @time_left = time_left
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -13,7 +14,7 @@ module Gitlab
return false unless @newrev && @project.lfs_enabled?
new_lfs_pointers = Gitlab::Git::LfsChanges.new(@project.repository, @newrev)
- .new_pointers(object_limit: ::Gitlab::Git::Repository::REV_LIST_COMMIT_LIMIT)
+ .new_pointers(object_limit: ::Gitlab::Git::Repository::REV_LIST_COMMIT_LIMIT, dynamic_timeout: @time_left)
return false unless new_lfs_pointers.present?
diff --git a/lib/gitlab/checks/timed_logger.rb b/lib/gitlab/checks/timed_logger.rb
new file mode 100644
index 00000000000..f365e0a43f6
--- /dev/null
+++ b/lib/gitlab/checks/timed_logger.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Checks
+ class TimedLogger
+ TimeoutError = Class.new(StandardError)
+
+ attr_reader :start_time, :header, :log, :timeout
+
+ def initialize(start_time: Time.now, log: [], header: "", timeout:)
+ @start_time = start_time
+ @timeout = timeout
+ @header = header
+ @log = log
+ end
+
+ # Adds trace of method being tracked with
+ # the correspondent time it took to run it.
+ # We make use of the start default argument
+ # on unit tests related to this method
+ #
+ def log_timed(log_message, start = Time.now)
+ check_timeout_reached
+
+ timed = true
+
+ yield
+
+ append_message(log_message + time_suffix_message(start: start))
+ rescue GRPC::DeadlineExceeded, TimeoutError
+ args = { cancelled: true }
+ args[:start] = start if timed
+
+ append_message(log_message + time_suffix_message(args))
+
+ raise TimeoutError
+ end
+
+ def check_timeout_reached
+ return unless time_expired?
+
+ raise TimeoutError
+ end
+
+ def time_left
+ (start_time + timeout.seconds) - Time.now
+ end
+
+ def full_message
+ header + log.join("\n")
+ end
+
+ # We always want to append in-place on the log
+ def append_message(message)
+ log << message
+ end
+
+ private
+
+ def time_expired?
+ time_left <= 0
+ end
+
+ def time_suffix_message(cancelled: false, start: nil)
+ return " (#{elapsed_time(start)}ms)" unless cancelled
+
+ if start
+ " (cancelled after #{elapsed_time(start)}ms)"
+ else
+ " (cancelled)"
+ end
+ end
+
+ def elapsed_time(start)
+ to_ms(Time.now - start)
+ end
+
+ def to_ms(elapsed)
+ (elapsed.to_f * 1000).round(2)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb
index 50b0d044265..4babc23a495 100644
--- a/lib/gitlab/ci/status/build/failed.rb
+++ b/lib/gitlab/ci/status/build/failed.rb
@@ -11,7 +11,8 @@ module Gitlab
runner_system_failure: 'runner system failure',
missing_dependency_failure: 'missing dependency failure',
runner_unsupported: 'unsupported runner',
- stale_schedule: 'stale schedule'
+ stale_schedule: 'stale schedule',
+ job_execution_timeout: 'job execution timeout'
}.freeze
private_constant :REASONS
diff --git a/lib/gitlab/cluster/lifecycle_events.rb b/lib/gitlab/cluster/lifecycle_events.rb
new file mode 100644
index 00000000000..b05dca409d1
--- /dev/null
+++ b/lib/gitlab/cluster/lifecycle_events.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Cluster
+ #
+ # LifecycleEvents lets Rails initializers register application startup hooks
+ # that are sensitive to forking. For example, to defer the creation of
+ # watchdog threads. This lets us abstract away the Unix process
+ # lifecycles of Unicorn, Sidekiq, Puma, Puma Cluster, etc.
+ #
+ # We have three lifecycle events.
+ #
+ # - before_fork (only in forking processes)
+ # - worker_start
+ # - before_master_restart (only in forking processes)
+ #
+ # Blocks will be executed in the order in which they are registered.
+ #
+ class LifecycleEvents
+ class << self
+ #
+ # Hook registration methods (called from initializers)
+ #
+ def on_worker_start(&block)
+ if in_clustered_environment?
+ # Defer block execution
+ (@worker_start_hooks ||= []) << block
+ else
+ yield
+ end
+ end
+
+ def on_before_fork(&block)
+ return unless in_clustered_environment?
+
+ # Defer block execution
+ (@before_fork_hooks ||= []) << block
+ end
+
+ def on_master_restart(&block)
+ return unless in_clustered_environment?
+
+ # Defer block execution
+ (@master_restart_hooks ||= []) << block
+ end
+
+ #
+ # Lifecycle integration methods (called from unicorn.rb, puma.rb, etc.)
+ #
+ def do_worker_start
+ @worker_start_hooks&.each do |block|
+ block.call
+ end
+ end
+
+ def do_before_fork
+ @before_fork_hooks&.each do |block|
+ block.call
+ end
+ end
+
+ def do_master_restart
+ @master_restart_hooks && @master_restart_hooks.each do |block|
+ block.call
+ end
+ end
+
+ # Puma doesn't use singletons (which is good) but
+ # this means we need to pass through whether the
+ # puma server is running in single mode or cluster mode
+ def set_puma_options(options)
+ @puma_options = options
+ end
+
+ private
+
+ def in_clustered_environment?
+ # Sidekiq doesn't fork
+ return false if Sidekiq.server?
+
+ # Unicorn always forks
+ return true if defined?(::Unicorn)
+
+ # Puma sometimes forks
+ return true if in_clustered_puma?
+
+ # Default assumption is that we don't fork
+ false
+ end
+
+ def in_clustered_puma?
+ return false unless defined?(::Puma)
+
+ @puma_options && @puma_options[:workers] && @puma_options[:workers] > 0
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cluster/puma_worker_killer_initializer.rb b/lib/gitlab/cluster/puma_worker_killer_initializer.rb
new file mode 100644
index 00000000000..331c39f7d6b
--- /dev/null
+++ b/lib/gitlab/cluster/puma_worker_killer_initializer.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Cluster
+ class PumaWorkerKillerInitializer
+ def self.start(puma_options, puma_per_worker_max_memory_mb: 650)
+ require 'puma_worker_killer'
+
+ PumaWorkerKiller.config do |config|
+ # Note! ram is expressed in megabytes (whereas GITLAB_UNICORN_MEMORY_MAX is in bytes)
+ # Importantly RAM is for _all_workers (ie, the cluster),
+ # not each worker as is the case with GITLAB_UNICORN_MEMORY_MAX
+ worker_count = puma_options[:workers] || 1
+ config.ram = worker_count * puma_per_worker_max_memory_mb
+
+ config.frequency = 20 # seconds
+
+ # We just want to limit to a fixed maximum, unrelated to the total amount
+ # of available RAM.
+ config.percent_usage = 0.98
+
+ # Ideally we'll never hit the maximum amount of memory. If so the worker
+ # is restarted already, thus periodically restarting workers shouldn't be
+ # needed.
+ config.rolling_restart_frequency = false
+ end
+
+ PumaWorkerKiller.start
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/lfs_changes.rb b/lib/gitlab/git/lfs_changes.rb
index f0fab1e76a3..d7148165408 100644
--- a/lib/gitlab/git/lfs_changes.rb
+++ b/lib/gitlab/git/lfs_changes.rb
@@ -6,8 +6,8 @@ module Gitlab
@newrev = newrev
end
- def new_pointers(object_limit: nil, not_in: nil)
- @repository.gitaly_blob_client.get_new_lfs_pointers(@newrev, object_limit, not_in)
+ def new_pointers(object_limit: nil, not_in: nil, dynamic_timeout: nil)
+ @repository.gitaly_blob_client.get_new_lfs_pointers(@newrev, object_limit, not_in, dynamic_timeout)
end
def all_pointers
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 827c04ae035..802fa65dd63 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -9,6 +9,7 @@ module Gitlab
UnauthorizedError = Class.new(StandardError)
NotFoundError = Class.new(StandardError)
ProjectCreationError = Class.new(StandardError)
+ TimeoutError = Class.new(StandardError)
ProjectMovedError = Class.new(NotFoundError)
ERROR_MESSAGES = {
@@ -26,11 +27,18 @@ module Gitlab
cannot_push_to_read_only: "You can't push code to a read-only GitLab instance."
}.freeze
+ INTERNAL_TIMEOUT = 50.seconds.freeze
+ LOG_HEADER = <<~MESSAGE
+ Push operation timed out
+
+ Timing information for debugging purposes:
+ MESSAGE
+
DOWNLOAD_COMMANDS = %w{git-upload-pack git-upload-archive}.freeze
PUSH_COMMANDS = %w{git-receive-pack}.freeze
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
- attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path, :auth_result_type, :changes
+ attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path, :auth_result_type, :changes, :logger
def initialize(actor, project, protocol, authentication_abilities:, namespace_path: nil, project_path: nil, redirected_path: nil, auth_result_type: nil)
@actor = actor
@@ -44,6 +52,7 @@ module Gitlab
end
def check(cmd, changes)
+ @logger = Checks::TimedLogger.new(timeout: INTERNAL_TIMEOUT, header: LOG_HEADER)
@changes = changes
check_protocol!
@@ -269,14 +278,19 @@ module Gitlab
end
def check_single_change_access(change, skip_lfs_integrity_check: false)
- Checks::ChangeAccess.new(
+ change_access = Checks::ChangeAccess.new(
change,
user_access: user_access,
project: project,
skip_authorization: deploy_key?,
skip_lfs_integrity_check: skip_lfs_integrity_check,
- protocol: protocol
- ).exec
+ protocol: protocol,
+ logger: logger
+ )
+
+ change_access.exec
+ rescue Checks::TimedLogger::TimeoutError
+ raise TimeoutError, logger.full_message
end
def deploy_key
diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb
index e731e654f3c..cf2329e489d 100644
--- a/lib/gitlab/git_post_receive.rb
+++ b/lib/gitlab/git_post_receive.rb
@@ -11,8 +11,8 @@ module Gitlab
@changes = deserialize_changes(changes)
end
- def identify(revision)
- super(identifier, project, revision)
+ def identify
+ super(identifier)
end
def changes_refs
diff --git a/lib/gitlab/gitaly_client/blob_service.rb b/lib/gitlab/gitaly_client/blob_service.rb
index 1840bf45154..086ce31e678 100644
--- a/lib/gitlab/gitaly_client/blob_service.rb
+++ b/lib/gitlab/gitaly_client/blob_service.rb
@@ -72,7 +72,7 @@ module Gitlab
GitalyClient::BlobsStitcher.new(response)
end
- def get_new_lfs_pointers(revision, limit, not_in)
+ def get_new_lfs_pointers(revision, limit, not_in, dynamic_timeout = nil)
request = Gitaly::GetNewLFSPointersRequest.new(
repository: @gitaly_repo,
revision: encode_binary(revision),
@@ -85,7 +85,20 @@ module Gitlab
request.not_in_refs += not_in
end
- response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_new_lfs_pointers, request, timeout: GitalyClient.medium_timeout)
+ timeout =
+ if dynamic_timeout
+ [dynamic_timeout, GitalyClient.medium_timeout].min
+ else
+ GitalyClient.medium_timeout
+ end
+
+ response = GitalyClient.call(
+ @gitaly_repo.storage_name,
+ :blob_service,
+ :get_new_lfs_pointers,
+ request,
+ timeout: timeout
+ )
map_lfs_pointers(response)
end
diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb
index 28a9fe29465..d5f94ad04f1 100644
--- a/lib/gitlab/identifier.rb
+++ b/lib/gitlab/identifier.rb
@@ -1,13 +1,11 @@
# frozen_string_literal: true
# Detect user based on identifier like
-# key-13 or user-36 or last commit
+# key-13 or user-36
module Gitlab
module Identifier
- def identify(identifier, project = nil, newrev = nil)
- if identifier.blank?
- identify_using_commit(project, newrev)
- elsif identifier =~ /\Auser-\d+\Z/
+ def identify(identifier)
+ if identifier =~ /\Auser-\d+\Z/
# git push over http
identify_using_user(identifier)
elsif identifier =~ /\Akey-\d+\Z/
@@ -16,19 +14,6 @@ module Gitlab
end
end
- # Tries to identify a user based on a commit SHA.
- def identify_using_commit(project, ref)
- return if project.nil? && ref.nil?
-
- commit = project.commit(ref)
-
- return if !commit || !commit.author_email
-
- identify_with_cache(:email, commit.author_email) do
- commit.author
- end
- end
-
# Tries to identify a user based on a user identifier (e.g. "user-123").
# rubocop: disable CodeReuse/ActiveRecord
def identify_using_user(identifier)
diff --git a/lib/gitlab/patch/draw_route.rb b/lib/gitlab/patch/draw_route.rb
new file mode 100644
index 00000000000..b00244a6e04
--- /dev/null
+++ b/lib/gitlab/patch/draw_route.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+# We're patching `ActionDispatch::Routing::Mapper` in
+# config/initializers/routing_draw.rb
+module Gitlab
+ module Patch
+ module DrawRoute
+ RoutesNotFound = Class.new(StandardError)
+
+ def draw(routes_name)
+ drawn_any = draw_ce(routes_name) | draw_ee(routes_name)
+
+ drawn_any || raise(RoutesNotFound.new("Cannot find #{routes_name}"))
+ end
+
+ def draw_ce(routes_name)
+ draw_route(route_path("config/routes/#{routes_name}.rb"))
+ end
+
+ def draw_ee(_)
+ true
+ end
+
+ def route_path(routes_name)
+ Rails.root.join(routes_name)
+ end
+
+ def draw_route(path)
+ if File.exist?(path)
+ instance_eval(File.read(path))
+ true
+ else
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/ldap.rake b/lib/tasks/gitlab/ldap.rake
index c66a2a263dc..0459de27c96 100644
--- a/lib/tasks/gitlab/ldap.rake
+++ b/lib/tasks/gitlab/ldap.rake
@@ -1,7 +1,7 @@
namespace :gitlab do
namespace :ldap do
desc 'GitLab | LDAP | Rename provider'
- task :rename_provider, [:old_provider, :new_provider] => :environment do |_, args|
+ task :rename_provider, [:old_provider, :new_provider] => :gitlab_environment do |_, args|
old_provider = args[:old_provider] ||
prompt('What is the old provider? Ex. \'ldapmain\': '.color(:blue))
new_provider = args[:new_provider] ||
diff --git a/qa/qa.rb b/qa/qa.rb
index 36a37dbb270..968687dab67 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -39,7 +39,6 @@ module QA
module Factory
autoload :ApiFabricator, 'qa/factory/api_fabricator'
autoload :Base, 'qa/factory/base'
- autoload :Dependency, 'qa/factory/dependency'
autoload :Product, 'qa/factory/product'
module Resource
@@ -100,7 +99,8 @@ module QA
module Integration
autoload :Github, 'qa/scenario/test/integration/github'
- autoload :LDAP, 'qa/scenario/test/integration/ldap'
+ autoload :LDAPNoTLS, 'qa/scenario/test/integration/ldap_no_tls'
+ autoload :LDAPTLS, 'qa/scenario/test/integration/ldap_tls'
autoload :InstanceSAML, 'qa/scenario/test/integration/instance_saml'
autoload :Kubernetes, 'qa/scenario/test/integration/kubernetes'
autoload :Mattermost, 'qa/scenario/test/integration/mattermost'
diff --git a/qa/qa/factory/README.md b/qa/qa/factory/README.md
index 10140e39510..cfce096ab39 100644
--- a/qa/qa/factory/README.md
+++ b/qa/qa/factory/README.md
@@ -26,11 +26,7 @@ module QA
module Factory
module Resource
class Shirt < Factory::Base
- attr_accessor :name, :size
-
- def initialize(name)
- @name = name
- end
+ attr_accessor :name
def fabricate!
Page::Dashboard::Index.perform do |dashboard_index|
@@ -64,21 +60,10 @@ module QA
module Factory
module Resource
class Shirt < Factory::Base
- attr_accessor :name, :size
-
- def initialize(name)
- @name = name
- end
+ attr_accessor :name
def fabricate!
- Page::Dashboard::Index.perform do |dashboard_index|
- dashboard_index.go_to_new_shirt
- end
-
- Page::Shirt::New.perform do |shirt_new|
- shirt_new.set_name(name)
- shirt_new.create_shirt!
- end
+ # ... same as before
end
def api_get_path
@@ -103,33 +88,69 @@ end
The [`Project` factory](./resource/project.rb) is a good real example of Browser
UI and API implementations.
-### Define dependencies
+### Define attributes
+
+After the resource is fabricated, we would like to access the attributes on
+the resource. We define the attributes with `attribute` method. Suppose
+we want to access the name on the resource, we could change `attr_accessor`
+to `attribute`:
+
+```ruby
+module QA
+ module Factory
+ module Resource
+ class Shirt < Factory::Base
+ attribute :name
-A resource may need an other resource to exist first. For instance, a project
+ # ... same as before
+ end
+ end
+ end
+end
+```
+
+The difference between `attr_accessor` and `attribute` is that by using
+`attribute` it can also be accessed from the product:
+
+```ruby
+shirt =
+ QA::Factory::Resource::Shirt.fabricate! do |resource|
+ resource.name = "GitLab QA"
+ end
+
+shirt.name # => "GitLab QA"
+```
+
+In the above example, if we use `attr_accessor :name` then `shirt.name` won't
+be available. On the other hand, using `attribute :name` will allow you to use
+`shirt.name`, so most of the time you'll want to use `attribute` instead of
+`attr_accessor` unless we clearly don't need it for the product.
+
+#### Resource attributes
+
+A resource may need another resource to exist first. For instance, a project
needs a group to be created in.
-To define a dependency, you can use the `dependency` DSL method.
-The first argument is a factory class, then you should pass `as: <name>` to give
-a name to the dependency.
-That will allow access to the dependency from your resource object's methods.
-You would usually use it in `#fabricate!`, `#api_get_path`, `#api_post_path`,
-`#api_post_body`.
+To define a resource attribute, you can use the `attribute` method with a
+block using the other factory to fabricate the resource.
-Let's take the `Shirt` factory, and add a `project` dependency to it:
+That will allow access to the other resource from your resource object's
+methods. You would usually use it in `#fabricate!`, `#api_get_path`,
+`#api_post_path`, `#api_post_body`.
+
+Let's take the `Shirt` factory, and add a `project` attribute to it:
```ruby
module QA
module Factory
module Resource
class Shirt < Factory::Base
- attr_accessor :name, :size
+ attribute :name
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-to-create-a-shirt'
- end
-
- def initialize(name)
- @name = name
+ attribute :project do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'project-to-create-a-shirt'
+ end
end
def fabricate!
@@ -164,19 +185,19 @@ module QA
end
```
-**Note that dependencies are always built via the API fabrication method if
-supported by their factories.**
+**Note that all the attributes are lazily constructed. This means if you want
+a specific attribute to be fabricated first, you'll need to call the
+attribute method first even if you're not using it.**
-### Define attributes on the created resource
+#### Product data attributes
Once created, you may want to populate a resource with attributes that can be
found in the Web page, or in the API response.
For instance, once you create a project, you may want to store its repository
SSH URL as an attribute.
-To define an attribute, you can use the `product` DSL method.
-The first argument is the attribute name, then you should define a name for the
-dependency to be accessible from your resource object's methods.
+Again we could use the `attribute` method with a block, using a page object
+to retrieve the data on the page.
Let's take the `Shirt` factory, and define a `:brand` attribute:
@@ -185,22 +206,74 @@ module QA
module Factory
module Resource
class Shirt < Factory::Base
- attr_accessor :name, :size
+ attribute :name
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-to-create-a-shirt'
+ attribute :project do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'project-to-create-a-shirt'
+ end
end
# Attribute populated from the Browser UI (using the block)
- product :brand do
+ attribute :brand do
Page::Shirt::Show.perform do |shirt_show|
shirt_show.fetch_brand_from_page
end
end
- def initialize(name)
- @name = name
- end
+ # ... same as before
+ end
+ end
+ end
+end
+```
+
+**Note again that all the attributes are lazily constructed. This means if
+you call `shirt.brand` after moving to the other page, it'll not properly
+retrieve the data because we're no longer on the expected page.**
+
+Consider this:
+
+```ruby
+shirt =
+ QA::Factory::Resource::Shirt.fabricate! do |resource|
+ resource.name = "GitLab QA"
+ end
+
+shirt.project.visit!
+
+shirt.brand # => FAIL!
+```
+
+The above example will fail because now we're on the project page, trying to
+construct the brand data from the shirt page, however we moved to the project
+page already. There are two ways to solve this, one is that we could try to
+retrieve the brand before visiting the project again:
+
+```ruby
+shirt =
+ QA::Factory::Resource::Shirt.fabricate! do |resource|
+ resource.name = "GitLab QA"
+ end
+
+shirt.brand # => OK!
+
+shirt.project.visit!
+
+shirt.brand # => OK!
+```
+
+The attribute will be stored in the instance therefore all the following calls
+will be fine, using the data previously constructed. If we think that this
+might be too brittle, we could eagerly construct the data right before
+ending fabrication:
+
+```ruby
+module QA
+ module Factory
+ module Resource
+ class Shirt < Factory::Base
+ # ... same as before
def fabricate!
project.visit!
@@ -213,20 +286,8 @@ module QA
shirt_new.set_name(name)
shirt_new.create_shirt!
end
- end
- def api_get_path
- "/project/#{project.path}/shirt/#{name}"
- end
-
- def api_post_path
- "/project/#{project.path}/shirts"
- end
-
- def api_post_body
- {
- name: name
- }
+ brand # Eagerly construct the data
end
end
end
@@ -234,74 +295,48 @@ module QA
end
```
-#### Inherit a factory's attribute
+This will make sure we construct the data right after we created the shirt.
+The drawback for this will become we're forced to construct the data even
+if we don't really need to use it.
-Sometimes, you want a resource to inherit its factory attributes. For instance,
-it could be useful to pass the `size` attribute from the `Shirt` factory to the
-created resource.
-You can do that by defining `product :attribute_name` without a block.
-
-Let's take the `Shirt` factory, and define a `:name` and a `:size` attributes:
+Alternatively, we could just make sure we're on the right page before
+constructing the brand data:
```ruby
module QA
module Factory
module Resource
class Shirt < Factory::Base
- attr_accessor :name, :size
-
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-to-create-a-shirt'
- end
+ attribute :name
- # Attribute from the Browser UI (using the block)
- product :brand do
- Page::Shirt::Show.perform do |shirt_show|
- shirt_show.fetch_brand_from_page
+ attribute :project do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'project-to-create-a-shirt'
end
end
- # Attribute inherited from the Shirt factory if present,
- # or a QA::Factory::Product::NoValueError is raised otherwise
- product :name
- product :size
-
- def initialize(name)
- @name = name
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Show.perform do |project_show|
- project_show.go_to_new_shirt
- end
+ # Attribute populated from the Browser UI (using the block)
+ attribute :brand do
+ back_url = current_url
+ visit!
- Page::Shirt::New.perform do |shirt_new|
- shirt_new.set_name(name)
- shirt_new.create_shirt!
+ Page::Shirt::Show.perform do |shirt_show|
+ shirt_show.fetch_brand_from_page
end
- end
- def api_get_path
- "/project/#{project.path}/shirt/#{name}"
+ visit(back_url)
end
- def api_post_path
- "/project/#{project.path}/shirts"
- end
-
- def api_post_body
- {
- name: name
- }
- end
+ # ... same as before
end
end
end
end
```
+This will make sure it's on the shirt page before constructing brand, and
+move back to the previous page to avoid breaking the state.
+
#### Define an attribute based on an API response
Sometimes, you want to define a resource attribute based on the API response
@@ -311,7 +346,6 @@ the API returns
```ruby
{
brand: 'a-brand-new-brand',
- size: 'extra-small',
style: 't-shirt',
materials: [[:cotton, 80], [:polyamide, 20]]
}
@@ -320,18 +354,6 @@ the API returns
you may want to store `style` as-is in the resource, and fetch the first value
of the first `materials` item in a `main_fabric` attribute.
-For both attributes, you will need to define an inherited attribute, as shown
-in "Inherit a factory's attribute" above, but in the case of `main_fabric`, you
-will need to implement the
-`#transform_api_resource` method to first populate the `:main_fabric` key in the
-API response so that it can be used later to automatically populate the
-attribute on your resource.
-
-If an attribute can only be retrieved from the API response, you should define
-a block to give it a default value, otherwise you could get a
-`QA::Factory::Product::NoValueError` when creating your resource via the
-Browser UI.
-
Let's take the `Shirt` factory, and define a `:style` and a `:main_fabric`
attributes:
@@ -340,69 +362,21 @@ module QA
module Factory
module Resource
class Shirt < Factory::Base
- attr_accessor :name, :size
+ # ... same as before
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-to-create-a-shirt'
- end
-
- # Attribute fetched from the API response if present,
- # or from the Browser UI otherwise (using the block)
- product :brand do
- Page::Shirt::Show.perform do |shirt_show|
- shirt_show.fetch_brand_from_page
- end
- end
-
- # Attribute fetched from the API response if present,
- # or from the Shirt factory if present,
- # or a QA::Factory::Product::NoValueError is raised otherwise
- product :name
- product :size
- product :style do
- 'unknown'
- end
- product :main_fabric do
- 'unknown'
- end
-
- def initialize(name)
- @name = name
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Show.perform do |project_show|
- project_show.go_to_new_shirt
- end
-
- Page::Shirt::New.perform do |shirt_new|
- shirt_new.set_name(name)
- shirt_new.create_shirt!
- end
- end
+ # Attribute from the Shirt factory if present,
+ # or fetched from the API response if present,
+ # or a QA::Factory::Base::NoValueError is raised otherwise
+ attribute :style
- def api_get_path
- "/project/#{project.path}/shirt/#{name}"
+ # If the attribute from the Shirt factory is not present,
+ # and if the API does not contain this field, this block will be
+ # used to construct the value based on the API response.
+ attribute :main_fabric do
+ api_response.&dig(:materials, 0, 0)
end
- def api_post_path
- "/project/#{project.path}/shirts"
- end
-
- def api_post_body
- {
- name: name
- }
- end
-
- private
-
- def transform_api_resource(api_response)
- api_response[:main_fabric] = api_response[:materials][0][0]
- api_response
- end
+ # ... same as before
end
end
end
@@ -411,11 +385,10 @@ end
**Notes on attributes precedence:**
+- attributes from the factory have the highest precedence
- attributes from the API response take precedence over attributes from the
- Browser UI
-- attributes from the Browser UI take precedence over attributes from the
- factory (i.e inherited)
-- attributes without a value will raise a `QA::Factory::Product::NoValueError` error
+ block (usually from Browser UI)
+- attributes without a value will raise a `QA::Factory::Base::NoValueError` error
## Creating resources in your tests
@@ -428,42 +401,40 @@ Here is an example that will use the API fabrication method under the hood since
it's supported by the `Shirt` factory:
```ruby
-my_shirt = Factory::Resource::Shirt.fabricate!('my-shirt') do |shirt|
- shirt.size = 'small'
+my_shirt = Factory::Resource::Shirt.fabricate! do |shirt|
+ shirt.name = 'my-shirt'
end
+expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute
expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response
-expect(page).to have_text(my_shirt.name) # => "my-shirt" from the inherited factory's attribute
-expect(page).to have_text(my_shirt.size) # => "extra-small" from the API response
expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response
-expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the (transformed) API response
+expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block
```
-If you explicitely want to use the Browser UI fabrication method, you can call
+If you explicitly want to use the Browser UI fabrication method, you can call
the `.fabricate_via_browser_ui!` method instead:
```ruby
-my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui!('my-shirt') do |shirt|
- shirt.size = 'small'
+my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui! do |shirt|
+ shirt.name = 'my-shirt'
end
-expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page
-expect(page).to have_text(my_shirt.name) # => "my-shirt" from the inherited factory's attribute
-expect(page).to have_text(my_shirt.size) # => "small" from the inherited factory's attribute
-expect(page).to have_text(my_shirt.style) # => "unknown" from the attribute block
-expect(page).to have_text(my_shirt.main_fabric) # => "unknown" from the attribute block
+expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute
+expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page via the block
+expect(page).to have_text(my_shirt.style) # => QA::Factory::Base::NoValueError will be raised because no API response nor a block is provided
+expect(page).to have_text(my_shirt.main_fabric) # => QA::Factory::Base::NoValueError will be raised because no API response and the block didn't provide a value (because it's also based on the API response)
```
-You can also explicitely use the API fabrication method, by calling the
+You can also explicitly use the API fabrication method, by calling the
`.fabricate_via_api!` method:
```ruby
-my_shirt = Factory::Resource::Shirt.fabricate_via_api!('my-shirt') do |shirt|
- shirt.size = 'small'
+my_shirt = Factory::Resource::Shirt.fabricate_via_api! do |shirt|
+ shirt.name = 'my-shirt'
end
```
-In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!('my-shirt')`.
+In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!`.
## Where to ask for help?
diff --git a/qa/qa/factory/base.rb b/qa/qa/factory/base.rb
index e1dc23d350d..e82e16f9415 100644
--- a/qa/qa/factory/base.rb
+++ b/qa/qa/factory/base.rb
@@ -10,13 +10,42 @@ module QA
include ApiFabricator
extend Capybara::DSL
- def_delegators :evaluator, :dependency, :dependencies
- def_delegators :evaluator, :product, :attributes
+ NoValueError = Class.new(RuntimeError)
+
+ def_delegators :evaluator, :attribute
def fabricate!(*_args)
raise NotImplementedError
end
+ def visit!
+ visit(web_url)
+ end
+
+ private
+
+ def populate_attribute(name, block)
+ value = attribute_value(name, block)
+
+ raise NoValueError, "No value was computed for product #{name} of factory #{self.class.name}." unless value
+
+ value
+ end
+
+ def attribute_value(name, block)
+ api_value = api_resource&.dig(name)
+
+ if api_value && block
+ log_having_both_api_result_and_block(name, api_value)
+ end
+
+ api_value || (block && instance_exec(&block))
+ end
+
+ def log_having_both_api_result_and_block(name, api_value)
+ QA::Runtime::Logger.info "<#{self.class}> Attribute #{name.inspect} has both API response `#{api_value}` and a block. API response will be picked. Block will be ignored."
+ end
+
def self.fabricate!(*args, &prepare_block)
fabricate_via_api!(*args, &prepare_block)
rescue NotImplementedError
@@ -52,13 +81,10 @@ module QA
def self.do_fabricate!(factory:, prepare_block:, parents: [])
prepare_block.call(factory) if prepare_block
- dependencies.each do |signature|
- Factory::Dependency.new(factory, signature).build!(parents: parents + [self])
- end
-
resource_web_url = yield
+ factory.web_url = resource_web_url
- Factory::Product.populate!(factory, resource_web_url)
+ Factory::Product.new(factory)
end
private_class_method :do_fabricate!
@@ -85,31 +111,40 @@ module QA
end
private_class_method :evaluator
- class DSL
- attr_reader :dependencies, :attributes
+ def self.dynamic_attributes
+ const_get(:DynamicAttributes)
+ rescue NameError
+ mod = const_set(:DynamicAttributes, Module.new)
+
+ include mod
+
+ mod
+ end
+ def self.attributes_names
+ dynamic_attributes.instance_methods(false).sort.grep_v(/=$/)
+ end
+
+ class DSL
def initialize(base)
@base = base
- @dependencies = []
- @attributes = []
end
- def dependency(factory, as:, &block)
- as.tap do |name|
- @base.class_eval { attr_accessor name }
+ def attribute(name, &block)
+ @base.dynamic_attributes.module_eval do
+ attr_writer(name)
- Dependency::Signature.new(name, factory, block).tap do |signature|
- @dependencies << signature
+ define_method(name) do
+ instance_variable_get("@#{name}") ||
+ instance_variable_set(
+ "@#{name}",
+ populate_attribute(name, block))
end
end
end
-
- def product(attribute, &block)
- Product::Attribute.new(attribute, block).tap do |signature|
- @attributes << signature
- end
- end
end
+
+ attribute :web_url
end
end
end
diff --git a/qa/qa/factory/dependency.rb b/qa/qa/factory/dependency.rb
deleted file mode 100644
index 655e2677db0..00000000000
--- a/qa/qa/factory/dependency.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-module QA
- module Factory
- class Dependency
- Signature = Struct.new(:name, :factory, :block)
-
- def initialize(caller_factory, dependency_signature)
- @caller_factory = caller_factory
- @dependency_signature = dependency_signature
- end
-
- def overridden?
- !!@caller_factory.public_send(@dependency_signature.name)
- end
-
- def build!(parents: [])
- return if overridden?
-
- dependency = @dependency_signature.factory.fabricate!(parents: parents) do |factory|
- @dependency_signature.block&.call(factory, @caller_factory)
- end
-
- dependency.tap do |dependency|
- @caller_factory.public_send("#{@dependency_signature.name}=", dependency)
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/product.rb b/qa/qa/factory/product.rb
index 17fe908eaa2..34df0bda8e5 100644
--- a/qa/qa/factory/product.rb
+++ b/qa/qa/factory/product.rb
@@ -5,46 +5,31 @@ module QA
class Product
include Capybara::DSL
- NoValueError = Class.new(RuntimeError)
+ attr_reader :factory
- attr_reader :factory, :web_url
-
- Attribute = Struct.new(:name, :block)
-
- def initialize(factory, web_url)
+ def initialize(factory)
@factory = factory
- @web_url = web_url
- populate_attributes!
+ define_attributes
end
def visit!
visit(web_url)
end
- def self.populate!(factory, web_url)
- new(factory, web_url)
+ def populate(*attributes)
+ attributes.each(&method(:public_send))
end
private
- def populate_attributes!
- factory.class.attributes.each do |attribute|
- instance_exec(factory, attribute.block) do |factory, block|
- value = attribute_value(attribute, block)
-
- raise NoValueError, "No value was computed for product #{attribute.name} of factory #{factory.class.name}." unless value
-
- define_singleton_method(attribute.name) { value }
+ def define_attributes
+ factory.class.attributes_names.each do |name|
+ define_singleton_method(name) do
+ factory.public_send(name)
end
end
end
-
- def attribute_value(attribute, block)
- factory.api_resource&.dig(attribute.name) ||
- (block && block.call(factory)) ||
- (factory.respond_to?(attribute.name) && factory.public_send(attribute.name))
- end
end
end
end
diff --git a/qa/qa/factory/repository/project_push.rb b/qa/qa/factory/repository/project_push.rb
index 6f878396f0e..a9dfbc0a783 100644
--- a/qa/qa/factory/repository/project_push.rb
+++ b/qa/qa/factory/repository/project_push.rb
@@ -2,13 +2,14 @@ module QA
module Factory
module Repository
class ProjectPush < Factory::Repository::Push
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-code'
- project.description = 'Project with repository'
+ attribute :project do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'project-with-code'
+ resource.description = 'Project with repository'
+ end
end
- product :output
- product :project
+ attribute :output
def initialize
@file_name = 'file.txt'
diff --git a/qa/qa/factory/repository/wiki_push.rb b/qa/qa/factory/repository/wiki_push.rb
index ecc6cc18c88..25b6ffe8323 100644
--- a/qa/qa/factory/repository/wiki_push.rb
+++ b/qa/qa/factory/repository/wiki_push.rb
@@ -2,10 +2,12 @@ module QA
module Factory
module Repository
class WikiPush < Factory::Repository::Push
- dependency Factory::Resource::Wiki, as: :wiki do |wiki|
- wiki.title = 'Home'
- wiki.content = '# My First Wiki Content'
- wiki.message = 'Update home'
+ attribute :wiki do
+ Factory::Resource::Wiki.fabricate! do |resource|
+ resource.title = 'Home'
+ resource.content = '# My First Wiki Content'
+ resource.message = 'Update home'
+ end
end
def initialize
diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb
index f3b52565d17..b05d1e252ec 100644
--- a/qa/qa/factory/resource/branch.rb
+++ b/qa/qa/factory/resource/branch.rb
@@ -5,8 +5,10 @@ module QA
attr_accessor :project, :branch_name,
:allow_to_push, :allow_to_merge, :protected
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'protected-branch-project'
+ attribute :project do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'protected-branch-project'
+ end
end
def initialize
@@ -43,9 +45,7 @@ module QA
# to `allow_to_push` variable.
return branch unless @protected
- Page::Project::Menu.act do
- click_repository_settings
- end
+ Page::Project::Menu.perform(&:click_repository_settings)
Page::Project::Settings::Repository.perform do |setting|
setting.expand_protected_branches do |page|
diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb
index 4c53c500c27..aea99c9f80d 100644
--- a/qa/qa/factory/resource/deploy_key.rb
+++ b/qa/qa/factory/resource/deploy_key.rb
@@ -4,11 +4,11 @@ module QA
class DeployKey < Factory::Base
attr_accessor :title, :key
- product :fingerprint do |resource|
- Page::Project::Settings::Repository.act do
- expand_deploy_keys do |key|
- key_offset = key.key_titles.index do |title|
- title.text == resource.title
+ attribute :fingerprint do
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_deploy_keys do |key|
+ key_offset = key.key_titles.index do |key_title|
+ key_title.text == title
end
key.key_fingerprints[key_offset].text
@@ -16,17 +16,17 @@ module QA
end
end
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-to-deploy'
- project.description = 'project for adding deploy key test'
+ attribute :project do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'project-to-deploy'
+ resource.description = 'project for adding deploy key test'
+ end
end
def fabricate!
project.visit!
- Page::Project::Menu.act do
- click_repository_settings
- end
+ Page::Project::Menu.perform(&:click_repository_settings)
Page::Project::Settings::Repository.perform do |setting|
setting.expand_deploy_keys do |page|
diff --git a/qa/qa/factory/resource/deploy_token.rb b/qa/qa/factory/resource/deploy_token.rb
index 159f79ac50b..68e98f0aa01 100644
--- a/qa/qa/factory/resource/deploy_token.rb
+++ b/qa/qa/factory/resource/deploy_token.rb
@@ -4,25 +4,27 @@ module QA
class DeployToken < Factory::Base
attr_accessor :name, :expires_at
- product :username do |resource|
- Page::Project::Settings::Repository.act do
- expand_deploy_tokens do |token|
+ attribute :username do
+ Page::Project::Settings::Repository.perform do |page|
+ page.expand_deploy_tokens do |token|
token.token_username
end
end
end
- product :password do |password|
- Page::Project::Settings::Repository.act do
- expand_deploy_tokens do |token|
+ attribute :password do
+ Page::Project::Settings::Repository.perform do |page|
+ page.expand_deploy_tokens do |token|
token.token_password
end
end
end
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-to-deploy'
- project.description = 'project for adding deploy token test'
+ attribute :project do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'project-to-deploy'
+ resource.description = 'project for adding deploy token test'
+ end
end
def fabricate!
diff --git a/qa/qa/factory/resource/file.rb b/qa/qa/factory/resource/file.rb
index f8dea06d361..1148876c2d3 100644
--- a/qa/qa/factory/resource/file.rb
+++ b/qa/qa/factory/resource/file.rb
@@ -8,8 +8,10 @@ module QA
:content,
:commit_message
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-new-file'
+ attribute :project do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'project-with-new-file'
+ end
end
def initialize
@@ -21,7 +23,7 @@ module QA
def fabricate!
project.visit!
- Page::Project::Show.act { create_new_file! }
+ Page::Project::Show.perform(&:create_new_file!)
Page::File::Form.perform do |page|
page.add_name(@name)
diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb
index 6e2a668df64..0fac4377040 100644
--- a/qa/qa/factory/resource/fork.rb
+++ b/qa/qa/factory/resource/fork.rb
@@ -2,17 +2,19 @@ module QA
module Factory
module Resource
class Fork < Factory::Base
- dependency Factory::Repository::ProjectPush, as: :push
+ attribute :push do
+ Factory::Repository::ProjectPush.fabricate!
+ end
- dependency Factory::Resource::User, as: :user do |user|
- if Runtime::Env.forker?
- user.username = Runtime::Env.forker_username
- user.password = Runtime::Env.forker_password
+ attribute :user do
+ Factory::Resource::User.fabricate! do |resource|
+ if Runtime::Env.forker?
+ resource.username = Runtime::Env.forker_username
+ resource.password = Runtime::Env.forker_password
+ end
end
end
- product :user
-
def visit_project_with_retry
# The user intermittently fails to stay signed in after visiting the
# project page. The new user is registered and then signs in and a
@@ -48,15 +50,20 @@ module QA
end
def fabricate!
+ push
+ user
+
visit_project_with_retry
- Page::Project::Show.act { fork_project }
+ Page::Project::Show.perform(&:fork_project)
Page::Project::Fork::New.perform do |fork_new|
fork_new.choose_namespace(user.name)
end
- Page::Layout::Banner.act { has_notice?('The project was successfully forked.') }
+ Page::Layout::Banner.perform do |page|
+ page.has_notice?('The project was successfully forked.')
+ end
end
end
end
diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb
index 2688328df92..45e49da86f9 100644
--- a/qa/qa/factory/resource/group.rb
+++ b/qa/qa/factory/resource/group.rb
@@ -4,12 +4,12 @@ module QA
class Group < Factory::Base
attr_accessor :path, :description
- dependency Factory::Resource::Sandbox, as: :sandbox
-
- product :id do
- true # We don't retrieve the Group ID when using the Browser UI
+ attribute :sandbox do
+ Factory::Resource::Sandbox.fabricate!
end
+ attribute :id
+
def initialize
@path = Runtime::Namespace.name
@description = "QA test run at #{Runtime::Namespace.time}"
diff --git a/qa/qa/factory/resource/issue.rb b/qa/qa/factory/resource/issue.rb
index 9b444cb0bf1..3a28e0d5aa6 100644
--- a/qa/qa/factory/resource/issue.rb
+++ b/qa/qa/factory/resource/issue.rb
@@ -2,22 +2,21 @@ module QA
module Factory
module Resource
class Issue < Factory::Base
- attr_accessor :title, :description, :project
+ attr_writer :description
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-for-issues'
- project.description = 'project for adding issues'
+ attribute :project do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'project-for-issues'
+ resource.description = 'project for adding issues'
+ end
end
- product :project
- product :title
+ attribute :title
def fabricate!
project.visit!
- Page::Project::Show.act do
- go_to_new_issue
- end
+ Page::Project::Show.perform(&:go_to_new_issue)
Page::Project::Issue::New.perform do |page|
page.add_title(@title)
diff --git a/qa/qa/factory/resource/kubernetes_cluster.rb b/qa/qa/factory/resource/kubernetes_cluster.rb
index cdee35c54e3..aac6864f42f 100644
--- a/qa/qa/factory/resource/kubernetes_cluster.rb
+++ b/qa/qa/factory/resource/kubernetes_cluster.rb
@@ -7,24 +7,21 @@ module QA
attr_writer :project, :cluster,
:install_helm_tiller, :install_ingress, :install_prometheus, :install_runner
- product :ingress_ip do
- Page::Project::Operations::Kubernetes::Show.perform do |page|
- page.ingress_ip
- end
+ attribute :ingress_ip do
+ Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip)
end
def fabricate!
@project.visit!
- Page::Project::Menu.act { click_operations_kubernetes }
+ Page::Project::Menu.perform(
+ &:click_operations_kubernetes)
- Page::Project::Operations::Kubernetes::Index.perform do |page|
- page.add_kubernetes_cluster
- end
+ Page::Project::Operations::Kubernetes::Index.perform(
+ &:add_kubernetes_cluster)
- Page::Project::Operations::Kubernetes::Add.perform do |page|
- page.add_existing_cluster
- end
+ Page::Project::Operations::Kubernetes::Add.perform(
+ &:add_existing_cluster)
Page::Project::Operations::Kubernetes::AddExisting.perform do |page|
page.set_cluster_name(@cluster.cluster_name)
diff --git a/qa/qa/factory/resource/label.rb b/qa/qa/factory/resource/label.rb
index 4080f15bf66..32bc519b48c 100644
--- a/qa/qa/factory/resource/label.rb
+++ b/qa/qa/factory/resource/label.rb
@@ -4,14 +4,14 @@ module QA
module Factory
module Resource
class Label < Factory::Base
- attr_accessor :title,
- :description,
- :color
+ attr_accessor :description, :color
- product(:title) { |factory| factory.title }
+ attribute :title
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-label'
+ attribute :project do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'project-with-label'
+ end
end
def initialize
@@ -23,8 +23,8 @@ module QA
def fabricate!
project.visit!
- Page::Project::Menu.act { go_to_labels }
- Page::Label::Index.act { go_to_new_label }
+ Page::Project::Menu.perform(&:go_to_labels)
+ Page::Label::Index.perform(&:go_to_new_label)
Page::Label::New.perform do |page|
page.fill_title(@title)
diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb
index d30da8a3db0..92b8bdf4a21 100644
--- a/qa/qa/factory/resource/merge_request.rb
+++ b/qa/qa/factory/resource/merge_request.rb
@@ -12,27 +12,33 @@ module QA
:milestone,
:labels
- product :project
- product :source_branch
+ attribute :source_branch
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-merge-request'
+ attribute :project do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'project-with-merge-request'
+ end
end
- dependency Factory::Repository::ProjectPush, as: :target do |push, factory|
- factory.project.visit!
- push.project = factory.project
- push.branch_name = 'master'
- push.remote_branch = factory.target_branch
+ attribute :target do
+ project.visit!
+
+ Factory::Repository::ProjectPush.fabricate! do |resource|
+ resource.project = project
+ resource.branch_name = 'master'
+ resource.remote_branch = target_branch
+ end
end
- dependency Factory::Repository::ProjectPush, as: :source do |push, factory|
- push.project = factory.project
- push.branch_name = factory.target_branch
- push.remote_branch = factory.source_branch
- push.new_branch = false
- push.file_name = "added_file.txt"
- push.file_content = "File Added"
+ attribute :source do
+ Factory::Repository::ProjectPush.fabricate! do |resource|
+ resource.project = project
+ resource.branch_name = target_branch
+ resource.remote_branch = source_branch
+ resource.new_branch = false
+ resource.file_name = "added_file.txt"
+ resource.file_content = "File Added"
+ end
end
def initialize
@@ -46,8 +52,10 @@ module QA
end
def fabricate!
+ target
+ source
project.visit!
- Page::Project::Show.act { new_merge_request }
+ Page::Project::Show.perform(&:new_merge_request)
Page::MergeRequest::New.perform do |page|
page.fill_title(@title)
page.fill_description(@description)
diff --git a/qa/qa/factory/resource/merge_request_from_fork.rb b/qa/qa/factory/resource/merge_request_from_fork.rb
index 6caaf65f673..fbe062539b9 100644
--- a/qa/qa/factory/resource/merge_request_from_fork.rb
+++ b/qa/qa/factory/resource/merge_request_from_fork.rb
@@ -4,19 +4,24 @@ module QA
class MergeRequestFromFork < MergeRequest
attr_accessor :fork_branch
- dependency Factory::Resource::Fork, as: :fork
+ attribute :fork do
+ Factory::Resource::Fork.fabricate!
+ end
- dependency Factory::Repository::ProjectPush, as: :push do |push, factory|
- push.project = factory.fork
- push.branch_name = factory.fork_branch
- push.file_name = 'file2.txt'
- push.user = factory.fork.user
+ attribute :push do
+ Factory::Repository::ProjectPush.fabricate! do |resource|
+ resource.project = fork
+ resource.branch_name = fork_branch
+ resource.file_name = 'file2.txt'
+ resource.user = fork.user
+ end
end
def fabricate!
+ push
fork.visit!
- Page::Project::Show.act { new_merge_request }
- Page::MergeRequest::New.act { create_merge_request }
+ Page::Project::Show.perform(&:new_merge_request)
+ Page::MergeRequest::New.perform(&:create_merge_request)
end
end
end
diff --git a/qa/qa/factory/resource/personal_access_token.rb b/qa/qa/factory/resource/personal_access_token.rb
index 166054cfcdc..ceb0f1c3d75 100644
--- a/qa/qa/factory/resource/personal_access_token.rb
+++ b/qa/qa/factory/resource/personal_access_token.rb
@@ -7,13 +7,13 @@ module QA
class PersonalAccessToken < Factory::Base
attr_accessor :name
- product :access_token do
- Page::Profile::PersonalAccessTokens.act { created_access_token }
+ attribute :access_token do
+ Page::Profile::PersonalAccessTokens.perform(&:created_access_token)
end
def fabricate!
- Page::Main::Menu.act { go_to_profile_settings }
- Page::Profile::Menu.act { click_access_tokens }
+ Page::Main::Menu.perform(&:go_to_profile_settings)
+ Page::Profile::Menu.perform(&:click_access_tokens)
Page::Profile::PersonalAccessTokens.perform do |page|
page.fill_token_name(name || 'api-test-token')
diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb
index 105e42b23ec..f691ae5a342 100644
--- a/qa/qa/factory/resource/project.rb
+++ b/qa/qa/factory/resource/project.rb
@@ -4,25 +4,24 @@ module QA
module Factory
module Resource
class Project < Factory::Base
- attr_accessor :description
- attr_reader :name
+ attribute :name
+ attribute :description
- dependency Factory::Resource::Group, as: :group
-
- product :group
- product :name
+ attribute :group do
+ Factory::Resource::Group.fabricate!
+ end
- product :repository_ssh_location do
- Page::Project::Show.act do
- choose_repository_clone_ssh
- repository_location
+ attribute :repository_ssh_location do
+ Page::Project::Show.perform do |page|
+ page.choose_repository_clone_ssh
+ page.repository_location
end
end
- product :repository_http_location do
- Page::Project::Show.act do
- choose_repository_clone_http
- repository_location
+ attribute :repository_http_location do
+ Page::Project::Show.perform do |page|
+ page.choose_repository_clone_http
+ page.repository_location
end
end
@@ -37,7 +36,7 @@ module QA
def fabricate!
group.visit!
- Page::Group::Show.act { go_to_new_project }
+ Page::Group::Show.perform(&:go_to_new_project)
Page::Project::New.perform do |page|
page.choose_test_namespace
diff --git a/qa/qa/factory/resource/project_imported_from_github.rb b/qa/qa/factory/resource/project_imported_from_github.rb
index a45e7fee03b..f62092ae122 100644
--- a/qa/qa/factory/resource/project_imported_from_github.rb
+++ b/qa/qa/factory/resource/project_imported_from_github.rb
@@ -6,14 +6,16 @@ module QA
class ProjectImportedFromGithub < Resource::Project
attr_writer :personal_access_token, :github_repository_path
- dependency Factory::Resource::Group, as: :group
+ attribute :group do
+ Factory::Resource::Group.fabricate!
+ end
- product :name
+ attribute :name
def fabricate!
group.visit!
- Page::Group::Show.act { go_to_new_project }
+ Page::Group::Show.perform(&:go_to_new_project)
Page::Project::New.perform do |page|
page.go_to_import_project
diff --git a/qa/qa/factory/resource/project_milestone.rb b/qa/qa/factory/resource/project_milestone.rb
index 35383842142..cfda58dc103 100644
--- a/qa/qa/factory/resource/project_milestone.rb
+++ b/qa/qa/factory/resource/project_milestone.rb
@@ -3,11 +3,12 @@ module QA
module Resource
class ProjectMilestone < Factory::Base
attr_accessor :description
- attr_reader :title
- dependency Factory::Resource::Project, as: :project
+ attribute :project do
+ Factory::Resource::Project.fabricate!
+ end
- product :title
+ attribute :title
def title=(title)
@title = "#{title}-#{SecureRandom.hex(4)}"
@@ -17,12 +18,12 @@ module QA
def fabricate!
project.visit!
- Page::Project::Menu.act do
- click_issues
- click_milestones
+ Page::Project::Menu.perform do |page|
+ page.click_issues
+ page.click_milestones
end
- Page::Project::Milestone::Index.act { click_new_milestone }
+ Page::Project::Milestone::Index.perform(&:click_new_milestone)
Page::Project::Milestone::New.perform do |milestone_new|
milestone_new.set_title(@title)
diff --git a/qa/qa/factory/resource/runner.rb b/qa/qa/factory/resource/runner.rb
index 7ac65fe6913..7108db1e55a 100644
--- a/qa/qa/factory/resource/runner.rb
+++ b/qa/qa/factory/resource/runner.rb
@@ -6,9 +6,11 @@ module QA
class Runner < Factory::Base
attr_writer :name, :tags, :image
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-ci-cd'
- project.description = 'Project with CI/CD Pipelines'
+ attribute :project do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'project-with-ci-cd'
+ resource.description = 'Project with CI/CD Pipelines'
+ end
end
def name
@@ -26,7 +28,7 @@ module QA
def fabricate!
project.visit!
- Page::Project::Menu.act { click_ci_cd_settings }
+ Page::Project::Menu.perform(&:click_ci_cd_settings)
Service::Runner.new(name).tap do |runner|
Page::Project::Settings::CICD.perform do |settings|
diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb
index e592f4e0dd2..56bcda9e2f3 100644
--- a/qa/qa/factory/resource/sandbox.rb
+++ b/qa/qa/factory/resource/sandbox.rb
@@ -8,17 +8,15 @@ module QA
class Sandbox < Factory::Base
attr_reader :path
- product :id do
- true # We don't retrieve the Group ID when using the Browser UI
- end
- product :path
+ attribute :id
+ attribute :path
def initialize
@path = Runtime::Namespace.sandbox_name
end
def fabricate!
- Page::Main::Menu.act { go_to_groups }
+ Page::Main::Menu.perform(&:go_to_groups)
Page::Dashboard::Groups.perform do |page|
if page.has_group?(path)
diff --git a/qa/qa/factory/resource/secret_variable.rb b/qa/qa/factory/resource/secret_variable.rb
index 4084a7fc2cd..24ba3408810 100644
--- a/qa/qa/factory/resource/secret_variable.rb
+++ b/qa/qa/factory/resource/secret_variable.rb
@@ -4,15 +4,17 @@ module QA
class SecretVariable < Factory::Base
attr_accessor :key, :value
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-with-secret-variables'
- project.description = 'project for adding secret variable test'
+ attribute :project do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'project-with-secret-variables'
+ resource.description = 'project for adding secret variable test'
+ end
end
def fabricate!
project.visit!
- Page::Project::Menu.act { click_ci_cd_settings }
+ Page::Project::Menu.perform(&:click_ci_cd_settings)
Page::Project::Settings::CICD.perform do |setting|
setting.expand_secret_variables do |page|
diff --git a/qa/qa/factory/resource/ssh_key.rb b/qa/qa/factory/resource/ssh_key.rb
index a512d071dd4..a48a93fbe65 100644
--- a/qa/qa/factory/resource/ssh_key.rb
+++ b/qa/qa/factory/resource/ssh_key.rb
@@ -6,21 +6,19 @@ module QA
class SSHKey < Factory::Base
extend Forwardable
- attr_accessor :title
- attr_reader :private_key, :public_key, :fingerprint
def_delegators :key, :private_key, :public_key, :fingerprint
- product :private_key
- product :title
- product :fingerprint
+ attribute :private_key
+ attribute :title
+ attribute :fingerprint
def key
@key ||= Runtime::Key::RSA.new
end
def fabricate!
- Page::Main::Menu.act { go_to_profile_settings }
- Page::Profile::Menu.act { click_ssh_keys }
+ Page::Main::Menu.perform(&:go_to_profile_settings)
+ Page::Profile::Menu.perform(&:click_ssh_keys)
Page::Profile::SSHKeys.perform do |page|
page.add_key(public_key, title)
diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb
index 36edf787b64..6e6f46f7a95 100644
--- a/qa/qa/factory/resource/user.rb
+++ b/qa/qa/factory/resource/user.rb
@@ -5,7 +5,6 @@ module QA
module Resource
class User < Factory::Base
attr_reader :unique_id
- attr_writer :username, :password, :name, :email
def initialize
@unique_id = SecureRandom.hex(8)
@@ -31,14 +30,14 @@ module QA
defined?(@username) && defined?(@password)
end
- product :name
- product :username
- product :email
- product :password
+ attribute :name
+ attribute :username
+ attribute :email
+ attribute :password
def fabricate!
# Don't try to log-out if we're not logged-in
- if Page::Main::Menu.act { has_personal_area?(wait: 0) }
+ if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) }
Page::Main::Menu.perform { |main| main.sign_out }
end
diff --git a/qa/qa/factory/resource/wiki.rb b/qa/qa/factory/resource/wiki.rb
index d697433736e..769f394e85c 100644
--- a/qa/qa/factory/resource/wiki.rb
+++ b/qa/qa/factory/resource/wiki.rb
@@ -4,9 +4,11 @@ module QA
class Wiki < Factory::Base
attr_accessor :title, :content, :message
- dependency Factory::Resource::Project, as: :project do |project|
- project.name = 'project-for-wikis'
- project.description = 'project for adding wikis'
+ attribute :project do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'project-for-wikis'
+ resource.description = 'project for adding wikis'
+ end
end
def fabricate!
diff --git a/qa/qa/factory/settings/hashed_storage.rb b/qa/qa/factory/settings/hashed_storage.rb
index 5e8f883e25f..4e32382f910 100644
--- a/qa/qa/factory/settings/hashed_storage.rb
+++ b/qa/qa/factory/settings/hashed_storage.rb
@@ -5,9 +5,9 @@ module QA
def fabricate!(*traits)
raise ArgumentError unless traits.include?(:enabled)
- Page::Main::Login.act { sign_in_using_credentials }
- Page::Main::Menu.act { go_to_admin_area }
- Page::Admin::Menu.act { go_to_repository_settings }
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+ Page::Main::Menu.perform(&:go_to_admin_area)
+ Page::Admin::Menu.perform(&:go_to_repository_settings)
Page::Admin::Settings::Repository.perform do |setting|
setting.expand_repository_storage do |page|
@@ -16,7 +16,7 @@ module QA
end
end
- QA::Page::Main::Menu.act { sign_out }
+ QA::Page::Main::Menu.perform(&:sign_out)
end
end
end
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index c6a8891d398..27a88534258 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -113,21 +113,17 @@ module QA
attr_reader :uri, :username, :password, :known_hosts_file, :private_key_file
- def debug?
- Runtime::Env.respond_to?(:verbose?) && Runtime::Env.verbose?
- end
-
def ssh_key_set?
!private_key_file.nil?
end
def run(command_str)
command = [env_vars, command_str, '2>&1'].compact.join(' ')
- warn "DEBUG: command=[#{command}]" if debug?
+ Runtime::Logger.debug "Git: command=[#{command}]"
output, _ = Open3.capture2(command)
output = output.chomp.gsub(/\s+$/, '')
- warn "DEBUG: output=[#{output}]" if debug?
+ Runtime::Logger.debug "Git: output=[#{output}]"
output
end
diff --git a/qa/qa/runtime/logger.rb b/qa/qa/runtime/logger.rb
index 3baa24de0ec..bd5c4fe5bf5 100644
--- a/qa/qa/runtime/logger.rb
+++ b/qa/qa/runtime/logger.rb
@@ -7,14 +7,16 @@ module QA
module Logger
extend SingleForwardable
- def_delegators :logger, :debug, :info, :error, :warn, :fatal, :unknown
+ def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unknown
singleton_class.module_eval do
+ attr_writer :logger
+
def logger
return @logger if @logger
@logger = ::Logger.new Runtime::Env.log_destination
- @logger.level = ::Logger::DEBUG
+ @logger.level = Runtime::Env.debug? ? ::Logger::DEBUG : ::Logger::ERROR
@logger
end
end
diff --git a/qa/qa/scenario/test/integration/ldap_no_tls.rb b/qa/qa/scenario/test/integration/ldap_no_tls.rb
new file mode 100644
index 00000000000..bbf4c847f33
--- /dev/null
+++ b/qa/qa/scenario/test/integration/ldap_no_tls.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module QA
+ module Scenario
+ module Test
+ module Integration
+ class LDAPNoTLS < Test::Instance::All
+ tags :ldap_no_tls
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/test/integration/ldap.rb b/qa/qa/scenario/test/integration/ldap_tls.rb
index 769fa389785..2a767e57bc6 100644
--- a/qa/qa/scenario/test/integration/ldap.rb
+++ b/qa/qa/scenario/test/integration/ldap_tls.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module QA
module Scenario
module Test
module Integration
- class LDAP < Test::Instance::All
- tags :ldap
+ class LDAPTLS < Test::Instance::All
+ tags :ldap_tls
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
index eb9e0297287..a397df03bd2 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Manage', :orchestrated, :ldap do
+ context 'Manage', :orchestrated, :ldap_no_tls, :ldap_tls do
describe 'LDAP login' do
it 'user logs into GitLab using LDAP credentials' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
index 45cb5df8252..44071ec3e45 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
@@ -22,7 +22,7 @@ module QA
end
end
- context 'Manage', :orchestrated, :ldap, :skip_signup_disabled do
+ context 'Manage', :orchestrated, :ldap_no_tls, :skip_signup_disabled do
describe 'while LDAP is enabled' do
it_behaves_like 'registration and login'
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
index 9c64a9a3439..b18dee53cbc 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
@@ -2,7 +2,7 @@
module QA
context 'Create' do
- describe 'Git clone over HTTP', :ldap do
+ describe 'Git clone over HTTP', :ldap_no_tls do
let(:location) do
Page::Project::Show.act do
choose_repository_clone_http
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
index b9bed39662f..2f63a07e0c3 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
@@ -2,7 +2,7 @@
module QA
context 'Create' do
- describe 'Git push over HTTP', :ldap do
+ describe 'Git push over HTTP', :ldap_no_tls do
it 'user pushes code to the repository' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb
index 5f42cb00bd3..ac71cf52b6f 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb
@@ -2,7 +2,7 @@
module QA
context 'Create' do
- describe 'Protected branch support', :ldap do
+ describe 'Protected branch support', :ldap_no_tls do
let(:branch_name) { 'protected-branch' }
let(:commit_message) { 'Protected push commit message' }
let(:project) do
diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
index c98ede25b68..40cae0793dd 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
@@ -49,11 +49,13 @@ module QA
cluster.install_prometheus = true
cluster.install_runner = true
end
+ kubernetes_cluster.populate(:ingress_ip)
project.visit!
Page::Project::Menu.act { click_ci_cd_settings }
Page::Project::Settings::CICD.perform do |p|
- p.enable_auto_devops_with_domain("#{kubernetes_cluster.ingress_ip}.nip.io")
+ p.enable_auto_devops_with_domain(
+ "#{kubernetes_cluster.ingress_ip}.nip.io")
end
project.visit!
diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/factory/base_spec.rb
index 229f93a1041..d7b92052894 100644
--- a/qa/spec/factory/base_spec.rb
+++ b/qa/spec/factory/base_spec.rb
@@ -19,7 +19,7 @@ describe QA::Factory::Base do
before do
allow(subject).to receive(:current_url).and_return(product_location)
allow(subject).to receive(:new).and_return(factory)
- allow(QA::Factory::Product).to receive(:populate!).with(factory, product_location).and_return(product)
+ allow(QA::Factory::Product).to receive(:new).with(factory).and_return(product)
end
end
@@ -115,73 +115,134 @@ describe QA::Factory::Base do
end
end
- describe '.dependency' do
- let(:dependency) { spy('dependency') }
+ shared_context 'simple factory' do
+ subject do
+ Class.new(QA::Factory::Base) do
+ attribute :test do
+ 'block'
+ end
- before do
- stub_const('Some::MyDependency', dependency)
- end
+ attribute :no_block
- subject do
- Class.new(described_class) do
- dependency Some::MyDependency, as: :mydep do |factory|
- factory.something!
+ def fabricate!
+ 'any'
+ end
+
+ def self.current_url
+ 'http://stub'
end
end
end
- it 'appends a new dependency and accessors' do
- expect(subject.dependencies).to be_one
+ let(:factory) { subject.new }
+ end
+
+ describe '.attribute' do
+ include_context 'simple factory'
+
+ it 'appends new product attribute' do
+ expect(subject.attributes_names).to eq([:no_block, :test, :web_url])
end
- it 'defines dependency accessors' do
- expect(subject.new).to respond_to :mydep, :mydep=
+ context 'when the product attribute is populated via a block' do
+ it 'returns a fabrication product and defines factory attributes as its methods' do
+ result = subject.fabricate!(factory: factory)
+
+ expect(result).to be_a(QA::Factory::Product)
+ expect(result.test).to eq('block')
+ end
end
- describe 'dependencies fabrication' do
- let(:dependency) { double('dependency') }
- let(:instance) { spy('instance') }
+ context 'when the product attribute is populated via the api' do
+ let(:api_resource) { { no_block: 'api' } }
+
+ before do
+ expect(factory).to receive(:api_resource).and_return(api_resource)
+ end
+
+ it 'returns a fabrication product and defines factory attributes as its methods' do
+ result = subject.fabricate!(factory: factory)
+
+ expect(result).to be_a(QA::Factory::Product)
+ expect(result.no_block).to eq('api')
+ end
+
+ context 'when the attribute also has a block in the factory' do
+ let(:api_resource) { { test: 'api_with_block' } }
+
+ before do
+ allow(QA::Runtime::Logger).to receive(:info)
+ end
+
+ it 'returns the api value and emits an INFO log entry' do
+ result = subject.fabricate!(factory: factory)
- subject do
- Class.new(described_class) do
- dependency Some::MyDependency, as: :mydep
+ expect(result).to be_a(QA::Factory::Product)
+ expect(result.test).to eq('api_with_block')
+ expect(QA::Runtime::Logger)
+ .to have_received(:info).with(/api_with_block/)
end
end
+ end
+ context 'when the product attribute is populated via a factory attribute' do
before do
- stub_const('Some::MyDependency', dependency)
+ factory.test = 'value'
+ end
+
+ it 'returns a fabrication product and defines factory attributes as its methods' do
+ result = subject.fabricate!(factory: factory)
+
+ expect(result).to be_a(QA::Factory::Product)
+ expect(result.test).to eq('value')
+ end
- allow(subject).to receive(:new).and_return(instance)
- allow(subject).to receive(:current_url).and_return(product_location)
- allow(instance).to receive(:mydep).and_return(nil)
- expect(QA::Factory::Product).to receive(:populate!)
+ context 'when the api also has such response' do
+ before do
+ allow(factory).to receive(:api_resource).and_return({ test: 'api' })
+ end
+
+ it 'returns the factory attribute for the product' do
+ result = subject.fabricate!(factory: factory)
+
+ expect(result).to be_a(QA::Factory::Product)
+ expect(result.test).to eq('value')
+ end
end
+ end
- it 'builds all dependencies first' do
- expect(dependency).to receive(:fabricate!).once
+ context 'when the product attribute has no value' do
+ it 'raises an error because no values could be found' do
+ result = subject.fabricate!(factory: factory)
- subject.fabricate!
+ expect { result.no_block }
+ .to raise_error(described_class::NoValueError, "No value was computed for product no_block of factory #{factory.class.name}.")
end
end
end
- describe '.product' do
- include_context 'fabrication context'
+ describe '#web_url' do
+ include_context 'simple factory'
- subject do
- Class.new(described_class) do
- def fabricate!
- "any"
- end
+ it 'sets #web_url to #current_url after fabrication' do
+ subject.fabricate!(factory: factory)
- product :token
- end
+ expect(factory.web_url).to eq(subject.current_url)
+ end
+ end
+
+ describe '#visit!' do
+ include_context 'simple factory'
+
+ before do
+ allow(factory).to receive(:visit)
end
- it 'appends new product attribute' do
- expect(subject.attributes).to be_one
- expect(subject.attributes[0]).to be_a(QA::Factory::Product::Attribute)
- expect(subject.attributes[0].name).to eq(:token)
+ it 'calls #visit with the underlying #web_url' do
+ factory.web_url = subject.current_url
+ factory.visit!
+
+ expect(factory).to have_received(:visit).with(subject.current_url)
end
end
end
diff --git a/qa/spec/factory/dependency_spec.rb b/qa/spec/factory/dependency_spec.rb
deleted file mode 100644
index 657beddffb1..00000000000
--- a/qa/spec/factory/dependency_spec.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-describe QA::Factory::Dependency do
- let(:dependency) { spy('dependency' ) }
- let(:factory) { spy('factory') }
- let(:block) { spy('block') }
-
- let(:signature) do
- double('signature', name: :mydep, factory: dependency, block: block)
- end
-
- subject do
- described_class.new(factory, signature)
- end
-
- describe '#overridden?' do
- it 'returns true if factory has overridden dependency' do
- allow(factory).to receive(:mydep).and_return('something')
-
- expect(subject).to be_overridden
- end
-
- it 'returns false if dependency has not been overridden' do
- allow(factory).to receive(:mydep).and_return(nil)
-
- expect(subject).not_to be_overridden
- end
- end
-
- describe '#build!' do
- context 'when dependency has been overridden' do
- before do
- allow(subject).to receive(:overridden?).and_return(true)
- end
-
- it 'does not fabricate dependency' do
- subject.build!
-
- expect(dependency).not_to have_received(:fabricate!)
- end
- end
-
- context 'when dependency has not been overridden' do
- before do
- allow(subject).to receive(:overridden?).and_return(false)
- end
-
- it 'fabricates dependency' do
- subject.build!
-
- expect(dependency).to have_received(:fabricate!)
- end
-
- it 'sets product in the factory' do
- subject.build!
-
- expect(factory).to have_received(:mydep=).with(dependency)
- end
-
- it 'calls given block with dependency factory and caller factory' do
- expect(dependency).to receive(:fabricate!).and_yield(dependency)
-
- subject.build!
-
- expect(block).to have_received(:call).with(dependency, factory)
- end
-
- context 'with no block given' do
- let(:signature) do
- double('signature', name: :mydep, factory: dependency, block: nil)
- end
-
- it 'does not error' do
- subject.build!
-
- expect(dependency).to have_received(:fabricate!)
- end
- end
- end
- end
-end
diff --git a/qa/spec/factory/product_spec.rb b/qa/spec/factory/product_spec.rb
index 43b1d93d769..5b6eaa13e9c 100644
--- a/qa/spec/factory/product_spec.rb
+++ b/qa/spec/factory/product_spec.rb
@@ -1,73 +1,21 @@
describe QA::Factory::Product do
let(:factory) do
Class.new(QA::Factory::Base) do
- def foo
- 'bar'
+ attribute :test do
+ 'block'
end
+
+ attribute :no_block
end.new
end
let(:product) { spy('product') }
let(:product_location) { 'http://product_location' }
- subject { described_class.new(factory, product_location) }
-
- describe '.populate!' do
- before do
- expect(factory.class).to receive(:attributes).and_return(attributes)
- end
-
- context 'when the product attribute is populated via a block' do
- let(:attributes) do
- [QA::Factory::Product::Attribute.new(:test, proc { 'returned' })]
- end
-
- it 'returns a fabrication product and defines factory attributes as its methods' do
- result = described_class.populate!(factory, product_location)
-
- expect(result).to be_a(described_class)
- expect(result.test).to eq('returned')
- end
- end
-
- context 'when the product attribute is populated via the api' do
- let(:attributes) do
- [QA::Factory::Product::Attribute.new(:test)]
- end
-
- it 'returns a fabrication product and defines factory attributes as its methods' do
- expect(factory).to receive(:api_resource).and_return({ test: 'returned' })
-
- result = described_class.populate!(factory, product_location)
+ subject { described_class.new(factory) }
- expect(result).to be_a(described_class)
- expect(result.test).to eq('returned')
- end
- end
-
- context 'when the product attribute is populated via a factory attribute' do
- let(:attributes) do
- [QA::Factory::Product::Attribute.new(:foo)]
- end
-
- it 'returns a fabrication product and defines factory attributes as its methods' do
- result = described_class.populate!(factory, product_location)
-
- expect(result).to be_a(described_class)
- expect(result.foo).to eq('bar')
- end
- end
-
- context 'when the product attribute has no value' do
- let(:attributes) do
- [QA::Factory::Product::Attribute.new(:bar)]
- end
-
- it 'returns a fabrication product and defines factory attributes as its methods' do
- expect { described_class.populate!(factory, product_location) }
- .to raise_error(described_class::NoValueError, "No value was computed for product bar of factory #{factory.class.name}.")
- end
- end
+ before do
+ factory.web_url = product_location
end
describe '.visit!' do
diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb
index 9f17de4edbf..9d56353062b 100644
--- a/qa/spec/page/logging_spec.rb
+++ b/qa/spec/page/logging_spec.rb
@@ -3,9 +3,15 @@
require 'capybara/dsl'
describe QA::Support::Page::Logging do
+ include Support::StubENV
+
let(:page) { double().as_null_object }
before do
+ logger = Logger.new $stdout
+ logger.level = ::Logger::DEBUG
+ QA::Runtime::Logger.logger = logger
+
allow(Capybara).to receive(:current_session).and_return(page)
allow(page).to receive(:current_url).and_return('http://current-url')
allow(page).to receive(:has_css?).with(any_args).and_return(true)
diff --git a/qa/spec/runtime/logger_spec.rb b/qa/spec/runtime/logger_spec.rb
index 794e1f9bfe6..44be3381bff 100644
--- a/qa/spec/runtime/logger_spec.rb
+++ b/qa/spec/runtime/logger_spec.rb
@@ -1,6 +1,12 @@
# frozen_string_literal: true
describe QA::Runtime::Logger do
+ before do
+ logger = Logger.new $stdout
+ logger.level = ::Logger::DEBUG
+ described_class.logger = logger
+ end
+
it 'logs debug' do
expect { described_class.debug('test') }.to output(/DEBUG -- : test/).to_stdout_from_any_process
end
diff --git a/qa/spec/scenario/test/integration/ldap_spec.rb b/qa/spec/scenario/test/integration/ldap_spec.rb
index 198856aec3f..b6d798bf504 100644
--- a/qa/spec/scenario/test/integration/ldap_spec.rb
+++ b/qa/spec/scenario/test/integration/ldap_spec.rb
@@ -1,9 +1,17 @@
# frozen_string_literal: true
-describe QA::Scenario::Test::Integration::LDAP do
+describe QA::Scenario::Test::Integration::LDAPNoTLS do
context '#perform' do
it_behaves_like 'a QA scenario class' do
- let(:tags) { [:ldap] }
+ let(:tags) { [:ldap_no_tls] }
+ end
+ end
+end
+
+describe QA::Scenario::Test::Integration::LDAPTLS do
+ context '#perform' do
+ it_behaves_like 'a QA scenario class' do
+ let(:tags) { [:ldap_tls] }
end
end
end
diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb
index c365988a100..98946e4287b 100644
--- a/spec/controllers/boards/issues_controller_spec.rb
+++ b/spec/controllers/boards/issues_controller_spec.rb
@@ -208,11 +208,22 @@ describe Boards::IssuesController do
end
end
- context 'with unauthorized user' do
- it 'returns a forbidden 403 response' do
- create_issue user: guest, board: board, list: list1, title: 'New issue'
+ context 'with guest user' do
+ context 'in open list' do
+ it 'returns a successful 200 response' do
+ open_list = board.lists.create(list_type: :backlog)
+ create_issue user: guest, board: board, list: open_list, title: 'New issue'
- expect(response).to have_gitlab_http_status(403)
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context 'in label list' do
+ it 'returns a forbidden 403 response' do
+ create_issue user: guest, board: board, list: list1, title: 'New issue'
+
+ expect(response).to have_gitlab_http_status(403)
+ end
end
end
diff --git a/spec/controllers/groups/boards_controller_spec.rb b/spec/controllers/groups/boards_controller_spec.rb
index bf41aa0706f..d1d08391164 100644
--- a/spec/controllers/groups/boards_controller_spec.rb
+++ b/spec/controllers/groups/boards_controller_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Groups::BoardsController do
let(:group) { create(:group) }
- let(:user) { create(:user) }
+ let(:user) { create(:user) }
before do
group.add_maintainer(user)
@@ -22,6 +22,27 @@ describe Groups::BoardsController do
expect(response.content_type).to eq 'text/html'
end
+ it 'redirects to latest visited board' do
+ board = create(:board, group: group)
+ create(:board_group_recent_visit, group: board.group, board: board, user: user)
+
+ list_boards
+
+ expect(response).to redirect_to(group_board_path(id: board.id))
+ end
+
+ it 'renders template if visited board is not found' do
+ visited = double
+
+ allow(visited).to receive(:board_id).and_return(12)
+ allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited)
+
+ list_boards
+
+ expect(response).to render_template :index
+ expect(response.content_type).to eq 'text/html'
+ end
+
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_cross_project, :global).and_return(true)
@@ -35,12 +56,30 @@ describe Groups::BoardsController do
expect(response.content_type).to eq 'text/html'
end
end
+
+ context 'when user is signed out' do
+ let(:group) { create(:group, :public) }
+
+ it 'renders template' do
+ sign_out(user)
+
+ board = create(:board, group: group)
+ create(:board_group_recent_visit, group: board.group, board: board, user: user)
+
+ list_boards
+
+ expect(response).to render_template :index
+ expect(response.content_type).to eq 'text/html'
+ end
+ end
end
context 'when format is JSON' do
it 'return an array with one group board' do
create(:board, group: group)
+ expect(Boards::Visits::LatestService).not_to receive(:new)
+
list_boards format: :json
parsed_response = JSON.parse(response.body)
@@ -74,7 +113,7 @@ describe Groups::BoardsController do
context 'when format is HTML' do
it 'renders template' do
- read_board board: board
+ expect { read_board board: board }.to change(BoardGroupRecentVisit, :count).by(1)
expect(response).to render_template :show
expect(response.content_type).to eq 'text/html'
@@ -93,10 +132,25 @@ describe Groups::BoardsController do
expect(response.content_type).to eq 'text/html'
end
end
+
+ context 'when user is signed out' do
+ let(:group) { create(:group, :public) }
+
+ it 'does not save visit' do
+ sign_out(user)
+
+ expect { read_board board: board }.to change(BoardGroupRecentVisit, :count).by(0)
+
+ expect(response).to render_template :show
+ expect(response.content_type).to eq 'text/html'
+ end
+ end
end
context 'when format is JSON' do
it 'returns project board' do
+ expect(Boards::Visits::CreateService).not_to receive(:new)
+
read_board board: board, format: :json
expect(response).to match_response_schema('board')
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index 096efc1c7b2..667eaa5e34f 100644
--- a/spec/controllers/projects/boards_controller_spec.rb
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -28,6 +28,27 @@ describe Projects::BoardsController do
expect(response.content_type).to eq 'text/html'
end
+ it 'redirects to latest visited board' do
+ board = create(:board, project: project)
+ create(:board_project_recent_visit, project: board.project, board: board, user: user)
+
+ list_boards
+
+ expect(response).to redirect_to(namespace_project_board_path(id: board.id))
+ end
+
+ it 'renders template if visited board is not found' do
+ visited = double
+
+ allow(visited).to receive(:board_id).and_return(12)
+ allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited)
+
+ list_boards
+
+ expect(response).to render_template :index
+ expect(response.content_type).to eq 'text/html'
+ end
+
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
@@ -41,12 +62,30 @@ describe Projects::BoardsController do
expect(response.content_type).to eq 'text/html'
end
end
+
+ context 'when user is signed out' do
+ let(:project) { create(:project, :public) }
+
+ it 'renders template' do
+ sign_out(user)
+
+ board = create(:board, project: project)
+ create(:board_project_recent_visit, project: board.project, board: board, user: user)
+
+ list_boards
+
+ expect(response).to render_template :index
+ expect(response.content_type).to eq 'text/html'
+ end
+ end
end
context 'when format is JSON' do
it 'returns a list of project boards' do
create_list(:board, 2, project: project)
+ expect(Boards::Visits::LatestService).not_to receive(:new)
+
list_boards format: :json
parsed_response = JSON.parse(response.body)
@@ -98,7 +137,7 @@ describe Projects::BoardsController do
context 'when format is HTML' do
it 'renders template' do
- read_board board: board
+ expect { read_board board: board }.to change(BoardProjectRecentVisit, :count).by(1)
expect(response).to render_template :show
expect(response.content_type).to eq 'text/html'
@@ -117,10 +156,25 @@ describe Projects::BoardsController do
expect(response.content_type).to eq 'text/html'
end
end
+
+ context 'when user is signed out' do
+ let(:project) { create(:project, :public) }
+
+ it 'does not save visit' do
+ sign_out(user)
+
+ expect { read_board board: board }.to change(BoardProjectRecentVisit, :count).by(0)
+
+ expect(response).to render_template :show
+ expect(response.content_type).to eq 'text/html'
+ end
+ end
end
context 'when format is JSON' do
it 'returns project board' do
+ expect(Boards::Visits::CreateService).not_to receive(:new)
+
read_board board: board, format: :json
expect(response).to match_response_schema('board')
diff --git a/spec/factories/board_group_recent_visit.rb b/spec/factories/board_group_recent_visit.rb
new file mode 100644
index 00000000000..97ad5d6d068
--- /dev/null
+++ b/spec/factories/board_group_recent_visit.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :board_group_recent_visit do
+ user
+ group
+ board
+ end
+end
diff --git a/spec/factories/board_project_recent_visit.rb b/spec/factories/board_project_recent_visit.rb
new file mode 100644
index 00000000000..49ae4d7b391
--- /dev/null
+++ b/spec/factories/board_project_recent_visit.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :board_project_recent_visit do
+ user
+ project
+ board
+ end
+end
diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb
index 134eb25e4b1..fe475e1f7a0 100644
--- a/spec/fast_spec_helper.rb
+++ b/spec/fast_spec_helper.rb
@@ -8,3 +8,4 @@ require_relative 'support/rspec'
require 'active_support/all'
ActiveSupport::Dependencies.autoload_paths << 'lib'
+ActiveSupport::Dependencies.autoload_paths << 'ee/lib'
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index 0bf1ecbc433..164442a47f5 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -94,8 +94,14 @@ describe 'Issue Boards new issue', :js do
wait_for_requests
end
- it 'does not display new issue button' do
- expect(page).to have_selector('.issue-count-badge-add-button', count: 0)
+ it 'displays new issue button in open list' do
+ expect(first('.board')).to have_selector('.issue-count-badge-add-button', count: 1)
+ end
+
+ it 'does not display new issue button in label list' do
+ page.within('.board:nth-child(2)') do
+ expect(page).not_to have_selector('.issue-count-badge-add-button')
+ end
end
end
end
diff --git a/spec/features/issues/resource_label_events_spec.rb b/spec/features/issues/resource_label_events_spec.rb
index 40c452c991a..b0764db7751 100644
--- a/spec/features/issues/resource_label_events_spec.rb
+++ b/spec/features/issues/resource_label_events_spec.rb
@@ -7,6 +7,7 @@ describe 'List issue resource label events', :js do
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project, author: user) }
let!(:label) { create(:label, project: project, title: 'foo') }
+ let!(:user_status) { create(:user_status, user: user) }
context 'when user displays the issue' do
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue, note: 'some note') }
@@ -23,6 +24,12 @@ describe 'List issue resource label events', :js do
expect(find("#note_#{event.discussion_id}")).to have_content 'added foo label'
end
end
+
+ it 'shows the user status on the system note for the label' do
+ page.within("#note_#{event.discussion_id}") do
+ expect(page).to show_user_status user_status
+ end
+ end
end
context 'when user adds label to the issue' do
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 0689c843104..2f164ffa8b0 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -57,11 +57,37 @@ describe IssuesFinder do
end
context 'filtering by no assignee' do
- let(:params) { { assignee_id: 0 } }
+ let(:params) { { assignee_id: 'None' } }
- it 'returns issues not assign to any assignee' do
+ it 'returns issues not assigned to any assignee' do
expect(issues).to contain_exactly(issue4)
end
+
+ it 'returns issues not assigned to any assignee' do
+ params[:assignee_id] = 0
+
+ expect(issues).to contain_exactly(issue4)
+ end
+
+ it 'returns issues not assigned to any assignee' do
+ params[:assignee_id] = 'none'
+
+ expect(issues).to contain_exactly(issue4)
+ end
+ end
+
+ context 'filtering by any assignee' do
+ let(:params) { { assignee_id: 'Any' } }
+
+ it 'returns issues assigned to any assignee' do
+ expect(issues).to contain_exactly(issue1, issue2, issue3)
+ end
+
+ it 'returns issues assigned to any assignee' do
+ params[:assignee_id] = 'any'
+
+ expect(issues).to contain_exactly(issue1, issue2, issue3)
+ end
end
context 'filtering by group_id' do
diff --git a/spec/javascripts/jobs/components/sidebar_spec.js b/spec/javascripts/jobs/components/sidebar_spec.js
index 460a2e1b5da..424092d2d88 100644
--- a/spec/javascripts/jobs/components/sidebar_spec.js
+++ b/spec/javascripts/jobs/components/sidebar_spec.js
@@ -140,10 +140,11 @@ describe('Sidebar details block', () => {
});
describe('while fetching stages', () => {
- it('renders dropdown with More label', () => {
+ it('it does not render dropdown', () => {
+ store.dispatch('requestStages');
vm = mountComponentWithStore(SidebarComponent, { store });
- expect(vm.$el.querySelector('.js-selected-stage').textContent.trim()).toEqual('More');
+ expect(vm.$el.querySelector('.js-selected-stage')).toBeNull();
});
});
diff --git a/spec/javascripts/jobs/store/mutations_spec.js b/spec/javascripts/jobs/store/mutations_spec.js
index 4230a7c42cf..d7908efcf13 100644
--- a/spec/javascripts/jobs/store/mutations_spec.js
+++ b/spec/javascripts/jobs/store/mutations_spec.js
@@ -124,8 +124,8 @@ describe('Jobs Store Mutations', () => {
expect(stateCopy.job).toEqual({ id: 1312321 });
});
- it('sets selectedStage when the selectedStage is More', () => {
- expect(stateCopy.selectedStage).toEqual('More');
+ it('sets selectedStage when the selectedStage is empty', () => {
+ expect(stateCopy.selectedStage).toEqual('');
mutations[types.RECEIVE_JOB_SUCCESS](stateCopy, { id: 1312321, stage: 'deploy' });
expect(stateCopy.selectedStage).toEqual('deploy');
diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/lib/utils/datetime_utility_spec.js
index 2821f4d6793..de6b96aab57 100644
--- a/spec/javascripts/datetime_utility_spec.js
+++ b/spec/javascripts/lib/utils/datetime_utility_spec.js
@@ -352,3 +352,21 @@ describe('prettyTime methods', () => {
});
});
});
+
+describe('calculateRemainingMilliseconds', () => {
+ beforeEach(() => {
+ spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime());
+ });
+
+ it('calculates the remaining time for a given end date', () => {
+ const milliseconds = datetimeUtility.calculateRemainingMilliseconds('2063-04-04T01:44:03Z');
+
+ expect(milliseconds).toBe(3723000);
+ });
+
+ it('returns 0 if the end date has passed', () => {
+ const milliseconds = datetimeUtility.calculateRemainingMilliseconds('2063-04-03T00:00:00Z');
+
+ expect(milliseconds).toBe(0);
+ });
+});
diff --git a/spec/javascripts/lib/utils/text_markdown_spec.js b/spec/javascripts/lib/utils/text_markdown_spec.js
index bb7a29fe30a..b9e805628f8 100644
--- a/spec/javascripts/lib/utils/text_markdown_spec.js
+++ b/spec/javascripts/lib/utils/text_markdown_spec.js
@@ -166,6 +166,33 @@ describe('init markdown', () => {
expect(textArea.selectionStart).toEqual(expectedText.lastIndexOf(select));
expect(textArea.selectionEnd).toEqual(expectedText.lastIndexOf(select) + select.length);
});
+
+ it('should support selected urls', () => {
+ const expectedUrl = 'http://www.gitlab.com';
+ const expectedSelectionText = 'text';
+ const expectedText = `text [${expectedSelectionText}](${expectedUrl}) text`;
+ const initialValue = `text ${expectedUrl} text`;
+
+ textArea.value = initialValue;
+ const selectedIndex = initialValue.indexOf(expectedUrl);
+ textArea.setSelectionRange(selectedIndex, selectedIndex + expectedUrl.length);
+
+ insertMarkdownText({
+ textArea,
+ text: textArea.value,
+ tag,
+ blockTag: null,
+ selected: expectedUrl,
+ wrap: false,
+ select,
+ });
+
+ expect(textArea.value).toEqual(expectedText);
+ expect(textArea.selectionStart).toEqual(expectedText.indexOf(expectedSelectionText, 1));
+ expect(textArea.selectionEnd).toEqual(
+ expectedText.indexOf(expectedSelectionText, 1) + expectedSelectionText.length,
+ );
+ });
});
});
});
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index a3477c5f8c6..565b87de248 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -113,6 +113,22 @@ describe('Dashboard', () => {
});
});
+ it('hides the dropdown list when there is no environments', done => {
+ const component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: { ...propsData, hasMetrics: true, showPanels: false },
+ });
+
+ component.store.storeEnvironmentsData([]);
+
+ setTimeout(() => {
+ const dropdownMenuEnvironments = component.$el.querySelectorAll('.dropdown-menu ul');
+
+ expect(dropdownMenuEnvironments.length).toEqual(0);
+ done();
+ });
+ });
+
it('renders the dropdown with a single is-active element', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
diff --git a/spec/javascripts/vue_shared/components/gl_countdown_spec.js b/spec/javascripts/vue_shared/components/gl_countdown_spec.js
new file mode 100644
index 00000000000..929ffe219f4
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/gl_countdown_spec.js
@@ -0,0 +1,77 @@
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import Vue from 'vue';
+import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+
+describe('GlCountdown', () => {
+ const Component = Vue.extend(GlCountdown);
+ let vm;
+ let now = '2000-01-01T00:00:00Z';
+
+ beforeEach(() => {
+ spyOn(Date, 'now').and.callFake(() => new Date(now).getTime());
+ jasmine.clock().install();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ jasmine.clock().uninstall();
+ });
+
+ describe('when there is time remaining', () => {
+ beforeEach(done => {
+ vm = mountComponent(Component, {
+ endDateString: '2000-01-01T01:02:03Z',
+ });
+
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('displays remaining time', () => {
+ expect(vm.$el).toContainText('01:02:03');
+ });
+
+ it('updates remaining time', done => {
+ now = '2000-01-01T00:00:01Z';
+ jasmine.clock().tick(1000);
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el).toContainText('01:02:02');
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('when there is no time remaining', () => {
+ beforeEach(done => {
+ vm = mountComponent(Component, {
+ endDateString: '1900-01-01T00:00:00Z',
+ });
+
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('displays 00:00:00', () => {
+ expect(vm.$el).toContainText('00:00:00');
+ });
+ });
+
+ describe('when an invalid date is passed', () => {
+ it('throws a validation error', () => {
+ spyOn(Vue.config, 'warnHandler').and.stub();
+ vm = mountComponent(Component, {
+ endDateString: 'this is invalid',
+ });
+
+ expect(Vue.config.warnHandler).toHaveBeenCalledTimes(1);
+ const [errorMessage] = Vue.config.warnHandler.calls.argsFor(0);
+
+ expect(errorMessage).toMatch(/^Invalid prop: .* "endDateString"/);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/directives/tooltip_spec.js b/spec/javascripts/vue_shared/directives/tooltip_spec.js
index 305d2fd5af4..1d516a280b0 100644
--- a/spec/javascripts/vue_shared/directives/tooltip_spec.js
+++ b/spec/javascripts/vue_shared/directives/tooltip_spec.js
@@ -13,24 +13,45 @@ describe('Tooltip directive', () => {
describe('with a single tooltip', () => {
beforeEach(() => {
- const SomeComponent = Vue.extend({
+ setFixtures('<div id="dummy-element"></div>');
+ vm = new Vue({
+ el: '#dummy-element',
directives: {
tooltip,
},
- template: `
- <div
- v-tooltip
- title="foo">
- </div>
- `,
+ data() {
+ return {
+ tooltip: 'some text',
+ };
+ },
+ template: '<div v-tooltip :title="tooltip"></div>',
});
-
- vm = new SomeComponent().$mount();
});
it('should have tooltip plugin applied', () => {
expect($(vm.$el).data('bs.tooltip')).toBeDefined();
});
+
+ it('displays the title as tooltip', () => {
+ $(vm.$el).tooltip('show');
+ const tooltipElement = document.querySelector('.tooltip-inner');
+
+ expect(tooltipElement.innerText).toContain('some text');
+ });
+
+ it('updates a visible tooltip', done => {
+ $(vm.$el).tooltip('show');
+ const tooltipElement = document.querySelector('.tooltip-inner');
+
+ vm.tooltip = 'other text';
+
+ Vue.nextTick()
+ .then(() => {
+ expect(tooltipElement).toContainText('other text');
+ done();
+ })
+ .catch(done.fail);
+ });
});
describe('with multiple tooltips', () => {
diff --git a/spec/lib/api/helpers/custom_validators_spec.rb b/spec/lib/api/helpers/custom_validators_spec.rb
new file mode 100644
index 00000000000..41e6fb47b11
--- /dev/null
+++ b/spec/lib/api/helpers/custom_validators_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe API::Helpers::CustomValidators do
+ let(:scope) do
+ Struct.new(:opts) do
+ def full_name(attr_name)
+ attr_name
+ end
+ end
+ end
+
+ describe API::Helpers::CustomValidators::Absence do
+ subject do
+ described_class.new(['test'], {}, false, scope.new)
+ end
+
+ context 'empty param' do
+ it 'does not raise a validation error' do
+ expect_no_validation_error({})
+ end
+ end
+
+ context 'invalid parameters' do
+ it 'should raise a validation error' do
+ expect_validation_error({ 'test' => 'some_value' })
+ end
+ end
+ end
+
+ describe API::Helpers::CustomValidators::IntegerNoneAny do
+ subject do
+ described_class.new(['test'], {}, false, scope.new)
+ end
+
+ context 'valid parameters' do
+ it 'does not raise a validation error' do
+ expect_no_validation_error({ 'test' => 2 })
+ expect_no_validation_error({ 'test' => 100 })
+ expect_no_validation_error({ 'test' => 'None' })
+ expect_no_validation_error({ 'test' => 'Any' })
+ expect_no_validation_error({ 'test' => 'none' })
+ expect_no_validation_error({ 'test' => 'any' })
+ end
+ end
+
+ context 'invalid parameters' do
+ it 'should raise a validation error' do
+ expect_validation_error({ 'test' => 'some_other_string' })
+ end
+ end
+ end
+
+ def expect_no_validation_error(params)
+ expect { validate_test_param!(params) }.not_to raise_error
+ end
+
+ def expect_validation_error(params)
+ expect { validate_test_param!(params) }.to raise_error(Grape::Exceptions::Validation)
+ end
+
+ def validate_test_param!(params)
+ subject.validate_param!('test', params)
+ end
+end
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb
index 4df426c54ae..81804ba5c76 100644
--- a/spec/lib/gitlab/checks/change_access_spec.rb
+++ b/spec/lib/gitlab/checks/change_access_spec.rb
@@ -10,13 +10,16 @@ describe Gitlab::Checks::ChangeAccess do
let(:ref) { 'refs/heads/master' }
let(:changes) { { oldrev: oldrev, newrev: newrev, ref: ref } }
let(:protocol) { 'ssh' }
+ let(:timeout) { Gitlab::GitAccess::INTERNAL_TIMEOUT }
+ let(:logger) { Gitlab::Checks::TimedLogger.new(timeout: timeout) }
subject(:change_access) do
described_class.new(
changes,
project: project,
user_access: user_access,
- protocol: protocol
+ protocol: protocol,
+ logger: logger
)
end
@@ -30,6 +33,19 @@ describe Gitlab::Checks::ChangeAccess do
end
end
+ context 'when time limit was reached' do
+ it 'raises a TimeoutError' do
+ logger = Gitlab::Checks::TimedLogger.new(start_time: timeout.ago, timeout: timeout)
+ access = described_class.new(changes,
+ project: project,
+ user_access: user_access,
+ protocol: protocol,
+ logger: logger)
+
+ expect { access.exec }.to raise_error(Gitlab::Checks::TimedLogger::TimeoutError)
+ end
+ end
+
context 'when the user is not allowed to push to the repo' do
it 'raises an error' do
expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false)
diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
index ec22e3a198e..0488720cec8 100644
--- a/spec/lib/gitlab/checks/lfs_integrity_spec.rb
+++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe Gitlab::Checks::LfsIntegrity do
include ProjectForksHelper
+ let!(:time_left) { 50 }
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:newrev) do
@@ -15,7 +16,7 @@ describe Gitlab::Checks::LfsIntegrity do
operations.commit_tree('8856a329dd38ca86dfb9ce5aa58a16d88cc119bd', "New LFS objects")
end
- subject { described_class.new(project, newrev) }
+ subject { described_class.new(project, newrev, time_left) }
describe '#objects_missing?' do
let(:blob_object) { repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') }
diff --git a/spec/lib/gitlab/checks/timed_logger_spec.rb b/spec/lib/gitlab/checks/timed_logger_spec.rb
new file mode 100644
index 00000000000..0ed3940c038
--- /dev/null
+++ b/spec/lib/gitlab/checks/timed_logger_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Checks::TimedLogger do
+ let!(:timeout) { 50.seconds }
+ let!(:start) { Time.now }
+ let!(:ref) { "bar" }
+ let!(:logger) { described_class.new(start_time: start, timeout: timeout) }
+ let!(:log_messages) do
+ {
+ foo: "Foo message..."
+ }
+ end
+
+ before do
+ logger.append_message("Checking ref: #{ref}")
+ end
+
+ describe '#log_timed' do
+ it 'logs message' do
+ Timecop.freeze(start + 30.seconds) do
+ logger.log_timed(log_messages[:foo], start) { bar_check }
+ end
+
+ expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (30000.0ms)")
+ end
+
+ context 'when time limit was reached' do
+ it 'cancels action' do
+ Timecop.freeze(start + 50.seconds) do
+ expect do
+ logger.log_timed(log_messages[:foo], start) do
+ bar_check
+ end
+ end.to raise_error(described_class::TimeoutError)
+ end
+
+ expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (cancelled)")
+ end
+
+ it 'cancels action with time elapsed if work was performed' do
+ Timecop.freeze(start + 30.seconds) do
+ expect do
+ logger.log_timed(log_messages[:foo], start) do
+ grpc_check
+ end
+ end.to raise_error(described_class::TimeoutError)
+
+ expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (cancelled after 30000.0ms)")
+ end
+ end
+ end
+ end
+
+ def bar_check
+ 2 + 2
+ end
+
+ def grpc_check
+ raise GRPC::DeadlineExceeded
+ end
+end
diff --git a/spec/lib/gitlab/git/lfs_changes_spec.rb b/spec/lib/gitlab/git/lfs_changes_spec.rb
index c5e7ab959b2..d035df7e0c2 100644
--- a/spec/lib/gitlab/git/lfs_changes_spec.rb
+++ b/spec/lib/gitlab/git/lfs_changes_spec.rb
@@ -15,5 +15,9 @@ describe Gitlab::Git::LfsChanges do
it 'limits new_objects using object_limit' do
expect(subject.new_pointers(object_limit: 1)).to eq([])
end
+
+ it 'times out if given a small dynamic timeout' do
+ expect { subject.new_pointers(dynamic_timeout: 0.001) }.to raise_error(GRPC::DeadlineExceeded)
+ end
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index e7da5565c26..a417ef77c9e 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -934,6 +934,16 @@ describe Gitlab::GitAccess do
# There is still an N+1 query with protected branches
expect { access.check('git-receive-pack', changes) }.not_to exceed_query_limit(control_count).with_threshold(1)
end
+
+ it 'raises TimeoutError when #check_single_change_access raises a timeout error' do
+ message = "Push operation timed out\n\nTiming information for debugging purposes:\nRunning checks for ref: wow"
+
+ expect_next_instance_of(Gitlab::Checks::ChangeAccess) do |check|
+ expect(check).to receive(:exec).and_raise(Gitlab::Checks::TimedLogger::TimeoutError)
+ end
+
+ expect { access.check('git-receive-pack', changes) }.to raise_error(described_class::TimeoutError, message)
+ end
end
end
diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb
index 0385dd762c2..1e583f4cee2 100644
--- a/spec/lib/gitlab/identifier_spec.rb
+++ b/spec/lib/gitlab/identifier_spec.rb
@@ -11,11 +11,8 @@ describe Gitlab::Identifier do
describe '#identify' do
context 'without an identifier' do
- it 'identifies the user using a commit' do
- expect(identifier).to receive(:identify_using_commit)
- .with(project, '123')
-
- identifier.identify('', project, '123')
+ it 'returns nil' do
+ expect(identifier.identify('')).to be nil
end
end
@@ -24,7 +21,7 @@ describe Gitlab::Identifier do
expect(identifier).to receive(:identify_using_user)
.with("user-#{user.id}")
- identifier.identify("user-#{user.id}", project, '123')
+ identifier.identify("user-#{user.id}")
end
end
@@ -33,49 +30,11 @@ describe Gitlab::Identifier do
expect(identifier).to receive(:identify_using_ssh_key)
.with("key-#{key.id}")
- identifier.identify("key-#{key.id}", project, '123')
+ identifier.identify("key-#{key.id}")
end
end
end
- describe '#identify_using_commit' do
- it "returns the User for an existing commit author's Email address" do
- commit = double(:commit, author: user, author_email: user.email)
-
- expect(project).to receive(:commit).with('123').and_return(commit)
-
- expect(identifier.identify_using_commit(project, '123')).to eq(user)
- end
-
- it 'returns nil when no user could be found' do
- allow(project).to receive(:commit).with('123').and_return(nil)
-
- expect(identifier.identify_using_commit(project, '123')).to be_nil
- end
-
- it 'returns nil when the commit does not have an author Email' do
- commit = double(:commit, author_email: nil)
-
- expect(project).to receive(:commit).with('123').and_return(commit)
-
- expect(identifier.identify_using_commit(project, '123')).to be_nil
- end
-
- it 'caches the found users per Email' do
- commit = double(:commit, author: user, author_email: user.email)
-
- expect(project).to receive(:commit).with('123').twice.and_return(commit)
-
- 2.times do
- expect(identifier.identify_using_commit(project, '123')).to eq(user)
- end
- end
-
- it 'returns nil if the project & ref are not present' do
- expect(identifier.identify_using_commit(nil, nil)).to be_nil
- end
- end
-
describe '#identify_using_user' do
it 'returns the User for an existing ID in the identifier' do
found = identifier.identify_using_user("user-#{user.id}")
diff --git a/spec/lib/gitlab/patch/draw_route_spec.rb b/spec/lib/gitlab/patch/draw_route_spec.rb
new file mode 100644
index 00000000000..4009b903dc3
--- /dev/null
+++ b/spec/lib/gitlab/patch/draw_route_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::Patch::DrawRoute do
+ subject do
+ Class.new do
+ include Gitlab::Patch::DrawRoute
+
+ def route_path(route_name)
+ File.expand_path("../../../../#{route_name}", __dir__)
+ end
+ end.new
+ end
+
+ before do
+ allow(subject).to receive(:instance_eval)
+ end
+
+ it 'evaluates CE only route' do
+ subject.draw(:help)
+
+ expect(subject).to have_received(:instance_eval)
+ .with(File.read(subject.route_path('config/routes/help.rb')))
+ .once
+
+ expect(subject).to have_received(:instance_eval)
+ .once
+ end
+end
diff --git a/spec/models/board_group_recent_visit_spec.rb b/spec/models/board_group_recent_visit_spec.rb
new file mode 100644
index 00000000000..59ad4e5417e
--- /dev/null
+++ b/spec/models/board_group_recent_visit_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BoardGroupRecentVisit do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:board) { create(:board, group: group) }
+
+ describe 'relationships' do
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:group) }
+ it { is_expected.to belong_to(:board) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:user) }
+ it { is_expected.to validate_presence_of(:group) }
+ it { is_expected.to validate_presence_of(:board) }
+ end
+
+ describe '#visited' do
+ it 'creates a visit if one does not exists' do
+ expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1)
+ end
+
+ shared_examples 'was visited previously' do
+ let!(:visit) { create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago }
+
+ it 'updates the timestamp' do
+ Timecop.freeze do
+ described_class.visited!(user, board)
+
+ expect(described_class.count).to eq 1
+ expect(described_class.first.updated_at).to be_like_time(Time.zone.now)
+ end
+ end
+ end
+
+ it_behaves_like 'was visited previously'
+
+ context 'when we try to create a visit that is not unique' do
+ before do
+ expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
+ expect(described_class).to receive(:find_or_create_by).and_return(visit)
+ end
+
+ it_behaves_like 'was visited previously'
+ end
+ end
+
+ describe '#latest' do
+ it 'returns the most recent visited' do
+ board2 = create(:board, group: group)
+ board3 = create(:board, group: group)
+
+ create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago
+ create :board_group_recent_visit, group: board2.group, board: board2, user: user, updated_at: 5.days.ago
+ recent = create :board_group_recent_visit, group: board3.group, board: board3, user: user, updated_at: 1.day.ago
+
+ expect(described_class.latest(user, group)).to eq recent
+ end
+ end
+end
diff --git a/spec/models/board_project_recent_visit_spec.rb b/spec/models/board_project_recent_visit_spec.rb
new file mode 100644
index 00000000000..275581945fa
--- /dev/null
+++ b/spec/models/board_project_recent_visit_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BoardProjectRecentVisit do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:board) { create(:board, project: project) }
+
+ describe 'relationships' do
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:board) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:user) }
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:board) }
+ end
+
+ describe '#visited' do
+ it 'creates a visit if one does not exists' do
+ expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1)
+ end
+
+ shared_examples 'was visited previously' do
+ let!(:visit) { create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago }
+
+ it 'updates the timestamp' do
+ Timecop.freeze do
+ described_class.visited!(user, board)
+
+ expect(described_class.count).to eq 1
+ expect(described_class.first.updated_at).to be_like_time(Time.zone.now)
+ end
+ end
+ end
+
+ it_behaves_like 'was visited previously'
+
+ context 'when we try to create a visit that is not unique' do
+ before do
+ expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
+ expect(described_class).to receive(:find_or_create_by).and_return(visit)
+ end
+
+ it_behaves_like 'was visited previously'
+ end
+ end
+
+ describe '#latest' do
+ it 'returns the most recent visited' do
+ board2 = create(:board, project: project)
+ board3 = create(:board, project: project)
+
+ create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago
+ create :board_project_recent_visit, project: board2.project, board: board2, user: user, updated_at: 5.days.ago
+ recent = create :board_project_recent_visit, project: board3.project, board: board3, user: user, updated_at: 1.day.ago
+
+ expect(described_class.latest(user, project)).to eq recent
+ end
+ end
+end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index c65e0b81451..1de95d881a7 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -334,7 +334,7 @@ describe Environment do
describe '#has_terminals?' do
subject { environment.has_terminals? }
- context 'when the enviroment is available' do
+ context 'when the environment is available' do
context 'with a deployment service' do
shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
context 'and a deployment' do
@@ -447,7 +447,7 @@ describe Environment do
describe '#has_metrics?' do
subject { environment.has_metrics? }
- context 'when the enviroment is available' do
+ context 'when the environment is available' do
context 'with a deployment service' do
let(:project) { create(:prometheus_project) }
@@ -456,8 +456,8 @@ describe Environment do
it { is_expected.to be_truthy }
end
- context 'but no deployments' do
- it { is_expected.to be_falsy }
+ context 'and no deployments' do
+ it { is_expected.to be_truthy }
end
end
@@ -504,39 +504,6 @@ describe Environment do
end
end
- describe '#has_metrics?' do
- subject { environment.has_metrics? }
-
- context 'when the enviroment is available' do
- context 'with a deployment service' do
- let(:project) { create(:prometheus_project) }
-
- context 'and a deployment' do
- let!(:deployment) { create(:deployment, environment: environment) }
- it { is_expected.to be_truthy }
- end
-
- context 'but no deployments' do
- it { is_expected.to be_falsy }
- end
- end
-
- context 'without a monitoring service' do
- it { is_expected.to be_falsy }
- end
- end
-
- context 'when the environment is unavailable' do
- let(:project) { create(:prometheus_project) }
-
- before do
- environment.stop
- end
-
- it { is_expected.to be_falsy }
- end
- end
-
describe '#additional_metrics' do
let(:project) { create(:prometheus_project) }
subject { environment.additional_metrics }
diff --git a/spec/rack_servers/configs/config.ru b/spec/rack_servers/configs/config.ru
new file mode 100644
index 00000000000..63daeb9eec5
--- /dev/null
+++ b/spec/rack_servers/configs/config.ru
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+app = proc do |env|
+ if env['REQUEST_METHOD'] == 'GET'
+ [200, {}, ["#{Process.pid}"]]
+ else
+ Process.kill(env['QUERY_STRING'], Process.pid)
+ [200, {}, ['Bye!']]
+ end
+end
+
+run app
diff --git a/spec/rack_servers/configs/puma.rb b/spec/rack_servers/configs/puma.rb
new file mode 100644
index 00000000000..d6b6d83d648
--- /dev/null
+++ b/spec/rack_servers/configs/puma.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+# Note: this file is used for testing puma in `spec/rack_servers/puma_spec.rb` only
+# Note: as per the convention in `config/puma.example.development.rb`,
+# this file will replace `/home/git` with the actual working directory
+
+directory '/home/git'
+threads 1, 10
+queue_requests false
+pidfile '/home/git/gitlab/tmp/pids/puma.pid'
+bind 'unix:///home/git/gitlab/tmp/tests/puma.socket'
+workers 1
+preload_app!
+worker_timeout 60
+
+require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"
+require_relative "/home/git/gitlab/lib/gitlab/cluster/puma_worker_killer_initializer"
+
+before_fork do
+ Gitlab::Cluster::PumaWorkerKillerInitializer.start @config.options
+ Gitlab::Cluster::LifecycleEvents.do_before_fork
+end
+
+Gitlab::Cluster::LifecycleEvents.set_puma_options @config.options
+on_worker_boot do
+ Gitlab::Cluster::LifecycleEvents.do_worker_start
+ File.write('/home/git/gitlab/tmp/tests/puma-worker-ready', Process.pid)
+end
+
+on_restart do
+ Gitlab::Cluster::LifecycleEvents.do_master_restart
+end
diff --git a/spec/rack_servers/puma_spec.rb b/spec/rack_servers/puma_spec.rb
new file mode 100644
index 00000000000..431fab87857
--- /dev/null
+++ b/spec/rack_servers/puma_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'fileutils'
+
+require 'excon'
+
+require 'spec_helper'
+
+describe 'Puma' do
+ before(:all) do
+ project_root = File.expand_path('../..', __dir__)
+
+ config_lines = File.read('spec/rack_servers/configs/puma.rb')
+ .gsub('/home/git/gitlab', project_root)
+ .gsub('/home/git', project_root)
+
+ config_path = File.join(project_root, "tmp/tests/puma.rb")
+ @socket_path = File.join(project_root, 'tmp/tests/puma.socket')
+
+ File.write(config_path, config_lines)
+
+ cmd = %W[puma -e test -C #{config_path} #{File.join(__dir__, 'configs/config.ru')}]
+ @puma_master_pid = spawn(*cmd)
+ wait_puma_boot!(@puma_master_pid, File.join(project_root, 'tmp/tests/puma-worker-ready'))
+ WebMock.allow_net_connect!
+ end
+
+ %w[SIGQUIT SIGTERM SIGKILL].each do |signal|
+ it "has a worker that self-terminates on signal #{signal}" do
+ response = Excon.get('unix://', socket: @socket_path)
+ expect(response.status).to eq(200)
+
+ worker_pid = response.body.to_i
+ expect(worker_pid).to be > 0
+
+ begin
+ Excon.post("unix://?#{signal}", socket: @socket_path)
+ rescue Excon::Error::Socket
+ # The connection may be closed abruptly
+ end
+
+ expect(pid_gone?(worker_pid)).to eq(true)
+ end
+ end
+
+ after(:all) do
+ begin
+ WebMock.disable_net_connect!(allow_localhost: true)
+ Process.kill('TERM', @puma_master_pid)
+ rescue Errno::ESRCH
+ end
+ end
+
+ def wait_puma_boot!(master_pid, ready_file)
+ # We have seen the boot timeout after 2 minutes in CI so let's set it to 5 minutes.
+ timeout = 5 * 60
+ timeout.times do
+ return if File.exist?(ready_file)
+
+ pid = Process.waitpid(master_pid, Process::WNOHANG)
+ raise "puma failed to boot: #{$?}" unless pid.nil?
+
+ sleep 1
+ end
+
+ raise "puma boot timed out after #{timeout} seconds"
+ end
+
+ def pid_gone?(pid)
+ # Worker termination should take less than a second. That makes 10
+ # seconds a generous timeout.
+ 10.times do
+ begin
+ Process.kill(0, pid)
+ rescue Errno::ESRCH
+ return true
+ end
+
+ sleep 1
+ end
+
+ false
+ end
+end
diff --git a/spec/unicorn/unicorn_spec.rb b/spec/rack_servers/unicorn_spec.rb
index a4cf479a339..6a02ebcd048 100644
--- a/spec/unicorn/unicorn_spec.rb
+++ b/spec/rack_servers/unicorn_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'fileutils'
require 'excon'
@@ -6,12 +8,16 @@ require 'spec_helper'
describe 'Unicorn' do
before(:all) do
- config_lines = File.read('config/unicorn.rb.example').split("\n")
+ project_root = File.expand_path('../..', __dir__)
+
+ config_lines = File.read('config/unicorn.rb.example')
+ .gsub('/home/git/gitlab', project_root)
+ .gsub('/home/git', project_root)
+ .split("\n")
# Remove these because they make setup harder.
config_lines = config_lines.reject do |line|
%w[
- working_directory
worker_processes
listen
pid
@@ -26,33 +32,18 @@ describe 'Unicorn' do
# predictable which process will handle our requests.
config_lines << 'worker_processes 1'
- @socket_path = File.join(Dir.pwd, 'tmp/tests/unicorn.socket')
+ @socket_path = File.join(project_root, 'tmp/tests/unicorn.socket')
config_lines << "listen '#{@socket_path}'"
- ready_file = 'tmp/tests/unicorn-worker-ready'
+ ready_file = File.join(project_root, 'tmp/tests/unicorn-worker-ready')
FileUtils.rm_f(ready_file)
after_fork_index = config_lines.index { |l| l.start_with?('after_fork') }
config_lines.insert(after_fork_index + 1, "File.write('#{ready_file}', Process.pid)")
- config_path = 'tmp/tests/unicorn.rb'
+ config_path = File.join(project_root, 'tmp/tests/unicorn.rb')
File.write(config_path, config_lines.join("\n") + "\n")
- rackup_path = 'tmp/tests/config.ru'
- File.write(rackup_path, <<~EOS)
- app =
- proc do |env|
- if env['REQUEST_METHOD'] == 'GET'
- [200, {}, [Process.pid]]
- else
- Process.kill(env['QUERY_STRING'], Process.pid)
- [200, {}, ['Bye!']]
- end
- end
-
- run app
- EOS
-
- cmd = %W[unicorn -E test -c #{config_path} #{rackup_path}]
+ cmd = %W[unicorn -E test -c #{config_path} spec/rack_servers/configs/config.ru]
@unicorn_master_pid = spawn(*cmd)
wait_unicorn_boot!(@unicorn_master_pid, ready_file)
WebMock.allow_net_connect!
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index e0b5b34f9c4..2ebcb787d06 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -494,6 +494,24 @@ describe API::Internal do
end
end
+ context 'request times out' do
+ context 'git push' do
+ it 'responds with a gateway timeout' do
+ personal_project = create(:project, namespace: user.namespace)
+
+ expect_next_instance_of(Gitlab::GitAccess) do |access|
+ expect(access).to receive(:check).and_raise(Gitlab::GitAccess::TimeoutError, "Foo")
+ end
+ push(key, personal_project)
+
+ expect(response).to have_gitlab_http_status(503)
+ expect(json_response['status']).to be_falsey
+ expect(json_response['message']).to eq("Foo")
+ expect(user.reload.last_activity_on).to be_nil
+ end
+ end
+ end
+
context "archived project" do
before do
project.add_developer(user)
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 9f6cf12f9a7..9cda39a569b 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -178,6 +178,24 @@ describe API::Issues do
expect(first_issue['id']).to eq(issue2.id)
end
+ it 'returns issues with no assignee' do
+ issue2 = create(:issue, author: user2, project: project)
+
+ get api('/issues', user), assignee_id: 'None', scope: 'all'
+
+ expect_paginated_array_response(size: 1)
+ expect(first_issue['id']).to eq(issue2.id)
+ end
+
+ it 'returns issues with any assignee' do
+ # This issue without assignee should not be returned
+ create(:issue, author: user2, project: project)
+
+ get api('/issues', user), assignee_id: 'Any', scope: 'all'
+
+ expect_paginated_array_response(size: 3)
+ end
+
it 'returns issues reacted by the authenticated user by the given emoji' do
issue2 = create(:issue, project: project, author: user, assignees: [user])
award_emoji = create(:award_emoji, awardable: issue2, user: user2, name: 'star')
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 07d19e3ad29..e4e0ca285e0 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -143,6 +143,23 @@ describe API::MergeRequests do
expect_response_ordered_exactly(merge_request3)
end
+ it 'returns an array of merge requests with no assignee' do
+ merge_request3 = create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+
+ get api('/merge_requests', user), assignee_id: 'None', scope: :all
+
+ expect_response_ordered_exactly(merge_request3)
+ end
+
+ it 'returns an array of merge requests with any assignee' do
+ # This MR with no assignee should not be returned
+ create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+
+ get api('/merge_requests', user), assignee_id: 'Any', scope: :all
+
+ expect_response_contain_exactly(merge_request, merge_request2, merge_request_closed, merge_request_merged, merge_request_locked)
+ end
+
it 'returns an array of merge requests assigned to me' do
merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch')
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 22e5a7c7174..62b6a3ce42e 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -200,6 +200,24 @@ describe API::Projects do
expect(json_response.first).to include 'statistics'
end
+ it "does not include license by default" do
+ get api('/projects', user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include('license', 'license_url')
+ end
+
+ it "does not include license if requested" do
+ get api('/projects', user), license: true
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include('license', 'license_url')
+ end
+
context 'when external issue tracker is enabled' do
let!(:jira_service) { create(:jira_service, project: project) }
@@ -994,6 +1012,26 @@ describe API::Projects do
})
end
+ it "does not include license fields by default" do
+ get api("/projects/#{project.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).not_to include('license', 'license_url')
+ end
+
+ it 'includes license fields when requested' do
+ get api("/projects/#{project.id}", user), license: true
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['license']).to eq({
+ 'key' => project.repository.license.key,
+ 'name' => project.repository.license.name,
+ 'nickname' => project.repository.license.nickname,
+ 'html_url' => project.repository.license.url,
+ 'source_url' => project.repository.license.meta['source']
+ })
+ end
+
it "does not include statistics by default" do
get api("/projects/#{project.id}", user)
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 43ceb332cfb..909703a8d47 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -797,6 +797,24 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
it { expect(job).to be_runner_system_failure }
end
+
+ context 'when failure_reason is unrecognized value' do
+ before do
+ update_job(state: 'failed', failure_reason: 'what_is_this')
+ job.reload
+ end
+
+ it { expect(job).to be_unknown_failure }
+ end
+
+ context 'when failure_reason is job_execution_timeout' do
+ before do
+ update_job(state: 'failed', failure_reason: 'job_execution_timeout')
+ job.reload
+ end
+
+ it { expect(job).to be_job_execution_timeout }
+ end
end
context 'when trace is given' do
diff --git a/spec/services/boards/visits/create_service_spec.rb b/spec/services/boards/visits/create_service_spec.rb
new file mode 100644
index 00000000000..6baf7ac9deb
--- /dev/null
+++ b/spec/services/boards/visits/create_service_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Boards::Visits::CreateService do
+ describe '#execute' do
+ let(:user) { create(:user) }
+
+ context 'when a project board' do
+ let(:project) { create(:project) }
+ let(:project_board) { create(:board, project: project) }
+
+ subject(:service) { described_class.new(project_board.parent, user) }
+
+ it 'returns nil when there is no user' do
+ service.current_user = nil
+
+ expect(service.execute(project_board)).to eq nil
+ end
+
+ it 'returns nil when database is read only' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+
+ expect(service.execute(project_board)).to eq nil
+ end
+
+ it 'records the visit' do
+ expect(BoardProjectRecentVisit).to receive(:visited!).once
+
+ service.execute(project_board)
+ end
+ end
+
+ context 'when a group board' do
+ let(:group) { create(:group) }
+ let(:group_board) { create(:board, group: group) }
+
+ subject(:service) { described_class.new(group_board.parent, user) }
+
+ it 'returns nil when there is no user' do
+ service.current_user = nil
+
+ expect(service.execute(group_board)).to eq nil
+ end
+
+ it 'records the visit' do
+ expect(BoardGroupRecentVisit).to receive(:visited!).once
+
+ service.execute(group_board)
+ end
+ end
+ end
+end
diff --git a/spec/services/boards/visits/latest_service_spec.rb b/spec/services/boards/visits/latest_service_spec.rb
new file mode 100644
index 00000000000..e55d599e2cc
--- /dev/null
+++ b/spec/services/boards/visits/latest_service_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Boards::Visits::LatestService do
+ describe '#execute' do
+ let(:user) { create(:user) }
+
+ context 'when a project board' do
+ let(:project) { create(:project) }
+ let(:project_board) { create(:board, project: project) }
+
+ subject(:service) { described_class.new(project_board.parent, user) }
+
+ it 'returns nil when there is no user' do
+ service.current_user = nil
+
+ expect(service.execute).to eq nil
+ end
+
+ it 'queries for most recent visit' do
+ expect(BoardProjectRecentVisit).to receive(:latest).once
+
+ service.execute
+ end
+ end
+
+ context 'when a group board' do
+ let(:group) { create(:group) }
+ let(:group_board) { create(:board, group: group) }
+
+ subject(:service) { described_class.new(group_board.parent, user) }
+
+ it 'returns nil when there is no user' do
+ service.current_user = nil
+
+ expect(service.execute).to eq nil
+ end
+
+ it 'queries for most recent visit' do
+ expect(BoardGroupRecentVisit).to receive(:latest).once
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/resource_events/merge_into_notes_service_spec.rb b/spec/services/resource_events/merge_into_notes_service_spec.rb
index 0d333d541c9..c76f6e6f77e 100644
--- a/spec/services/resource_events/merge_into_notes_service_spec.rb
+++ b/spec/services/resource_events/merge_into_notes_service_spec.rb
@@ -66,5 +66,14 @@ describe ResourceEvents::MergeIntoNotesService do
expect(notes.count).to eq 1
expect(notes.first.discussion_id).to eq event.discussion_id
end
+
+ it "preloads the note author's status" do
+ event = create_event(created_at: time)
+ create(:user_status, user: event.user)
+
+ notes = described_class.new(resource, user).execute
+
+ expect(notes.first.author.association(:status)).to be_loaded
+ end
end
end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index cd6661f09a1..9176eb12b12 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -6,7 +6,7 @@ describe PostReceive do
let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) }
let(:gl_repository) { "project-#{project.id}" }
let(:key) { create(:key, user: project.owner) }
- let(:key_id) { key.shell_id }
+ let!(:key_id) { key.shell_id }
let(:project) do
create(:project, :repository, auto_cancel_pending_pipelines: 'disabled')
@@ -31,85 +31,108 @@ describe PostReceive do
end
describe "#process_project_changes" do
- before do
- allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
+ context 'empty changes' do
+ it "does not call any PushService but runs after project hooks" do
+ expect(GitPushService).not_to receive(:new)
+ expect(GitTagPushService).not_to receive(:new)
+ expect_next_instance_of(SystemHooksService) { |service| expect(service).to receive(:execute_hooks) }
+
+ described_class.new.perform(gl_repository, key_id, "")
+ end
end
- context "branches" do
- let(:changes) { "123456 789012 refs/heads/tést" }
+ context 'unidentified user' do
+ let!(:key_id) { "" }
- it "calls GitTagPushService" do
- expect_any_instance_of(GitPushService).to receive(:execute).and_return(true)
- expect_any_instance_of(GitTagPushService).not_to receive(:execute)
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ it 'returns false' do
+ expect(GitPushService).not_to receive(:new)
+ expect(GitTagPushService).not_to receive(:new)
+
+ expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be false
end
end
- context "tags" do
- let(:changes) { "123456 789012 refs/tags/tag" }
+ context 'with changes' do
+ before do
+ allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
+ end
+
+ context "branches" do
+ let(:changes) { "123456 789012 refs/heads/tést" }
- it "calls GitTagPushService" do
- expect_any_instance_of(GitPushService).not_to receive(:execute)
- expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true)
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ it "calls GitPushService" do
+ expect_any_instance_of(GitPushService).to receive(:execute).and_return(true)
+ expect_any_instance_of(GitTagPushService).not_to receive(:execute)
+ described_class.new.perform(gl_repository, key_id, base64_changes)
+ end
end
- end
- context "merge-requests" do
- let(:changes) { "123456 789012 refs/merge-requests/123" }
+ context "tags" do
+ let(:changes) { "123456 789012 refs/tags/tag" }
- it "does not call any of the services" do
- expect_any_instance_of(GitPushService).not_to receive(:execute)
- expect_any_instance_of(GitTagPushService).not_to receive(:execute)
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ it "calls GitTagPushService" do
+ expect_any_instance_of(GitPushService).not_to receive(:execute)
+ expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true)
+ described_class.new.perform(gl_repository, key_id, base64_changes)
+ end
end
- end
- context "gitlab-ci.yml" do
- let(:changes) { "123456 789012 refs/heads/feature\n654321 210987 refs/tags/tag" }
+ context "merge-requests" do
+ let(:changes) { "123456 789012 refs/merge-requests/123" }
- subject { described_class.new.perform(gl_repository, key_id, base64_changes) }
+ it "does not call any of the services" do
+ expect_any_instance_of(GitPushService).not_to receive(:execute)
+ expect_any_instance_of(GitTagPushService).not_to receive(:execute)
+ described_class.new.perform(gl_repository, key_id, base64_changes)
+ end
+ end
- context "creates a Ci::Pipeline for every change" do
- before do
- stub_ci_pipeline_to_return_yaml_file
+ context "gitlab-ci.yml" do
+ let(:changes) { "123456 789012 refs/heads/feature\n654321 210987 refs/tags/tag" }
- allow_any_instance_of(Project)
- .to receive(:commit)
- .and_return(project.commit)
+ subject { described_class.new.perform(gl_repository, key_id, base64_changes) }
- allow_any_instance_of(Repository)
- .to receive(:branch_exists?)
- .and_return(true)
- end
+ context "creates a Ci::Pipeline for every change" do
+ before do
+ stub_ci_pipeline_to_return_yaml_file
- it { expect { subject }.to change { Ci::Pipeline.count }.by(2) }
- end
+ allow_any_instance_of(Project)
+ .to receive(:commit)
+ .and_return(project.commit)
- context "does not create a Ci::Pipeline" do
- before do
- stub_ci_pipeline_yaml_file(nil)
+ allow_any_instance_of(Repository)
+ .to receive(:branch_exists?)
+ .and_return(true)
+ end
+
+ it { expect { subject }.to change { Ci::Pipeline.count }.by(2) }
end
- it { expect { subject }.not_to change { Ci::Pipeline.count } }
+ context "does not create a Ci::Pipeline" do
+ before do
+ stub_ci_pipeline_yaml_file(nil)
+ end
+
+ it { expect { subject }.not_to change { Ci::Pipeline.count } }
+ end
end
- end
- context 'after project changes hooks' do
- let(:changes) { '123456 789012 refs/heads/tést' }
- let(:fake_hook_data) { Hash.new(event_name: 'repository_update') }
+ context 'after project changes hooks' do
+ let(:changes) { '123456 789012 refs/heads/tést' }
+ let(:fake_hook_data) { Hash.new(event_name: 'repository_update') }
- before do
- allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data)
- # silence hooks so we can isolate
- allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true)
- allow_any_instance_of(GitPushService).to receive(:execute).and_return(true)
- end
+ before do
+ allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data)
+ # silence hooks so we can isolate
+ allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true)
+ allow_any_instance_of(GitPushService).to receive(:execute).and_return(true)
+ end
- it 'calls SystemHooksService' do
- expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true)
+ it 'calls SystemHooksService' do
+ expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true)
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ described_class.new.perform(gl_repository, key_id, base64_changes)
+ end
end
end
end
diff --git a/vendor/prometheus/values.yaml b/vendor/prometheus/values.yaml
index c432be72163..02ec3e2d9fe 100644
--- a/vendor/prometheus/values.yaml
+++ b/vendor/prometheus/values.yaml
@@ -1,5 +1,7 @@
alertmanager:
enabled: false
+ image:
+ tag: v0.15.2
kubeStateMetrics:
enabled: true
@@ -16,7 +18,7 @@ rbac:
server:
fullnameOverride: "prometheus-prometheus-server"
image:
- tag: v2.1.0
+ tag: v2.4.3
serverFiles:
alerts: {}