summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--.gitlab/issue_templates/Security Developer Workflow.md70
-rw-r--r--CONTRIBUTING.md54
-rw-r--r--Gemfile10
-rw-r--r--Gemfile.lock22
-rw-r--r--Gemfile.rails5.lock40
-rw-r--r--LICENSE26
-rw-r--r--README.md6
-rw-r--r--app/assets/javascripts/branches/branches_delete_modal.js11
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue185
-rw-r--r--app/assets/javascripts/due_date_select.js9
-rw-r--r--app/assets/javascripts/ide/components/changed_file_icon.vue74
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue58
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue93
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue153
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue121
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue60
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/message_field.vue130
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue88
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue59
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue45
-rw-r--r--app/assets/javascripts/ide/components/ide_context_bar.vue42
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue7
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue123
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue9
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue5
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue13
-rw-r--r--app/assets/javascripts/ide/constants.js3
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js37
-rw-r--r--app/assets/javascripts/ide/lib/common/model_manager.js4
-rw-r--r--app/assets/javascripts/ide/lib/decorations/controller.js9
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js25
-rw-r--r--app/assets/javascripts/ide/lib/editor.js4
-rw-r--r--app/assets/javascripts/ide/stores/actions.js25
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js38
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js100
-rw-r--r--app/assets/javascripts/ide/stores/getters.js12
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js37
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js11
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js5
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js21
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js70
-rw-r--r--app/assets/javascripts/ide/stores/mutations/tree.js8
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/utils.js3
-rw-r--r--app/assets/javascripts/issuable_context.js4
-rw-r--r--app/assets/javascripts/jobs/components/header.vue150
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_details_block.vue185
-rw-r--r--app/assets/javascripts/jobs/job_details_bundle.js5
-rw-r--r--app/assets/javascripts/labels_select.js5
-rw-r--r--app/assets/javascripts/milestone_select.js14
-rw-r--r--app/assets/javascripts/notes.js46
-rw-r--r--app/assets/javascripts/performance_bar/services/performance_bar_service.js30
-rw-r--r--app/assets/javascripts/right_sidebar.js12
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue20
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue12
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue19
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue21
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue18
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue32
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js1
-rw-r--r--app/assets/javascripts/users_select.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue25
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/callout.vue27
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue39
-rw-r--r--app/assets/stylesheets/framework/buttons.scss4
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss9
-rw-r--r--app/assets/stylesheets/framework/variables.scss3
-rw-r--r--app/assets/stylesheets/pages/builds.scss41
-rw-r--r--app/assets/stylesheets/pages/commits.scss4
-rw-r--r--app/assets/stylesheets/pages/diff.scss5
-rw-r--r--app/assets/stylesheets/pages/issuable.scss28
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss17
-rw-r--r--app/assets/stylesheets/pages/milestone.scss14
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss5
-rw-r--r--app/assets/stylesheets/pages/repo.scss169
-rw-r--r--app/controllers/application_controller.rb1
-rw-r--r--app/controllers/concerns/notes_actions.rb4
-rw-r--r--app/controllers/dashboard/todos_controller.rb2
-rw-r--r--app/controllers/groups/variables_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb2
-rw-r--r--app/controllers/projects/jobs_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb4
-rw-r--r--app/controllers/projects/repositories_controller.rb11
-rw-r--r--app/controllers/projects/variables_controller.rb2
-rw-r--r--app/controllers/projects/wikis_controller.rb19
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/controllers/users_controller.rb2
-rw-r--r--app/finders/group_descendants_finder.rb6
-rw-r--r--app/helpers/blob_helper.rb2
-rw-r--r--app/helpers/diff_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb38
-rw-r--r--app/helpers/milestones_helper.rb91
-rw-r--r--app/helpers/nav_helper.rb2
-rw-r--r--app/helpers/safe_params_helper.rb11
-rw-r--r--app/helpers/tree_helper.rb2
-rw-r--r--app/mailers/emails/issues.rb6
-rw-r--r--app/models/ci/build.rb34
-rw-r--r--app/models/ci/group_variable.rb2
-rw-r--r--app/models/ci/job_artifact.rb40
-rw-r--r--app/models/ci/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb2
-rw-r--r--app/models/commit.rb2
-rw-r--r--app/models/commit_status.rb2
-rw-r--r--app/models/concerns/cache_markdown_field.rb30
-rw-r--r--app/models/concerns/group_descendant.rb15
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/models/concerns/milestoneish.rb4
-rw-r--r--app/models/concerns/resolvable_discussion.rb2
-rw-r--r--app/models/deploy_key.rb2
-rw-r--r--app/models/deploy_token.rb2
-rw-r--r--app/models/fork_network.rb2
-rw-r--r--app/models/group.rb6
-rw-r--r--app/models/issue.rb3
-rw-r--r--app/models/label.rb4
-rw-r--r--app/models/lfs_object.rb8
-rw-r--r--app/models/milestone.rb2
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/models/notification_recipient.rb12
-rw-r--r--app/models/notification_setting.rb3
-rw-r--r--app/models/project.rb34
-rw-r--r--app/models/project_statistics.rb20
-rw-r--r--app/models/project_wiki.rb6
-rw-r--r--app/models/repository.rb1
-rw-r--r--app/models/todo.rb2
-rw-r--r--app/models/user.rb33
-rw-r--r--app/models/wiki_page.rb18
-rw-r--r--app/presenters/ci/build_presenter.rb21
-rw-r--r--app/serializers/entity_date_helper.rb27
-rw-r--r--app/serializers/job_entity.rb18
-rw-r--r--app/services/ci/register_job_service.rb21
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb2
-rw-r--r--app/services/clusters/gcp/verify_provision_status_service.rb2
-rw-r--r--app/services/create_deployment_service.rb4
-rw-r--r--app/services/import_export_clean_up_service.rb2
-rw-r--r--app/services/labels/transfer_service.rb11
-rw-r--r--app/services/notification_recipient_service.rb11
-rw-r--r--app/services/notification_service.rb14
-rw-r--r--app/services/projects/create_from_template_service.rb3
-rw-r--r--app/services/projects/destroy_service.rb2
-rw-r--r--app/services/repository_archive_clean_up_service.rb2
-rw-r--r--app/services/system_note_service.rb2
-rw-r--r--app/services/test_hooks/base_service.rb2
-rw-r--r--app/uploaders/job_artifact_uploader.rb8
-rw-r--r--app/uploaders/object_storage.rb22
-rw-r--r--app/views/dashboard/issues.atom.builder2
-rw-r--r--app/views/dashboard/issues.html.haml4
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml7
-rw-r--r--app/views/groups/issues.atom.builder2
-rw-r--r--app/views/groups/issues.html.haml2
-rw-r--r--app/views/layouts/mailer.text.erb2
-rw-r--r--app/views/layouts/notify.text.erb2
-rw-r--r--app/views/notify/issue_due_email.html.haml12
-rw-r--r--app/views/notify/issue_due_email.text.erb7
-rw-r--r--app/views/projects/blob/_viewer.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/clusters/new.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/diffs/_collapsed.html.haml2
-rw-r--r--app/views/projects/issues/_issue.html.haml2
-rw-r--r--app/views/projects/issues/_nav_btns.html.haml2
-rw-r--r--app/views/projects/issues/index.atom.builder2
-rw-r--r--app/views/projects/issues/index.html.haml2
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml9
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_submit.html.haml8
-rw-r--r--app/views/projects/pipelines/new.html.haml15
-rw-r--r--app/views/projects/protected_branches/_create_protected_branch.html.haml4
-rw-r--r--app/views/projects/protected_branches/_update_protected_branch.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_branches_list.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_dropdown.html.haml4
-rw-r--r--app/views/projects/protected_branches/shared/_index.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_protected_branch.html.haml2
-rw-r--r--app/views/shared/_ref_switcher.html.haml4
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml13
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar_todo.html.haml6
-rw-r--r--app/views/shared/issuable/form/_merge_request_assignee.html.haml2
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml35
-rw-r--r--app/workers/all_queues.yml3
-rw-r--r--app/workers/concerns/mail_scheduler_queue.rb7
-rw-r--r--app/workers/issue_due_scheduler_worker.rb10
-rw-r--r--app/workers/mail_scheduler/issue_due_worker.rb14
-rw-r--r--app/workers/post_receive.rb2
-rw-r--r--app/workers/stuck_ci_jobs_worker.rb2
-rwxr-xr-xbin/secpick47
-rw-r--r--changelogs/unreleased/16957-issue-due-email.yml5
-rw-r--r--changelogs/unreleased/21677-run-pipeline-word.yml5
-rw-r--r--changelogs/unreleased/25010-collapsed-sidebar-tooltips.yml5
-rw-r--r--changelogs/unreleased/34262-show-current-labels-when-editing.yml5
-rw-r--r--changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml5
-rw-r--r--changelogs/unreleased/41059-calculate-artifact-size-more-efficiently.yml5
-rw-r--r--changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml5
-rw-r--r--changelogs/unreleased/42889-avoid-return-inside-block.yml5
-rw-r--r--changelogs/unreleased/43404-pipelines-commit.yml5
-rw-r--r--changelogs/unreleased/43567-replace-gke.yml5
-rw-r--r--changelogs/unreleased/43617-mailsig.yml5
-rw-r--r--changelogs/unreleased/44834-ide-remove-branch-from-bottom-bar.yml5
-rw-r--r--changelogs/unreleased/44981-http-io-trace-with-multi-byte-char.yml5
-rw-r--r--changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml5
-rw-r--r--changelogs/unreleased/45271-collpased-diff-loading.yml5
-rw-r--r--changelogs/unreleased/45363-optional-params-on-api-endpoint-produce-invalid-pagination-header-links.yml6
-rw-r--r--changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml5
-rw-r--r--changelogs/unreleased/45476-geo-statement-timeout-counting-local-job-artifacts.yml5
-rw-r--r--changelogs/unreleased/45507-fix-repository-archive-url.yml6
-rw-r--r--changelogs/unreleased/bvl-shared-groups-on-group-page.yml5
-rw-r--r--changelogs/unreleased/fix-direct-upload-for-old-records.yml5
-rw-r--r--changelogs/unreleased/fix-gb-fix-empty-secret-variables.yml5
-rw-r--r--changelogs/unreleased/fix-n-plus-one-when-getting-notification-settings-for-recipients.yml5
-rw-r--r--changelogs/unreleased/fj-42354-custom-hooks-not-triggered-by-UI-wiki-edit.yml5
-rw-r--r--changelogs/unreleased/fj-42601-respect-visibility-options.yml5
-rw-r--r--changelogs/unreleased/ide-tree-loading-fix.yml5
-rw-r--r--changelogs/unreleased/improve-jobs-queuing-time-metric.yml5
-rw-r--r--changelogs/unreleased/label-links-on-project-transfer.yml5
-rw-r--r--changelogs/unreleased/move-pipeline-failed-vue-component.yml5
-rw-r--r--changelogs/unreleased/sh-fix-award-emoji-nplus-one-participants.yml5
-rw-r--r--changelogs/unreleased/sh-memoize-repository-empty.yml5
-rw-r--r--changelogs/unreleased/zj-ref-exists-opt-out.yml5
-rw-r--r--config/initializers/1_settings.rb4
-rw-r--r--config/initializers/active_record_avoid_type_casting_in_uniqueness_validator.rb98
-rw-r--r--config/initializers/forbid_sidekiq_in_transactions.rb12
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--db/migrate/20180330121048_add_issue_due_to_notification_settings.rb9
-rw-r--r--db/migrate/20180418053107_add_index_to_ci_job_artifacts_file_store.rb15
-rw-r--r--db/schema.rb4
-rw-r--r--doc/README.md1
-rw-r--r--doc/administration/index.md2
-rw-r--r--doc/api/notification_settings.md4
-rw-r--r--doc/api/repositories.md2
-rw-r--r--doc/ci/examples/code_climate.md6
-rw-r--r--doc/ci/examples/container_scanning.md3
-rw-r--r--doc/ci/examples/dast.md6
-rw-r--r--doc/ci/pipelines.md5
-rw-r--r--doc/development/background_migrations.md15
-rw-r--r--doc/development/doc_styleguide.md33
-rw-r--r--doc/development/fe_guide/vue.md4
-rw-r--r--doc/development/i18n/proofreader.md1
-rw-r--r--doc/install/README.md4
-rw-r--r--doc/install/installation.md6
-rw-r--r--doc/user/group/index.md11
-rw-r--r--doc/user/project/index.md6
-rw-r--r--doc/user/project/issues/due_dates.md4
-rw-r--r--doc/user/project/issues/issues_functionalities.md12
-rw-r--r--doc/user/project/labels.md3
-rw-r--r--doc/user/project/merge_requests/index.md12
-rw-r--r--doc/user/project/settings/index.md2
-rw-r--r--doc/user/project/web_ide/index.md7
-rw-r--r--doc/workflow/notifications.md14
-rw-r--r--doc/workflow/todos.md4
-rw-r--r--features/project/builds/permissions.feature54
-rw-r--r--features/steps/project/builds/permissions.rb7
-rw-r--r--features/steps/shared/builds.rb25
-rw-r--r--features/steps/shared/paths.rb8
-rw-r--r--features/steps/shared/project.rb29
-rw-r--r--lib/api/discussions.rb10
-rw-r--r--lib/api/group_variables.rb4
-rw-r--r--lib/api/internal.rb10
-rw-r--r--lib/api/issues.rb2
-rw-r--r--lib/api/job_artifacts.rb2
-rw-r--r--lib/api/jobs.rb4
-rw-r--r--lib/api/project_snippets.rb2
-rw-r--r--lib/api/projects.rb2
-rw-r--r--lib/api/repositories.rb4
-rw-r--r--lib/api/runner.rb6
-rw-r--r--lib/api/snippets.rb8
-rw-r--r--lib/api/triggers.rb8
-rw-r--r--lib/api/v3/builds.rb8
-rw-r--r--lib/api/v3/projects.rb2
-rw-r--r--lib/api/v3/snippets.rb6
-rw-r--r--lib/api/v3/triggers.rb4
-rw-r--r--lib/api/variables.rb4
-rw-r--r--lib/banzai/renderer/common_mark/html.rb2
-rw-r--r--lib/banzai/renderer/redcarpet/html.rb2
-rw-r--r--lib/declarative_policy/runner.rb2
-rw-r--r--lib/gitlab.rb6
-rw-r--r--lib/gitlab/auth.rb2
-rw-r--r--lib/gitlab/ci/trace.rb2
-rw-r--r--lib/gitlab/ci/trace/http_io.rb22
-rw-r--r--lib/gitlab/ci/trace/stream.rb6
-rw-r--r--lib/gitlab/daemon.rb4
-rw-r--r--lib/gitlab/diff/highlight.rb5
-rw-r--r--lib/gitlab/ee_compat_check.rb2
-rw-r--r--lib/gitlab/etag_caching/middleware.rb2
-rw-r--r--lib/gitlab/gfm/uploads_rewriter.rb2
-rw-r--r--lib/gitlab/git/commit.rb2
-rw-r--r--lib/gitlab/git/diff.rb2
-rw-r--r--lib/gitlab/git/popen.rb4
-rw-r--r--lib/gitlab/git/repository.rb26
-rw-r--r--lib/gitlab/git/repository_mirroring.rb2
-rw-r--r--lib/gitlab/git/wiki.rb60
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb2
-rw-r--r--lib/gitlab/gl_id.rb8
-rw-r--r--lib/gitlab/optimistic_locking.rb19
-rw-r--r--lib/gitlab/sentry.rb23
-rw-r--r--lib/gitlab/shell.rb2
-rw-r--r--lib/gitlab/sidekiq_middleware/shutdown.rb2
-rw-r--r--lib/gitlab/view/presenter/base.rb4
-rw-r--r--lib/gitlab/wiki/committer_with_hooks.rb39
-rw-r--r--lib/tasks/gitlab/setup.rake11
-rw-r--r--lib/tasks/gitlab/storage.rake4
-rw-r--r--locale/gitlab.pot311
-rw-r--r--qa/qa.rb2
-rw-r--r--qa/qa/factory/base.rb2
-rw-r--r--qa/qa/factory/resource/branch.rb73
-rw-r--r--qa/qa/git/repository.rb6
-rw-r--r--qa/qa/page/group/show.rb2
-rw-r--r--qa/qa/page/project/pipeline/show.rb4
-rw-r--r--qa/qa/page/project/settings/protected_branches.rb69
-rw-r--r--qa/qa/page/project/settings/repository.rb6
-rw-r--r--qa/qa/page/project/show.rb17
-rw-r--r--qa/qa/scenario/template.rb2
-rw-r--r--qa/qa/specs/features/repository/protected_branches_spec.rb63
-rw-r--r--rubocop/cop/avoid_break_from_strong_memoize.rb48
-rw-r--r--rubocop/cop/avoid_return_from_blocks.rb77
-rw-r--r--rubocop/cop/gitlab/has_many_through_scope.rb45
-rw-r--r--rubocop/cop/migration/safer_boolean_column.rb4
-rw-r--r--rubocop/rubocop.rb5
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb5
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb24
-rw-r--r--spec/factories/ci/builds.rb6
-rw-r--r--spec/features/boards/sidebar_spec.rb20
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb9
-rw-r--r--spec/features/issues/todo_spec.rb4
-rw-r--r--spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb17
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb8
-rw-r--r--spec/features/projects/clusters_spec.rb2
-rw-r--r--spec/features/projects/files/user_uploads_files_spec.rb4
-rw-r--r--spec/features/projects/jobs/permissions_spec.rb130
-rw-r--r--spec/features/projects/jobs_spec.rb8
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb8
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb2
-rw-r--r--spec/features/projects/tree/create_file_spec.rb2
-rw-r--r--spec/finders/group_descendants_finder_spec.rb9
-rw-r--r--spec/fixtures/trace/sample_trace3480
-rw-r--r--spec/helpers/issuables_helper_spec.rb8
-rw-r--r--spec/helpers/milestones_helper_spec.rb54
-rw-r--r--spec/javascripts/branches/branches_delete_modal_spec.js40
-rw-r--r--spec/javascripts/collapsed_sidebar_todo_spec.js10
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js95
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js51
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_item_spec.js15
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_spec.js42
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/message_field_spec.js174
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js13
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js46
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js39
-rw-r--r--spec/javascripts/ide/components/repo_commit_section_spec.js101
-rw-r--r--spec/javascripts/ide/components/repo_editor_spec.js18
-rw-r--r--spec/javascripts/ide/lib/common/model_spec.js30
-rw-r--r--spec/javascripts/ide/lib/decorations/controller_spec.js29
-rw-r--r--spec/javascripts/ide/lib/diff/controller_spec.js68
-rw-r--r--spec/javascripts/ide/lib/editor_spec.js2
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js65
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js43
-rw-r--r--spec/javascripts/ide/stores/getters_spec.js14
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js65
-rw-r--r--spec/javascripts/ide/stores/modules/commit/getters_spec.js10
-rw-r--r--spec/javascripts/ide/stores/mutations/file_spec.js48
-rw-r--r--spec/javascripts/ide/stores/mutations/tree_spec.js10
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js10
-rw-r--r--spec/javascripts/jobs/header_spec.js34
-rw-r--r--spec/javascripts/jobs/sidebar_details_block_spec.js61
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js11
-rw-r--r--spec/javascripts/vue_shared/components/callout_spec.js45
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js86
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/trace_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb9
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb11
-rw-r--r--spec/lib/gitlab/git/wiki_spec.rb2
-rw-r--r--spec/lib/gitlab/sentry_spec.rb44
-rw-r--r--spec/lib/gitlab/shell_spec.rb2
-rw-r--r--spec/lib/gitlab/view/presenter/base_spec.rb7
-rw-r--r--spec/lib/gitlab/wiki/committer_with_hooks_spec.rb154
-rw-r--r--spec/models/ci/build_spec.rb58
-rw-r--r--spec/models/ci/job_artifact_spec.rb95
-rw-r--r--spec/models/commit_spec.rb5
-rw-r--r--spec/models/commit_status_spec.rb32
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb183
-rw-r--r--spec/models/concerns/group_descendant_spec.rb17
-rw-r--r--spec/models/lfs_object_spec.rb39
-rw-r--r--spec/models/milestone_spec.rb12
-rw-r--r--spec/models/note_spec.rb17
-rw-r--r--spec/models/project_statistics_spec.rb80
-rw-r--r--spec/models/project_wiki_spec.rb2
-rw-r--r--spec/models/repository_spec.rb6
-rw-r--r--spec/models/wiki_page_spec.rb2
-rw-r--r--spec/presenters/ci/build_presenter_spec.rb35
-rw-r--r--spec/requests/api/repositories_spec.rb15
-rw-r--r--spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb74
-rw-r--r--spec/rubocop/cop/avoid_return_from_blocks_spec.rb127
-rw-r--r--spec/rubocop/cop/gitlab/has_many_through_scope_spec.rb74
-rw-r--r--spec/serializers/entity_date_helper_spec.rb55
-rw-r--r--spec/serializers/job_entity_spec.rb63
-rw-r--r--spec/services/ci/register_job_service_spec.rb105
-rw-r--r--spec/services/labels/transfer_service_spec.rb10
-rw-r--r--spec/services/notification_service_spec.rb40
-rw-r--r--spec/services/projects/create_from_template_service_spec.rb22
-rw-r--r--spec/services/system_note_service_spec.rb8
-rw-r--r--spec/support/http_io/http_io_helpers.rb3
-rw-r--r--spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb2
-rw-r--r--spec/uploaders/object_storage_spec.rb101
-rw-r--r--spec/workers/issue_due_scheduler_worker_spec.rb22
-rw-r--r--spec/workers/mail_scheduler/issue_due_worker_spec.rb21
-rw-r--r--vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml4
410 files changed, 9476 insertions, 2713 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index eed1a50cc8f..f97feca9f7b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-65.0-node-8.x-yarn-1.2-postgresql-9.6"
.dedicated-runner: &dedicated-runner
retry: 1
diff --git a/.gitlab/issue_templates/Security Developer Workflow.md b/.gitlab/issue_templates/Security Developer Workflow.md
new file mode 100644
index 00000000000..8dd447ed121
--- /dev/null
+++ b/.gitlab/issue_templates/Security Developer Workflow.md
@@ -0,0 +1,70 @@
+<!--
+# Read me first!
+
+Create this issue under https://dev.gitlab.org/gitlab/gitlabhq
+
+Set the title to: `[Security] Description of the original issue`
+-->
+
+### Prior to the security release
+
+- [ ] Read the [security process for developers] if you are not familiar with it.
+- [ ] Link to the original issue adding it to the [links section](#links)
+- [ ] Run `scripts/security-harness` in the CE, EE, and/or Omnibus to prevent pushing to any remote besides `dev.gitlab.org`
+- [ ] Create an MR targetting `org` `master`, prefixing your branch with `security-`
+- [ ] Label your MR with the ~security label, prefix the title with `WIP: [master]`
+- [ ] Add a link to the MR to the [links section](#links)
+- [ ] Add a link to an EE MR if required
+- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**.
+- [ ] Assign the MR to a RM once is reviewed and ready to be merged. Check the [RM list] to see who to ping.
+
+#### Backports
+
+- [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases
+ - [ ] At this point, it might be easy to squash the commits from the MR into one
+ - You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [seckpick documentation]
+ - [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable)
+ - [ ] Create each MR targetting the security branch `security-X-Y`
+ - [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
+- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager.
+
+[seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/process.md#secpick-script
+
+#### Documentation and final details
+
+- [ ] Check the topic on #security to see when the next release is going ot happen and add a link to the [links section](#links)
+- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
+- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
+- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
+- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
+
+### Summary
+#### Links
+
+| Description | Link |
+| -------- | -------- |
+| Original issue | #TODO |
+| Security release issue | #TODO |
+| `master` MR | !TODO |
+| `master` MR (EE) | !TODO |
+| `Backport X.Y` MR | !TODO |
+| `Backport X.Y` MR | !TODO |
+| `Backport X.Y` MR | !TODO |
+| `Backport X.Y` MR (EE) | !TODO |
+| `Backport X.Y` MR (EE) | !TODO |
+| `Backport X.Y` MR (EE) | !TODO |
+
+#### Details
+
+| Description | Details | Further details|
+| -------- | -------- | -------- |
+| Versions affected | X.Y | |
+| Upgrade notes | | |
+| GitLab Settings updated | Yes/No| |
+| Migration required | Yes/No | |
+| Thanks | | |
+
+[security process for developers]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md
+[RM list]: https://about.gitlab.com/release-managers/
+
+/label ~security
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9c8fdc1275b..02599907af7 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -126,7 +126,7 @@ Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, etc.
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.
-- Priority: ~Deliverable, ~Stretch, ~"Next Patch Release"
+- Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release"
All labels, their meaning and priority are defined on the
[labels page][labels-page].
@@ -185,10 +185,10 @@ indicate if an issue needs backend work, frontend work, or both.
Team labels are always capitalized so that they show up as the first label for
any issue.
-### Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")
+### Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")
-Priority labels help us clearly communicate expectations of the work for the
-release. There are two levels of priority labels:
+Milestone labels help us clearly communicate expectations of the work for the
+release. There are three levels of Milestone labels:
- ~Deliverable: Issues that are expected to be delivered in the current
milestone.
@@ -203,16 +203,46 @@ Each issue scheduled for the current milestone should be labeled ~Deliverable
or ~"Stretch". Any open issue for a previous milestone should be labeled
~"Next Patch Release", or otherwise rescheduled to a different milestone.
-### Severity labels (~S1, ~S2, etc.)
+### Bug Priority labels (~P1, ~P2, ~P3 & etc.)
-Severity labels help us clearly communicate the impact of a ~bug on users.
+Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be. If there are multiple defects, the priority decides which defect has to be fixed immediately versus later.
+This label documents the planned timeline & urgency which is used to measure against our actual SLA on delivering ~bug fixes.
-| Label | Meaning | Example |
-|-------|------------------------------------------|---------|
-| ~S1 | Feature broken, no workaround | Unable to create an issue |
-| ~S2 | Feature broken, workaround unacceptable | Can push commits, but only via the command line |
-| ~S3 | Feature broken, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue |
-| ~S4 | Cosmetic issue | Label colors are incorrect / not being displayed |
+| Label | Meaning | Estimate time to fix | Guidance |
+|-------|-----------------|------------------------------------------------------------------|----------|
+| ~P1 | Urgent Priority | The current release | |
+| ~P2 | High Priority | The next release | |
+| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
+| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
+
+#### Specific Priority guidance
+
+| Label | Availability / Performance |
+|-------|--------------------------------------------------------------|
+| ~P1 | |
+| ~P2 | The issue is (almost) guaranteed to occur in the near future |
+| ~P3 | The issue is likely to occur in the near future |
+| ~P4 | The issue _may_ occur but it's not likely |
+
+### Bug Severity labels (~S1, ~S2, ~S3 & etc.)
+
+Severity labels help us clearly communicate the impact of a ~bug on users.
+
+| Label | Meaning | Impact of the defect | Example |
+|-------|-------------------|-------------------------------------------------------|---------|
+| ~S1 | Blocker | Outage, broken feature with no workaround | Unable to create an issue. Data corruption/loss. Security breach. |
+| ~S2 | Critical Severity | Broken Feature, workaround too complex & unacceptable | Can push commits, but only via the command line. |
+| ~S3 | Major Severity | Broken Feature, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue. |
+| ~S4 | Low Severity | Functionality inconvenience or cosmetic issue | Label colors are incorrect / not being displayed. |
+
+#### Specific Severity guidance
+
+| Label | Security Impact |
+|-------|-------------------------------------------------------------------|
+| ~S1 | >50% customers impacted (possible company extinction level event) |
+| ~S2 | Multiple customers impacted (but not apocalyptic) |
+| ~S3 | A single customer impacted |
+| ~S4 | No customer impact, or expected impact within 30 days |
### Label for community contributors (~"Accepting Merge Requests")
diff --git a/Gemfile b/Gemfile
index a8cc34125f5..71f27e0f6de 100644
--- a/Gemfile
+++ b/Gemfile
@@ -62,7 +62,7 @@ gem 'akismet', '~> 2.0'
# Two-factor authentication
gem 'devise-two-factor', '~> 3.0.0'
gem 'rqrcode-rails3', '~> 0.1.7'
-gem 'attr_encrypted', '~> 3.0.0'
+gem 'attr_encrypted', '~> 3.1.0'
gem 'u2f', '~> 0.2.1'
# GitLab Pages
@@ -82,7 +82,7 @@ gem 'net-ldap'
# Git Wiki
# Required manually in config/initializers/gollum.rb to control load order
-gem 'gitlab-gollum-lib', '~> 4.2'
+gem 'gitlab-gollum-lib', '~> 4.2', require: false
gem 'gitlab-gollum-rugged_adapter', '~> 0.4.4', require: false
@@ -140,7 +140,7 @@ gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.6'
gem 'asciidoctor-plantuml', '0.0.8'
-gem 'rouge', '~> 2.0'
+gem 'rouge', '~> 3.1'
gem 'truncato', '~> 0.7.9'
gem 'bootstrap_form', '~> 2.7.0'
gem 'nokogiri', '~> 1.8.2'
@@ -415,7 +415,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.94.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.97.0', require: 'gitaly'
gem 'grpc', '~> 1.10.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
@@ -434,5 +434,3 @@ gem 'grape_logging', '~> 1.7'
# Asset synchronization
gem 'asset_sync', '~> 2.2.0'
-
-gem 'goldiloader', '~> 2.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index bbe3c9e49b8..745732f3537 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -66,7 +66,7 @@ GEM
unf
ast (2.4.0)
atomic (1.1.99)
- attr_encrypted (3.0.3)
+ attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
attr_required (1.0.0)
autoprefixer-rails (6.2.3)
@@ -290,9 +290,9 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.94.0)
+ gitaly-proto (0.97.0)
google-protobuf (~> 3.1)
- grpc (~> 1.0)
+ grpc (~> 1.10)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
@@ -303,12 +303,12 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
- gitlab-gollum-lib (4.2.7.1)
+ gitlab-gollum-lib (4.2.7.2)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
- rouge (~> 2.1)
+ rouge (~> 3.1)
sanitize (~> 2.1)
stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4)
@@ -331,9 +331,6 @@ GEM
rubyntlm (~> 0.5)
globalid (0.4.1)
activesupport (>= 4.2.0)
- goldiloader (2.0.1)
- activerecord (>= 4.2, < 5.2)
- activesupport (>= 4.2, < 5.2)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
gon (6.1.0)
@@ -750,7 +747,7 @@ GEM
retriable (3.1.1)
rinku (2.0.0)
rotp (2.1.2)
- rouge (2.2.1)
+ rouge (3.1.1)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
@@ -1004,7 +1001,7 @@ DEPENDENCIES
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.2.0)
- attr_encrypted (~> 3.0.0)
+ attr_encrypted (~> 3.1.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
base32 (~> 0.3.0)
@@ -1064,7 +1061,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.94.0)
+ gitaly-proto (~> 0.97.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
@@ -1072,7 +1069,6 @@ DEPENDENCIES
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
- goldiloader (~> 2.0)
gon (~> 6.1.0)
google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)
@@ -1164,7 +1160,7 @@ DEPENDENCIES
redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
- rouge (~> 2.0)
+ rouge (~> 3.1)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.6.0)
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index c953b9708a0..a0330cbdd02 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -69,7 +69,7 @@ GEM
unf
ast (2.4.0)
atomic (1.1.100)
- attr_encrypted (3.0.3)
+ attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
attr_required (1.0.1)
autoprefixer-rails (8.1.0.1)
@@ -291,9 +291,9 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.94.0)
+ gitaly-proto (0.97.0)
google-protobuf (~> 3.1)
- grpc (~> 1.0)
+ grpc (~> 1.10)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
@@ -304,6 +304,17 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
+ gitlab-gollum-lib (4.2.7.1)
+ gemojione (~> 3.2)
+ github-markup (~> 1.6)
+ gollum-grit_adapter (~> 1.0)
+ nokogiri (>= 1.6.1, < 2.0)
+ rouge (~> 2.1)
+ sanitize (~> 2.1)
+ stringex (~> 2.6)
+ gitlab-gollum-rugged_adapter (0.4.4)
+ mime-types (>= 1.15)
+ rugged (~> 0.25)
gitlab-grit (2.8.2)
charlock_holmes (~> 0.6)
diff-lcs (~> 1.1)
@@ -321,22 +332,8 @@ GEM
rubyntlm (~> 0.5)
globalid (0.4.1)
activesupport (>= 4.2.0)
- goldiloader (2.0.1)
- activerecord (>= 4.2, < 5.2)
- activesupport (>= 4.2, < 5.2)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
- gollum-lib (4.2.7)
- gemojione (~> 3.2)
- github-markup (~> 1.6)
- gollum-grit_adapter (~> 1.0)
- nokogiri (>= 1.6.1, < 2.0)
- rouge (~> 2.1)
- sanitize (~> 2.1)
- stringex (~> 2.6)
- gollum-rugged_adapter (0.4.4)
- mime-types (>= 1.15)
- rugged (~> 0.25)
gon (6.1.0)
actionpack (>= 3.0)
json
@@ -1009,7 +1006,7 @@ DEPENDENCIES
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.2.0)
- attr_encrypted (~> 3.0.0)
+ attr_encrypted (~> 3.1.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
base32 (~> 0.3.0)
@@ -1069,15 +1066,14 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.94.0)
+ gitaly-proto (~> 0.97.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
+ gitlab-gollum-lib (~> 4.2)
+ gitlab-gollum-rugged_adapter (~> 0.4.4)
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
- goldiloader (~> 2.0)
- gollum-lib (~> 4.2)
- gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)
diff --git a/LICENSE b/LICENSE
index 15c423e1416..a76372fad2c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,25 +1,7 @@
-Copyright (c) 2011-2017 GitLab B.V.
+Copyright GitLab B.V.
-With regard to the GitLab Software:
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
-For all third party components incorporated into the GitLab Software, those
-components are licensed under the original license provided by the owner of the
-applicable component. \ No newline at end of file
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file
diff --git a/README.md b/README.md
index 9ead6d51c5d..9c1aad65307 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,12 @@ You can access a new installation with the login **`root`** and password **`5ive
GitLab is an open source project and we are very happy to accept community contributions. Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details.
+## Licensing
+
+GitLab Community Edition (CE) is available freely under the MIT Expat license.
+
+All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component.
+
## Install a development environment
To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit).
diff --git a/app/assets/javascripts/branches/branches_delete_modal.js b/app/assets/javascripts/branches/branches_delete_modal.js
index 839e369eaf6..f34496f84c6 100644
--- a/app/assets/javascripts/branches/branches_delete_modal.js
+++ b/app/assets/javascripts/branches/branches_delete_modal.js
@@ -16,6 +16,7 @@ class DeleteModal {
bindEvents() {
this.$toggleBtns.on('click', this.setModalData.bind(this));
this.$confirmInput.on('input', this.setDeleteDisabled.bind(this));
+ this.$deleteBtn.on('click', this.setDisableDeleteButton.bind(this));
}
setModalData(e) {
@@ -30,6 +31,16 @@ class DeleteModal {
this.$deleteBtn.attr('disabled', e.currentTarget.value !== this.branchName);
}
+ setDisableDeleteButton(e) {
+ if (this.$deleteBtn.is('[disabled]')) {
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ }
+
+ return true;
+ }
+
updateModal() {
this.$branchName.text(this.branchName);
this.$confirmInput.val('');
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index f8dcdf3f60a..9c12b89240c 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -1,96 +1,102 @@
<script>
- import _ from 'underscore';
- import { s__, sprintf } from '../../locale';
- import applicationRow from './application_row.vue';
- import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
- import {
- APPLICATION_INSTALLED,
- INGRESS,
- } from '../constants';
+import _ from 'underscore';
+import { s__, sprintf } from '../../locale';
+import applicationRow from './application_row.vue';
+import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import { APPLICATION_INSTALLED, INGRESS } from '../constants';
- export default {
- components: {
- applicationRow,
- clipboardButton,
+export default {
+ components: {
+ applicationRow,
+ clipboardButton,
+ },
+ props: {
+ applications: {
+ type: Object,
+ required: false,
+ default: () => ({}),
},
- props: {
- applications: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- helpPath: {
- type: String,
- required: false,
- default: '',
- },
- ingressHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- ingressDnsHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- managePrometheusPath: {
- type: String,
- required: false,
- default: '',
- },
+ helpPath: {
+ type: String,
+ required: false,
+ default: '',
},
- computed: {
- generalApplicationDescription() {
- return sprintf(
- _.escape(s__(
+ ingressHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ ingressDnsHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ managePrometheusPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ generalApplicationDescription() {
+ return sprintf(
+ _.escape(
+ s__(
`ClusterIntegration|Install applications on your Kubernetes cluster.
Read more about %{helpLink}`,
- )), {
- helpLink: `<a href="${this.helpPath}">
+ ),
+ ),
+ {
+ helpLink: `<a href="${this.helpPath}">
${_.escape(s__('ClusterIntegration|installing applications'))}
</a>`,
- },
- false,
- );
- },
- ingressId() {
- return INGRESS;
- },
- ingressInstalled() {
- return this.applications.ingress.status === APPLICATION_INSTALLED;
- },
- ingressExternalIp() {
- return this.applications.ingress.externalIp;
- },
- ingressDescription() {
- const extraCostParagraph = sprintf(
- _.escape(s__(
+ },
+ false,
+ );
+ },
+ ingressId() {
+ return INGRESS;
+ },
+ ingressInstalled() {
+ return this.applications.ingress.status === APPLICATION_INSTALLED;
+ },
+ ingressExternalIp() {
+ return this.applications.ingress.externalIp;
+ },
+ ingressDescription() {
+ const extraCostParagraph = sprintf(
+ _.escape(
+ s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources
like a load balancer, which may incur additional costs depending on
- the hosting provider your Kubernetes cluster is installed on. If you are using GKE,
- you can %{pricingLink}.`,
- )), {
- boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
- pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
+ the hosting provider your Kubernetes cluster is installed on. If you are using
+ Google Kubernetes Engine, you can %{pricingLink}.`,
+ ),
+ ),
+ {
+ boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
+ pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|check the pricing here'))}</a>`,
- },
- false,
- );
+ },
+ false,
+ );
- const externalIpParagraph = sprintf(
- _.escape(s__(
+ const externalIpParagraph = sprintf(
+ _.escape(
+ s__(
`ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS
at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}`,
- )), {
- ingressHelpLink: `<a href="${this.ingressHelpPath}">
+ ),
+ ),
+ {
+ ingressHelpLink: `<a href="${this.ingressHelpPath}">
${_.escape(s__('ClusterIntegration|More information'))}
</a>`,
- },
- false,
- );
+ },
+ false,
+ );
- return `
+ return `
<p>
${extraCostParagraph}
</p>
@@ -98,22 +104,25 @@
${externalIpParagraph}
</p>
`;
- },
- prometheusDescription() {
- return sprintf(
- _.escape(s__(
+ },
+ prometheusDescription() {
+ return sprintf(
+ _.escape(
+ s__(
`ClusterIntegration|Prometheus is an open-source monitoring system
with %{gitlabIntegrationLink} to monitor deployed applications.`,
- )), {
- gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
+ ),
+ ),
+ {
+ gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`,
- },
- false,
- );
- },
+ },
+ false,
+ );
},
- };
+ },
+};
</script>
<template>
@@ -205,7 +214,7 @@
>
{{ s__(`ClusterIntegration|The IP address is in
the process of being assigned. Please check your Kubernetes
- cluster or Quotas on GKE if it takes a long time.`) }}
+ cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }}
<a
:href="ingressHelpPath"
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index 842a4255f08..4164149dd06 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -2,7 +2,9 @@
import $ from 'jquery';
import Pikaday from 'pikaday';
+import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
+import { timeFor } from './lib/utils/datetime_utility';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
class DueDateSelect {
@@ -14,6 +16,7 @@ class DueDateSelect {
this.$dropdownParent = $dropdownParent;
this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
this.$block = $block;
+ this.$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
this.$selectbox = $dropdown.closest('.selectbox');
this.$value = $block.find('.value');
this.$valueContent = $block.find('.value-content');
@@ -128,7 +131,8 @@ class DueDateSelect {
submitSelectedDate(isDropdown) {
const selectedDateValue = this.datePayload[this.abilityName].due_date;
- const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
+ const hasDueDate = this.displayedDate !== 'No due date';
+ const displayedDateStyle = hasDueDate ? 'bold' : 'no-value';
this.$loading.removeClass('hidden').fadeIn();
@@ -145,10 +149,13 @@ class DueDateSelect {
return axios.put(this.issueUpdateURL, this.datePayload)
.then(() => {
+ const tooltipText = hasDueDate ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})` : __('Due date');
if (isDropdown) {
this.$dropdown.trigger('loaded.gl.dropdown');
this.$dropdown.dropdown('toggle');
}
+ this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
+
return this.$loading.fadeOut();
});
}
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue
index 037e3efb4ce..1fc11c84639 100644
--- a/app/assets/javascripts/ide/components/changed_file_icon.vue
+++ b/app/assets/javascripts/ide/components/changed_file_icon.vue
@@ -1,31 +1,87 @@
<script>
-import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+import { pluralize } from '~/lib/utils/text_utility';
+import { __, sprintf } from '~/locale';
export default {
components: {
- icon,
+ Icon,
+ },
+ directives: {
+ tooltip,
},
props: {
file: {
type: Object,
required: true,
},
+ showTooltip: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showStagedIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
changedIcon() {
- return this.file.tempFile ? 'file-addition' : 'file-modified';
+ const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : '';
+ return this.file.tempFile ? `file-addition${suffix}` : `file-modified${suffix}`;
+ },
+ stagedIcon() {
+ return `${this.changedIcon}-solid`;
},
changedIconClass() {
- return `multi-${this.changedIcon}`;
+ return `multi-${this.changedIcon} prepend-left-5 pull-left`;
+ },
+ tooltipTitle() {
+ if (!this.showTooltip) return undefined;
+
+ const type = this.file.tempFile ? 'addition' : 'modification';
+
+ if (this.file.changed && !this.file.staged) {
+ return sprintf(__('Unstaged %{type}'), {
+ type,
+ });
+ } else if (!this.file.changed && this.file.staged) {
+ return sprintf(__('Staged %{type}'), {
+ type,
+ });
+ } else if (this.file.changed && this.file.staged) {
+ return sprintf(__('Unstaged and staged %{type}'), {
+ type: pluralize(type),
+ });
+ }
+
+ return undefined;
},
},
};
</script>
<template>
- <icon
- :name="changedIcon"
- :size="12"
- :css-classes="`ide-file-changed-icon ${changedIconClass}`"
- />
+ <span
+ v-tooltip
+ :title="tooltipTitle"
+ data-container="body"
+ data-placement="right"
+ class="ide-file-changed-icon"
+ >
+ <icon
+ v-if="file.staged && showStagedIcon"
+ :name="stagedIcon"
+ :size="12"
+ :css-classes="changedIconClass"
+ />
+ <icon
+ v-if="file.changed || file.tempFile || (file.staged && !showStagedIcon)"
+ :name="changedIcon"
+ :size="12"
+ :css-classes="changedIconClass"
+ />
+ </span>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index 2cbd982af19..45321df191c 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -1,41 +1,27 @@
<script>
- import { mapState } from 'vuex';
- import { sprintf, __ } from '~/locale';
- import * as consts from '../../stores/modules/commit/constants';
- import RadioGroup from './radio_group.vue';
+import { mapState } from 'vuex';
+import { sprintf, __ } from '~/locale';
+import * as consts from '../../stores/modules/commit/constants';
+import RadioGroup from './radio_group.vue';
- export default {
- components: {
- RadioGroup,
+export default {
+ components: {
+ RadioGroup,
+ },
+ computed: {
+ ...mapState(['currentBranchId']),
+ commitToCurrentBranchText() {
+ return sprintf(
+ __('Commit to %{branchName} branch'),
+ { branchName: `<strong class="monospace">${this.currentBranchId}</strong>` },
+ false,
+ );
},
- computed: {
- ...mapState([
- 'currentBranchId',
- ]),
- newMergeRequestHelpText() {
- return sprintf(
- __('Creates a new branch from %{branchName} and re-directs to create a new merge request'),
- { branchName: this.currentBranchId },
- );
- },
- commitToCurrentBranchText() {
- return sprintf(
- __('Commit to %{branchName} branch'),
- { branchName: `<strong>${this.currentBranchId}</strong>` },
- false,
- );
- },
- commitToNewBranchText() {
- return sprintf(
- __('Creates a new branch from %{branchName}'),
- { branchName: this.currentBranchId },
- );
- },
- },
- commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
- commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
- commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
- };
+ },
+ commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
+ commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
+ commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
+};
</script>
<template>
@@ -53,13 +39,11 @@
:value="$options.commitToNewBranch"
:label="__('Create a new branch')"
:show-input="true"
- :help-text="commitToNewBranchText"
/>
<radio-group
:value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')"
:show-input="true"
- :help-text="newMergeRequestHelpText"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
new file mode 100644
index 00000000000..6424b93ce54
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
@@ -0,0 +1,93 @@
+<script>
+import { mapActions, mapState, mapGetters } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ noChangesStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ committedStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['lastCommitMsg', 'rightPanelCollapsed']),
+ ...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
+ statusSvg() {
+ return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
+ },
+ },
+ methods: {
+ ...mapActions(['toggleRightPanelCollapsed']),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state"
+ >
+ <header
+ class="multi-file-commit-panel-header"
+ :class="{
+ 'is-collapsed': rightPanelCollapsed,
+ }"
+ >
+ <button
+ v-tooltip
+ :title="collapseButtonTooltip"
+ data-container="body"
+ data-placement="left"
+ type="button"
+ class="btn btn-transparent multi-file-commit-panel-collapse-btn"
+ :aria-label="__('Toggle sidebar')"
+ @click.stop="toggleRightPanelCollapsed"
+ >
+ <icon
+ :name="collapseButtonIcon"
+ :size="18"
+ />
+ </button>
+ </header>
+ <div
+ class="ide-commit-empty-state-container"
+ v-if="!rightPanelCollapsed"
+ >
+ <div class="svg-content svg-80">
+ <img :src="statusSvg" />
+ </div>
+ <div class="append-right-default prepend-left-default">
+ <div
+ class="text-content text-center"
+ v-if="!lastCommitMsg"
+ >
+ <h4>
+ {{ __('No changes') }}
+ </h4>
+ <p>
+ {{ __('Edit files in the editor and commit changes here') }}
+ </p>
+ </div>
+ <div
+ class="text-content text-center"
+ v-else
+ >
+ <h4>
+ {{ __('All changes are committed') }}
+ </h4>
+ <p v-html="lastCommitMsg"></p>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index 453208f3f19..ff05ee8682a 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -1,56 +1,132 @@
<script>
- import { mapState } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
- import listItem from './list_item.vue';
- import listCollapsed from './list_collapsed.vue';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import { __, sprintf } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import ListItem from './list_item.vue';
+import ListCollapsed from './list_collapsed.vue';
- export default {
- components: {
- icon,
- listItem,
- listCollapsed,
+export default {
+ components: {
+ Icon,
+ ListItem,
+ ListCollapsed,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
},
- props: {
- title: {
- type: String,
- required: true,
- },
- fileList: {
- type: Array,
- required: true,
- },
+ fileList: {
+ type: Array,
+ required: true,
},
- computed: {
- ...mapState([
- 'currentProjectId',
- 'currentBranchId',
- 'rightPanelCollapsed',
- ]),
- isCommitInfoShown() {
- return this.rightPanelCollapsed || this.fileList.length;
- },
+ showToggle: {
+ type: Boolean,
+ required: false,
+ default: true,
},
- methods: {
- toggleCollapsed() {
- this.$emit('toggleCollapsed');
- },
+ iconName: {
+ type: String,
+ required: true,
},
- };
+ action: {
+ type: String,
+ required: true,
+ },
+ actionBtnText: {
+ type: String,
+ required: true,
+ },
+ itemActionComponent: {
+ type: String,
+ required: true,
+ },
+ stagedList: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapState(['rightPanelCollapsed']),
+ ...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
+ titleText() {
+ return sprintf(__('%{title} changes'), {
+ title: this.title,
+ });
+ },
+ },
+ methods: {
+ ...mapActions(['toggleRightPanelCollapsed', 'stageAllChanges', 'unstageAllChanges']),
+ actionBtnClicked() {
+ this[this.action]();
+ },
+ },
+};
</script>
<template>
<div
+ class="ide-commit-list-container"
:class="{
- 'multi-file-commit-list': isCommitInfoShown
+ 'is-collapsed': rightPanelCollapsed,
}"
>
+ <header
+ class="multi-file-commit-panel-header"
+ >
+ <div
+ v-if="!rightPanelCollapsed"
+ class="multi-file-commit-panel-header-title"
+ :class="{
+ 'append-right-10': showToggle,
+ }"
+ >
+ <icon
+ v-once
+ :name="iconName"
+ :size="18"
+ />
+ {{ titleText }}
+ <button
+ type="button"
+ class="btn btn-blank btn-link ide-staged-action-btn"
+ @click="actionBtnClicked"
+ >
+ {{ actionBtnText }}
+ </button>
+ </div>
+ <button
+ v-if="showToggle"
+ v-tooltip
+ :title="collapseButtonTooltip"
+ data-container="body"
+ data-placement="left"
+ type="button"
+ class="btn btn-transparent multi-file-commit-panel-collapse-btn"
+ :aria-label="__('Toggle sidebar')"
+ @click.stop="toggleRightPanelCollapsed"
+ >
+ <icon
+ :name="collapseButtonIcon"
+ :size="18"
+ />
+ </button>
+ </header>
<list-collapsed
v-if="rightPanelCollapsed"
+ :files="fileList"
+ :icon-name="iconName"
+ :title="title"
/>
<template v-else>
<ul
v-if="fileList.length"
- class="list-unstyled append-bottom-0"
+ class="multi-file-commit-list list-unstyled append-bottom-0"
>
<li
v-for="file in fileList"
@@ -58,9 +134,18 @@
>
<list-item
:file="file"
+ :action-component="itemActionComponent"
+ :key-prefix="title"
+ :staged-list="stagedList"
/>
</li>
</ul>
+ <p
+ v-else
+ class="multi-file-commit-list help-block"
+ >
+ {{ __('No changes') }}
+ </p>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
index 15918ac9631..2254271c679 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
@@ -1,35 +1,110 @@
<script>
- import { mapGetters } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { sprintf, n__, __ } from '~/locale';
- export default {
- components: {
- icon,
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ files: {
+ type: Array,
+ required: true,
},
- computed: {
- ...mapGetters([
- 'addedFiles',
- 'modifiedFiles',
- ]),
+ iconName: {
+ type: String,
+ required: true,
},
- };
+ title: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ addedFilesLength() {
+ return this.files.filter(f => f.tempFile).length;
+ },
+ modifiedFilesLength() {
+ return this.files.filter(f => !f.tempFile).length;
+ },
+ addedFilesIconClass() {
+ return this.addedFilesLength ? 'multi-file-addition' : '';
+ },
+ modifiedFilesClass() {
+ return this.modifiedFilesLength ? 'multi-file-modified' : '';
+ },
+ additionsTooltip() {
+ return sprintf(n__('1 %{type} addition', '%d %{type} additions', this.addedFilesLength), {
+ type: this.title.toLowerCase(),
+ });
+ },
+ modifiedTooltip() {
+ return sprintf(
+ n__('1 %{type} modification', '%d %{type} modifications', this.modifiedFilesLength),
+ { type: this.title.toLowerCase() },
+ );
+ },
+ titleTooltip() {
+ return sprintf(__('%{title} changes'), { title: this.title });
+ },
+ additionIconName() {
+ return this.title.toLowerCase() === 'staged' ? 'file-addition-solid' : 'file-addition';
+ },
+ modifiedIconName() {
+ return this.title.toLowerCase() === 'staged' ? 'file-modified-solid' : 'file-modified';
+ },
+ },
+};
</script>
<template>
<div
class="multi-file-commit-list-collapsed text-center"
>
- <icon
- name="file-addition"
- :size="18"
- css-classes="multi-file-addition append-bottom-10"
- />
- {{ addedFiles.length }}
- <icon
- name="file-modified"
- :size="18"
- css-classes="multi-file-modified prepend-top-10 append-bottom-10"
- />
- {{ modifiedFiles.length }}
+ <div
+ v-tooltip
+ :title="titleTooltip"
+ data-container="body"
+ data-placement="left"
+ class="append-bottom-15"
+ >
+ <icon
+ v-once
+ :name="iconName"
+ :size="18"
+ />
+ </div>
+ <div
+ v-tooltip
+ :title="additionsTooltip"
+ data-container="body"
+ data-placement="left"
+ class="append-bottom-10"
+ >
+ <icon
+ :name="additionIconName"
+ :size="18"
+ :css-classes="addedFilesIconClass"
+ />
+ </div>
+ {{ addedFilesLength }}
+ <div
+ v-tooltip
+ :title="modifiedTooltip"
+ data-container="body"
+ data-placement="left"
+ class="prepend-top-10 append-bottom-10"
+ >
+ <icon
+ :name="modifiedIconName"
+ :size="18"
+ :css-classes="modifiedFilesClass"
+ />
+ </div>
+ {{ modifiedFilesLength }}
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 560cdd941cd..ad4713c40d5 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -1,34 +1,69 @@
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
+import StageButton from './stage_button.vue';
+import UnstageButton from './unstage_button.vue';
export default {
components: {
Icon,
+ StageButton,
+ UnstageButton,
},
props: {
file: {
type: Object,
required: true,
},
+ actionComponent: {
+ type: String,
+ required: true,
+ },
+ keyPrefix: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ stagedList: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
iconName() {
- return this.file.tempFile ? 'file-addition' : 'file-modified';
+ const prefix = this.stagedList ? '-solid' : '';
+ return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`;
},
iconClass() {
- return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
+ return `multi-file-${this.file.tempFile ? 'additions' : 'modified'} append-right-8`;
},
},
methods: {
- ...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']),
- openFileInEditor(file) {
- return this.openPendingTab(file).then(changeViewer => {
+ ...mapActions([
+ 'discardFileChanges',
+ 'updateViewer',
+ 'openPendingTab',
+ 'unstageChange',
+ 'stageChange',
+ ]),
+ openFileInEditor() {
+ return this.openPendingTab({
+ file: this.file,
+ keyPrefix: this.keyPrefix.toLowerCase(),
+ }).then(changeViewer => {
if (changeViewer) {
this.updateViewer('diff');
}
});
},
+ fileAction() {
+ if (this.file.staged) {
+ this.unstageChange(this.file.path);
+ } else {
+ this.stageChange(this.file.path);
+ }
+ },
},
};
</script>
@@ -38,7 +73,9 @@ export default {
<button
type="button"
class="multi-file-commit-list-path"
- @click="openFileInEditor(file)">
+ @dblclick="fileAction"
+ @click="openFileInEditor"
+ >
<span class="multi-file-commit-list-file-path">
<icon
:name="iconName"
@@ -47,12 +84,9 @@ export default {
/>{{ file.path }}
</span>
</button>
- <button
- type="button"
- class="btn btn-blank multi-file-discard-btn"
- @click="discardFileChanges(file.path)"
- >
- Discard
- </button>
+ <component
+ :is="actionComponent"
+ :path="file.path"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
new file mode 100644
index 00000000000..dcd934f76b7
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
@@ -0,0 +1,130 @@
+<script>
+import { __, sprintf } from '../../../locale';
+import Icon from '../../../vue_shared/components/icon.vue';
+import popover from '../../../vue_shared/directives/popover';
+import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants';
+
+export default {
+ directives: {
+ popover,
+ },
+ components: {
+ Icon,
+ },
+ props: {
+ text: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ scrollTop: 0,
+ isFocused: false,
+ };
+ },
+ computed: {
+ allLines() {
+ return this.text.split('\n').map((line, i) => ({
+ text: line.substr(0, this.getLineLength(i)) || ' ',
+ highlightedText: line.substr(this.getLineLength(i)),
+ }));
+ },
+ },
+ methods: {
+ handleScroll() {
+ if (this.$refs.textarea) {
+ this.$nextTick(() => {
+ this.scrollTop = this.$refs.textarea.scrollTop;
+ });
+ }
+ },
+ getLineLength(i) {
+ return i === 0 ? MAX_TITLE_LENGTH : MAX_BODY_LENGTH;
+ },
+ onInput(e) {
+ this.$emit('input', e.target.value);
+ },
+ updateIsFocused(isFocused) {
+ this.isFocused = isFocused;
+ },
+ },
+ popoverOptions: {
+ trigger: 'hover',
+ placement: 'top',
+ content: sprintf(
+ __(`
+ The character highligher helps you keep the subject line to %{titleLength} characters
+ and wrap the body at %{bodyLength} so they are readable in git.
+ `),
+ { titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH },
+ ),
+ },
+};
+</script>
+
+<template>
+ <fieldset class="common-note-form ide-commit-message-field">
+ <div
+ class="md-area"
+ :class="{
+ 'is-focused': isFocused
+ }"
+ >
+ <div
+ v-once
+ class="md-header"
+ >
+ <ul class="nav-links">
+ <li>
+ {{ __('Commit Message') }}
+ <span
+ v-popover="$options.popoverOptions"
+ class="help-block prepend-left-10"
+ >
+ <icon
+ name="question"
+ />
+ </span>
+ </li>
+ </ul>
+ </div>
+ <div class="ide-commit-message-textarea-container">
+ <div class="ide-commit-message-highlights-container">
+ <div
+ class="note-textarea highlights monospace"
+ :style="{
+ transform: `translate3d(0, ${-scrollTop}px, 0)`
+ }"
+ >
+ <div
+ v-for="(line, index) in allLines"
+ :key="index"
+ >
+ <span
+ v-text="line.text"
+ >
+ </span><mark
+ v-show="line.highlightedText"
+ v-text="line.highlightedText"
+ >
+ </mark>
+ </div>
+ </div>
+ </div>
+ <textarea
+ class="note-textarea ide-commit-message-textarea"
+ name="commit-message"
+ :placeholder="__('Write a commit message...')"
+ :value="text"
+ @scroll="handleScroll"
+ @input="onInput"
+ @focus="updateIsFocused(true)"
+ @blur="updateIsFocused(false)"
+ ref="textarea"
+ >
+ </textarea>
+ </div>
+ </div>
+ </fieldset>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
index 4310d762c78..b660a2961cb 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
@@ -1,52 +1,40 @@
<script>
- import { mapActions, mapState, mapGetters } from 'vuex';
- import tooltip from '~/vue_shared/directives/tooltip';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import tooltip from '~/vue_shared/directives/tooltip';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ props: {
+ value: {
+ type: String,
+ required: true,
},
- props: {
- value: {
- type: String,
- required: true,
- },
- label: {
- type: String,
- required: false,
- default: null,
- },
- checked: {
- type: Boolean,
- required: false,
- default: false,
- },
- showInput: {
- type: Boolean,
- required: false,
- default: false,
- },
- helpText: {
- type: String,
- required: false,
- default: null,
- },
+ label: {
+ type: String,
+ required: false,
+ default: null,
},
- computed: {
- ...mapState('commit', [
- 'commitAction',
- ]),
- ...mapGetters('commit', [
- 'newBranchName',
- ]),
+ checked: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- methods: {
- ...mapActions('commit', [
- 'updateCommitAction',
- 'updateBranchName',
- ]),
+ showInput: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- };
+ },
+ computed: {
+ ...mapState('commit', ['commitAction']),
+ ...mapGetters('commit', ['newBranchName']),
+ },
+ methods: {
+ ...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
+ },
+};
</script>
<template>
@@ -65,18 +53,6 @@
{{ label }}
</template>
<slot v-else></slot>
- <span
- v-if="helpText"
- v-tooltip
- class="help-block inline"
- :title="helpText"
- >
- <i
- class="fa fa-question-circle"
- aria-hidden="true"
- >
- </i>
- </span>
</span>
</label>
<div
@@ -85,7 +61,7 @@
>
<input
type="text"
- class="form-control"
+ class="form-control monospace"
:placeholder="newBranchName"
@input="updateBranchName($event.target.value)"
/>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
new file mode 100644
index 00000000000..52dce8412ab
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
@@ -0,0 +1,59 @@
+<script>
+import { mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ ...mapActions(['stageChange', 'discardFileChanges']),
+ },
+};
+</script>
+
+<template>
+ <div
+ v-once
+ class="multi-file-discard-btn"
+ >
+ <button
+ v-tooltip
+ type="button"
+ class="btn btn-blank append-right-5"
+ :aria-label="__('Stage changes')"
+ :title="__('Stage changes')"
+ data-container="body"
+ @click.stop="stageChange(path)"
+ >
+ <icon
+ name="mobile-issue-close"
+ :size="12"
+ />
+ </button>
+ <button
+ v-tooltip
+ type="button"
+ class="btn btn-blank"
+ :aria-label="__('Discard changes')"
+ :title="__('Discard changes')"
+ data-container="body"
+ @click.stop="discardFileChanges(path)"
+ >
+ <icon
+ name="remove"
+ :size="12"
+ />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
new file mode 100644
index 00000000000..123d60da47e
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
@@ -0,0 +1,45 @@
+<script>
+import { mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ ...mapActions(['unstageChange']),
+ },
+};
+</script>
+
+<template>
+ <div
+ v-once
+ class="multi-file-discard-btn"
+ >
+ <button
+ v-tooltip
+ type="button"
+ class="btn btn-blank"
+ :aria-label="__('Unstage changes')"
+ :title="__('Unstage changes')"
+ data-container="body"
+ @click="unstageChange(path)"
+ >
+ <icon
+ name="history"
+ :size="12"
+ />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue
index 79a83b47994..627fbeb9adf 100644
--- a/app/assets/javascripts/ide/components/ide_context_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_context_bar.vue
@@ -1,5 +1,4 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import repoCommitSection from './repo_commit_section.vue';
@@ -22,13 +21,6 @@ export default {
required: true,
},
},
- computed: {
- ...mapState(['changedFiles', 'rightPanelCollapsed']),
- ...mapGetters(['currentIcon']),
- },
- methods: {
- ...mapActions(['setPanelCollapsedStatus']),
- },
};
</script>
@@ -41,40 +33,6 @@ export default {
<div
class="multi-file-commit-panel-section"
>
- <header
- class="multi-file-commit-panel-header"
- :class="{
- 'is-collapsed': rightPanelCollapsed,
- }"
- >
- <div
- class="multi-file-commit-panel-header-title"
- v-if="!rightPanelCollapsed"
- >
- <div
- v-if="changedFiles.length"
- >
- <icon
- name="list-bulleted"
- :size="18"
- />
- Staged
- </div>
- </div>
- <button
- type="button"
- class="btn btn-transparent multi-file-commit-panel-collapse-btn"
- @click.stop="setPanelCollapsedStatus({
- side: 'right',
- collapsed: !rightPanelCollapsed,
- })"
- >
- <icon
- :name="currentIcon"
- :size="18"
- />
- </button>
- </header>
<repo-commit-section
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 152a5f632ad..c13eeeace3f 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -22,13 +22,6 @@ export default {
<template>
<div class="ide-status-bar">
- <div class="ref-name">
- <icon
- name="branch"
- :size="12"
- />
- {{ file.branchId }}
- </div>
<div>
<div v-if="file.lastCommit && file.lastCommit.id">
Last commit:
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index d885ed5e301..877d1b5e026 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -1,20 +1,24 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
-import icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
-import commitFilesList from './commit_sidebar/list.vue';
+import CommitFilesList from './commit_sidebar/list.vue';
+import EmptyState from './commit_sidebar/empty_state.vue';
+import CommitMessageField from './commit_sidebar/message_field.vue';
import * as consts from '../stores/modules/commit/constants';
import Actions from './commit_sidebar/actions.vue';
export default {
components: {
DeprecatedModal,
- icon,
- commitFilesList,
+ Icon,
+ CommitFilesList,
+ EmptyState,
Actions,
LoadingButton,
+ CommitMessageField,
},
directives: {
tooltip,
@@ -30,43 +34,19 @@ export default {
},
},
computed: {
- ...mapState([
- 'currentProjectId',
- 'currentBranchId',
- 'rightPanelCollapsed',
- 'lastCommitMsg',
- 'changedFiles',
- ]),
+ ...mapState(['changedFiles', 'stagedFiles', 'rightPanelCollapsed']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
- ...mapGetters('commit', [
- 'commitButtonDisabled',
- 'discardDraftButtonDisabled',
- 'branchName',
- ]),
- statusSvg() {
- return this.lastCommitMsg
- ? this.committedStateSvgPath
- : this.noChangesStateSvgPath;
- },
+ ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled', 'branchName']),
},
methods: {
- ...mapActions(['setPanelCollapsedStatus']),
...mapActions('commit', [
'updateCommitMessage',
'discardDraft',
'commitChanges',
'updateCommitAction',
]),
- toggleCollapsed() {
- this.setPanelCollapsedStatus({
- side: 'right',
- collapsed: !this.rightPanelCollapsed,
- });
- },
forceCreateNewBranch() {
- return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() =>
- this.commitChanges(),
- );
+ return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
},
},
};
@@ -75,9 +55,6 @@ export default {
<template>
<div
class="multi-file-commit-panel-section"
- :class="{
- 'multi-file-commit-empty-state-container': !changedFiles.length
- }"
>
<deprecated-modal
id="ide-create-branch-modal"
@@ -91,30 +68,36 @@ export default {
Would you like to create a new branch?`) }}
</template>
</deprecated-modal>
- <commit-files-list
- title="Staged"
- :file-list="changedFiles"
- :collapsed="rightPanelCollapsed"
- @toggleCollapsed="toggleCollapsed"
- />
<template
- v-if="changedFiles.length"
+ v-if="changedFiles.length || stagedFiles.length"
>
+ <commit-files-list
+ icon-name="unstaged"
+ :title="__('Unstaged')"
+ :file-list="changedFiles"
+ action="stageAllChanges"
+ :action-btn-text="__('Stage all')"
+ item-action-component="stage-button"
+ />
+ <commit-files-list
+ icon-name="staged"
+ :title="__('Staged')"
+ :file-list="stagedFiles"
+ action="unstageAllChanges"
+ :action-btn-text="__('Unstage all')"
+ item-action-component="unstage-button"
+ :show-toggle="false"
+ :staged-list="true"
+ />
<form
class="form-horizontal multi-file-commit-form"
@submit.prevent.stop="commitChanges"
v-if="!rightPanelCollapsed"
>
- <div class="multi-file-commit-fieldset">
- <textarea
- class="form-control multi-file-commit-message"
- name="commit-message"
- :value="commitMessage"
- :placeholder="__('Write a commit message...')"
- @input="updateCommitMessage($event.target.value)"
- >
- </textarea>
- </div>
+ <commit-message-field
+ :text="commitMessage"
+ @input="updateCommitMessage"
+ />
<div class="clearfix prepend-top-15">
<actions />
<loading-button
@@ -135,38 +118,10 @@ export default {
</div>
</form>
</template>
- <div
- v-else-if="!rightPanelCollapsed"
- class="row js-empty-state"
- >
- <div class="col-xs-10 col-xs-offset-1">
- <div class="svg-content svg-80">
- <img :src="statusSvg" />
- </div>
- </div>
- <div class="col-xs-10 col-xs-offset-1">
- <div
- class="text-content text-center"
- v-if="!lastCommitMsg"
- >
- <h4>
- {{ __('No changes') }}
- </h4>
- <p>
- {{ __('Edit files in the editor and commit changes here') }}
- </p>
- </div>
- <div
- class="text-content text-center"
- v-else
- >
- <h4>
- {{ __('All changes are committed') }}
- </h4>
- <p v-html="lastCommitMsg">
- </p>
- </div>
- </div>
- </div>
+ <empty-state
+ v-else
+ :no-changes-state-svg-path="noChangesStateSvgPath"
+ :committed-state-svg-path="committedStateSvgPath"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 711bafa17a9..3a04cdd8e46 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -20,7 +20,7 @@ export default {
},
computed: {
...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
- ...mapGetters(['currentMergeRequest']),
+ ...mapGetters(['currentMergeRequest', 'getStagedFile']),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.content;
},
@@ -120,7 +120,12 @@ export default {
setupEditor() {
if (!this.file || !this.editor.instance) return;
- this.model = this.editor.createModel(this.file);
+ const head = this.getStagedFile(this.file.path);
+
+ this.model = this.editor.createModel(
+ this.file,
+ this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null,
+ );
if (this.viewer === 'mrdiff') {
this.editor.attachMergeRequestModel(this.model);
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 3b5068d4910..8b18c7d28b4 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -102,8 +102,11 @@ export default {
v-if="file.mrChange"
/>
<changed-file-icon
+ v-if="file.changed || file.tempFile || file.staged"
:file="file"
- v-if="file.changed || file.tempFile"
+ :show-tooltip="true"
+ :show-staged-icon="true"
+ class="prepend-top-5 pull-right"
/>
</span>
<new-dropdown
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index 304a73ed1ad..35a362b01e0 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -26,13 +26,16 @@ export default {
},
computed: {
closeLabel() {
- if (this.tab.changed || this.tab.tempFile) {
+ if (this.fileHasChanged) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
showChangedIcon() {
- return this.tab.changed ? !this.tabMouseOver : false;
+ return this.fileHasChanged ? !this.tabMouseOver : false;
+ },
+ fileHasChanged() {
+ return this.tab.changed || this.tab.tempFile || this.tab.staged;
},
},
@@ -42,18 +45,18 @@ export default {
this.updateDelayViewerUpdated(true);
if (tab.pending) {
- this.openPendingTab(tab);
+ this.openPendingTab({ file: tab, keyPrefix: tab.staged ? 'staged' : 'unstaged' });
} else {
this.$router.push(`/project${tab.url}`);
}
},
mouseOverTab() {
- if (this.tab.changed) {
+ if (this.fileHasChanged) {
this.tabMouseOver = true;
}
},
mouseOutTab() {
- if (this.tab.changed) {
+ if (this.fileHasChanged) {
this.tabMouseOver = false;
}
},
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
new file mode 100644
index 00000000000..b60d042e0be
--- /dev/null
+++ b/app/assets/javascripts/ide/constants.js
@@ -0,0 +1,3 @@
+// Fuzzy file finder
+export const MAX_TITLE_LENGTH = 50;
+export const MAX_BODY_LENGTH = 72;
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index e47adae99ed..016dcda1fa1 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -3,15 +3,16 @@ import Disposable from './disposable';
import eventHub from '../../eventhub';
export default class Model {
- constructor(monaco, file) {
+ constructor(monaco, file, head = null) {
this.monaco = monaco;
this.disposable = new Disposable();
this.file = file;
+ this.head = head;
this.content = file.content !== '' ? file.content : file.raw;
this.disposable.add(
(this.originalModel = this.monaco.editor.createModel(
- this.file.raw,
+ head ? head.content : this.file.raw,
undefined,
new this.monaco.Uri(null, null, `original/${this.file.key}`),
)),
@@ -31,13 +32,15 @@ export default class Model {
);
}
- this.events = new Map();
+ this.events = new Set();
this.updateContent = this.updateContent.bind(this);
+ this.updateNewContent = this.updateNewContent.bind(this);
this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.key}`, this.dispose);
- eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
+ eventHub.$on(`editor.update.model.content.${this.file.key}`, this.updateContent);
+ eventHub.$on(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent);
}
get url() {
@@ -73,22 +76,36 @@ export default class Model {
}
onChange(cb) {
- this.events.set(
- this.path,
- this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))),
- );
+ this.events.add(this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))));
+ }
+
+ onDispose(cb) {
+ this.events.add(cb);
}
- updateContent(content) {
+ updateContent({ content, changed }) {
this.getOriginalModel().setValue(content);
+
+ if (!changed) {
+ this.getModel().setValue(content);
+ }
+ }
+
+ updateNewContent(content) {
this.getModel().setValue(content);
}
dispose() {
this.disposable.dispose();
+
+ this.events.forEach(cb => {
+ if (typeof cb === 'function') cb();
+ });
+
this.events.clear();
eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose);
- eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
+ eventHub.$off(`editor.update.model.content.${this.file.key}`, this.updateContent);
+ eventHub.$off(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent);
}
}
diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js
index 0e7b563b5d6..7f643969480 100644
--- a/app/assets/javascripts/ide/lib/common/model_manager.js
+++ b/app/assets/javascripts/ide/lib/common/model_manager.js
@@ -17,12 +17,12 @@ export default class ModelManager {
return this.models.get(key);
}
- addModel(file) {
+ addModel(file, head = null) {
if (this.hasCachedModel(file.key)) {
return this.getModel(file.key);
}
- const model = new Model(this.monaco, file);
+ const model = new Model(this.monaco, file, head);
this.models.set(model.path, model);
this.disposable.add(model);
diff --git a/app/assets/javascripts/ide/lib/decorations/controller.js b/app/assets/javascripts/ide/lib/decorations/controller.js
index 42904774747..13d477bb2cf 100644
--- a/app/assets/javascripts/ide/lib/decorations/controller.js
+++ b/app/assets/javascripts/ide/lib/decorations/controller.js
@@ -38,6 +38,15 @@ export default class DecorationsController {
);
}
+ hasDecorations(model) {
+ return this.decorations.has(model.url);
+ }
+
+ removeDecorations(model) {
+ this.decorations.delete(model.url);
+ this.editorDecorations.delete(model.url);
+ }
+
dispose() {
this.decorations.clear();
this.editorDecorations.clear();
diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
index b136545ad11..f579424cf33 100644
--- a/app/assets/javascripts/ide/lib/diff/controller.js
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
@@ -3,7 +3,7 @@ import { throttle } from 'underscore';
import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable';
-export const getDiffChangeType = (change) => {
+export const getDiffChangeType = change => {
if (change.modified) {
return 'modified';
} else if (change.added) {
@@ -16,12 +16,7 @@ export const getDiffChangeType = (change) => {
};
export const getDecorator = change => ({
- range: new monaco.Range(
- change.lineNumber,
- 1,
- change.endLineNumber,
- 1,
- ),
+ range: new monaco.Range(change.lineNumber, 1, change.endLineNumber, 1),
options: {
isWholeLine: true,
linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
@@ -31,6 +26,7 @@ export const getDecorator = change => ({
export default class DirtyDiffController {
constructor(modelManager, decorationsController) {
this.disposable = new Disposable();
+ this.models = new Map();
this.editorSimpleWorker = null;
this.modelManager = modelManager;
this.decorationsController = decorationsController;
@@ -42,7 +38,15 @@ export default class DirtyDiffController {
}
attachModel(model) {
+ if (this.models.has(model.url)) return;
+
model.onChange(() => this.throttledComputeDiff(model));
+ model.onDispose(() => {
+ this.decorationsController.removeDecorations(model);
+ this.models.delete(model.url);
+ });
+
+ this.models.set(model.url, model);
}
computeDiff(model) {
@@ -54,7 +58,11 @@ export default class DirtyDiffController {
}
reDecorate(model) {
- this.decorationsController.decorate(model);
+ if (this.decorationsController.hasDecorations(model)) {
+ this.decorationsController.decorate(model);
+ } else {
+ this.computeDiff(model);
+ }
}
decorate({ data }) {
@@ -65,6 +73,7 @@ export default class DirtyDiffController {
dispose() {
this.disposable.dispose();
+ this.models.clear();
this.dirtyDiffWorker.removeEventListener('message', this.decorate);
this.dirtyDiffWorker.terminate();
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 001737d6ee8..2d3ee7d4f48 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -77,8 +77,8 @@ export default class Editor {
}
}
- createModel(file) {
- return this.modelManager.addModel(file);
+ createModel(file, head = null) {
+ return this.modelManager.addModel(file, head);
}
attachModel(model) {
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index c6ba679d99c..cecb4d215ba 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -1,3 +1,4 @@
+import $ from 'jquery';
import Vue from 'vue';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
@@ -32,6 +33,22 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
}
};
+export const toggleRightPanelCollapsed = (
+ { dispatch, state },
+ e = undefined,
+) => {
+ if (e) {
+ $(e.currentTarget)
+ .tooltip('hide')
+ .blur();
+ }
+
+ dispatch('setPanelCollapsedStatus', {
+ side: 'right',
+ collapsed: !state.rightPanelCollapsed,
+ });
+};
+
export const setResizingStatus = ({ commit }, resizing) => {
commit(types.SET_RESIZING_STATUS, resizing);
};
@@ -104,6 +121,14 @@ export const scrollToTab = () => {
});
};
+export const stageAllChanges = ({ state, commit }) => {
+ state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
+};
+
+export const unstageAllChanges = ({ state, commit }) => {
+ state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path));
+};
+
export const updateViewer = ({ commit }, viewer) => {
commit(types.UPDATE_VIEWER, viewer);
};
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 1a17320a1ea..d782e0a84d2 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -24,7 +24,10 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
if (nextFileToOpen.pending) {
dispatch('updateViewer', 'diff');
- dispatch('openPendingTab', nextFileToOpen);
+ dispatch('openPendingTab', {
+ file: nextFileToOpen,
+ keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
+ });
} else {
dispatch('updateDelayViewerUpdated', true);
router.push(`/project${nextFileToOpen.url}`);
@@ -153,7 +156,7 @@ export const setFileViewMode = ({ state, commit }, { file, viewMode }) => {
commit(types.SET_FILE_VIEWMODE, { file, viewMode });
};
-export const discardFileChanges = ({ state, commit }, path) => {
+export const discardFileChanges = ({ dispatch, state, commit, getters }, path) => {
const file = state.entries[path];
commit(types.DISCARD_FILE_CHANGES, path);
@@ -161,17 +164,40 @@ export const discardFileChanges = ({ state, commit }, path) => {
if (file.tempFile && file.opened) {
commit(types.TOGGLE_FILE_OPEN, path);
+ } else if (getters.activeFile && file.path === getters.activeFile.path) {
+ dispatch('updateDelayViewerUpdated', true)
+ .then(() => {
+ router.push(`/project${file.url}`);
+ })
+ .catch(e => {
+ throw e;
+ });
+ }
+
+ eventHub.$emit(`editor.update.model.new.content.${file.key}`, file.content);
+ eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content);
+};
+
+export const stageChange = ({ commit, state }, path) => {
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
+ commit(types.STAGE_CHANGE, path);
+
+ if (stagedFile) {
+ eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
}
+};
- eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
+export const unstageChange = ({ commit }, path) => {
+ commit(types.UNSTAGE_CHANGE, path);
};
-export const openPendingTab = ({ commit, getters, dispatch, state }, file) => {
- if (getters.activeFile && getters.activeFile.path === file.path && state.viewer === 'diff') {
+export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => {
+ if (getters.activeFile && getters.activeFile === file && state.viewer === 'diff') {
return false;
}
- commit(types.ADD_PENDING_TAB, { file });
+ commit(types.ADD_PENDING_TAB, { file, keyPrefix });
dispatch('scrollToTab');
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index b3882cb8d21..4eb23b2ee0f 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -5,45 +5,71 @@ import * as types from '../mutation_types';
export const getProjectData = (
{ commit, state, dispatch },
{ namespace, projectId, force = false } = {},
-) => new Promise((resolve, reject) => {
- if (!state.projects[`${namespace}/${projectId}`] || force) {
- commit(types.TOGGLE_LOADING, { entry: state });
- service.getProjectData(namespace, projectId)
- .then(res => res.data)
- .then((data) => {
+) =>
+ new Promise((resolve, reject) => {
+ if (!state.projects[`${namespace}/${projectId}`] || force) {
commit(types.TOGGLE_LOADING, { entry: state });
- commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
- if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
- resolve(data);
- })
- .catch(() => {
- flash('Error loading project data. Please try again.', 'alert', document, null, false, true);
- reject(new Error(`Project not loaded ${namespace}/${projectId}`));
- });
- } else {
- resolve(state.projects[`${namespace}/${projectId}`]);
- }
-});
+ service
+ .getProjectData(namespace, projectId)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.TOGGLE_LOADING, { entry: state });
+ commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
+ if (!state.currentProjectId)
+ commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
+ resolve(data);
+ })
+ .catch(() => {
+ flash(
+ 'Error loading project data. Please try again.',
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+ reject(new Error(`Project not loaded ${namespace}/${projectId}`));
+ });
+ } else {
+ resolve(state.projects[`${namespace}/${projectId}`]);
+ }
+ });
export const getBranchData = (
{ commit, state, dispatch },
{ projectId, branchId, force = false } = {},
-) => new Promise((resolve, reject) => {
- if ((typeof state.projects[`${projectId}`] === 'undefined' ||
- !state.projects[`${projectId}`].branches[branchId])
- || force) {
- service.getBranchData(`${projectId}`, branchId)
- .then(({ data }) => {
- const { id } = data.commit;
- commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
- commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
- resolve(data);
- })
- .catch(() => {
- flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
- reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
- });
- } else {
- resolve(state.projects[`${projectId}`].branches[branchId]);
- }
-});
+) =>
+ new Promise((resolve, reject) => {
+ if (
+ typeof state.projects[`${projectId}`] === 'undefined' ||
+ !state.projects[`${projectId}`].branches[branchId] ||
+ force
+ ) {
+ service
+ .getBranchData(`${projectId}`, branchId)
+ .then(({ data }) => {
+ const { id } = data.commit;
+ commit(types.SET_BRANCH, {
+ projectPath: `${projectId}`,
+ branchName: branchId,
+ branch: data,
+ });
+ commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
+ commit(types.SET_CURRENT_BRANCH, branchId);
+ resolve(data);
+ })
+ .catch(() => {
+ flash(
+ 'Error loading branch data. Please try again.',
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+ reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
+ });
+ } else {
+ resolve(state.projects[`${projectId}`].branches[branchId]);
+ }
+ });
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index a77cdbc13c8..8518d2f6f06 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
@@ -29,9 +31,15 @@ export const currentMergeRequest = state => {
};
// eslint-disable-next-line no-confusing-arrow
-export const currentIcon = state =>
+export const collapseButtonIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
-export const hasChanges = state => !!state.changedFiles.length;
+export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
+
+// eslint-disable-next-line no-confusing-arrow
+export const collapseButtonTooltip = state =>
+ state.rightPanelCollapsed ? __('Expand sidebar') : __('Collapse sidebar');
export const hasMergeRequest = state => !!state.currentMergeRequestId;
+
+export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 367c45f7e2d..b26512e213a 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -98,40 +98,25 @@ export const updateFilesAfterCommit = (
{ root: true },
);
- rootState.changedFiles.forEach(entry => {
- commit(
- rootTypes.SET_LAST_COMMIT_DATA,
- {
- entry,
- lastCommit,
- },
- { root: true },
- );
-
- eventHub.$emit(`editor.update.model.content.${entry.path}`, entry.content);
+ rootState.stagedFiles.forEach(file => {
+ const changedFile = rootState.changedFiles.find(f => f.path === file.path);
commit(
- rootTypes.SET_FILE_RAW_DATA,
+ rootTypes.UPDATE_FILE_AFTER_COMMIT,
{
- file: entry,
- raw: entry.content,
+ file,
+ lastCommit,
},
{ root: true },
);
- commit(
- rootTypes.TOGGLE_FILE_CHANGED,
- {
- file: entry,
- changed: false,
- },
- { root: true },
- );
+ eventHub.$emit(`editor.update.model.content.${file.key}`, {
+ content: file.content,
+ changed: !!changedFile,
+ });
});
- commit(rootTypes.REMOVE_ALL_CHANGES_FILES, null, { root: true });
-
- if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) {
+ if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH && rootGetters.activeFile) {
router.push(
`/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`,
);
@@ -184,6 +169,8 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
{ root: true },
);
}
+
+ commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
})
.then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH));
})
diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js
index f7cdd6adb0c..9c3905a0b0d 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js
@@ -1,12 +1,17 @@
import * as consts from './constants';
-export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading;
+const BRANCH_SUFFIX_COUNT = 5;
+
+export const discardDraftButtonDisabled = state =>
+ state.commitMessage === '' || state.submitCommitLoading;
export const commitButtonDisabled = (state, getters, rootState) =>
- getters.discardDraftButtonDisabled || !rootState.changedFiles.length;
+ getters.discardDraftButtonDisabled || !rootState.stagedFiles.length;
export const newBranchName = (state, _, rootState) =>
- `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(-5)}`;
+ `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(
+ -BRANCH_SUFFIX_COUNT,
+ )}`;
export const branchName = (state, getters, rootState) => {
if (
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index e3f504e5ab0..f5f95b755c8 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -51,5 +51,10 @@ export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
+export const CLEAR_STAGED_CHANGES = 'CLEAR_STAGED_CHANGES';
+export const STAGE_CHANGE = 'STAGE_CHANGE';
+export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE';
+
+export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 5e5eb831662..fbe342f9126 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -49,6 +49,11 @@ export default {
lastCommitMsg,
});
},
+ [types.CLEAR_STAGED_CHANGES](state) {
+ Object.assign(state, {
+ stagedFiles: [],
+ });
+ },
[types.SET_ENTRIES](state, entries) {
Object.assign(state, {
entries,
@@ -95,6 +100,22 @@ export default {
delayViewerUpdated,
});
},
+ [types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) {
+ const changedFile = state.changedFiles.find(f => f.path === file.path);
+
+ Object.assign(state.entries[file.path], {
+ raw: file.content,
+ changed: !!changedFile,
+ staged: false,
+ lastCommit: Object.assign(state.entries[file.path].lastCommit, {
+ id: lastCommit.commit.id,
+ url: lastCommit.commit_path,
+ message: lastCommit.commit.message,
+ author: lastCommit.commit.author_name,
+ updatedAt: lastCommit.commit.authored_date,
+ }),
+ });
+ },
...projectMutations,
...mergeRequestMutation,
...fileMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index eeb14b5490c..dd7dcba8ac7 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -57,7 +57,9 @@ export default {
});
},
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
- const changed = content !== state.entries[path].raw;
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+ const rawContent = stagedFile ? stagedFile.content : state.entries[path].raw;
+ const changed = content !== rawContent;
Object.assign(state.entries[path], {
content,
@@ -91,8 +93,10 @@ export default {
});
},
[types.DISCARD_FILE_CHANGES](state, path) {
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
Object.assign(state.entries[path], {
- content: state.entries[path].raw,
+ content: stagedFile ? stagedFile.content : state.entries[path].raw,
changed: false,
});
},
@@ -106,16 +110,67 @@ export default {
changedFiles: state.changedFiles.filter(f => f.path !== path),
});
},
+ [types.STAGE_CHANGE](state, path) {
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
+ Object.assign(state, {
+ changedFiles: state.changedFiles.filter(f => f.path !== path),
+ entries: Object.assign(state.entries, {
+ [path]: Object.assign(state.entries[path], {
+ staged: true,
+ changed: false,
+ }),
+ }),
+ });
+
+ if (stagedFile) {
+ Object.assign(stagedFile, {
+ ...state.entries[path],
+ });
+ } else {
+ Object.assign(state, {
+ stagedFiles: state.stagedFiles.concat({
+ ...state.entries[path],
+ }),
+ });
+ }
+ },
+ [types.UNSTAGE_CHANGE](state, path) {
+ const changedFile = state.changedFiles.find(f => f.path === path);
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
+ if (!changedFile && stagedFile) {
+ Object.assign(state.entries[path], {
+ ...stagedFile,
+ key: state.entries[path].key,
+ active: state.entries[path].active,
+ opened: state.entries[path].opened,
+ changed: true,
+ });
+
+ Object.assign(state, {
+ changedFiles: state.changedFiles.concat(state.entries[path]),
+ });
+ }
+
+ Object.assign(state, {
+ stagedFiles: state.stagedFiles.filter(f => f.path !== path),
+ entries: Object.assign(state.entries, {
+ [path]: Object.assign(state.entries[path], {
+ staged: false,
+ }),
+ }),
+ });
+ },
[types.TOGGLE_FILE_CHANGED](state, { file, changed }) {
Object.assign(state.entries[file.path], {
changed,
});
},
[types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
- const pendingTab = state.openFiles.find(f => f.path === file.path && f.pending);
- let openFiles = state.openFiles.map(f =>
- Object.assign(f, { active: f.path === file.path, opened: false }),
- );
+ const key = `${keyPrefix}-${file.key}`;
+ const pendingTab = state.openFiles.find(f => f.key === key && f.pending);
+ let openFiles = state.openFiles.map(f => Object.assign(f, { active: false, opened: false }));
if (!pendingTab) {
const openFile = openFiles.find(f => f.path === file.path);
@@ -126,10 +181,11 @@ export default {
if (f.path === file.path) {
return acc.concat({
...f,
+ content: file.content,
active: true,
pending: true,
opened: true,
- key: `${keyPrefix}-${f.key}`,
+ key,
});
}
diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js
index 7f7e470c9bb..1176c040fb9 100644
--- a/app/assets/javascripts/ide/stores/mutations/tree.js
+++ b/app/assets/javascripts/ide/stores/mutations/tree.js
@@ -17,12 +17,8 @@ export default {
});
},
[types.SET_DIRECTORY_DATA](state, { data, treePath }) {
- Object.assign(state, {
- trees: Object.assign(state.trees, {
- [treePath]: {
- tree: data,
- },
- }),
+ Object.assign(state.trees[treePath], {
+ tree: data,
});
},
[types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index e5cc8814000..34975ac3144 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -3,6 +3,7 @@ export default () => ({
currentBranchId: '',
currentMergeRequestId: '',
changedFiles: [],
+ stagedFiles: [],
endpoints: {},
lastCommitMsg: '',
lastCommitPath: '',
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 05a019de54f..8a222da14c0 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -15,6 +15,7 @@ export const dataStructure = () => ({
opened: false,
active: false,
changed: false,
+ staged: false,
lastCommitPath: '',
lastCommit: {
id: '',
@@ -101,7 +102,7 @@ export const setPageTitle = title => {
export const createCommitPayload = (branch, newBranch, state, rootState) => ({
branch,
commit_message: state.commitMessage,
- actions: rootState.changedFiles.map(f => ({
+ actions: rootState.stagedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.content,
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 7470d634b99..f3d722409b0 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -30,10 +30,10 @@ export default class IssuableContext {
const $selectbox = $block.find('.selectbox');
if ($selectbox.is(':visible')) {
$selectbox.hide();
- $block.find('.value').show();
+ $block.find('.value:not(.dont-hide)').show();
} else {
$selectbox.show();
- $block.find('.value').hide();
+ $block.find('.value:not(.dont-hide)').hide();
}
if ($selectbox.is(':visible')) {
diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue
index 357bc9aab17..21b545d6cab 100644
--- a/app/assets/javascripts/jobs/components/header.vue
+++ b/app/assets/javascripts/jobs/components/header.vue
@@ -1,82 +1,94 @@
<script>
- import ciHeader from '../../vue_shared/components/header_ci_component.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import ciHeader from '../../vue_shared/components/header_ci_component.vue';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import callout from '../../vue_shared/components/callout.vue';
- export default {
- name: 'JobHeaderSection',
- components: {
- ciHeader,
- loadingIcon,
+export default {
+ name: 'JobHeaderSection',
+ components: {
+ ciHeader,
+ loadingIcon,
+ callout,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
},
- props: {
- job: {
- type: Object,
- required: true,
- },
- isLoading: {
- type: Boolean,
- required: true,
- },
+ isLoading: {
+ type: Boolean,
+ required: true,
},
- data() {
- return {
- actions: this.getActions(),
- };
+ },
+ data() {
+ return {
+ actions: this.getActions(),
+ };
+ },
+ computed: {
+ status() {
+ return this.job && this.job.status;
},
- computed: {
- status() {
- return this.job && this.job.status;
- },
- shouldRenderContent() {
- return !this.isLoading && Object.keys(this.job).length;
- },
- /**
- * When job has not started the key will be `false`
- * When job started the key will be a string with a date.
- */
- jobStarted() {
- return !this.job.started === false;
- },
+ shouldRenderContent() {
+ return !this.isLoading && Object.keys(this.job).length;
},
- watch: {
- job() {
- this.actions = this.getActions();
- },
+ shouldRenderReason() {
+ return !!(this.job.status && this.job.callout_message);
},
- methods: {
- getActions() {
- const actions = [];
+ /**
+ * When job has not started the key will be `false`
+ * When job started the key will be a string with a date.
+ */
+ jobStarted() {
+ return !this.job.started === false;
+ },
+ },
+ watch: {
+ job() {
+ this.actions = this.getActions();
+ },
+ },
+ methods: {
+ getActions() {
+ const actions = [];
- if (this.job.new_issue_path) {
- actions.push({
- label: 'New issue',
- path: this.job.new_issue_path,
- cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
- type: 'link',
- });
- }
- return actions;
- },
+ if (this.job.new_issue_path) {
+ actions.push({
+ label: 'New issue',
+ path: this.job.new_issue_path,
+ cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
+ type: 'link',
+ });
+ }
+ return actions;
},
- };
+ },
+};
</script>
<template>
- <div class="js-build-header build-header top-area">
- <ci-header
- v-if="shouldRenderContent"
- :status="status"
- item-name="Job"
- :item-id="job.id"
- :time="job.created_at"
- :user="job.user"
- :actions="actions"
- :has-sidebar-button="true"
- :should-render-triggered-label="jobStarted"
- />
- <loading-icon
- v-if="isLoading"
- size="2"
- class="prepend-top-default append-bottom-default"
+ <header>
+ <div class="js-build-header build-header top-area">
+ <ci-header
+ v-if="shouldRenderContent"
+ :status="status"
+ item-name="Job"
+ :item-id="job.id"
+ :time="job.created_at"
+ :user="job.user"
+ :actions="actions"
+ :has-sidebar-button="true"
+ :should-render-triggered-label="jobStarted"
+ />
+ <loading-icon
+ v-if="isLoading"
+ size="2"
+ class="prepend-top-default append-bottom-default"
+ />
+ </div>
+
+ <callout
+ v-if="shouldRenderReason"
+ :message="job.callout_message"
/>
- </div>
+ </header>
</template>
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
index af47056d98f..4cd44bf7a76 100644
--- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
@@ -1,80 +1,119 @@
<script>
- import detailRow from './sidebar_detail_row.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import timeagoMixin from '../../vue_shared/mixins/timeago';
- import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
+import detailRow from './sidebar_detail_row.vue';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import timeagoMixin from '../../vue_shared/mixins/timeago';
+import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
- export default {
- name: 'SidebarDetailsBlock',
- components: {
- detailRow,
- loadingIcon,
+export default {
+ name: 'SidebarDetailsBlock',
+ components: {
+ detailRow,
+ loadingIcon,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ job: {
+ type: Object,
+ required: true,
},
- mixins: [
- timeagoMixin,
- ],
- props: {
- job: {
- type: Object,
- required: true,
- },
- isLoading: {
- type: Boolean,
- required: true,
- },
- runnerHelpUrl: {
- type: String,
- required: false,
- default: '',
- },
+ isLoading: {
+ type: Boolean,
+ required: true,
},
- computed: {
- shouldRenderContent() {
- return !this.isLoading && Object.keys(this.job).length > 0;
- },
- coverage() {
- return `${this.job.coverage}%`;
- },
- duration() {
- return timeIntervalInWords(this.job.duration);
- },
- queued() {
- return timeIntervalInWords(this.job.queued);
- },
- runnerId() {
- return `#${this.job.runner.id}`;
- },
- hasTimeout() {
- return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
- },
- timeout() {
- if (this.job.metadata == null) {
- return '';
- }
+ canUserRetry: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ runnerHelpUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ shouldRenderContent() {
+ return !this.isLoading && Object.keys(this.job).length > 0;
+ },
+ coverage() {
+ return `${this.job.coverage}%`;
+ },
+ duration() {
+ return timeIntervalInWords(this.job.duration);
+ },
+ queued() {
+ return timeIntervalInWords(this.job.queued);
+ },
+ runnerId() {
+ return `#${this.job.runner.id}`;
+ },
+ retryButtonClass() {
+ let className = 'js-retry-button pull-right btn btn-retry visible-md-block visible-lg-block';
+ className +=
+ this.job.status && this.job.recoverable
+ ? ' btn-primary'
+ : ' btn-inverted-secondary';
+ return className;
+ },
+ hasTimeout() {
+ return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
+ },
+ timeout() {
+ if (this.job.metadata == null) {
+ return '';
+ }
- let t = this.job.metadata.timeout_human_readable;
- if (this.job.metadata.timeout_source !== '') {
- t += ` (from ${this.job.metadata.timeout_source})`;
- }
+ let t = this.job.metadata.timeout_human_readable;
+ if (this.job.metadata.timeout_source !== '') {
+ t += ` (from ${this.job.metadata.timeout_source})`;
+ }
- return t;
- },
- renderBlock() {
- return this.job.merge_request ||
- this.job.duration ||
- this.job.finished_data ||
- this.job.erased_at ||
- this.job.queued ||
- this.job.runner ||
- this.job.coverage ||
- this.job.tags.length ||
- this.job.cancel_path;
- },
+ return t;
},
- };
+ renderBlock() {
+ return (
+ this.job.merge_request ||
+ this.job.duration ||
+ this.job.finished_data ||
+ this.job.erased_at ||
+ this.job.queued ||
+ this.job.runner ||
+ this.job.coverage ||
+ this.job.tags.length ||
+ this.job.cancel_path
+ );
+ },
+ },
+};
</script>
<template>
<div>
+ <div class="block">
+ <strong class="inline prepend-top-8">
+ {{ job.name }}
+ </strong>
+ <a
+ v-if="canUserRetry"
+ :class="retryButtonClass"
+ :href="job.retry_path"
+ data-method="post"
+ rel="nofollow"
+ >
+ {{ __('Retry') }}
+ </a>
+ <button
+ type="button"
+ :aria-label="__('Toggle Sidebar')"
+ class="btn btn-blank gutter-toggle pull-right
+ visible-xs-block visible-sm-block js-sidebar-build-toggle"
+ >
+ <i
+ aria-hidden="true"
+ data-hidden="true"
+ class="fa fa-angle-double-right"
+ ></i>
+ </button>
+ </div>
<template v-if="shouldRenderContent">
<div
class="block retry-link"
@@ -85,16 +124,16 @@
class="js-new-issue btn btn-new btn-inverted"
:href="job.new_issue_path"
>
- New issue
+ {{ __('New issue') }}
</a>
<a
- v-if="job.retry_path"
+ v-if="canUserRetry"
class="js-retry-job btn btn-inverted-secondary"
:href="job.retry_path"
data-method="post"
rel="nofollow"
>
- Retry
+ {{ __('Retry') }}
</a>
</div>
<div :class="{block : renderBlock }">
@@ -103,7 +142,7 @@
v-if="job.merge_request"
>
<span class="build-light-text">
- Merge Request:
+ {{ __('Merge Request:') }}
</span>
<a :href="job.merge_request.path">
!{{ job.merge_request.iid }}
@@ -158,7 +197,7 @@
v-if="job.tags.length"
>
<span class="build-light-text">
- Tags:
+ {{ __('Tags:') }}
</span>
<span
v-for="(tag, i) in job.tags"
@@ -178,7 +217,7 @@
data-method="post"
rel="nofollow"
>
- Cancel
+ {{ __('Cancel') }}
</a>
</div>
</div>
diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js
index 656676ead91..f2939ad4dbe 100644
--- a/app/assets/javascripts/jobs/job_details_bundle.js
+++ b/app/assets/javascripts/jobs/job_details_bundle.js
@@ -35,9 +35,11 @@ export default () => {
});
// Sidebar information block
+ const detailsBlockElement = document.getElementById('js-details-block-vue');
+ const detailsBlockDataset = detailsBlockElement.dataset;
// eslint-disable-next-line
new Vue({
- el: '#js-details-block-vue',
+ el: detailsBlockElement,
components: {
detailsBlock,
},
@@ -50,6 +52,7 @@ export default () => {
return createElement('details-block', {
props: {
isLoading: this.mediator.state.isLoading,
+ canUserRetry: !!('canUserRetry' in detailsBlockDataset),
job: this.mediator.store.state.job,
runnerHelpUrl: dataset.runnerHelpUrl,
},
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index d0050abb8e9..9b62cfb8206 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -83,7 +83,7 @@ export default class LabelsSelect {
$dropdown.trigger('loading.gl.dropdown');
axios.put(issueUpdateURL, data)
.then(({ data }) => {
- var labelCount, template, labelTooltipTitle, labelTitles;
+ var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels;
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
@@ -115,8 +115,7 @@ export default class LabelsSelect {
labelTooltipTitle = labelTitles.join(', ');
}
else {
- labelTooltipTitle = '';
- $sidebarLabelTooltip.tooltip('destroy');
+ labelTooltipTitle = __('Labels');
}
$sidebarLabelTooltip
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index d0a2b27b0e6..7e9a50a885d 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -4,6 +4,7 @@
import $ from 'jquery';
import _ from 'underscore';
+import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store';
@@ -25,7 +26,7 @@ export default class MilestoneSelect {
}
$els.each((i, dropdown) => {
- let collapsedSidebarLabelTemplate, milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
+ let milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
const $dropdown = $(dropdown);
const projectId = $dropdown.data('projectId');
const milestonesUrl = $dropdown.data('milestones');
@@ -52,7 +53,6 @@ export default class MilestoneSelect {
if (issueUpdateURL) {
milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
- collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>');
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
@@ -214,10 +214,16 @@ export default class MilestoneSelect {
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
- return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
+ return $sidebarCollapsedValue
+ .attr('data-original-title', `${data.milestone.name}<br />${data.milestone.remaining}`)
+ .find('span')
+ .text(data.milestone.title);
} else {
$value.html(milestoneLinkNoneTemplate);
- return $sidebarCollapsedValue.find('span').text('No');
+ return $sidebarCollapsedValue
+ .attr('data-original-title', __('Milestone'))
+ .find('span')
+ .text(__('None'));
}
})
.catch(() => {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index b0573510ff9..2121907dff0 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -19,7 +19,6 @@ import AjaxCache from '~/lib/utils/ajax_cache';
import Vue from 'vue';
import syntaxHighlight from '~/syntax_highlight';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
@@ -198,6 +197,8 @@ export default class Notes {
);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff);
+ this.$wrapperEl.on('click', '.js-toggle-lazy-diff-retry-button', this.onClickRetryLazyLoad.bind(this));
+
// fetch notes when tab becomes visible
this.$wrapperEl.on('visibilitychange', this.visibilityChange);
// when issue status changes, we need to refresh data
@@ -244,6 +245,7 @@ export default class Notes {
this.$wrapperEl.off('click', '.js-comment-resolve-button');
this.$wrapperEl.off('click', '.system-note-commit-list-toggler');
this.$wrapperEl.off('click', '.js-toggle-lazy-diff');
+ this.$wrapperEl.off('click', '.js-toggle-lazy-diff-retry-button');
this.$wrapperEl.off('ajax:success', '.js-main-target-form');
this.$wrapperEl.off('ajax:success', '.js-discussion-note-form');
this.$wrapperEl.off('ajax:complete', '.js-main-target-form');
@@ -1431,16 +1433,15 @@ export default class Notes {
syntaxHighlight(fileHolder);
}
- static renderDiffError($container) {
- $container.find('.line_content').html(
- $(`
- <div class="nothing-here-block">
- ${__(
- 'Unable to load the diff.',
- )} <a class="js-toggle-lazy-diff" href="javascript:void(0)">Try again</a>?
- </div>
- `),
- );
+ onClickRetryLazyLoad(e) {
+ const $retryButton = $(e.currentTarget);
+
+ $retryButton.prop('disabled', true);
+
+ return this.loadLazyDiff(e)
+ .then(() => {
+ $retryButton.prop('disabled', false);
+ });
}
loadLazyDiff(e) {
@@ -1449,20 +1450,35 @@ export default class Notes {
$container.find('.js-toggle-lazy-diff').removeClass('js-toggle-lazy-diff');
- const tableEl = $container.find('tbody');
- if (tableEl.length === 0) return;
+ const $tableEl = $container.find('tbody');
+ if ($tableEl.length === 0) return;
const fileHolder = $container.find('.file-holder');
const url = fileHolder.data('linesPath');
- axios
+ const $errorContainer = $container.find('.js-error-lazy-load-diff');
+ const $successContainer = $container.find('.js-success-lazy-load');
+
+ /**
+ * We only fetch resolved discussions.
+ * Unresolved discussions don't have an endpoint being provided.
+ */
+ if (url) {
+ return axios
.get(url)
.then(({ data }) => {
+ // Reset state in case last request returned error
+ $successContainer.removeClass('hidden');
+ $errorContainer.addClass('hidden');
+
Notes.renderDiffContent($container, data);
})
.catch(() => {
- Notes.renderDiffError($container);
+ $successContainer.addClass('hidden');
+ $errorContainer.removeClass('hidden');
});
+ }
+ return Promise.resolve();
}
toggleCommitList(e) {
diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
index 3ebfaa87a4e..bc71911ae35 100644
--- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js
+++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
@@ -10,29 +10,25 @@ export default class PerformanceBarService {
}
static registerInterceptor(peekUrl, callback) {
- vueResourceInterceptor = (request, next) => {
- next(response => {
- const requestId = response.headers['x-request-id'];
- const requestUrl = response.url;
-
- if (requestUrl !== peekUrl && requestId) {
- callback(requestId, requestUrl);
- }
- });
- };
-
- Vue.http.interceptors.push(vueResourceInterceptor);
-
- return axios.interceptors.response.use(response => {
+ const interceptor = response => {
const requestId = response.headers['x-request-id'];
- const requestUrl = response.config.url;
+ // Get the request URL from response.config for Axios, and response for
+ // Vue Resource.
+ const requestUrl = (response.config || response).url;
+ const cachedResponse = response.headers['x-gitlab-from-cache'] === 'true';
- if (requestUrl !== peekUrl && requestId) {
+ if (requestUrl !== peekUrl && requestId && !cachedResponse) {
callback(requestId, requestUrl);
}
return response;
- });
+ };
+
+ vueResourceInterceptor = (request, next) => next(interceptor);
+
+ Vue.http.interceptors.push(vueResourceInterceptor);
+
+ return axios.interceptors.response.use(interceptor);
}
static removeInterceptor(interceptor) {
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 2088a49590a..6eb0b62fa1c 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -5,6 +5,7 @@ import _ from 'underscore';
import Cookies from 'js-cookie';
import flash from './flash';
import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
function Sidebar(currentUser) {
this.toggleTodo = this.toggleTodo.bind(this);
@@ -41,12 +42,14 @@ Sidebar.prototype.addEventListeners = function() {
};
Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
- var $allGutterToggleIcons, $this, $thisIcon;
+ var $allGutterToggleIcons, $this, isExpanded, tooltipLabel;
e.preventDefault();
$this = $(this);
- $thisIcon = $this.find('i');
+ isExpanded = $this.find('i').hasClass('fa-angle-double-right');
+ tooltipLabel = isExpanded ? __('Expand sidebar') : __('Collapse sidebar');
$allGutterToggleIcons = $('.js-sidebar-toggle i');
- if ($thisIcon.hasClass('fa-angle-double-right')) {
+
+ if (isExpanded) {
$allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
$('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
$('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
@@ -57,6 +60,9 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
}
+
+ $this.attr('data-original-title', tooltipLabel);
+
if (!triggered) {
Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
}
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index 1e7f46454bf..2d00e8ac7e0 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -1,6 +1,12 @@
<script>
+import { __ } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+
export default {
name: 'Assignees',
+ directives: {
+ tooltip,
+ },
props: {
rootPath: {
type: String,
@@ -14,6 +20,11 @@ export default {
type: Boolean,
required: true,
},
+ issuableType: {
+ type: String,
+ require: true,
+ default: 'issue',
+ },
},
data() {
return {
@@ -62,6 +73,12 @@ export default {
names.push(`+ ${this.users.length - maxRender} more`);
}
+ if (!this.users.length) {
+ const emptyTooltipLabel = this.issuableType === 'issue' ?
+ __('Assignee(s)') : __('Assignee');
+ names.push(emptyTooltipLabel);
+ }
+
return names.join(', ');
},
sidebarAvatarCounter() {
@@ -109,7 +126,8 @@ export default {
<div>
<div
class="sidebar-collapsed-icon sidebar-collapsed-user"
- :class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }"
+ :class="{ 'multiple-users': hasMoreThanOneAssignee }"
+ v-tooltip
data-container="body"
data-placement="left"
:title="collapsedTooltipTitle"
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 3c6b9c27814..b04a2eff798 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -1,9 +1,9 @@
<script>
-import Flash from '../../../flash';
+import Flash from '~/flash';
+import eventHub from '~/sidebar/event_hub';
+import Store from '~/sidebar/stores/sidebar_store';
import AssigneeTitle from './assignee_title.vue';
import Assignees from './assignees.vue';
-import Store from '../../stores/sidebar_store';
-import eventHub from '../../event_hub';
export default {
name: 'SidebarAssignees',
@@ -25,6 +25,11 @@ export default {
required: false,
default: false,
},
+ issuableType: {
+ type: String,
+ require: true,
+ default: 'issue',
+ },
},
data() {
return {
@@ -90,6 +95,7 @@ export default {
:users="store.assignees"
:editable="store.editable"
@assign-self="assignSelf"
+ :issuable-type="issuableType"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index ceb02309959..7f0de722f61 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,15 +1,19 @@
<script>
-import Flash from '../../../flash';
+import { __ } from '~/locale';
+import Flash from '~/flash';
+import tooltip from '~/vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
-import Icon from '../../../vue_shared/components/icon.vue';
-import { __ } from '../../../locale';
-import eventHub from '../../event_hub';
export default {
components: {
editForm,
Icon,
},
+ directives: {
+ tooltip,
+ },
props: {
isConfidential: {
required: true,
@@ -33,6 +37,9 @@ export default {
confidentialityIcon() {
return this.isConfidential ? 'eye-slash' : 'eye';
},
+ tooltipLabel() {
+ return this.isConfidential ? __('Confidential') : __('Not confidential');
+ },
},
created() {
eventHub.$on('closeConfidentialityForm', this.toggleForm);
@@ -65,6 +72,10 @@ export default {
<div
class="sidebar-collapsed-icon"
@click="toggleForm"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="tooltipLabel"
>
<icon
:name="confidentialityIcon"
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index e4893451af3..1a5e7b67eca 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -1,15 +1,22 @@
<script>
+import { __ } from '~/locale';
import Flash from '~/flash';
+import tooltip from '~/vue_shared/directives/tooltip';
+import issuableMixin from '~/vue_shared/mixins/issuable';
+import Icon from '~/vue_shared/components/icon.vue';
+import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
-import issuableMixin from '../../../vue_shared/mixins/issuable';
-import Icon from '../../../vue_shared/components/icon.vue';
-import eventHub from '../../event_hub';
export default {
components: {
editForm,
Icon,
},
+
+ directives: {
+ tooltip,
+ },
+
mixins: [issuableMixin],
props: {
@@ -44,6 +51,10 @@ export default {
isLockDialogOpen() {
return this.mediator.store.isLockDialogOpen;
},
+
+ tooltipLabel() {
+ return this.isLocked ? __('Locked') : __('Unlocked');
+ },
},
created() {
@@ -85,6 +96,10 @@ export default {
<div
class="sidebar-collapsed-icon"
@click="toggleForm"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="tooltipLabel"
>
<icon
:name="lockIcon"
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 006a6d2905d..6d95153af28 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -1,9 +1,13 @@
<script>
- import { __, n__, sprintf } from '../../../locale';
- import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
- import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue';
+ import { __, n__, sprintf } from '~/locale';
+ import tooltip from '~/vue_shared/directives/tooltip';
+ import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+ import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
+ directives: {
+ tooltip,
+ },
components: {
loadingIcon,
userAvatarImage,
@@ -72,7 +76,13 @@
<template>
<div>
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="participantLabel"
+ >
<i
class="fa fa-users"
aria-hidden="true"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
index 3b86f1145d1..9d9ee9dea4d 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -1,12 +1,17 @@
<script>
- import icon from '../../../vue_shared/components/icon.vue';
- import { abbreviateTime } from '../../../lib/utils/pretty_time';
+ import { __, sprintf } from '~/locale';
+ import { abbreviateTime } from '~/lib/utils/pretty_time';
+ import icon from '~/vue_shared/components/icon.vue';
+ import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'TimeTrackingCollapsedState',
components: {
icon,
},
+ directives: {
+ tooltip,
+ },
props: {
showComparisonState: {
type: Boolean,
@@ -79,6 +84,21 @@
return '';
},
+ timeTrackedTooltipText() {
+ let title;
+ if (this.showComparisonState) {
+ title = __('Time remaining');
+ } else if (this.showEstimateOnlyState) {
+ title = __('Estimated');
+ } else if (this.showSpentOnlyState) {
+ title = __('Time spent');
+ }
+
+ return sprintf('%{title}: %{text}', ({ title, text: this.text }));
+ },
+ tooltipText() {
+ return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText;
+ },
},
methods: {
abbreviateTime(timeStr) {
@@ -89,7 +109,13 @@
</script>
<template>
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="tooltipText"
+ >
<icon name="timer" />
<div class="time-tracking-collapsed-summary">
<div :class="divClass">
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 9f5d852260e..26eb4cffba3 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -27,6 +27,7 @@ function mountAssigneesComponent(mediator) {
mediator,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
+ issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
},
}),
});
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 520a0b3f424..8486019897d 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -5,6 +5,7 @@
import $ from 'jquery';
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
import ModalStore from './boards/stores/modal_store';
// TODO: remove eventHub hack after code splitting refactor
@@ -182,7 +183,7 @@ function UsersSelect(currentUser, els, options = {}) {
return axios.put(issueURL, data)
.then(({ data }) => {
- var user;
+ var user, tooltipTitle;
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
if (data.assignee) {
@@ -191,15 +192,17 @@ function UsersSelect(currentUser, els, options = {}) {
username: data.assignee.username,
avatar: data.assignee.avatar_url
};
+ tooltipTitle = _.escape(user.name);
} else {
user = {
name: 'Unassigned',
username: '',
avatar: ''
};
+ tooltipTitle = __('Assignee');
}
$value.html(assigneeTemplate(user));
- $collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle');
+ $collapsedSidebar.attr('title', tooltipTitle).tooltip('fixTitle');
return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
});
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
deleted file mode 100644
index 4d9a2ca530f..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import statusIcon from '../mr_widget_status_icon.vue';
-
-export default {
- name: 'MRWidgetPipelineBlocked',
- components: {
- statusIcon,
- },
- template: `
- <div class="mr-widget-body media">
- <status-icon status="warning" :show-disabled-button="true" />
- <div class="media-body space-children">
- <span class="bold">
- The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
- </span>
- </div>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
new file mode 100644
index 00000000000..8d55477929f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
@@ -0,0 +1,25 @@
+<script>
+import statusIcon from '../mr_widget_status_icon.vue';
+
+export default {
+ name: 'PipelineFailed',
+ components: {
+ statusIcon,
+ },
+};
+</script>
+
+<template>
+ <div class="mr-widget-body media">
+ <status-icon
+ status="warning"
+ :show-disabled-button="true"
+ />
+ <div class="media-body space-children">
+ <span class="bold">
+ {{ s__(`mrWidget|The pipeline for this merge request failed.
+Please retry the job or push a new commit to fix the failure`) }}
+ </span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index 86d52f89d38..3b5c973e4a0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -31,7 +31,7 @@ export { default as ReadyToMergeState } from './components/states/ready_to_merge
export { default as ShaMismatchState } from './components/states/sha_mismatch.vue';
export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue';
-export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
+export { default as PipelineFailedState } from './components/states/pipeline_failed.vue';
export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
export { default as RebaseState } from './components/states/mr_widget_rebase.vue';
export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue';
diff --git a/app/assets/javascripts/vue_shared/components/callout.vue b/app/assets/javascripts/vue_shared/components/callout.vue
new file mode 100644
index 00000000000..ccf802c456c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/callout.vue
@@ -0,0 +1,27 @@
+<script>
+const calloutVariants = ['danger', 'success', 'info', 'warning'];
+
+export default {
+ props: {
+ category: {
+ type: String,
+ required: false,
+ default: calloutVariants[0],
+ validator: value => calloutVariants.includes(value),
+ },
+ message: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div
+ :class="`bs-callout bs-callout-${category}`"
+ role="alert"
+ aria-live="assertive"
+ >
+ {{ message }}
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 97789636787..b8875d04488 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -175,7 +175,7 @@
</a>
</span>
<span v-else>
- Cant find HEAD commit for this branch
+ Can't find HEAD commit for this branch
</span>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
index 8211d425b1f..de6f8c32e74 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
@@ -1,18 +1,29 @@
<script>
- export default {
- name: 'ToggleSidebar',
- props: {
- collapsed: {
- type: Boolean,
- required: true,
- },
+import { __ } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ name: 'ToggleSidebar',
+ directives: {
+ tooltip,
+ },
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ tooltipLabel() {
+ return this.collapsed ? __('Expand sidebar') : __('Collapse sidebar');
},
- methods: {
- toggle() {
- this.$emit('toggle');
- },
+ },
+ methods: {
+ toggle() {
+ this.$emit('toggle');
},
- };
+ },
+};
</script>
<template>
@@ -20,6 +31,10 @@
type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action"
@click="toggle"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="tooltipLabel"
>
<i
aria-label="toggle collapse"
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 48f20029383..f4f5926e198 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -503,3 +503,7 @@ fieldset[disabled] .btn,
@extend %disabled;
}
}
+
+.btn-no-padding {
+ padding: 0;
+}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 798f248dad4..64fff7463d2 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -16,7 +16,7 @@
.nav-header-btn {
padding: 10px $gl-sidebar-padding;
color: inherit;
- transition-duration: .3s;
+ transition-duration: 0.3s;
position: absolute;
top: 0;
cursor: pointer;
@@ -137,6 +137,12 @@
}
}
+.issuable-sidebar .labels {
+ .value.dont-hide ~ .selectbox {
+ padding-top: $gl-padding-8;
+ }
+}
+
.pikaday-container {
.pika-single {
margin-top: 2px;
@@ -151,4 +157,3 @@
.sidebar-collapsed-icon .sidebar-collapsed-value {
font-size: 12px;
}
-
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 37223175199..8c44ebc85ef 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -247,6 +247,7 @@ $btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
$issue-status-expired: $orange-500;
$issuable-sidebar-color: $gl-text-color-secondary;
+$sidebar-block-hover-color: #ebebeb;
$group-path-color: #999;
$namespace-kind-color: #aaa;
$panel-heading-link-color: #777;
@@ -373,6 +374,8 @@ $dropdown-hover-color: $blue-400;
$link-active-background: rgba(0, 0, 0, 0.04);
$link-hover-background: rgba(0, 0, 0, 0.06);
$inactive-badge-background: rgba(0, 0, 0, 0.08);
+$sidebar-toggle-height: 60px;
+$sidebar-milestone-toggle-bottom-margin: 10px;
/*
* Buttons
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 7a6352e45f1..50f32660445 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -1,39 +1,56 @@
@keyframes fade-out-status {
- 0%, 50% { opacity: 1; }
- 100% { opacity: 0; }
+ 0%,
+ 50% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ }
}
@keyframes blinking-dots {
0% {
background-color: rgba($white-light, 1);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ 24px 0 0 0 rgba($white-light, 0.2);
}
25% {
background-color: rgba($white-light, 0.4);
box-shadow: 12px 0 0 0 rgba($white-light, 2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ 24px 0 0 0 rgba($white-light, 0.2);
}
75% {
background-color: rgba($white-light, 0.4);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 1);
+ 24px 0 0 0 rgba($white-light, 1);
}
100% {
background-color: rgba($white-light, 1);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ 24px 0 0 0 rgba($white-light, 0.2);
}
}
@keyframes blinking-scroll-button {
- 0% { opacity: 0.2; }
- 25% { opacity: 0.5; }
- 50% { opacity: 0.7; }
- 100% { opacity: 1; }
+ 0% {
+ opacity: 0.2;
+ }
+
+ 25% {
+ opacity: 0.5;
+ }
+
+ 50% {
+ opacity: 0.7;
+ }
+
+ 100% {
+ opacity: 1;
+ }
}
.build-page {
@@ -125,12 +142,12 @@
.btn-scroll.animate {
.first-triangle {
animation: blinking-scroll-button 1s ease infinite;
- animation-delay: .3s;
+ animation-delay: 0.3s;
}
.second-triangle {
animation: blinking-scroll-button 1s ease infinite;
- animation-delay: .2s;
+ animation-delay: 0.2s;
}
.third-triangle {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 86cdda0359e..e9384d41e00 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -180,10 +180,6 @@
justify-content: space-between;
align-items: center;
flex-grow: 1;
-
- .merge-request-branches & {
- flex-direction: column;
- }
}
.commit-content {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 7f037582ca0..11052be40a8 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -160,6 +160,11 @@
}
}
}
+
+ .diff-loading-error-block {
+ padding: $gl-padding * 2 $gl-padding;
+ text-align: center;
+ }
}
.image {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 2c0ed976301..b2dad4a358a 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -187,7 +187,12 @@
padding-left: 10px;
&:hover {
- color: $gray-darkest;
+ color: $gl-text-color;
+ }
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
}
}
@@ -368,6 +373,14 @@
padding: 15px 0 0;
border-bottom: 0;
overflow: hidden;
+
+ &:hover {
+ background-color: $sidebar-block-hover-color;
+ }
+
+ &.issuable-sidebar-header {
+ padding-top: 0;
+ }
}
.participants {
@@ -380,8 +393,17 @@
.gutter-toggle {
width: 100%;
+ height: $sidebar-toggle-height;
margin-left: 0;
- padding-left: 25px;
+ padding-left: 0;
+ border-bottom: 1px solid $border-gray-dark;
+ }
+
+ a.gutter-toggle {
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ text-align: center;
}
.sidebar-collapsed-icon {
@@ -428,10 +450,10 @@
.btn-clipboard {
border: 0;
+ background: transparent;
color: $issuable-sidebar-color;
&:hover {
- background: transparent;
color: $gl-text-color;
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 4692d0fb873..66db4917178 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -762,3 +762,20 @@
max-width: 100%;
}
}
+
+// Hack alert: we've rewritten `btn` class in a way that
+// we've broken it and it is not possible to use with `btn-link`
+// which causes a blank button when it's disabled and hovering
+// The css in here is the boostrap one
+.btn-link-retry {
+ &[disabled] {
+ cursor: not-allowed;
+ box-shadow: none;
+ opacity: .65;
+
+ &:hover {
+ color: $file-mode-changed;
+ text-decoration: none;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 3af8d80daab..bac3b70c734 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -53,10 +53,6 @@
}
.milestone-sidebar {
- .gutter-toggle {
- margin-bottom: 10px;
- }
-
.milestone-progress {
.title {
padding-top: 5px;
@@ -102,7 +98,17 @@
margin-right: 0;
}
+ .right-sidebar-expanded & {
+ .gutter-toggle {
+ margin-bottom: $sidebar-milestone-toggle-bottom-margin;
+ }
+ }
+
.right-sidebar-collapsed & {
+ .milestone-progress {
+ padding-top: 0;
+ }
+
.reference {
border-top: 1px solid $border-gray-normal;
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 2c840cb407a..855ebf7d86d 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -14,6 +14,11 @@
.commit-title {
margin: 0;
+ white-space: normal;
+
+ @media (max-width: $screen-sm-max) {
+ justify-content: flex-end;
+ }
}
.ci-table {
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 5f46e69a56d..450ef7d6b7e 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -68,6 +68,10 @@
.ide-file-changed-icon {
margin-left: auto;
+
+ > svg {
+ display: block;
+ }
}
.ide-new-btn {
@@ -378,7 +382,11 @@
padding: $gl-bar-padding $gl-padding;
background: $white-light;
display: flex;
- justify-content: space-between;
+ justify-content: flex-end;
+
+ > div + div {
+ padding-left: $gl-padding;
+ }
svg {
vertical-align: middle;
@@ -521,9 +529,13 @@
overflow: auto;
}
-.multi-file-commit-empty-state-container {
- align-items: center;
- justify-content: center;
+.ide-commit-empty-state {
+ padding: 0 $gl-padding;
+}
+
+.ide-commit-empty-state-container {
+ margin-top: auto;
+ margin-bottom: auto;
}
.multi-file-commit-panel-header {
@@ -532,35 +544,22 @@
margin-bottom: 0;
border-bottom: 1px solid $white-dark;
padding: $gl-btn-padding 0;
-
- &.is-collapsed {
- border-bottom: 1px solid $white-dark;
-
- svg {
- margin-left: auto;
- margin-right: auto;
- }
-
- .multi-file-commit-panel-collapse-btn {
- margin-right: auto;
- margin-left: auto;
- border-left: 0;
- }
- }
}
.multi-file-commit-panel-header-title {
display: flex;
flex: 1;
- padding: 0 $gl-btn-padding;
+ padding-left: $grid-size;
svg {
margin-right: $gl-btn-padding;
+ color: $theme-gray-700;
}
}
.multi-file-commit-panel-collapse-btn {
border-left: 1px solid $white-dark;
+ margin-left: auto;
}
.multi-file-commit-list {
@@ -574,12 +573,14 @@
display: flex;
padding: 0;
align-items: center;
+ border-radius: $border-radius-default;
.multi-file-discard-btn {
display: none;
+ margin-top: -2px;
margin-left: auto;
+ margin-right: $grid-size;
color: $gl-link-color;
- padding: 0 2px;
&:focus,
&:hover {
@@ -591,26 +592,31 @@
background: $white-normal;
.multi-file-discard-btn {
- display: block;
+ display: flex;
}
}
}
-.multi-file-addition {
+.multi-file-additions,
+.multi-file-additions-solid {
fill: $green-500;
}
-.multi-file-modified {
+.multi-file-modified,
+.multi-file-modified-solid {
fill: $orange-500;
}
.multi-file-commit-list-collapsed {
display: flex;
flex-direction: column;
+ padding: $gl-padding 0;
- > svg {
+ svg {
+ display: block;
margin-left: auto;
margin-right: auto;
+ color: $theme-gray-700;
}
.file-status-icon {
@@ -622,7 +628,7 @@
.multi-file-commit-list-path {
padding: $grid-size / 2;
- padding-left: $gl-padding;
+ padding-left: $grid-size;
background: none;
border: 0;
text-align: left;
@@ -662,11 +668,6 @@
}
}
-.multi-file-commit-message.form-control {
- height: 160px;
- resize: none;
-}
-
.dirty-diff {
// !important need to override monaco inline style
width: 4px !important;
@@ -812,6 +813,41 @@
}
}
+.ide-commit-list-container {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ padding: 0 16px;
+
+ &:not(.is-collapsed) {
+ flex: 1;
+ min-height: 140px;
+ }
+
+ &.is-collapsed {
+ .multi-file-commit-panel-header {
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+
+ svg {
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ .multi-file-commit-panel-collapse-btn {
+ margin-right: auto;
+ margin-left: auto;
+ border-left: 0;
+ }
+ }
+ }
+}
+
+.ide-staged-action-btn {
+ margin-left: auto;
+ color: $gl-link-color;
+}
+
.ide-commit-radios {
label {
font-weight: normal;
@@ -839,3 +875,74 @@
align-items: center;
font-weight: $gl-font-weight-bold;
}
+
+.ide-commit-message-field {
+ height: 200px;
+ background-color: $white-light;
+
+ .md-area {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+
+ .nav-links {
+ height: 30px;
+ }
+
+ .help-block {
+ margin-top: 2px;
+ color: $blue-500;
+ cursor: pointer;
+ }
+}
+
+.ide-commit-message-textarea-container {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+
+ .note-textarea {
+ font-family: $monospace_font;
+ }
+}
+
+.ide-commit-message-highlights-container {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: -100px;
+ bottom: 0;
+ padding-right: 100px;
+ pointer-events: none;
+ z-index: 1;
+
+ .highlights {
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ color: transparent;
+ }
+
+ mark {
+ margin-left: -1px;
+ padding: 0 2px;
+ border-radius: $border-radius-small;
+ background-color: $orange-200;
+ color: transparent;
+ opacity: 0.6;
+ }
+}
+
+.ide-commit-message-textarea {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 2;
+ background: transparent;
+ resize: none;
+}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 24651dd392c..0fdd4d2cb47 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -5,6 +5,7 @@ class ApplicationController < ActionController::Base
include Gitlab::GonHelper
include GitlabRoutingHelper
include PageLayoutHelper
+ include SafeParamsHelper
include SentryHelper
include WorkhorseHelper
include EnforcesTwoFactorAuthentication
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index ad4e936a3d4..0c34e49206a 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -217,7 +217,7 @@ module NotesActions
def note_project
strong_memoize(:note_project) do
- return nil unless project
+ next nil unless project
note_project_id = params[:note_project_id]
@@ -228,7 +228,7 @@ module NotesActions
project
end
- return access_denied! unless can?(current_user, :create_note, the_project)
+ next access_denied! unless can?(current_user, :create_note, the_project)
the_project
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index e89eaf7edda..f9e8fe624e8 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -86,7 +86,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
out_of_range = todos.current_page > total_pages
if out_of_range
- redirect_to url_for(params.merge(page: total_pages, only_path: true))
+ redirect_to url_for(safe_params.merge(page: total_pages, only_path: true))
end
out_of_range
diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb
index 6142e75b4c1..4d8a20de017 100644
--- a/app/controllers/groups/variables_controller.rb
+++ b/app/controllers/groups/variables_controller.rb
@@ -15,7 +15,7 @@ module Groups
def update
if @group.update(group_variables_params)
respond_to do |format|
- format.json { return render_group_variables }
+ format.json { render_group_variables }
end
else
respond_to do |format|
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 5ac4b8710e2..79fa5818359 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -189,6 +189,6 @@ class GroupsController < Groups::ApplicationController
params[:id] = group.to_param
- url_for(params)
+ url_for(safe_params)
end
end
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index dd12d30a085..7497b5012ec 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -78,6 +78,8 @@ class Projects::JobsController < Projects::ApplicationController
result.merge!(trace.to_h)
end
+ result[:html] = result[:html].presence || 'No job log'
+
render json: result
end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 54e7d81de6a..62b739918e6 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -60,13 +60,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
format.patch do
- return render_404 unless @merge_request.diff_refs
+ break render_404 unless @merge_request.diff_refs
send_git_patch @project.repository, @merge_request.diff_refs
end
format.diff do
- return render_404 unless @merge_request.diff_refs
+ break render_404 unless @merge_request.diff_refs
send_git_diff @project.repository, @merge_request.diff_refs
end
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index 937b0e39cbd..d01f324e6fd 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -28,11 +28,12 @@ class Projects::RepositoriesController < Projects::ApplicationController
end
def assign_archive_vars
- @id = params[:id]
-
- return unless @id
-
- @ref, @filename = extract_ref(@id)
+ if params[:id]
+ @ref, @filename = extract_ref(params[:id])
+ else
+ @ref = params[:ref]
+ @filename = nil
+ end
rescue InvalidPathError
render_404
end
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index 517d0b026c2..bf09ea7e4d8 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -12,7 +12,7 @@ class Projects::VariablesController < Projects::ApplicationController
def update
if @project.update(variables_params)
respond_to do |format|
- format.json { return render_variables }
+ format.json { render_variables }
end
else
respond_to do |format|
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index c4930d3d18d..1b0751f48c5 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -29,8 +29,7 @@ class Projects::WikisController < Projects::ApplicationController
else
return render('empty') unless can?(current_user, :create_wiki, @project)
- @page = WikiPage.new(@project_wiki)
- @page.title = params[:id]
+ @page = build_page(title: params[:id])
render 'edit'
end
@@ -54,7 +53,7 @@ class Projects::WikisController < Projects::ApplicationController
else
render 'edit'
end
- rescue WikiPage::PageChangedError, WikiPage::PageRenameError => e
+ rescue WikiPage::PageChangedError, WikiPage::PageRenameError, Gitlab::Git::Wiki::OperationError => e
@error = e
render 'edit'
end
@@ -70,6 +69,11 @@ class Projects::WikisController < Projects::ApplicationController
else
render action: "edit"
end
+ rescue Gitlab::Git::Wiki::OperationError => e
+ @page = build_page(wiki_params)
+ @error = e
+
+ render 'edit'
end
def history
@@ -94,6 +98,9 @@ class Projects::WikisController < Projects::ApplicationController
redirect_to project_wiki_path(@project, :home),
status: 302,
notice: "Page was successfully deleted"
+ rescue Gitlab::Git::Wiki::OperationError => e
+ @error = e
+ render 'edit'
end
def git_access
@@ -116,4 +123,10 @@ class Projects::WikisController < Projects::ApplicationController
def wiki_params
params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha)
end
+
+ def build_page(args)
+ WikiPage.new(@project_wiki).tap do |page|
+ page.update_attributes(args)
+ end
+ end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 37f14230196..a93b116c6fe 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -404,7 +404,7 @@ class ProjectsController < Projects::ApplicationController
params[:namespace_id] = project.namespace.to_param
params[:id] = project.to_param
- url_for(params)
+ url_for(safe_params)
end
def project_export_enabled
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 956df4a0a16..31f47a7aa7c 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -146,6 +146,6 @@ class UsersController < ApplicationController
end
def build_canonical_path(user)
- url_for(params.merge(username: user.to_param))
+ url_for(safe_params.merge(username: user.to_param))
end
end
diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb
index e72fd8eb3a5..051ea108e06 100644
--- a/app/finders/group_descendants_finder.rb
+++ b/app/finders/group_descendants_finder.rb
@@ -134,10 +134,8 @@ class GroupDescendantsFinder
end
def direct_child_projects
- GroupProjectsFinder.new(group: parent_group,
- current_user: current_user,
- options: { only_owned: true },
- params: params).execute
+ GroupProjectsFinder.new(group: parent_group, current_user: current_user, params: params)
+ .execute
end
# Finds all projects nested under `parent_group` or any of its descendant
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 866b8773db6..fef29789832 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -259,7 +259,7 @@ module BlobHelper
options = []
if error == :collapsed
- options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, expanded: true, format: nil)))
+ options << link_to('load it anyway', url_for(safe_params.merge(viewer: viewer.type, expanded: true, format: nil)))
end
# If the error is `:server_side_but_stored_externally`, the simple viewer will show the same error,
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index b5ca39711bc..1bb82fd8150 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -180,7 +180,7 @@ module DiffHelper
private
def diff_btn(title, name, selected)
- params_copy = params.dup
+ params_copy = safe_params.dup
params_copy[:view] = name
# Always use HTML to handle case where JSON diff rendered this button
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 06c3e569c84..f39a62bccc8 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -9,6 +9,32 @@ module IssuablesHelper
"right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}"
end
+ def sidebar_gutter_tooltip_text
+ sidebar_gutter_collapsed? ? _('Expand sidebar') : _('Collapse sidebar')
+ end
+
+ def sidebar_assignee_tooltip_label(issuable)
+ if issuable.assignee
+ issuable.assignee.name
+ else
+ issuable.allows_multiple_assignees? ? _('Assignee(s)') : _('Assignee')
+ end
+ end
+
+ def sidebar_due_date_tooltip_label(issuable)
+ if issuable.due_date
+ "#{_('Due date')}<br />#{due_date_remaining_days(issuable)}"
+ else
+ _('Due date')
+ end
+ end
+
+ def due_date_remaining_days(issuable)
+ remaining_days_in_words = remaining_days_in_words(issuable)
+
+ "#{issuable.due_date.to_s(:medium)} (#{remaining_days_in_words})"
+ end
+
def multi_label_name(current_labels, default_label)
if current_labels && current_labels.any?
title = current_labels.first.try(:title)
@@ -153,10 +179,14 @@ module IssuablesHelper
def issuable_labels_tooltip(labels, limit: 5)
first, last = labels.partition.with_index { |_, i| i < limit }
- label_names = first.collect(&:name)
- label_names << "and #{last.size} more" unless last.empty?
+ if labels && labels.any?
+ label_names = first.collect(&:name)
+ label_names << "and #{last.size} more" unless last.empty?
- label_names.join(', ')
+ label_names.join(', ')
+ else
+ _("Labels")
+ end
end
def issuables_state_counter_text(issuable_type, state, display_count)
@@ -321,7 +351,7 @@ module IssuablesHelper
def issuable_todo_button_data(issuable, todo, is_collapsed)
{
todo_text: "Add todo",
- mark_text: "Mark done",
+ mark_text: "Mark todo as done",
todo_icon: (is_collapsed ? icon('plus-square') : nil),
mark_icon: (is_collapsed ? icon('check-square', class: 'todo-undone') : nil),
issuable_id: issuable.id,
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index be8cb358de2..e8caab3e50c 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -1,4 +1,6 @@
module MilestonesHelper
+ include EntityDateHelper
+
def milestones_filter_path(opts = {})
if @project
project_milestones_path(@project, opts)
@@ -72,6 +74,19 @@ module MilestonesHelper
end
end
+ def milestone_progress_tooltip_text(milestone)
+ has_issues = milestone.total_issues_count(current_user) > 0
+
+ if has_issues
+ [
+ _('Progress'),
+ _("%{percent}%% complete") % { percent: milestone.percent_complete(current_user) }
+ ].join('<br />')
+ else
+ _('Progress')
+ end
+ end
+
def milestone_progress_bar(milestone)
options = {
class: 'progress-bar progress-bar-success',
@@ -95,27 +110,69 @@ module MilestonesHelper
end
def milestone_tooltip_title(milestone)
- if milestone.due_date
- [milestone.due_date.to_s(:medium), "(#{milestone_remaining_days(milestone)})"].join(' ')
+ if milestone
+ "#{milestone.title}<br />#{milestone_tooltip_due_date(milestone)}"
+ else
+ _('Milestone')
end
end
- def milestone_remaining_days(milestone)
- if milestone.expired?
- content_tag(:strong, 'Past due')
- elsif milestone.upcoming?
- content_tag(:strong, 'Upcoming')
- elsif milestone.due_date
- time_ago = time_ago_in_words(milestone.due_date)
- content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" }
- content.slice!("about ")
- content << " remaining"
- content.html_safe
- elsif milestone.start_date && milestone.start_date.past?
- days = milestone.elapsed_days
- content = content_tag(:strong, days)
- content << " #{'day'.pluralize(days)} elapsed"
+ def milestone_time_for(date, date_type)
+ title = date_type == :start ? "Start date" : "End date"
+
+ if date
+ time_ago = time_ago_in_words(date)
+ time_ago.slice!("about ")
+
+ time_ago << if date.past?
+ " ago"
+ else
+ " remaining"
+ end
+
+ content = [
+ title,
+ "<br />",
+ date.to_s(:medium),
+ "(#{time_ago})"
+ ].join(" ")
+
content.html_safe
+ else
+ title
+ end
+ end
+
+ def milestone_issues_tooltip_text(milestone)
+ issues = milestone.count_issues_by_state(current_user)
+
+ return _("Issues") if issues.empty?
+
+ content = []
+
+ content << n_("1 open issue", "%d open issues", issues["opened"]) % issues["opened"] if issues["opened"]
+ content << n_("1 closed issue", "%d closed issues", issues["closed"]) % issues["closed"] if issues["closed"]
+
+ content.join('<br />').html_safe
+ end
+
+ def milestone_merge_requests_tooltip_text(milestone)
+ merge_requests = milestone.merge_requests
+
+ return _("Merge requests") if merge_requests.empty?
+
+ content = []
+
+ content << n_("1 open merge request", "%d open merge requests", merge_requests.opened.count) % merge_requests.opened.count if merge_requests.opened.any?
+ content << n_("1 closed merge request", "%d closed merge requests", merge_requests.closed.count) % merge_requests.closed.count if merge_requests.closed.any?
+ content << n_("1 merged merge request", "%d merged merge requests", merge_requests.merged.count) % merge_requests.merged.count if merge_requests.merged.any?
+
+ content.join('<br />').html_safe
+ end
+
+ def milestone_tooltip_due_date(milestone)
+ if milestone.due_date
+ "#{milestone.due_date.to_s(:medium)} (#{remaining_days_in_words(milestone)})"
end
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 56c88e6eab0..7754c34d6f0 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -28,7 +28,7 @@ module NavHelper
end
elsif current_path?('jobs#show')
%w[page-gutter build-sidebar right-sidebar-expanded]
- elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access')
+ elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access', 'destroy')
%w[page-gutter wiki-sidebar right-sidebar-expanded]
else
[]
diff --git a/app/helpers/safe_params_helper.rb b/app/helpers/safe_params_helper.rb
new file mode 100644
index 00000000000..b568e8810cc
--- /dev/null
+++ b/app/helpers/safe_params_helper.rb
@@ -0,0 +1,11 @@
+module SafeParamsHelper
+ # Rails 5.0 requires to permit `params` if they're used in url helpers.
+ # Use this helper when generating links with `params.merge(...)`
+ def safe_params
+ if params.respond_to?(:permit!)
+ params.except(:host, :port, :protocol).permit!
+ else
+ params
+ end
+ end
+end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 5e7c20ef51e..dc42caa70e5 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -90,7 +90,7 @@ module TreeHelper
end
def commit_in_single_accessible_branch
- branch_name = html_escape(selected_branch)
+ branch_name = ERB::Util.html_escape(selected_branch)
message = _("Your changes can be committed to %{branch_name} because a merge "\
"request is open.") % { branch_name: "<strong>#{branch_name}</strong>" }
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index b33131becd3..392cc0bee03 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -6,6 +6,12 @@ module Emails
mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason))
end
+ def issue_due_email(recipient_id, issue_id, reason = nil)
+ setup_issue_mail(issue_id, recipient_id)
+
+ mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason))
+ end
+
def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 4aa65bf4273..b0c02cdeec7 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -20,7 +20,7 @@ module Ci
has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment'
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
- has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent
has_one :job_artifacts_archive, -> { where(file_type: Ci::JobArtifact.file_types[:archive]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
@@ -95,8 +95,8 @@ module Ci
run_after_commit { BuildHooksWorker.perform_async(build.id) }
end
- after_commit :update_project_statistics_after_save, on: [:create, :update]
- after_commit :update_project_statistics, on: :destroy
+ after_save :update_project_statistics_after_save, if: :artifacts_size_changed?
+ after_destroy :update_project_statistics_after_destroy, unless: :project_destroyed?
class << self
# This is needed for url_for to work,
@@ -162,7 +162,7 @@ module Ci
build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies')
end
- before_transition pending: :running do |build|
+ after_transition pending: :running do |build|
build.ensure_metadata.update_timeout_state
end
end
@@ -479,7 +479,7 @@ module Ci
def user_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- return variables if user.blank?
+ break variables if user.blank?
variables.append(key: 'GITLAB_USER_ID', value: user.id.to_s)
variables.append(key: 'GITLAB_USER_EMAIL', value: user.email)
@@ -594,7 +594,7 @@ module Ci
def persisted_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- return variables unless persisted?
+ break variables unless persisted?
variables
.append(key: 'CI_JOB_ID', value: id.to_s)
@@ -611,7 +611,7 @@ module Ci
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI', value: 'true')
variables.append(key: 'GITLAB_CI', value: 'true')
- variables.append(key: 'GITLAB_FEATURES', value: project.namespace.features.join(','))
+ variables.append(key: 'GITLAB_FEATURES', value: project.licensed_features.join(','))
variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION)
@@ -643,7 +643,7 @@ module Ci
def persisted_environment_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- return variables unless persisted? && persisted_environment.present?
+ break variables unless persisted? && persisted_environment.present?
variables.concat(persisted_environment.predefined_variables)
@@ -664,16 +664,20 @@ module Ci
pipeline.config_processor.build_attributes(name)
end
- def update_project_statistics
- return unless project
+ def update_project_statistics_after_save
+ update_project_statistics(read_attribute(:artifacts_size).to_i - artifacts_size_was.to_i)
+ end
- ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size])
+ def update_project_statistics_after_destroy
+ update_project_statistics(-artifacts_size)
end
- def update_project_statistics_after_save
- if previous_changes.include?('artifacts_size')
- update_project_statistics
- end
+ def update_project_statistics(difference)
+ ProjectStatistics.increment_statistic(project_id, :build_artifacts_size, difference)
+ end
+
+ def project_destroyed?
+ project.pending_delete?
end
end
end
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
index 62d768cc6cf..44cb583e1bd 100644
--- a/app/models/ci/group_variable.rb
+++ b/app/models/ci/group_variable.rb
@@ -4,7 +4,7 @@ module Ci
include HasVariable
include Presentable
- belongs_to :group
+ belongs_to :group, class_name: "::Group"
alias_attribute :secret_value, :value
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index fbb95fe16df..39676efa08c 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -7,12 +7,15 @@ module Ci
belongs_to :project
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
- before_save :update_file_store
+ mount_uploader :file, JobArtifactUploader
+
before_save :set_size, if: :file_changed?
+ after_save :update_project_statistics_after_save, if: :size_changed?
+ after_destroy :update_project_statistics_after_destroy, unless: :project_destroyed?
- scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
+ after_save :update_file_store
- mount_uploader :file, JobArtifactUploader
+ scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
delegate :exists?, :open, to: :file
@@ -23,7 +26,9 @@ module Ci
}
def update_file_store
- self.file_store = file.object_store
+ # The file.object_store is set during `uploader.store!`
+ # which happens after object is inserted/updated
+ self.update_column(:file_store, file.object_store)
end
def self.artifacts_size_for(project)
@@ -34,10 +39,6 @@ module Ci
[nil, ::JobArtifactUploader::Store::LOCAL].include?(self.file_store)
end
- def set_size
- self.size = file.size
- end
-
def expire_in
expire_at - Time.now if expire_at
end
@@ -48,5 +49,28 @@ module Ci
ChronicDuration.parse(value)&.seconds&.from_now
end
end
+
+ private
+
+ def set_size
+ self.size = file.size
+ end
+
+ def update_project_statistics_after_save
+ update_project_statistics(size.to_i - size_was.to_i)
+ end
+
+ def update_project_statistics_after_destroy
+ update_project_statistics(-self.size)
+ end
+
+ def update_project_statistics(difference)
+ ProjectStatistics.increment_statistic(project_id, :build_artifacts_size, difference)
+ end
+
+ def project_destroyed?
+ # Use job.project to avoid extra DB query for project
+ job.project.pending_delete?
+ end
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index ee0d8df8eb7..5a4c56ec0dc 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -13,7 +13,7 @@ module Ci
has_many :builds
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :projects, -> { auto_include(false) }, through: :runner_projects
+ has_many :projects, through: :runner_projects
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index e4a06f3f976..77947d515c1 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -15,7 +15,7 @@ module Clusters
belongs_to :user
has_many :cluster_projects, class_name: 'Clusters::Project'
- has_many :projects, -> { auto_include(false) }, through: :cluster_projects, class_name: '::Project'
+ has_many :projects, through: :cluster_projects, class_name: '::Project'
# we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
diff --git a/app/models/commit.rb b/app/models/commit.rb
index de860df4b9c..9750e9298ec 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -248,7 +248,7 @@ class Commit
end
def notes_with_associations
- notes.includes(:author)
+ notes.includes(:author, :award_emoji)
end
def merge_requests
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 3469d5d795c..b6276c2fb50 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -87,7 +87,7 @@ class CommitStatus < ActiveRecord::Base
transition [:created, :pending, :running, :manual] => :canceled
end
- before_transition created: [:pending, :running] do |commit_status|
+ before_transition [:created, :skipped, :manual] => :pending do |commit_status|
commit_status.queued_at = Time.now
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 4ae5dd8c677..db8cf322ef7 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -11,7 +11,9 @@ module CacheMarkdownField
extend ActiveSupport::Concern
# Increment this number every time the renderer changes its output
- CACHE_VERSION = 3
+ CACHE_REDCARPET_VERSION = 3
+ CACHE_COMMONMARK_VERSION_START = 10
+ CACHE_COMMONMARK_VERSION = 11
# changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze
@@ -49,12 +51,14 @@ module CacheMarkdownField
# Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project)
- group = self.group if self.respond_to?(:group)
+ group = self.group if self.respond_to?(:group)
context = cached_markdown_fields[field].merge(project: project, group: group)
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
+ context[:markdown_engine] = markdown_engine
+
context
end
@@ -69,7 +73,7 @@ module CacheMarkdownField
Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
]
end.to_h
- updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION
+ updates['cached_markdown_version'] = latest_cached_markdown_version
updates.each {|html_field, data| write_attribute(html_field, data) }
end
@@ -90,7 +94,7 @@ module CacheMarkdownField
markdown_changed = attribute_changed?(markdown_field) || false
html_changed = attribute_changed?(html_field) || false
- CacheMarkdownField::CACHE_VERSION == cached_markdown_version &&
+ latest_cached_markdown_version == cached_markdown_version &&
(html_changed || markdown_changed == html_changed)
end
@@ -109,6 +113,24 @@ module CacheMarkdownField
__send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend
end
+ def latest_cached_markdown_version
+ return CacheMarkdownField::CACHE_REDCARPET_VERSION unless cached_markdown_version
+
+ if cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
+ CacheMarkdownField::CACHE_REDCARPET_VERSION
+ else
+ CacheMarkdownField::CACHE_COMMONMARK_VERSION
+ end
+ end
+
+ def markdown_engine
+ if latest_cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
+ :redcarpet
+ else
+ :common_mark
+ end
+ end
+
included do
cattr_reader :cached_markdown_fields do
FieldData.new
diff --git a/app/models/concerns/group_descendant.rb b/app/models/concerns/group_descendant.rb
index 01957da0bf3..261ace57a17 100644
--- a/app/models/concerns/group_descendant.rb
+++ b/app/models/concerns/group_descendant.rb
@@ -37,7 +37,20 @@ module GroupDescendant
parent ||= preloaded.detect { |possible_parent| possible_parent.is_a?(Group) && possible_parent.id == child.parent_id }
if parent.nil? && !child.parent_id.nil?
- raise ArgumentError.new('parent was not preloaded')
+ parent = child.parent
+
+ exception = ArgumentError.new <<~MSG
+ parent: [GroupDescendant: #{parent.inspect}] was not preloaded for [#{child.inspect}]")
+ This error is not user facing, but causes a +1 query.
+ MSG
+ extras = {
+ parent: parent,
+ child: child,
+ preloaded: preloaded.map(&:full_path)
+ }
+ issue_url = 'https://gitlab.com/gitlab-org/gitlab-ce/issues/40785'
+
+ Gitlab::Sentry.track_exception(exception, issue_url: issue_url, extra: extras)
end
if parent.nil? && hierarchy_top.present?
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index d9416352f9c..b45395343cc 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -48,7 +48,7 @@ module Issuable
end
has_many :label_links, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :labels, -> { auto_include(false) }, through: :label_links
+ has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :metrics
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 5130ecec472..967fd9c5eea 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -102,14 +102,14 @@ module Milestoneish
Gitlab::TimeTrackingFormatter.output(total_issue_time_estimate)
end
- private
-
def count_issues_by_state(user)
memoize_per_user(user, :count_issues_by_state) do
issues_visible_to_user(user).reorder(nil).group(:state).count
end
end
+ private
+
def memoize_per_user(user, method_name)
memoized_users[method_name][user&.id] ||= yield
end
diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb
index 399abb67c9d..7c236369793 100644
--- a/app/models/concerns/resolvable_discussion.rb
+++ b/app/models/concerns/resolvable_discussion.rb
@@ -102,7 +102,7 @@ module ResolvableDiscussion
yield(notes_relation)
# Set the notes array to the updated notes
- @notes = notes_relation.fresh.auto_include(false).to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @notes = notes_relation.fresh.to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables
self.class.memoized_values.each do |name|
clear_memoization(name)
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index 858b7ef533e..89a74b7dcb1 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -2,7 +2,7 @@ class DeployKey < Key
include IgnorableColumn
has_many :deploy_keys_projects, inverse_of: :deploy_key, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :projects, -> { auto_include(false) }, through: :deploy_keys_projects
+ has_many :projects, through: :deploy_keys_projects
scope :in_projects, ->(projects) { joins(:deploy_keys_projects).where('deploy_keys_projects.project_id in (?)', projects) }
scope :are_public, -> { where(public: true) }
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index 8dae821a10e..979e9232fda 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -8,7 +8,7 @@ class DeployToken < ActiveRecord::Base
default_value_for(:expires_at) { Forever.date }
has_many :project_deploy_tokens, inverse_of: :deploy_token
- has_many :projects, -> { auto_include(false) }, through: :project_deploy_tokens
+ has_many :projects, through: :project_deploy_tokens
validate :ensure_at_least_one_scope
before_save :ensure_token
diff --git a/app/models/fork_network.rb b/app/models/fork_network.rb
index aad3509b895..7f1728e8c77 100644
--- a/app/models/fork_network.rb
+++ b/app/models/fork_network.rb
@@ -1,7 +1,7 @@
class ForkNetwork < ActiveRecord::Base
belongs_to :root_project, class_name: 'Project'
has_many :fork_network_members
- has_many :projects, -> { auto_include(false) }, through: :fork_network_members
+ has_many :projects, through: :fork_network_members
after_create :add_root_as_member, if: :root_project
diff --git a/app/models/group.rb b/app/models/group.rb
index 202988d743d..8ff781059cc 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -12,9 +12,9 @@ class Group < Namespace
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
- has_many :users, -> { auto_include(false) }, through: :group_members
+ has_many :users, through: :group_members
has_many :owners,
- -> { where(members: { access_level: Gitlab::Access::OWNER }).auto_include(false) },
+ -> { where(members: { access_level: Gitlab::Access::OWNER }) },
through: :group_members,
source: :user
@@ -23,7 +23,7 @@ class Group < Namespace
has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :shared_projects, -> { auto_include(false) }, through: :project_group_links, source: :project
+ has_many :shared_projects, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, class_name: 'GroupLabel'
has_many :variables, class_name: 'Ci::GroupVariable'
diff --git a/app/models/issue.rb b/app/models/issue.rb
index c1ffe6512ea..7611e83647c 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -34,7 +34,7 @@ class Issue < ActiveRecord::Base
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
- has_many :assignees, -> { auto_include(false) }, class_name: "User", through: :issue_assignees
+ has_many :assignees, class_name: "User", through: :issue_assignees
validates :project, presence: true
@@ -49,6 +49,7 @@ class Issue < ActiveRecord::Base
scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }
+ scope :due_tomorrow, -> { where(due_date: Date.tomorrow) }
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
diff --git a/app/models/label.rb b/app/models/label.rb
index f3496884cff..de7f1d56c64 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -18,8 +18,8 @@ class Label < ActiveRecord::Base
has_many :lists, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :priorities, class_name: 'LabelPriority'
has_many :label_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :issues, -> { auto_include(false) }, through: :label_links, source: :target, source_type: 'Issue'
- has_many :merge_requests, -> { auto_include(false) }, through: :label_links, source: :target, source_type: 'MergeRequest'
+ has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
+ has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
before_validation :strip_whitespace_from_title_and_color
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index ed95613ee59..6b7f280fb70 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -3,7 +3,7 @@ class LfsObject < ActiveRecord::Base
include ObjectStorage::BackgroundMove
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :projects, -> { auto_include(false) }, through: :lfs_objects_projects
+ has_many :projects, through: :lfs_objects_projects
scope :with_files_stored_locally, -> { where(file_store: [nil, LfsObjectUploader::Store::LOCAL]) }
@@ -11,10 +11,12 @@ class LfsObject < ActiveRecord::Base
mount_uploader :file, LfsObjectUploader
- before_save :update_file_store
+ after_save :update_file_store
def update_file_store
- self.file_store = file.object_store
+ # The file.object_store is set during `uploader.store!`
+ # which happens after object is inserted/updated
+ self.update_column(:file_store, file.object_store)
end
def project_allowed_access?(project)
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 8e33bab81c1..a66a0015827 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -22,7 +22,7 @@ class Milestone < ActiveRecord::Base
belongs_to :group
has_many :issues
- has_many :labels, -> { distinct.reorder('labels.title').auto_include(false) }, through: :issues
+ has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 2b63aa33222..c29a53e5ce7 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -248,10 +248,6 @@ class Namespace < ActiveRecord::Base
all_projects.with_storage_feature(:repository).find_each(&:remove_exports)
end
- def features
- []
- end
-
def refresh_project_authorizations
owner.refresh_authorized_projects
end
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index b3ffad00a07..2c3580bbdc6 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -83,14 +83,14 @@ class NotificationRecipient
def has_access?
DeclarativePolicy.subject_scope do
- return false unless user.can?(:receive_notifications)
- return true if @skip_read_ability
+ break false unless user.can?(:receive_notifications)
+ break true if @skip_read_ability
- return false if @target && !user.can?(:read_cross_project)
- return false if @project && !user.can?(:read_project, @project)
+ break false if @target && !user.can?(:read_cross_project)
+ break false if @project && !user.can?(:read_project, @project)
- return true unless read_ability
- return true unless DeclarativePolicy.has_policy?(@target)
+ break true unless read_ability
+ break true unless DeclarativePolicy.has_policy?(@target)
user.can?(read_ability, @target)
end
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index f6d9b0215fc..9195408551f 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -47,7 +47,8 @@ class NotificationSetting < ActiveRecord::Base
].freeze
EXCLUDED_WATCHER_EVENTS = [
- :push_to_merge_request
+ :push_to_merge_request,
+ :issue_due
].push(*EXCLUDED_PARTICIPATING_EVENTS).freeze
def self.find_or_create_for(source)
diff --git a/app/models/project.rb b/app/models/project.rb
index ffd78d3ab70..cec1e705aa8 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -138,11 +138,11 @@ class Project < ActiveRecord::Base
has_one :packagist_service
# TODO: replace these relations with the fork network versions
- has_one :forked_project_link, foreign_key: "forked_to_project_id"
- has_one :forked_from_project, -> { auto_include(false) }, through: :forked_project_link
+ has_one :forked_project_link, foreign_key: "forked_to_project_id"
+ has_one :forked_from_project, through: :forked_project_link
has_many :forked_project_links, foreign_key: "forked_from_project_id"
- has_many :forks, -> { auto_include(false) }, through: :forked_project_links, source: :forked_to_project
+ has_many :forks, through: :forked_project_links, source: :forked_to_project
# TODO: replace these relations with the fork network versions
has_one :root_of_fork_network,
@@ -150,7 +150,7 @@ class Project < ActiveRecord::Base
inverse_of: :root_project,
class_name: 'ForkNetwork'
has_one :fork_network_member
- has_one :fork_network, -> { auto_include(false) }, through: :fork_network_member
+ has_one :fork_network, through: :fork_network_member
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id'
@@ -167,27 +167,27 @@ class Project < ActiveRecord::Base
has_many :protected_tags
has_many :project_authorizations
- has_many :authorized_users, -> { auto_include(false) }, through: :project_authorizations, source: :user, class_name: 'User'
+ has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
has_many :project_members, -> { where(requested_at: nil) },
as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :project_members
- has_many :users, -> { auto_include(false) }, through: :project_members
+ has_many :users, through: :project_members
has_many :requesters, -> { where.not(requested_at: nil) },
as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
has_many :deploy_keys_projects
- has_many :deploy_keys, -> { auto_include(false) }, through: :deploy_keys_projects
+ has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects
- has_many :starrers, -> { auto_include(false) }, through: :users_star_projects, source: :user
+ has_many :starrers, through: :users_star_projects, source: :user
has_many :releases
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :lfs_objects, -> { auto_include(false) }, through: :lfs_objects_projects
+ has_many :lfs_objects, through: :lfs_objects_projects
has_many :lfs_file_locks
has_many :project_group_links
- has_many :invited_groups, -> { auto_include(false) }, through: :project_group_links, source: :group
+ has_many :invited_groups, through: :project_group_links, source: :group
has_many :pages_domains
has_many :todos
has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
@@ -199,7 +199,7 @@ class Project < ActiveRecord::Base
has_one :statistics, class_name: 'ProjectStatistics'
has_one :cluster_project, class_name: 'Clusters::Project'
- has_many :clusters, -> { auto_include(false) }, through: :cluster_project, class_name: 'Clusters::Cluster'
+ has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
@@ -216,16 +216,16 @@ class Project < ActiveRecord::Base
has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
has_many :runner_projects, class_name: 'Ci::RunnerProject'
- has_many :runners, -> { auto_include(false) }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
+ has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, class_name: 'Ci::Variable'
has_many :triggers, class_name: 'Ci::Trigger'
has_many :environments
has_many :deployments
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
has_many :project_deploy_tokens
- has_many :deploy_tokens, -> { auto_include(false) }, through: :project_deploy_tokens
+ has_many :deploy_tokens, through: :project_deploy_tokens
- has_many :active_runners, -> { active.auto_include(false) }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
+ has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_one :auto_devops, class_name: 'ProjectAutoDevops'
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
@@ -1637,7 +1637,7 @@ class Project < ActiveRecord::Base
def container_registry_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- return variables unless Gitlab.config.registry.enabled
+ break variables unless Gitlab.config.registry.enabled
variables.append(key: 'CI_REGISTRY', value: Gitlab.config.registry.host_port)
@@ -1875,6 +1875,10 @@ class Project < ActiveRecord::Base
memoized_results[cache_key]
end
+ def licensed_features
+ []
+ end
+
private
def storage
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 87a4350f022..5d4e3c34b39 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -4,15 +4,15 @@ class ProjectStatistics < ActiveRecord::Base
before_save :update_storage_size
- STORAGE_COLUMNS = [:repository_size, :lfs_objects_size, :build_artifacts_size].freeze
- STATISTICS_COLUMNS = [:commit_count] + STORAGE_COLUMNS
+ COLUMNS_TO_REFRESH = [:repository_size, :lfs_objects_size, :commit_count].freeze
+ INCREMENTABLE_COLUMNS = [:build_artifacts_size].freeze
def total_repository_size
repository_size + lfs_objects_size
end
def refresh!(only: nil)
- STATISTICS_COLUMNS.each do |column, generator|
+ COLUMNS_TO_REFRESH.each do |column, generator|
if only.blank? || only.include?(column)
public_send("update_#{column}") # rubocop:disable GitlabSecurity/PublicSend
end
@@ -34,13 +34,15 @@ class ProjectStatistics < ActiveRecord::Base
self.lfs_objects_size = project.lfs_objects.sum(:size)
end
- def update_build_artifacts_size
- self.build_artifacts_size =
- project.builds.sum(:artifacts_size) +
- Ci::JobArtifact.artifacts_size_for(self.project)
+ def update_storage_size
+ self.storage_size = repository_size + lfs_objects_size + build_artifacts_size
end
- def update_storage_size
- self.storage_size = STORAGE_COLUMNS.sum(&method(:read_attribute))
+ def self.increment_statistic(project_id, key, amount)
+ raise ArgumentError, "Cannot increment attribute: #{key}" unless key.in?(INCREMENTABLE_COLUMNS)
+ return if amount == 0
+
+ where(project_id: project_id)
+ .update_all(["#{key} = COALESCE(#{key}, 0) + (?)", amount])
end
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 52e067cb44c..b7e38ceb502 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -179,7 +179,11 @@ class ProjectWiki
def commit_details(action, message = nil, title = nil)
commit_message = message || default_message(action, title)
- Gitlab::Git::Wiki::CommitDetails.new(@user.name, @user.email, commit_message)
+ Gitlab::Git::Wiki::CommitDetails.new(@user.id,
+ @user.username,
+ @user.name,
+ @user.email,
+ commit_message)
end
def default_message(action, title)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index fd1afafe4df..5bdaa7f0720 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -331,6 +331,7 @@ class Repository
return unless empty?
expire_method_caches(%i(has_visible_content?))
+ raw_repository.expire_has_local_branches_cache
end
def lookup_cache
diff --git a/app/models/todo.rb b/app/models/todo.rb
index aad2c1dac4e..a2ab405fdbe 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -22,7 +22,7 @@ class Todo < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :note
belongs_to :project
- belongs_to :target, -> { auto_include(false) }, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
+ belongs_to :target, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user
delegate :name, :email, to: :author, prefix: true, allow_nil: true
diff --git a/app/models/user.rb b/app/models/user.rb
index d5c5c0964c5..b0668148972 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -96,23 +96,23 @@ class User < ActiveRecord::Base
# Groups
has_many :members
has_many :group_members, -> { where(requested_at: nil) }, source: 'GroupMember'
- has_many :groups, -> { auto_include(false) }, through: :group_members
- has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }).auto_include(false) }, through: :group_members, source: :group
- has_many :masters_groups, -> { where(members: { access_level: Gitlab::Access::MASTER }).auto_include(false) }, through: :group_members, source: :group
+ has_many :groups, through: :group_members
+ has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group
+ has_many :masters_groups, -> { where(members: { access_level: Gitlab::Access::MASTER }) }, through: :group_members, source: :group
# Projects
- has_many :groups_projects, -> { auto_include(false) }, through: :groups, source: :projects
- has_many :personal_projects, -> { auto_include(false) }, through: :namespace, source: :projects
+ has_many :groups_projects, through: :groups, source: :projects
+ has_many :personal_projects, through: :namespace, source: :projects
has_many :project_members, -> { where(requested_at: nil) }
- has_many :projects, -> { auto_include(false) }, through: :project_members
- has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
+ has_many :projects, through: :project_members
+ has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :starred_projects, -> { auto_include(false) }, through: :users_star_projects, source: :project
+ has_many :starred_projects, through: :users_star_projects, source: :project
has_many :project_authorizations
- has_many :authorized_projects, -> { auto_include(false) }, through: :project_authorizations, source: :project
+ has_many :authorized_projects, through: :project_authorizations, source: :project
has_many :user_interacted_projects
- has_many :project_interactions, -> { auto_include(false) }, through: :user_interacted_projects, source: :project, class_name: 'Project'
+ has_many :project_interactions, through: :user_interacted_projects, source: :project, class_name: 'Project'
has_many :snippets, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :notes, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
@@ -132,7 +132,7 @@ class User < ActiveRecord::Base
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
- has_many :assigned_issues, -> { auto_include(false) }, class_name: "Issue", through: :issue_assignees, source: :issue
+ has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
has_many :custom_attributes, class_name: 'UserCustomAttribute'
@@ -947,10 +947,13 @@ class User < ActiveRecord::Base
end
def manageable_groups
- union = Gitlab::SQL::Union.new([owned_groups.select(:id),
- masters_groups.select(:id)])
- arel_union = Arel::Nodes::SqlLiteral.new(union.to_sql)
- owned_and_master_groups = Group.where(Group.arel_table[:id].in(arel_union))
+ union_sql = Gitlab::SQL::Union.new([owned_groups.select(:id), masters_groups.select(:id)]).to_sql
+
+ # Update this line to not use raw SQL when migrated to Rails 5.2.
+ # Either ActiveRecord or Arel constructions are fine.
+ # This was replaced with the raw SQL construction because of bugs in the arel gem.
+ # Bugs were fixed in arel 9.0.0 (Rails 5.2).
+ owned_and_master_groups = Group.where("namespaces.id IN (#{union_sql})") # rubocop:disable GitlabSecurity/SqlInjection
Gitlab::GroupHierarchy.new(owned_and_master_groups).base_and_descendants
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 0f5536415f7..cde79b95062 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -265,6 +265,15 @@ class WikiPage
title.present? && self.class.unhyphenize(@page.url_path) != title
end
+ # Updates the current @attributes hash by merging a hash of params
+ def update_attributes(attrs)
+ attrs[:title] = process_title(attrs[:title]) if attrs[:title].present?
+
+ attrs.slice!(:content, :format, :message, :title)
+
+ @attributes.merge!(attrs)
+ end
+
private
# Process and format the title based on the user input.
@@ -290,15 +299,6 @@ class WikiPage
File.join(components)
end
- # Updates the current @attributes hash by merging a hash of params
- def update_attributes(attrs)
- attrs[:title] = process_title(attrs[:title]) if attrs[:title].present?
-
- attrs.slice!(:content, :format, :message, :title)
-
- @attributes.merge!(attrs)
- end
-
def set_attributes
attributes[:slug] = @page.url_path
attributes[:title] = @page.title
diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb
index 9afebda19be..4873d7ce662 100644
--- a/app/presenters/ci/build_presenter.rb
+++ b/app/presenters/ci/build_presenter.rb
@@ -1,5 +1,14 @@
module Ci
class BuildPresenter < Gitlab::View::Presenter::Delegated
+ CALLOUT_FAILURE_MESSAGES = {
+ unknown_failure: 'There is an unknown failure, please try again',
+ script_failure: 'There has been a script failure. Check the job log for more information',
+ api_failure: 'There has been an API failure, please try again',
+ stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again',
+ runner_system_failure: 'There has been a runner system failure, please try again',
+ missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information'
+ }.freeze
+
presents :build
def erased_by_user?
@@ -35,6 +44,14 @@ module Ci
"#{subject.name} - #{detailed_status.status_tooltip}"
end
+ def callout_failure_message
+ CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym]
+ end
+
+ def recoverable?
+ failed? && !unrecoverable?
+ end
+
private
def tooltip_for_badge
@@ -44,5 +61,9 @@ module Ci
def detailed_status
@detailed_status ||= subject.detailed_status(user)
end
+
+ def unrecoverable?
+ script_failure? || missing_dependency_failure?
+ end
end
end
diff --git a/app/serializers/entity_date_helper.rb b/app/serializers/entity_date_helper.rb
index 71d9a65fb58..464217123b4 100644
--- a/app/serializers/entity_date_helper.rb
+++ b/app/serializers/entity_date_helper.rb
@@ -1,5 +1,6 @@
module EntityDateHelper
include ActionView::Helpers::DateHelper
+ include ActionView::Helpers::TagHelper
def interval_in_words(diff)
return 'Not started' unless diff
@@ -34,4 +35,30 @@ module EntityDateHelper
duration_hash
end
+
+ # Generates an HTML-formatted string for remaining dates based on start_date and due_date
+ #
+ # It returns "Past due" for expired entities
+ # It returns "Upcoming" for upcoming entities
+ # If due date is provided, it returns "# days|weeks|months remaining|ago"
+ # If start date is provided and elapsed, with no due date, it returns "# days elapsed"
+ def remaining_days_in_words(entity)
+ if entity.try(:expired?)
+ content_tag(:strong, 'Past due')
+ elsif entity.try(:upcoming?)
+ content_tag(:strong, 'Upcoming')
+ elsif entity.due_date
+ is_upcoming = (entity.due_date - Date.today).to_i > 0
+ time_ago = time_ago_in_words(entity.due_date)
+ content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" }
+ content.slice!("about ")
+ content << " " + (is_upcoming ? _("remaining") : _("ago"))
+ content.html_safe
+ elsif entity.start_date && entity.start_date.past?
+ days = entity.elapsed_days
+ content = content_tag(:strong, days)
+ content << " #{'day'.pluralize(days)} elapsed"
+ content.html_safe
+ end
+ end
end
diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb
index 523b522d449..3076fed1674 100644
--- a/app/serializers/job_entity.rb
+++ b/app/serializers/job_entity.rb
@@ -26,6 +26,8 @@ class JobEntity < Grape::Entity
expose :created_at
expose :updated_at
expose :detailed_status, as: :status, with: StatusEntity
+ expose :callout_message, if: -> (*) { failed? }
+ expose :recoverable, if: -> (*) { failed? }
private
@@ -50,4 +52,20 @@ class JobEntity < Grape::Entity
def path_to(route, build)
send("#{route}_path", build.project.namespace, build.project, build) # rubocop:disable GitlabSecurity/PublicSend
end
+
+ def failed?
+ build.failed?
+ end
+
+ def callout_message
+ build_presenter.callout_failure_message
+ end
+
+ def recoverable
+ build_presenter.recoverable?
+ end
+
+ def build_presenter
+ @build_presenter ||= build.present
+ end
end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index e09b445636f..0b087ad73da 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -4,6 +4,9 @@ module Ci
class RegisterJobService
attr_reader :runner
+ JOB_QUEUE_DURATION_SECONDS_BUCKETS = [1, 3, 10, 30].freeze
+ JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET = 5.freeze
+
Result = Struct.new(:build, :valid?)
def initialize(runner)
@@ -41,7 +44,7 @@ module Ci
build.run!
register_success(build)
- return Result.new(build, true)
+ return Result.new(build, true) # rubocop:disable Cop/AvoidReturnFromBlocks
rescue Ci::Build::MissingDependenciesError
build.drop!(:missing_dependency_failure)
end
@@ -104,10 +107,22 @@ module Ci
end
def register_success(job)
- job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at)
+ labels = { shared_runner: runner.shared?,
+ jobs_running_for_project: jobs_running_for_project(job) }
+
+ job_queue_duration_seconds.observe(labels, Time.now - job.queued_at) unless job.queued_at.nil?
attempt_counter.increment
end
+ def jobs_running_for_project(job)
+ return '+Inf' unless runner.shared?
+
+ # excluding currently started job
+ running_jobs_count = job.project.builds.running.where(runner: Ci::Runner.shared)
+ .limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1
+ running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+"
+ end
+
def failed_attempt_counter
@failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job")
end
@@ -117,7 +132,7 @@ module Ci
end
def job_queue_duration_seconds
- @job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time')
+ @job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time', {}, JOB_QUEUE_DURATION_SECONDS_BUCKETS)
end
end
end
diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb
index 15ab2d54404..84944e95542 100644
--- a/app/services/clusters/gcp/finalize_creation_service.rb
+++ b/app/services/clusters/gcp/finalize_creation_service.rb
@@ -13,7 +13,7 @@ module Clusters
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
rescue ActiveRecord::RecordInvalid => e
- provider.make_errored!("Failed to configure GKE Cluster: #{e.message}")
+ provider.make_errored!("Failed to configure Google Kubernetes Engine Cluster: #{e.message}")
end
private
diff --git a/app/services/clusters/gcp/verify_provision_status_service.rb b/app/services/clusters/gcp/verify_provision_status_service.rb
index f994aacd086..7cc4324677e 100644
--- a/app/services/clusters/gcp/verify_provision_status_service.rb
+++ b/app/services/clusters/gcp/verify_provision_status_service.rb
@@ -17,7 +17,7 @@ module Clusters
when 'DONE'
finalize_creation
else
- return provider.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
+ provider.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
end
end
end
diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb
index 88dfb7a4a90..7e5a77fb056 100644
--- a/app/services/create_deployment_service.rb
+++ b/app/services/create_deployment_service.rb
@@ -19,8 +19,8 @@ class CreateDeploymentService
environment.fire_state_event(action)
- return unless environment.save
- return if environment.stopped?
+ break unless environment.save
+ break if environment.stopped?
deploy.tap(&:update_merge_request_metrics!)
end
diff --git a/app/services/import_export_clean_up_service.rb b/app/services/import_export_clean_up_service.rb
index 6442406d77e..74088b970c9 100644
--- a/app/services/import_export_clean_up_service.rb
+++ b/app/services/import_export_clean_up_service.rb
@@ -10,7 +10,7 @@ class ImportExportCleanUpService
def execute
Gitlab::Metrics.measure(:import_export_clean_up) do
- return unless File.directory?(path)
+ next unless File.directory?(path)
clean_up_export_files
end
diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb
index 775efed48eb..9b7486cf53b 100644
--- a/app/services/labels/transfer_service.rb
+++ b/app/services/labels/transfer_service.rb
@@ -64,9 +64,14 @@ module Labels
end
def update_label_links(labels, old_label_id:, new_label_id:)
- LabelLink.joins(:label)
- .merge(labels)
- .where(label_id: old_label_id)
+ # use 'labels' relation to get label_link ids only of issues/MRs
+ # in the project being transferred.
+ # IDs are fetched in a separate query because MySQL doesn't
+ # allow referring of 'label_links' table in UPDATE query:
+ # https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/62435068
+ link_ids = labels.pluck('label_links.id')
+
+ LabelLink.where(id: link_ids, label_id: old_label_id)
.update_all(label_id: new_label_id)
end
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index b82d9c64296..83e59a649b6 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -203,10 +203,11 @@ module NotificationRecipientService
attr_reader :action
attr_reader :previous_assignee
attr_reader :skip_current_user
- def initialize(target, current_user, action:, previous_assignee: nil, skip_current_user: true)
+ def initialize(target, current_user, action:, custom_action: nil, previous_assignee: nil, skip_current_user: true)
@target = target
@current_user = current_user
@action = action
+ @custom_action = custom_action
@previous_assignee = previous_assignee
@skip_current_user = skip_current_user
end
@@ -236,7 +237,13 @@ module NotificationRecipientService
add_mentions(current_user, target: target)
# Add the assigned users, if any
- assignees = custom_action == :new_issue ? target.assignees : target.assignee
+ assignees = case custom_action
+ when :new_issue
+ target.assignees
+ else
+ target.assignee
+ end
+
# We use the `:participating` notification level in order to match existing legacy behavior as captured
# in existing specs (notification_service_spec.rb ~ line 507)
add_recipients(assignees, :participating, NotificationReason::ASSIGNED) if assignees
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index f94c76cf3ac..274161df946 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -373,6 +373,20 @@ class NotificationService
end
end
+ def issue_due(issue)
+ recipients = NotificationRecipientService.build_recipients(
+ issue,
+ issue.author,
+ action: 'due',
+ custom_action: :issue_due,
+ skip_current_user: false
+ )
+
+ recipients.each do |recipient|
+ mailer.send(:issue_due_email, recipient.user.id, issue.id, recipient.reason).deliver_later
+ end
+ end
+
protected
def new_resource_email(target, method)
diff --git a/app/services/projects/create_from_template_service.rb b/app/services/projects/create_from_template_service.rb
index a549cfbabea..29b133cc466 100644
--- a/app/services/projects/create_from_template_service.rb
+++ b/app/services/projects/create_from_template_service.rb
@@ -8,9 +8,10 @@ module Projects
template_name = params.delete(:template_name)
file = Gitlab::ProjectTemplate.find(template_name).file
+ override_params = params.dup
params[:file] = file
- GitlabProjectsImportService.new(current_user, params).execute
+ GitlabProjectsImportService.new(current_user, params, override_params).execute
ensure
file&.close
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index aa14206db3b..44e869851ca 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -137,7 +137,7 @@ module Projects
return true unless Gitlab.config.registry.enabled
ContainerRepository.build_root_repository(project).tap do |repository|
- return repository.has_tags? ? repository.delete_tags! : true
+ break repository.has_tags? ? repository.delete_tags! : true
end
end
diff --git a/app/services/repository_archive_clean_up_service.rb b/app/services/repository_archive_clean_up_service.rb
index aa84d36a206..9a88459b511 100644
--- a/app/services/repository_archive_clean_up_service.rb
+++ b/app/services/repository_archive_clean_up_service.rb
@@ -10,7 +10,7 @@ class RepositoryArchiveCleanUpService
def execute
Gitlab::Metrics.measure(:repository_archive_clean_up) do
- return unless File.directory?(path)
+ next unless File.directory?(path)
clean_up_old_archives
clean_up_empty_directories
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 00bf5434b7f..958ef065012 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -159,7 +159,7 @@ module SystemNoteService
body = if noteable.time_estimate == 0
"removed time estimate"
else
- "changed time estimate to #{parsed_time}"
+ "changed time estimate to #{parsed_time},"
end
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
diff --git a/app/services/test_hooks/base_service.rb b/app/services/test_hooks/base_service.rb
index e9aefb1fb75..aadc1ea644b 100644
--- a/app/services/test_hooks/base_service.rb
+++ b/app/services/test_hooks/base_service.rb
@@ -19,7 +19,7 @@ module TestHooks
error_message = catch(:validation_error) do
sample_data = self.__send__(trigger_data_method) # rubocop:disable GitlabSecurity/PublicSend
- return hook.execute(sample_data, trigger_key)
+ return hook.execute(sample_data, trigger_key) # rubocop:disable Cop/AvoidReturnFromBlocks
end
error(error_message)
diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb
index dd86753479d..2a5a830ce4f 100644
--- a/app/uploaders/job_artifact_uploader.rb
+++ b/app/uploaders/job_artifact_uploader.rb
@@ -6,10 +6,10 @@ class JobArtifactUploader < GitlabUploader
storage_options Gitlab.config.artifacts
- def size
- return super if model.size.nil?
+ def cached_size
+ return model.size if model.size.present? && !model.file_changed?
- model.size
+ size
end
def store_dir
@@ -20,7 +20,7 @@ class JobArtifactUploader < GitlabUploader
if file_storage?
File.open(path, "rb") if path
else
- ::Gitlab::Ci::Trace::HttpIO.new(url, size) if url
+ ::Gitlab::Ci::Trace::HttpIO.new(url, cached_size) if url
end
end
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index bd258e04d3f..a3549cada95 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -183,14 +183,6 @@ module ObjectStorage
StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options)
}
end
-
- def default_object_store
- if self.object_store_enabled? && self.direct_upload_enabled?
- Store::REMOTE
- else
- Store::LOCAL
- end
- end
end
# allow to configure and overwrite the filename
@@ -211,12 +203,13 @@ module ObjectStorage
end
def object_store
- @object_store ||= model.try(store_serialization_column) || self.class.default_object_store
+ # We use Store::LOCAL as null value indicates the local storage
+ @object_store ||= model.try(store_serialization_column) || Store::LOCAL
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def object_store=(value)
- @object_store = value || self.class.default_object_store
+ @object_store = value || Store::LOCAL
@storage = storage_for(object_store)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
@@ -302,6 +295,15 @@ module ObjectStorage
super
end
+ def store!(new_file = nil)
+ # when direct upload is enabled, always store on remote storage
+ if self.class.object_store_enabled? && self.class.direct_upload_enabled?
+ self.object_store = Store::REMOTE
+ end
+
+ super
+ end
+
private
def schedule_background_upload?
diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder
index 70ec6bc6257..d7b6fb9a4a1 100644
--- a/app/views/dashboard/issues.atom.builder
+++ b/app/views/dashboard/issues.atom.builder
@@ -1,5 +1,5 @@
xml.title "#{current_user.name} issues"
-xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
+xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
xml.id issues_dashboard_url
xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index bb472b4c900..4bf04dadf01 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -2,12 +2,12 @@
- page_title _("Issues")
- @breadcrumb_link = issues_dashboard_path(assignee_id: current_user.id)
= content_for :meta_tags do
- = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues")
+ = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{current_user.name} issues")
.top-area
= render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set
.nav-controls
- = link_to params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do
+ = link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do
= icon('rss')
= render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index 8680ec2e298..646e89e9bd1 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -7,7 +7,7 @@
- unless expanded
- diff_data = { lines_path: project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion) }
-.diff-file.file-holder{ class: diff_file_class, data: diff_data }
+.diff-file.file-holder.js-lazy-load-discussion{ class: diff_file_class, data: diff_data }
.js-file-title.file-title.file-title-flex-parent
.file-header-content
= render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false
@@ -28,8 +28,11 @@
%tr.line_holder.line-holder-placeholder
%td.old_line.diff-line-num
%td.new_line.diff-line-num
- %td.line_content
+ %td.line_content.js-success-lazy-load
.js-code-placeholder
+ %td.js-error-lazy-load-diff.hidden.diff-loading-error-block
+ - button = button_tag(_("Try again"), class: "btn-link btn-link-retry btn-no-padding js-toggle-lazy-diff-retry-button")
+ = _("Unable to load the diff. %{button_try_again}").html_safe % { button_try_again: button}
= render "discussions/diff_discussion", discussions: [discussion], expanded: true
- else
- partial = (diff_file.new_file? || diff_file.deleted_file?) ? 'single_image_diff' : 'replaced_image_diff'
diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder
index a239ea8caf0..2a385b661e5 100644
--- a/app/views/groups/issues.atom.builder
+++ b/app/views/groups/issues.atom.builder
@@ -1,5 +1,5 @@
xml.title "#{@group.name} issues"
-xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
+xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
xml.link href: issues_group_url, rel: "alternate", type: "text/html"
xml.id issues_group_url
xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 36df03302e8..bbfbea4ac7a 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -1,6 +1,6 @@
- page_title "Issues"
= content_for :meta_tags do
- = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues")
+ = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@group.name} issues")
- if group_issues_count(state: 'all').zero?
= render 'shared/empty_states/issues', project_select_button: true
diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb
index 198f30a1dc4..8e20c4a4b2a 100644
--- a/app/views/layouts/mailer.text.erb
+++ b/app/views/layouts/mailer.text.erb
@@ -1,4 +1,4 @@
<%= yield -%>
----
+-- <%# signature marker %>
You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
diff --git a/app/views/layouts/notify.text.erb b/app/views/layouts/notify.text.erb
index de48f548a1b..9dc490efa9a 100644
--- a/app/views/layouts/notify.text.erb
+++ b/app/views/layouts/notify.text.erb
@@ -1,6 +1,6 @@
<%= yield -%>
----
+-- <%# signature marker %>
<% if @target_url -%>
<% if @reply_by_email -%>
<%= "Reply to this email directly or view it on GitLab: #{@target_url}" -%>
diff --git a/app/views/notify/issue_due_email.html.haml b/app/views/notify/issue_due_email.html.haml
new file mode 100644
index 00000000000..e81144b8fcb
--- /dev/null
+++ b/app/views/notify/issue_due_email.html.haml
@@ -0,0 +1,12 @@
+%p.details
+ #{link_to @issue.author_name, user_url(@issue.author)}'s issue is due soon.
+
+- if @issue.assignees.any?
+ %p
+ Assignee: #{@issue.assignee_list}
+%p
+ This issue is due on: #{@issue.due_date.to_s(:medium)}
+
+- if @issue.description
+ %div
+ = markdown(@issue.description, pipeline: :email, author: @issue.author)
diff --git a/app/views/notify/issue_due_email.text.erb b/app/views/notify/issue_due_email.text.erb
new file mode 100644
index 00000000000..3c7a57a8a2e
--- /dev/null
+++ b/app/views/notify/issue_due_email.text.erb
@@ -0,0 +1,7 @@
+The following issue is due on <%= @issue.due_date %>:
+
+Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
+Author: <%= @issue.author_name %>
+Assignee: <%= @issue.assignee_list %>
+
+<%= @issue.description %>
diff --git a/app/views/projects/blob/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml
index 9c760c81527..b9663bbba15 100644
--- a/app/views/projects/blob/_viewer.html.haml
+++ b/app/views/projects/blob/_viewer.html.haml
@@ -4,7 +4,7 @@
- load_async = local_assigns.fetch(:load_async, viewer.load_async? && render_error.nil?)
- external_embed = local_assigns.fetch(:external_embed, false)
-- viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_async
+- viewer_url = local_assigns.fetch(:viewer_url) { url_for(safe_params.merge(viewer: viewer.type, format: :json)) } if load_async
.blob-viewer{ data: { type: viewer.type, rich_type: rich_type, url: viewer_url }, class: ('hidden' if hidden) }
- if render_error
= render 'projects/blob/render_error', viewer: viewer
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 71176acd12d..d0c01f95cb7 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -29,7 +29,7 @@
= s_('Branches|Cant find HEAD commit for this branch')
- if branch.name != @repository.root_ref
- .divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
+ .divergence-graph.hidden-xs{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
default_branch: @repository.root_ref,
number_commits_ahead: diverging_count_label(number_commits_ahead) } }
.graph-side
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml
index ebb7d247125..e004966bdcc 100644
--- a/app/views/projects/clusters/new.html.haml
+++ b/app/views/projects/clusters/new.html.haml
@@ -8,6 +8,6 @@
%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up Kubernetes cluster integration')
%p= s_('ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab')
- = link_to s_('ClusterIntegration|Create on GKE'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
+ = link_to s_('ClusterIntegration|Create on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
%p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster')
= link_to s_('ClusterIntegration|Add an existing Kubernetes cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 289bfdd69bc..3fd0fa348b3 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -22,7 +22,7 @@
= author_avatar(commit, size: 36)
.commit-detail.flex-list
- .commit-content
+ .commit-content.qa-commit-content
- if view_details && merge_request
= link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title"
- else
diff --git a/app/views/projects/diffs/_collapsed.html.haml b/app/views/projects/diffs/_collapsed.html.haml
index 8772bd4705f..5762f4d86d7 100644
--- a/app/views/projects/diffs/_collapsed.html.haml
+++ b/app/views/projects/diffs/_collapsed.html.haml
@@ -1,5 +1,5 @@
- diff_file = viewer.diff_file
-- url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
+- url = url_for(safe_params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
.nothing-here-block.diff-collapsed{ data: { diff_for_path: url } }
This diff is collapsed.
%a.click-to-expand Click to expand it.
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 0c58dd60e2c..e27f5658e87 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -26,7 +26,7 @@
- if issue.milestone
%span.issuable-milestone.hidden-xs
&nbsp;
- = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: issuable_milestone_tooltip_title(issue) } do
+ = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_due_date(issue.milestone) } do
= icon('clock-o')
= issue.milestone.title
- if issue.due_date
diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml
index dd1a836fa20..297b928f020 100644
--- a/app/views/projects/issues/_nav_btns.html.haml
+++ b/app/views/projects/issues/_nav_btns.html.haml
@@ -1,4 +1,4 @@
-= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
+= link_to safe_params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
= icon('rss')
- if @can_bulk_update
= button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder
index 4029926f373..6330245954e 100644
--- a/app/views/projects/issues/index.atom.builder
+++ b/app/views/projects/issues/index.atom.builder
@@ -1,5 +1,5 @@
xml.title "#{@project.name} issues"
-xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
+xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
xml.link href: project_issues_url(@project), rel: "alternate", type: "text/html"
xml.id project_issues_url(@project)
xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index c427a9eedc2..1e7737aeb97 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -5,7 +5,7 @@
- new_issue_email = @project.new_issuable_address(current_user, 'issue')
= content_for :meta_tags do
- = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues")
+ = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@project.name} issues")
- if project_issues(@project).exists?
%div{ class: (container_class) }
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index 0b57ebedebd..7f0bef5ede0 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -1,15 +1,8 @@
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.sidebar-container
.blocks-container
- .block
- %strong.inline.prepend-top-8
- = @build.name
- - if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'js-retry-button pull-right btn btn-inverted-secondary btn-retry visible-md-block visible-lg-block', method: :post
- %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-build-toggle{ href: "#", 'aria-label': 'Toggle Sidebar', role: 'button' }
- = icon('angle-double-right')
- #js-details-block-vue
+ #js-details-block-vue{ data: { can_user_retry: can?(current_user, :update_build, @build) && @build.retryable? } }
- if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
.block
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index a94267deeb2..027a9ff1416 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -23,7 +23,7 @@
- if merge_request.milestone
%span.issuable-milestone.hidden-xs
&nbsp;
- = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: issuable_milestone_tooltip_title(merge_request) } do
+ = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_due_date(merge_request.milestone) } do
= icon('clock-o')
= merge_request.milestone.title
- if merge_request.target_project.default_branch != merge_request.target_branch
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index 9d5cebdda53..f81db9b4e28 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -3,7 +3,7 @@
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f|
.hide.alert.alert-danger.mr-compare-errors
- .merge-request-branches.js-merge-request-new-compare.row{ 'data-target-project-url': project_new_merge_request_update_branches_path(@source_project), 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
+ .js-merge-request-new-compare.row{ 'data-target-project-url': project_new_merge_request_update_branches_path(@source_project), 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
.col-md-6
.panel.panel-default.panel-new-merge-request
.panel-heading
diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml
index 376ac377562..68780cedeb1 100644
--- a/app/views/projects/merge_requests/creations/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml
@@ -26,16 +26,16 @@
- else
%ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.commits-tab.active
- = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
+ = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
Commits
%span.badge= @commits.size
- if @pipelines.any?
%li.builds-tab
- = link_to url_for(params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
+ = link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
Pipelines
%span.badge= @pipelines.size
%li.diffs-tab
- = link_to url_for(params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
+ = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
Changes
%span.badge= @merge_request.diff_size
@@ -46,7 +46,7 @@
-# This tab is always loaded via AJAX
- if @pipelines.any?
#pipelines.pipelines.tab-pane
- = render 'projects/merge_requests/pipelines', endpoint: url_for(params.merge(action: 'pipelines', format: :json)), disable_initialization: true
+ = render 'projects/merge_requests/pipelines', endpoint: url_for(safe_params.merge(action: 'pipelines', format: :json)), disable_initialization: true
.mr-loading-status
= spinner
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 877101b05ca..8f2142af2ce 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -1,24 +1,25 @@
- breadcrumb_title "Pipelines"
-- page_title "New Pipeline"
+- page_title = s_("Pipeline|Run Pipeline")
%h3.page-title
- New Pipeline
+ = s_("Pipeline|Run Pipeline")
%hr
= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f|
= form_errors(@pipeline)
.form-group
- = f.label :ref, 'Create for', class: 'control-label'
+ = f.label :ref, s_('Pipeline|Run on'), class: 'control-label'
.col-sm-10
= hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
= dropdown_tag(params[:ref] || @project.default_branch,
options: { toggle_class: 'js-branch-select wide git-revision-dropdown-toggle',
- filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: "Search branches",
+ filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: s_("Pipeline|Search branches"),
data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
- .help-block Existing branch name, tag
+ .help-block
+ = s_("Pipeline|Existing branch name, tag")
.form-actions
- = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
- = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-cancel'
+ = f.submit s_('Pipeline|Run pipeline'), class: 'btn btn-success', tabindex: 3
+ = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-default pull-right'
-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml
index 98d56a3e5c5..12ccae10260 100644
--- a/app/views/projects/protected_branches/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml
@@ -7,8 +7,8 @@
- content_for :push_access_levels do
.push_access_levels-container
= dropdown_tag('Select',
- options: { toggle_class: 'js-allowed-to-push wide',
- dropdown_class: 'dropdown-menu-selectable capitalize-header',
+ options: { toggle_class: 'js-allowed-to-push qa-allowed-to-push-select wide',
+ dropdown_class: 'dropdown-menu-selectable qa-allowed-to-push-dropdown capitalize-header',
data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})
= render 'projects/protected_branches/shared/create_protected_branch'
diff --git a/app/views/projects/protected_branches/_update_protected_branch.html.haml b/app/views/projects/protected_branches/_update_protected_branch.html.haml
index c61b2951e1e..98363f2018a 100644
--- a/app/views/projects/protected_branches/_update_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_update_protected_branch.html.haml
@@ -6,5 +6,5 @@
%td
= hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level
= dropdown_tag( (protected_branch.push_access_levels.first.humanize || 'Select') ,
- options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
+ options: { toggle_class: 'js-allowed-to-push qa-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
data: { field_name: "allowed_to_push_#{protected_branch.id}", access_level_id: protected_branch.push_access_levels.first.id }})
diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml
index 300055a4207..d1ed438eb21 100644
--- a/app/views/projects/protected_branches/shared/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml
@@ -1,4 +1,4 @@
-.protected-branches-list.js-protected-branches-list
+.protected-branches-list.js-protected-branches-list.qa-protected-branches-list
- if @protected_branches.empty?
.panel-heading
%h3.panel-title
diff --git a/app/views/projects/protected_branches/shared/_dropdown.html.haml b/app/views/projects/protected_branches/shared/_dropdown.html.haml
index 74435236808..b3d6068039a 100644
--- a/app/views/projects/protected_branches/shared/_dropdown.html.haml
+++ b/app/views/projects/protected_branches/shared/_dropdown.html.haml
@@ -1,8 +1,8 @@
= f.hidden_field(:name)
= dropdown_tag('Select branch or create wildcard',
- options: { toggle_class: 'js-protected-branch-select js-filter-submit wide git-revision-dropdown-toggle',
- filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: "Search protected branches",
+ options: { toggle_class: 'js-protected-branch-select js-filter-submit wide git-revision-dropdown-toggle qa-protected-branch-select',
+ filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown qa-protected-branch-dropdown", placeholder: "Search protected branches",
footer_content: true,
data: { show_no: true, show_any: true, show_upcoming: true,
selected: params[:protected_branch_name],
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml
index 55d87c35a80..fd5c1aa342a 100644
--- a/app/views/projects/protected_branches/shared/_index.html.haml
+++ b/app/views/projects/protected_branches/shared/_index.html.haml
@@ -4,7 +4,7 @@
.settings-header
%h4
Protected Branches
- %button.btn.js-settings-toggle{ type: 'button' }
+ %button.btn.js-settings-toggle.qa-expand-protected-branches{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Keep stable branches secure and force developers to use merge requests.
diff --git a/app/views/projects/protected_branches/shared/_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_protected_branch.html.haml
index 10b81e42572..f5b21f0e887 100644
--- a/app/views/projects/protected_branches/shared/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/shared/_protected_branch.html.haml
@@ -2,7 +2,7 @@
%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) } }
%td
- %span.ref-name= protected_branch.name
+ %span.ref-name.qa-protected-branch-name= protected_branch.name
- if @project.root_ref?(protected_branch.name)
%span.label.label-info.prepend-left-5 default
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index 4c8c92d722a..f1c39b9e923 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -8,8 +8,8 @@
- @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil
.dropdown
- = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" }
- .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
+ = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown qa-branches-select" }
+ .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging.qa-branches-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
.dropdown-page-one
= dropdown_title _("Switch branch/tag")
= dropdown_filter _("Search branches and tags")
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index 87e6b52f46e..1c73534c642 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -4,7 +4,7 @@
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
- .value.issuable-show-labels
+ .value.issuable-show-labels.dont-hide
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
None
%a{ href: "#",
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 975b9cb4729..093033775a9 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -7,7 +7,7 @@
- if current_user
%span.issuable-header-text.hide-collapsed.pull-left
= _('Todo')
- %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
+ %a.gutter-toggle.pull-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left' } }
= sidebar_gutter_toggle_icon
- if current_user
= render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable
@@ -19,12 +19,11 @@
.block.assignee
= render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present?
.block.milestone
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } }
= icon('clock-o', 'aria-hidden': 'true')
%span.milestone-title
- if issuable.milestone
- %span.has-tooltip{ title: "#{issuable.milestone.title}<br>#{milestone_tooltip_title(issuable.milestone)}", data: { container: 'body', html: 1, placement: 'left' } }
- = issuable.milestone.title
+ = issuable.milestone.title
- else
= _('None')
.title.hide-collapsed
@@ -34,7 +33,7 @@
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed
- if issuable.milestone
- = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_title(issuable.milestone), data: { container: "body", html: 1 }
+ = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_due_date(issuable.milestone), data: { container: "body", html: 1 }
- else
%span.no-value
= _('None')
@@ -50,7 +49,7 @@
= icon('spinner spin', 'aria-hidden': 'true')
- if issuable.has_attribute?(:due_date)
.block.due_date
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 1 }, title: sidebar_due_date_tooltip_label(issuable) }
= icon('calendar', 'aria-hidden': 'true')
%span.js-due-date-sidebar-value
= issuable.due_date.try(:to_s, :medium) || 'None'
@@ -96,7 +95,7 @@
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
- .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
+ .value.issuable-show-labels.dont-hide.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- selected_labels.each do |label|
= link_to_label(label, subject: issuable.project, type: issuable.to_ability_name)
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 304df38a096..21006a76b28 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -4,7 +4,7 @@
= _('Assignee')
= icon('spinner spin')
- else
- .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee.name if issuable.assignee) }
+ .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: sidebar_assignee_tooltip_label(issuable) }
- if issuable.assignee
= link_to_member(@project, issuable.assignee, size: 24)
- else
diff --git a/app/views/shared/issuable/_sidebar_todo.html.haml b/app/views/shared/issuable/_sidebar_todo.html.haml
index b77e104c072..74327fb1ba8 100644
--- a/app/views/shared/issuable/_sidebar_todo.html.haml
+++ b/app/views/shared/issuable/_sidebar_todo.html.haml
@@ -1,11 +1,11 @@
- is_collapsed = local_assigns.fetch(:is_collapsed, false)
-- mark_content = is_collapsed ? icon('check-square', class: 'todo-undone') : _('Mark done')
+- mark_content = is_collapsed ? icon('check-square', class: 'todo-undone') : _('Mark todo as done')
- todo_content = is_collapsed ? icon('plus-square') : _('Add todo')
%button.issuable-todo-btn.js-issuable-todo{ type: 'button',
class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn pull-right'),
- title: (todo.nil? ? _('Add todo') : _('Mark done')),
- 'aria-label' => (todo.nil? ? _('Add todo') : _('Mark done')),
+ title: (todo.nil? ? _('Add todo') : _('Mark todo as done')),
+ 'aria-label' => (todo.nil? ? _('Add todo') : _('Mark todo as done')),
data: issuable_todo_button_data(issuable, todo, is_collapsed) }
%span.issuable-todo-inner.js-issuable-todo-inner<
- if todo
diff --git a/app/views/shared/issuable/form/_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_merge_request_assignee.html.haml
index bf8613b0f0d..d7740eddcca 100644
--- a/app/views/shared/issuable/form/_merge_request_assignee.html.haml
+++ b/app/views/shared/issuable/form/_merge_request_assignee.html.haml
@@ -1,6 +1,6 @@
- merge_request = issuable
.block.assignee
- .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (merge_request.assignee.name if merge_request.assignee) }
+ .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: sidebar_assignee_tooltip_label(issuable) }
- if merge_request.assignee
= link_to_member(@project, merge_request.assignee, size: 24)
- else
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index ee134480705..8e9a1b56bb8 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -4,12 +4,8 @@
%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar.milestone-sidebar
.block.milestone-progress.issuable-sidebar-header
- %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
+ %a.gutter-toggle.pull-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left' } }
= sidebar_gutter_toggle_icon
-
- .sidebar-collapsed-icon
- %span== #{milestone.percent_complete(current_user)}%
- = milestone_progress_bar(milestone)
.title.hide-collapsed
%strong.bold== #{milestone.percent_complete(current_user)}%
%span.hide-collapsed
@@ -17,6 +13,11 @@
.value.hide-collapsed
= milestone_progress_bar(milestone)
+ .block.milestone-progress.hide-expanded
+ .sidebar-collapsed-icon.has-tooltip{ title: milestone_progress_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } }
+ %span== #{milestone.percent_complete(current_user)}%
+ = milestone_progress_bar(milestone)
+
.block.start_date.hide-collapsed
.title
Start date
@@ -35,19 +36,25 @@
%span.collapsed-milestone-date
- if milestone.start_date && milestone.due_date
- if milestone.start_date.year == milestone.due_date.year
- .milestone-date= milestone.start_date.strftime('%b %-d')
+ .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
+ = milestone.start_date.strftime('%b %-d')
- else
- .milestone-date= milestone.start_date.strftime('%b %-d %Y')
+ .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
+ = milestone.start_date.strftime('%b %-d %Y')
.date-separator -
- .due_date= milestone.due_date.strftime('%b %-d %Y')
+ .due_date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 1, placement: 'left' } }
+ = milestone.due_date.strftime('%b %-d %Y')
- elsif milestone.start_date
From
- .milestone-date= milestone.start_date.strftime('%b %-d %Y')
+ .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
+ = milestone.start_date.strftime('%b %-d %Y')
- elsif milestone.due_date
Until
- .milestone-date= milestone.due_date.strftime('%b %-d %Y')
+ .milestone-date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 1, placement: 'left' } }
+ = milestone.due_date.strftime('%b %-d %Y')
- else
- None
+ .has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
+ None
.title.hide-collapsed
Due date
- if @project && can?(current_user, :admin_milestone, @project)
@@ -58,14 +65,14 @@
%span.bold= milestone.due_date.to_s(:medium)
- else
%span.no-value No due date
- - remaining_days = milestone_remaining_days(milestone)
+ - remaining_days = remaining_days_in_words(milestone)
- if remaining_days.present?
= surround '(', ')' do
%span.remaining-days= remaining_days
- if !project || can?(current_user, :read_issue, project)
.block.issues
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.has-tooltip{ title: milestone_issues_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } }
%strong
= custom_icon('issues')
%span= milestone.issues_visible_to_user(current_user).count
@@ -93,7 +100,7 @@
= icon('spinner spin')
.block.merge-requests
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } }
%strong
= custom_icon('mr_bold')
%span= milestone.merge_requests.count
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 9a11cdb121e..9aea3bad27b 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -18,6 +18,7 @@
- cronjob:stuck_import_jobs
- cronjob:stuck_merge_jobs
- cronjob:trending_projects
+- cronjob:issue_due_scheduler
- gcp_cluster:cluster_install_app
- gcp_cluster:cluster_provision
@@ -39,6 +40,8 @@
- github_importer:github_import_stage_import_pull_requests
- github_importer:github_import_stage_import_repository
+- mail_scheduler:mail_scheduler_issue_due
+
- object_storage_upload
- object_storage:object_storage_background_move
- object_storage:object_storage_migrate_uploads
diff --git a/app/workers/concerns/mail_scheduler_queue.rb b/app/workers/concerns/mail_scheduler_queue.rb
new file mode 100644
index 00000000000..9df55ad9522
--- /dev/null
+++ b/app/workers/concerns/mail_scheduler_queue.rb
@@ -0,0 +1,7 @@
+module MailSchedulerQueue
+ extend ActiveSupport::Concern
+
+ included do
+ queue_namespace :mail_scheduler
+ end
+end
diff --git a/app/workers/issue_due_scheduler_worker.rb b/app/workers/issue_due_scheduler_worker.rb
new file mode 100644
index 00000000000..16ab5d069e0
--- /dev/null
+++ b/app/workers/issue_due_scheduler_worker.rb
@@ -0,0 +1,10 @@
+class IssueDueSchedulerWorker
+ include ApplicationWorker
+ include CronjobQueue
+
+ def perform
+ project_ids = Issue.opened.due_tomorrow.group(:project_id).pluck(:project_id).map { |id| [id] }
+
+ MailScheduler::IssueDueWorker.bulk_perform_async(project_ids)
+ end
+end
diff --git a/app/workers/mail_scheduler/issue_due_worker.rb b/app/workers/mail_scheduler/issue_due_worker.rb
new file mode 100644
index 00000000000..b06079d68ca
--- /dev/null
+++ b/app/workers/mail_scheduler/issue_due_worker.rb
@@ -0,0 +1,14 @@
+module MailScheduler
+ class IssueDueWorker
+ include ApplicationWorker
+ include MailSchedulerQueue
+
+ def perform(project_id)
+ notification_service = NotificationService.new
+
+ Issue.opened.due_tomorrow.in_projects(project_id).preload(:project).find_each do |issue|
+ notification_service.issue_due(issue)
+ end
+ end
+ end
+end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 3909dbf7d7f..f88b3fdbfb1 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -33,7 +33,7 @@ class PostReceive
unless @user
log("Triggered hook for non-existing user \"#{post_received.identifier}\"")
- return false
+ return false # rubocop:disable Cop/AvoidReturnFromBlocks
end
if Gitlab::Git.tag_ref?(ref)
diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb
index fb26fa4c515..7ebf69bdc39 100644
--- a/app/workers/stuck_ci_jobs_worker.rb
+++ b/app/workers/stuck_ci_jobs_worker.rb
@@ -38,7 +38,7 @@ class StuckCiJobsWorker
def drop_stuck(status, timeout)
search(status, timeout) do |build|
- return unless build.stuck?
+ break unless build.stuck?
drop_build :stuck, build, status, timeout
end
diff --git a/bin/secpick b/bin/secpick
new file mode 100755
index 00000000000..76ae231e913
--- /dev/null
+++ b/bin/secpick
@@ -0,0 +1,47 @@
+#!/usr/bin/env ruby
+require 'optparse'
+require 'open3'
+require 'rainbow/refinement'
+using Rainbow
+
+BRANCH_PREFIX = 'security'.freeze
+STABLE_BRANCH_SUFFIX = 'stable'.freeze
+REMOTE = 'dev'.freeze
+
+options = { version: nil, branch: nil, sha: nil }
+
+parser = OptionParser.new do |opts|
+ opts.banner = "Usage: #{$0} [options]"
+ opts.on('-v', '--version 10.0', 'Version') do |version|
+ options[:version] = version&.tr('.', '-')
+ end
+
+ opts.on('-b', '--branch security-fix-branch', 'Original branch name') do |branch|
+ options[:branch] = branch
+ end
+
+ opts.on('-s', '--sha abcd', 'SHA to cherry pick') do |sha|
+ options[:sha] = sha
+ end
+
+ opts.on('-h', '--help', 'Displays Help') do
+ puts opts
+
+ exit
+ end
+end
+
+parser.parse!
+
+abort("Missing options. Use #{$0} --help to see the list of options available".red) if options.values.include?(nil)
+abort("Wrong version format #{options[:version].bold}".red) unless options[:version] =~ /\A\d*\-\d*\Z/
+
+branch = [BRANCH_PREFIX, options[:branch], options[:version]].join('-').freeze
+stable_branch = "#{options[:version]}-#{STABLE_BRANCH_SUFFIX}".freeze
+
+command = "git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch}"
+
+_stdin, stdout, stderr = Open3.popen3(command)
+
+puts stdout.read&.green
+puts stderr.read&.red
diff --git a/changelogs/unreleased/16957-issue-due-email.yml b/changelogs/unreleased/16957-issue-due-email.yml
new file mode 100644
index 00000000000..83944ca4f73
--- /dev/null
+++ b/changelogs/unreleased/16957-issue-due-email.yml
@@ -0,0 +1,5 @@
+---
+title: Add cron job to email users on issue due date
+merge_request: 17985
+author: Stuart Nelson
+type: added
diff --git a/changelogs/unreleased/21677-run-pipeline-word.yml b/changelogs/unreleased/21677-run-pipeline-word.yml
new file mode 100644
index 00000000000..9cc280244e4
--- /dev/null
+++ b/changelogs/unreleased/21677-run-pipeline-word.yml
@@ -0,0 +1,5 @@
+---
+title: Improves wording in new pipeline page
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/25010-collapsed-sidebar-tooltips.yml b/changelogs/unreleased/25010-collapsed-sidebar-tooltips.yml
new file mode 100644
index 00000000000..1226fb4eefd
--- /dev/null
+++ b/changelogs/unreleased/25010-collapsed-sidebar-tooltips.yml
@@ -0,0 +1,5 @@
+---
+title: Improve tooltips in collapsed right sidebar
+merge_request: 17714
+author:
+type: changed
diff --git a/changelogs/unreleased/34262-show-current-labels-when-editing.yml b/changelogs/unreleased/34262-show-current-labels-when-editing.yml
new file mode 100644
index 00000000000..d3b15b9ddd1
--- /dev/null
+++ b/changelogs/unreleased/34262-show-current-labels-when-editing.yml
@@ -0,0 +1,5 @@
+---
+title: Keep current labels visible when editing them in the sidebar
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml b/changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml
new file mode 100644
index 00000000000..e47577f9058
--- /dev/null
+++ b/changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml
@@ -0,0 +1,5 @@
+---
+title: Add a comma to the time estimate system notes
+merge_request: 18326
+author:
+type: changed
diff --git a/changelogs/unreleased/41059-calculate-artifact-size-more-efficiently.yml b/changelogs/unreleased/41059-calculate-artifact-size-more-efficiently.yml
new file mode 100644
index 00000000000..e3f94bbf081
--- /dev/null
+++ b/changelogs/unreleased/41059-calculate-artifact-size-more-efficiently.yml
@@ -0,0 +1,5 @@
+---
+title: Improve DB performance of calculating total artifacts size
+merge_request: 17839
+author:
+type: performance
diff --git a/changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml b/changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml
new file mode 100644
index 00000000000..7452a264bfd
--- /dev/null
+++ b/changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml
@@ -0,0 +1,5 @@
+---
+title: Remove ahead/behind graphs on project branches on mobile
+merge_request: 18415
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/42889-avoid-return-inside-block.yml b/changelogs/unreleased/42889-avoid-return-inside-block.yml
new file mode 100644
index 00000000000..e3e1341ddcc
--- /dev/null
+++ b/changelogs/unreleased/42889-avoid-return-inside-block.yml
@@ -0,0 +1,5 @@
+---
+title: Rubocop rule to avoid returning from a block
+merge_request: 18000
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/43404-pipelines-commit.yml b/changelogs/unreleased/43404-pipelines-commit.yml
new file mode 100644
index 00000000000..0b9a4a6451f
--- /dev/null
+++ b/changelogs/unreleased/43404-pipelines-commit.yml
@@ -0,0 +1,5 @@
+---
+title: Breaks commit not found message in pipelines table
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/43567-replace-gke.yml b/changelogs/unreleased/43567-replace-gke.yml
new file mode 100644
index 00000000000..8ec79fc3d4d
--- /dev/null
+++ b/changelogs/unreleased/43567-replace-gke.yml
@@ -0,0 +1,5 @@
+---
+title: Replace GKE acronym with Google Kubernetes Engine
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/43617-mailsig.yml b/changelogs/unreleased/43617-mailsig.yml
new file mode 100644
index 00000000000..2c7568e32ca
--- /dev/null
+++ b/changelogs/unreleased/43617-mailsig.yml
@@ -0,0 +1,5 @@
+---
+title: Use RFC 3676 mail signature delimiters
+merge_request: 17979
+author: Enrico Scholz
+type: changed
diff --git a/changelogs/unreleased/44834-ide-remove-branch-from-bottom-bar.yml b/changelogs/unreleased/44834-ide-remove-branch-from-bottom-bar.yml
new file mode 100644
index 00000000000..d3f838ad362
--- /dev/null
+++ b/changelogs/unreleased/44834-ide-remove-branch-from-bottom-bar.yml
@@ -0,0 +1,5 @@
+---
+title: Remove branch name from the status bar of WebIDE
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/44981-http-io-trace-with-multi-byte-char.yml b/changelogs/unreleased/44981-http-io-trace-with-multi-byte-char.yml
new file mode 100644
index 00000000000..64a17990ebc
--- /dev/null
+++ b/changelogs/unreleased/44981-http-io-trace-with-multi-byte-char.yml
@@ -0,0 +1,5 @@
+---
+title: Fix `Trace::HttpIO` can not render multi-byte chars
+merge_request: 18417
+author:
+type: fixed
diff --git a/changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml b/changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml
new file mode 100644
index 00000000000..4af2af2a561
--- /dev/null
+++ b/changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml
@@ -0,0 +1,5 @@
+---
+title: Fix confirmation modal for deleting a protected branch
+merge_request: 18176
+author: Paul Bonaud @PaulRbR
+type: fixed
diff --git a/changelogs/unreleased/45271-collpased-diff-loading.yml b/changelogs/unreleased/45271-collpased-diff-loading.yml
new file mode 100644
index 00000000000..fdd13a82a4c
--- /dev/null
+++ b/changelogs/unreleased/45271-collpased-diff-loading.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes unresolved discussions rendering the error state instead of the diff
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/45363-optional-params-on-api-endpoint-produce-invalid-pagination-header-links.yml b/changelogs/unreleased/45363-optional-params-on-api-endpoint-produce-invalid-pagination-header-links.yml
new file mode 100644
index 00000000000..963ec893963
--- /dev/null
+++ b/changelogs/unreleased/45363-optional-params-on-api-endpoint-produce-invalid-pagination-header-links.yml
@@ -0,0 +1,6 @@
+---
+title: '[API] Fix URLs in the `Link` header for `GET /projects/:id/repository/contributors`
+ when no value is passed for `order_by` or `sort`'
+merge_request: 18393
+author:
+type: fixed
diff --git a/changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml b/changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml
new file mode 100644
index 00000000000..0f1d111ca58
--- /dev/null
+++ b/changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml
@@ -0,0 +1,5 @@
+---
+title: Fix undefined `html_escape` method during markdown rendering
+merge_request: 18418
+author:
+type: fixed
diff --git a/changelogs/unreleased/45476-geo-statement-timeout-counting-local-job-artifacts.yml b/changelogs/unreleased/45476-geo-statement-timeout-counting-local-job-artifacts.yml
new file mode 100644
index 00000000000..763d3a28200
--- /dev/null
+++ b/changelogs/unreleased/45476-geo-statement-timeout-counting-local-job-artifacts.yml
@@ -0,0 +1,5 @@
+---
+title: Add index to file_store on ci_job_artifacts
+merge_request: 18444
+author:
+type: performance
diff --git a/changelogs/unreleased/45507-fix-repository-archive-url.yml b/changelogs/unreleased/45507-fix-repository-archive-url.yml
new file mode 100644
index 00000000000..548c9c38689
--- /dev/null
+++ b/changelogs/unreleased/45507-fix-repository-archive-url.yml
@@ -0,0 +1,6 @@
+---
+title: Fix specifying a non-default ref when requesting an archive using the legacy
+ URL
+merge_request: 18468
+author:
+type: fixed
diff --git a/changelogs/unreleased/bvl-shared-groups-on-group-page.yml b/changelogs/unreleased/bvl-shared-groups-on-group-page.yml
new file mode 100644
index 00000000000..6c0703fd138
--- /dev/null
+++ b/changelogs/unreleased/bvl-shared-groups-on-group-page.yml
@@ -0,0 +1,5 @@
+---
+title: Show shared projects on group page
+merge_request: 18390
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-direct-upload-for-old-records.yml b/changelogs/unreleased/fix-direct-upload-for-old-records.yml
new file mode 100644
index 00000000000..a062b9e73e9
--- /dev/null
+++ b/changelogs/unreleased/fix-direct-upload-for-old-records.yml
@@ -0,0 +1,5 @@
+---
+title: Fix direct_upload when records with null file_store are used
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-gb-fix-empty-secret-variables.yml b/changelogs/unreleased/fix-gb-fix-empty-secret-variables.yml
new file mode 100644
index 00000000000..94010c06a07
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-fix-empty-secret-variables.yml
@@ -0,0 +1,5 @@
+---
+title: Fix a case with secret variables being empty sometimes
+merge_request: 18400
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-n-plus-one-when-getting-notification-settings-for-recipients.yml b/changelogs/unreleased/fix-n-plus-one-when-getting-notification-settings-for-recipients.yml
deleted file mode 100644
index 837bfed19b7..00000000000
--- a/changelogs/unreleased/fix-n-plus-one-when-getting-notification-settings-for-recipients.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Goldiloader to fix N+1 issues when calculating email recipients
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/fj-42354-custom-hooks-not-triggered-by-UI-wiki-edit.yml b/changelogs/unreleased/fj-42354-custom-hooks-not-triggered-by-UI-wiki-edit.yml
new file mode 100644
index 00000000000..9fe458aba4a
--- /dev/null
+++ b/changelogs/unreleased/fj-42354-custom-hooks-not-triggered-by-UI-wiki-edit.yml
@@ -0,0 +1,5 @@
+---
+title: Triggering custom hooks by Wiki UI edit
+merge_request: 18251
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-42601-respect-visibility-options.yml b/changelogs/unreleased/fj-42601-respect-visibility-options.yml
new file mode 100644
index 00000000000..eabb337234c
--- /dev/null
+++ b/changelogs/unreleased/fj-42601-respect-visibility-options.yml
@@ -0,0 +1,5 @@
+---
+title: Respect visibility options and description when importing project from template
+merge_request: 18473
+author:
+type: fixed
diff --git a/changelogs/unreleased/ide-tree-loading-fix.yml b/changelogs/unreleased/ide-tree-loading-fix.yml
new file mode 100644
index 00000000000..2fb43380a48
--- /dev/null
+++ b/changelogs/unreleased/ide-tree-loading-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed IDE not showing loading state when tree is loading
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/improve-jobs-queuing-time-metric.yml b/changelogs/unreleased/improve-jobs-queuing-time-metric.yml
new file mode 100644
index 00000000000..cee8b8523fd
--- /dev/null
+++ b/changelogs/unreleased/improve-jobs-queuing-time-metric.yml
@@ -0,0 +1,5 @@
+---
+title: Partition job_queue_duration_seconds with jobs_running_for_project
+merge_request: 17730
+author:
+type: changed
diff --git a/changelogs/unreleased/label-links-on-project-transfer.yml b/changelogs/unreleased/label-links-on-project-transfer.yml
new file mode 100644
index 00000000000..fabedb77cb3
--- /dev/null
+++ b/changelogs/unreleased/label-links-on-project-transfer.yml
@@ -0,0 +1,5 @@
+---
+title: Fix label links update on project transfer
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/move-pipeline-failed-vue-component.yml b/changelogs/unreleased/move-pipeline-failed-vue-component.yml
new file mode 100644
index 00000000000..38d42134876
--- /dev/null
+++ b/changelogs/unreleased/move-pipeline-failed-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Move PipelineFailed vue component
+merge_request: 18277
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/sh-fix-award-emoji-nplus-one-participants.yml b/changelogs/unreleased/sh-fix-award-emoji-nplus-one-participants.yml
new file mode 100644
index 00000000000..aee26f9824a
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-award-emoji-nplus-one-participants.yml
@@ -0,0 +1,5 @@
+---
+title: Fix N+1 queries when loading participants for a commit note
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-memoize-repository-empty.yml b/changelogs/unreleased/sh-memoize-repository-empty.yml
new file mode 100644
index 00000000000..64db3ca0371
--- /dev/null
+++ b/changelogs/unreleased/sh-memoize-repository-empty.yml
@@ -0,0 +1,5 @@
+---
+title: Memoize Git::Repository#has_visible_content?
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/zj-ref-exists-opt-out.yml b/changelogs/unreleased/zj-ref-exists-opt-out.yml
new file mode 100644
index 00000000000..cdffecb0d0a
--- /dev/null
+++ b/changelogs/unreleased/zj-ref-exists-opt-out.yml
@@ -0,0 +1,5 @@
+---
+title: Check if a ref exists is done by Gitaly by default
+merge_request:
+author:
+type: performance
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index acf7754abe6..dc7999ac556 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -455,6 +455,10 @@ Settings.cron_jobs['pages_domain_verification_cron_worker'] ||= Settingslogic.ne
Settings.cron_jobs['pages_domain_verification_cron_worker']['cron'] ||= '*/15 * * * *'
Settings.cron_jobs['pages_domain_verification_cron_worker']['job_class'] = 'PagesDomainVerificationCronWorker'
+Settings.cron_jobs['issue_due_scheduler_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['issue_due_scheduler_worker']['cron'] ||= '50 00 * * *'
+Settings.cron_jobs['issue_due_scheduler_worker']['job_class'] = 'IssueDueSchedulerWorker'
+
#
# Sidekiq
#
diff --git a/config/initializers/active_record_avoid_type_casting_in_uniqueness_validator.rb b/config/initializers/active_record_avoid_type_casting_in_uniqueness_validator.rb
new file mode 100644
index 00000000000..d9418caf68b
--- /dev/null
+++ b/config/initializers/active_record_avoid_type_casting_in_uniqueness_validator.rb
@@ -0,0 +1,98 @@
+# This is a monkey patch which must be removed when migrating to Rails 5.1 from 5.0.
+#
+# In Rails 5.0 there was introduced a bug which casts types in the uniqueness validator.
+# https://github.com/rails/rails/pull/23523/commits/811a4fa8eb6ceea841e61e8ac05747ffb69595ae
+#
+# That causes to bugs like this:
+#
+# 1) API::Users POST /user/:id/gpg_keys/:key_id/revoke when authenticated revokes existing key
+# Failure/Error: let(:gpg_key) { create(:gpg_key, user: user) }
+#
+# TypeError:
+# can't cast Hash
+# # ./spec/requests/api/users_spec.rb:7:in `block (2 levels) in <top (required)>'
+# # ./spec/requests/api/users_spec.rb:908:in `block (4 levels) in <top (required)>'
+# # ------------------
+# # --- Caused by: ---
+# # TypeError:
+# # TypeError
+# # ./spec/requests/api/users_spec.rb:7:in `block (2 levels) in <top (required)>'
+#
+# This bug was fixed in Rails 5.1 by https://github.com/rails/rails/pull/24745/commits/aa062318c451512035c10898a1af95943b1a3803
+
+if Gitlab.rails5?
+ ActiveSupport::Deprecation.warn("#{__FILE__} is a monkey patch which must be removed when upgrading to Rails 5.1")
+
+ if Rails.version.start_with?("5.1")
+ raise "Remove this monkey patch: #{__FILE__}"
+ end
+
+ # Copy-paste from https://github.com/kamipo/rails/blob/aa062318c451512035c10898a1af95943b1a3803/activerecord/lib/active_record/validations/uniqueness.rb
+ # including local fixes to make Rubocop happy again.
+ module ActiveRecord
+ module Validations
+ class UniquenessValidator < ActiveModel::EachValidator # :nodoc:
+ def validate_each(record, attribute, value)
+ finder_class = find_finder_class_for(record)
+ table = finder_class.arel_table
+ value = map_enum_attribute(finder_class, attribute, value)
+
+ relation = build_relation(finder_class, table, attribute, value)
+
+ if record.persisted?
+ if finder_class.primary_key
+ relation = relation.where.not(finder_class.primary_key => record.id_was || record.id)
+ else
+ raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.")
+ end
+ end
+
+ relation = scope_relation(record, table, relation)
+ relation = relation.merge(options[:conditions]) if options[:conditions]
+
+ if relation.exists?
+ error_options = options.except(:case_sensitive, :scope, :conditions)
+ error_options[:value] = value
+
+ record.errors.add(attribute, :taken, error_options)
+ end
+ rescue RangeError
+ end
+
+ protected
+
+ def build_relation(klass, table, attribute, value) #:nodoc:
+ if reflection = klass._reflect_on_association(attribute)
+ attribute = reflection.foreign_key
+ value = value.attributes[reflection.klass.primary_key] unless value.nil?
+ end
+
+ # the attribute may be an aliased attribute
+ if klass.attribute_alias?(attribute)
+ attribute = klass.attribute_alias(attribute)
+ end
+
+ attribute_name = attribute.to_s
+
+ column = klass.columns_hash[attribute_name]
+ cast_type = klass.type_for_attribute(attribute_name)
+
+ comparison =
+ if !options[:case_sensitive] && !value.nil?
+ # will use SQL LOWER function before comparison, unless it detects a case insensitive collation
+ klass.connection.case_insensitive_comparison(table, attribute, column, value)
+ else
+ klass.connection.case_sensitive_comparison(table, attribute, column, value)
+ end
+
+ if value.nil?
+ klass.unscoped.where(comparison)
+ else
+ bind = Relation::QueryAttribute.new(attribute_name, value, cast_type)
+ klass.unscoped.where(comparison, bind)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/config/initializers/forbid_sidekiq_in_transactions.rb b/config/initializers/forbid_sidekiq_in_transactions.rb
index 4cf1d455eb4..4603123665d 100644
--- a/config/initializers/forbid_sidekiq_in_transactions.rb
+++ b/config/initializers/forbid_sidekiq_in_transactions.rb
@@ -27,16 +27,8 @@ module Sidekiq
Use an `after_commit` hook, or include `AfterCommitQueue` and use a `run_after_commit` block instead.
MSG
rescue Sidekiq::Worker::EnqueueFromTransactionError => e
- if Rails.env.production?
- Rails.logger.error(e.message)
-
- if Gitlab::Sentry.enabled?
- Gitlab::Sentry.context
- Raven.capture_exception(e)
- end
- else
- raise
- end
+ Rails.logger.error(e.message) if Rails.env.production?
+ Gitlab::Sentry.track_exception(e)
end
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index c811034b29d..47fbbed44cf 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -34,6 +34,7 @@
- [email_receiver, 2]
- [emails_on_push, 2]
- [mailers, 2]
+ - [mail_scheduler, 2]
- [invalid_gpg_signature_update, 2]
- [create_gpg_signature, 2]
- [rebase, 2]
diff --git a/db/migrate/20180330121048_add_issue_due_to_notification_settings.rb b/db/migrate/20180330121048_add_issue_due_to_notification_settings.rb
new file mode 100644
index 00000000000..c64a481fcf0
--- /dev/null
+++ b/db/migrate/20180330121048_add_issue_due_to_notification_settings.rb
@@ -0,0 +1,9 @@
+class AddIssueDueToNotificationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :notification_settings, :issue_due, :boolean
+ end
+end
diff --git a/db/migrate/20180418053107_add_index_to_ci_job_artifacts_file_store.rb b/db/migrate/20180418053107_add_index_to_ci_job_artifacts_file_store.rb
new file mode 100644
index 00000000000..1084ca14a34
--- /dev/null
+++ b/db/migrate/20180418053107_add_index_to_ci_job_artifacts_file_store.rb
@@ -0,0 +1,15 @@
+class AddIndexToCiJobArtifactsFileStore < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_job_artifacts, :file_store
+ end
+
+ def down
+ remove_index :ci_job_artifacts, :file_store if index_exists?(:ci_job_artifacts, :file_store)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index fd75b176318..1d272bdb779 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20180405142733) do
+ActiveRecord::Schema.define(version: 20180418053107) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -367,6 +367,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
end
add_index "ci_job_artifacts", ["expire_at", "job_id"], name: "index_ci_job_artifacts_on_expire_at_and_job_id", using: :btree
+ add_index "ci_job_artifacts", ["file_store"], name: "index_ci_job_artifacts_on_file_store", using: :btree
add_index "ci_job_artifacts", ["job_id", "file_type"], name: "index_ci_job_artifacts_on_job_id_and_file_type", unique: true, using: :btree
add_index "ci_job_artifacts", ["project_id"], name: "index_ci_job_artifacts_on_project_id", using: :btree
@@ -1325,6 +1326,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
t.boolean "failed_pipeline"
t.boolean "success_pipeline"
t.boolean "push_to_merge_request"
+ t.boolean "issue_due"
end
add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree
diff --git a/doc/README.md b/doc/README.md
index 080341e4f4f..a841a4cfbf1 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -80,6 +80,7 @@ on projects and code.
- [Search through GitLab](user/search/index.md): Search for issues, merge requests, projects, groups, todos, and issues in Issue Boards.
- [Snippets](user/snippets.md): Snippets allow you to create little bits of code.
- [Wikis](user/project/wiki/index.md): Enhance your repository documentation with built-in wikis.
+- [Web IDE](user/project/web_ide/index.md)
#### Repositories
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 0906821d6a3..b472ca5b4d8 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -1,4 +1,4 @@
-# Administrator documentation
+# Administrator documentation **[CORE ONLY]**
Learn how to administer your GitLab instance (Community Edition and
Enterprise Edition).
diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md
index f05ae647577..682b90361bd 100644
--- a/doc/api/notification_settings.md
+++ b/doc/api/notification_settings.md
@@ -23,6 +23,7 @@ new_issue
reopen_issue
close_issue
reassign_issue
+issue_due
new_merge_request
push_to_merge_request
reopen_merge_request
@@ -75,6 +76,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `reopen_issue` | boolean | no | Enable/disable this notification |
| `close_issue` | boolean | no | Enable/disable this notification |
| `reassign_issue` | boolean | no | Enable/disable this notification |
+| `issue_due` | boolean | no | Enable/disable this notification |
| `new_merge_request` | boolean | no | Enable/disable this notification |
| `push_to_merge_request` | boolean | no | Enable/disable this notification |
| `reopen_merge_request` | boolean | no | Enable/disable this notification |
@@ -142,6 +144,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `reopen_issue` | boolean | no | Enable/disable this notification |
| `close_issue` | boolean | no | Enable/disable this notification |
| `reassign_issue` | boolean | no | Enable/disable this notification |
+| `issue_due` | boolean | no | Enable/disable this notification |
| `new_merge_request` | boolean | no | Enable/disable this notification |
| `push_to_merge_request` | boolean | no | Enable/disable this notification |
| `reopen_merge_request` | boolean | no | Enable/disable this notification |
@@ -166,6 +169,7 @@ Example responses:
"reopen_issue": false,
"close_issue": false,
"reassign_issue": false,
+ "issue_due": false,
"new_merge_request": false,
"push_to_merge_request": false,
"reopen_merge_request": false,
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index 96609cd530f..5aff255c20a 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -183,7 +183,7 @@ GET /projects/:id/repository/contributors
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
-- `order_by` (optional) - Return contributors ordered by `name`, `email`, or `commits` fields. If not given contributors are ordered by commit date.
+- `order_by` (optional) - Return contributors ordered by `name`, `email`, or `commits` (orders by commit date) fields. Default is `commits`
- `sort` (optional) - Return contributors sorted in `asc` or `desc` order. Default is `asc`
Response:
diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md
index 92317c77427..d1aa783cc9c 100644
--- a/doc/ci/examples/code_climate.md
+++ b/doc/ci/examples/code_climate.md
@@ -17,7 +17,11 @@ codequality:
- docker:stable-dind
script:
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- - docker run --env SOURCE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
+ - docker run
+ --env SOURCE_CODE="$PWD"
+ --volume "$PWD":/code
+ --volume /var/run/docker.sock:/var/run/docker.sock
+ "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
artifacts:
paths: [codeclimate.json]
```
diff --git a/doc/ci/examples/container_scanning.md b/doc/ci/examples/container_scanning.md
index dc34f4acd75..eb76521cc02 100644
--- a/doc/ci/examples/container_scanning.md
+++ b/doc/ci/examples/container_scanning.md
@@ -31,6 +31,9 @@ sast:container:
- chmod +x clair-scanner
- touch clair-whitelist.yml
- while( ! wget -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; done
+ - retries=0
+ - echo "Waiting for clair daemon to start"
+ - while( ! wget -T 10 -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done
- ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-sast-container-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true
artifacts:
paths: [gl-sast-container-report.json]
diff --git a/doc/ci/examples/dast.md b/doc/ci/examples/dast.md
index 8df223ee560..a8720f0b7ea 100644
--- a/doc/ci/examples/dast.md
+++ b/doc/ci/examples/dast.md
@@ -42,9 +42,9 @@ dast:
allow_failure: true
script:
- mkdir /zap/wrk/
- - /zap/zap-baseline.py -J gl-dast-report.json -t $website \
- --auth-url $login_url \
- --auth-username "john.doe@example.com" \
+ - /zap/zap-baseline.py -J gl-dast-report.json -t $website
+ --auth-url $login_url
+ --auth-username "john.doe@example.com"
--auth-password "john-doe-password" || true
- cp /zap/wrk/gl-dast-report.json .
artifacts:
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index f14e0527998..b16cbc61d14 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -75,7 +75,7 @@ cancel the job, retry it, or erase the job trace.
## Seeing the failure reason for jobs
-> [Introduced][ce-5742] in GitLab 10.7.
+> [Introduced][ce-17782] in GitLab 10.7.
When a pipeline fails or is allowed to fail, there are several places where you
can quickly check the reason it failed:
@@ -88,6 +88,8 @@ In any case, if you hover over the failed job you can see the reason it failed.
![Pipeline detail](img/job_failure_reason.png)
+From [GitLab 10.8][ce-17814] you can also see the reason it failed on the Job detail page.
+
## Pipeline graphs
> [Introduced][ce-5742] in GitLab 8.11.
@@ -279,4 +281,5 @@ runners will not use regular runners, they must be tagged accordingly.
[ce-7931]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7931
[ce-9760]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9760
[ce-17782]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17782
+[ce-17814]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17814
[regexp]: https://gitlab.com/gitlab-org/gitlab-ce/blob/2f3dc314f42dbd79813e6251792853bc231e69dd/app/models/commit_status.rb#L99
diff --git a/doc/development/background_migrations.md b/doc/development/background_migrations.md
index fc1b202b5eb..ce69694ab6a 100644
--- a/doc/development/background_migrations.md
+++ b/doc/development/background_migrations.md
@@ -133,11 +133,19 @@ roughly be as follows:
1. Release B:
1. Deploy code so that the application starts using the new column and stops
scheduling jobs for newly created data.
- 1. In a post-deployment migration you'll need to ensure no jobs remain. To do
- so you can use `Gitlab::BackgroundMigration.steal` to process any remaining
- jobs before continuing.
+ 1. In a post-deployment migration you'll need to ensure no jobs remain.
+ 1. Use `Gitlab::BackgroundMigration.steal` to process any remaining
+ jobs in Sidekiq.
+ 1. Reschedule the migration to be run directly (i.e. not through Sidekiq)
+ on any rows that weren't migrated by Sidekiq. This can happen if, for
+ instance, Sidekiq received a SIGKILL, or if a particular batch failed
+ enough times to be marked as dead.
1. Remove the old column.
+This may also require a bump to the [import/export version][import-export], if
+importing a project from a prior version of GitLab requires the data to be in
+the new format.
+
## Example
To explain all this, let's use the following example: the table `services` has a
@@ -296,3 +304,4 @@ for more details.
[migrations-readme]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md
[issue-rspec-hooks]: https://gitlab.com/gitlab-org/gitlab-ce/issues/35351
[reliable-sidekiq]: https://gitlab.com/gitlab-org/gitlab-ce/issues/36791
+[import-export]: ../user/project/settings/import_export.md
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 41e3412c7ff..0550ea527cb 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -157,6 +157,39 @@ below.
Otherwise, leave this mention out.
+### Product badges
+
+When a feature is available in EE-only tiers, add the corresponding tier according to the
+feature availability:
+
+- For GitLab Starter and GitLab.com Bronze: `**[STARTER]**`
+- For GitLab Premium and GitLab.com Silver: `**[PREMIUM]**`
+- For GitLab Ultimate and GitLab.com Gold: `**[ULTIMATE]**`
+- For GitLab Core and GitLab.com Free: `**[CORE]**`
+
+To exclude GitLab.com tiers (when the feature is not available in GitLab.com), add the
+keyword "only":
+
+- For GitLab Starter: `**[STARTER ONLY]**`
+- For GitLab Premium: `**[PREMIUM ONLY]**`
+- For GitLab Ultimate: `**[ULTIMATE ONLY]**`
+- For GitLab Core: `**[CORE ONLY]**`
+
+The tier should be ideally added to headers, so that the full badge will be displayed.
+But it can be also mentioned from paragraphs, list items, and table cells. For these cases,
+the tier mention will be represented by an orange question mark.
+E.g., `**[STARTER]**` renders **[STARTER]**, `**[STARTER ONLY]**` renders **[STARTER ONLY]**.
+
+The absence of tiers' mentions mean that the feature is available in GitLab Core,
+GitLab.com Free, and higher tiers.
+
+#### How it works
+
+Introduced by [!244](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/244),
+the special markup `**[STARTER]**` will generate a `span` element to trigger the
+badges and tooltips (`<span class="badge-trigger starter">`). When the keyword
+"only" is added, the corresponding GitLab.com badge will not be displayed.
+
### GitLab Restart
There are many cases that a restart/reconfigure of GitLab is required. To
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index 944b56a65f1..9c4b0e86351 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -524,7 +524,7 @@ export default new Vuex.Store({
_Note:_ If the state of the application is too complex, an individual file for the state may be better.
##### `actions.js`
-An action commits a mutatation. In this file, we will write the actions that will call the respective mutation:
+An action commits a mutation. In this file, we will write the actions that will commit the respective mutation:
```javascript
import * as types from './mutation_types';
@@ -661,7 +661,7 @@ describe('component', () => {
};
// populate the store
- store.dipatch('addUser', user);
+ store.dispatch('addUser', user);
vm = new Component({
store,
diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index cf62314bc29..5185d843ccb 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -23,6 +23,7 @@ are very appreciative of the work done by translators and proofreaders!
- Italian
- Paolo Falomo - [GitLab](https://gitlab.com/paolofalomo), [Crowdin](https://crowdin.com/profile/paolo.falomo)
- Japanese
+ - Yamana Tokiuji - [GitLab](https://gitlab.com/tokiuji), [Crowdin](https://crowdin.com/profile/yamana)
- Korean
- Chang-Ho Cha - [GitLab](https://gitlab.com/changho-cha), [Crowdin](https://crowdin.com/profile/zzazang)
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
diff --git a/doc/install/README.md b/doc/install/README.md
index 9724b56910d..5dadf57ea9a 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -31,8 +31,8 @@ the hardware requirements.
- [Install GitLab on DC/OS](https://mesosphere.com/blog/gitlab-dcos/) via [GitLab-Mesosphere integration](https://about.gitlab.com/2016/09/16/announcing-gitlab-and-mesosphere/)
- [Install GitLab on Azure](azure/index.md)
- [Install GitLab on Google Cloud Platform](google_cloud_platform/index.md)
-- [Install GitLab on Google Container Engine (GKE)](https://about.gitlab.com/2017/01/23/video-tutorial-idea-to-production-on-google-container-engine-gke/): video tutorial on
-the full process of installing GitLab on Google Container Engine (GKE), pushing an application to GitLab, building the app with GitLab CI/CD, and deploying to production.
+- [Install GitLab on Google Kubernetes Engine (GKE)](https://about.gitlab.com/2017/01/23/video-tutorial-idea-to-production-on-google-container-engine-gke/): video tutorial on
+the full process of installing GitLab on Google Kubernetes Engine (GKE), pushing an application to GitLab, building the app with GitLab CI/CD, and deploying to production.
- [Install on AWS](https://about.gitlab.com/aws/)
- _Testing only!_ [DigitalOcean and Docker Machine](digitaloceandocker.md) -
Quickly test any version of GitLab on DigitalOcean using Docker Machine.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 85174b64ff7..fa5bcfa6f07 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -93,9 +93,9 @@ Is the system packaged Git too old? Remove it and compile from source.
# Download and compile from source
cd /tmp
- curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.16.2.tar.gz
- echo '9acc4339b7a2ab484eea69d705923271682b7058015219cf5a7e6ed8dee5b5fb git-2.16.2.tar.gz' | shasum -a256 -c - && tar -xzf git-2.16.2.tar.gz
- cd git-2.16.2/
+ curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.16.3.tar.gz
+ echo 'dda229e9c73f4fbb7d4324e0d993e11311673df03f73b194c554c2e9451e17cd git-2.16.3.tar.gz' | shasum -a256 -c - && tar -xzf git-2.16.3.tar.gz
+ cd git-2.16.3/
./configure
make prefix=/usr/local all
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 88efddbfba8..88f4bb2ee04 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -245,10 +245,7 @@ To enable this feature, navigate to the group settings page. Select
![Checkbox for share with group lock](img/share_with_group_lock.png)
-#### Member Lock
-
-> Available in [GitLab Starter](https://about.gitlab.com/products/) and
-[GitLab.com Bronze](https://about.gitlab.com/gitlab-com/).
+#### Member Lock **[STARTER]**
With **Member Lock** it is possible to lock membership in project to the
level of members in group.
@@ -259,8 +256,8 @@ Learn more about [Member Lock](https://docs.gitlab.com/ee/user/group/index.html#
- **Projects**: view all projects within that group, add members to each project,
access each project's settings, and remove any project from the same screen.
-- **Webhooks**: configure [webhooks](../project/integrations/webhooks.md)
-and [push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html#push-rules) to your group (Push Rules is available in [GitLab Starter](https://about.gitlab.com/products/).)
+- **Webhooks**: configure [webhooks](../project/integrations/webhooks.md) to your group.
+- **Push rules**: configure [push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html#push-rules) to your group. **[STARTER]**
- **Audit Events**: view [Audit Events](https://docs.gitlab.com/ee/administration/audit_events.html#audit-events)
-for the group (GitLab admins only, available in [GitLab Starter][ee]).
+for the group. **[STARTER ONLY]**
- **Pipelines quota**: keep track of the [pipeline quota](../admin_area/settings/continuous_integration.md) for the group
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index a3d53fde399..557375a1da9 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -17,7 +17,7 @@ When you create a project in GitLab, you'll have access to a large number of
- [Issue tracker](issues/index.md): Discuss implementations with your team within issues
- [Issue Boards](issue_board.md): Organize and prioritize your workflow
- - [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards) (**Starter/Premium**): Allow your teams to create their own workflows (Issue Boards) for the same project
+ - [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards): Allow your teams to create their own workflows (Issue Boards) for the same project **[STARTER]**
- [Repositories](repository/index.md): Host your code in a fully
integrated platform
- [Branches](repository/branches/index.md): use Git branching strategies to
@@ -30,8 +30,8 @@ integrated platform
- [Deploy tokens](deploy_tokens/index.md): Manage project-based deploy tokens that allow permanent access to the repository and Container Registry.
- [Merge Requests](merge_requests/index.md): Apply your branching
strategy and get reviewed by your team
- - [Merge Request Approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) (**Starter/Premium**): Ask for approval before
- implementing a change
+ - [Merge Request Approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html): Ask for approval before
+ implementing a change **[STARTER]**
- [Fix merge conflicts from the UI](merge_requests/resolve_conflicts.md):
Your Git diff tool right from GitLab's UI
- [Review Apps](../../ci/review_apps/index.md): Live preview the results
diff --git a/doc/user/project/issues/due_dates.md b/doc/user/project/issues/due_dates.md
index e0c405353ce..1bf8b776c2e 100644
--- a/doc/user/project/issues/due_dates.md
+++ b/doc/user/project/issues/due_dates.md
@@ -35,5 +35,9 @@ Due dates also appear in your [todos list](../../../workflow/todos.md).
![Issues with due dates in the todos](img/due_dates_todos.png)
+The day before an open issue is due, an email will be sent to all participants
+of the issue. Both the due date and the day before are calculated using the
+server's timezone.
+
[ce-3614]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3614
[permissions]: ../../permissions.md#project
diff --git a/doc/user/project/issues/issues_functionalities.md b/doc/user/project/issues/issues_functionalities.md
index f2ca6a6822e..cf5cf1794ee 100644
--- a/doc/user/project/issues/issues_functionalities.md
+++ b/doc/user/project/issues/issues_functionalities.md
@@ -28,7 +28,7 @@ Comments and system notes also appear automatically in response to various actio
#### 2. Todos
- Add todo: add that issue to your [GitLab Todo](../../../workflow/todos.html) list
-- Mark done: mark that issue as done (reflects on the Todo list)
+- Mark todo as done: mark that issue as done (reflects on the Todo list)
#### 3. Assignee
@@ -41,10 +41,7 @@ it's reassigned to someone else to take it from there.
if a user is not member of that project, it can only be
assigned to them if they created the issue themselves.
-##### 3.1. Multiple Assignees
-
-> Available in [GitLab Starter](https://about.gitlab.com/products/) and
-[GitLab.com Bronze](https://about.gitlab.com/gitlab-com/).
+##### 3.1. Multiple Assignees **[STARTER]**
Often multiple people likely work on the same issue together,
which can especially be difficult to track in large teams
@@ -89,10 +86,7 @@ but they are immediately available to all projects in the group.
> **Tip:**
if the label doesn't exist yet, when you click **Edit**, it opens a dropdown menu from which you can select **Create new label**.
-#### 8. Weight
-
-> Available in [GitLab Starter](https://about.gitlab.com/products/) and
-[GitLab.com Bronze](https://about.gitlab.com/gitlab-com/).
+#### 8. Weight **[STARTER]**
- Attribute a weight (in a 0 to 9 range) to that issue. Easy to complete
should weight 1 and very hard to complete should weight 9.
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index a89a1206170..914898ea2ea 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -9,8 +9,7 @@ Labels allow you to categorize issues or merge requests using descriptive titles
In GitLab, you can create project and group labels:
- **Project labels** can be assigned to issues or merge requests in that project only.
-- **Group labels** can be assigned to any issue or merge request of any project in that group or subgroup.
-- In the [future](https://gitlab.com/gitlab-org/gitlab-ce/issues/40915), you will be able to assign group labels to issues and merge reqeusts of projects in [subgroups](../group/subgroups/index.md).
+- **Group labels** can be assigned to any issue or merge request of any project in that group or any subgroups of the group.
## Creating labels
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 3640d236db4..a6c0fd49c45 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -32,10 +32,10 @@ With GitLab merge requests, you can:
With **[GitLab Enterprise Edition][ee]**, you can also:
-- View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) (available only in GitLab Premium)
-- Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers (available in GitLab Starter)
-- [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history (available in GitLab Starter)
-- Analyze the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Starter)
+- View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) **[PREMIUM]**
+- Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers **[STARTER]**
+- [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history **[STARTER]**
+- Analyze the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) **[STARTER]**
## Use cases
@@ -43,7 +43,7 @@ A. Consider you are a software developer working in a team:
1. You checkout a new branch, and submit your changes through a merge request
1. You gather feedback from your team
-1. You work on the implementation optimizing code with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Starter)
+1. You work on the implementation optimizing code with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) **[STARTER]**
1. You build and test your changes with GitLab CI/CD
1. You request the approval from your manager
1. Your manager pushes a commit with his final review, [approves the merge request](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html), and set it to [merge when pipeline succeeds](#merge-when-pipeline-succeeds) (Merge Request Approvals are available in GitLab Starter)
@@ -56,7 +56,7 @@ B. Consider you're a web developer writing a webpage for your company's:
1. You gather feedback from your reviewers
1. Your changes are previewed with [Review Apps](../../../ci/review_apps/index.md)
1. You request your web designers for their implementation
-1. You request the [approval](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your manager (available in GitLab Starter)
+1. You request the [approval](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your manager **[STARTER]**
1. Once approved, your merge request is [squashed and merged](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html), and [deployed to staging with GitLab Pages](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) (Squash and Merge is available in GitLab Starter)
1. Your production team [cherry picks](#cherry-pick-changes) the merge commit into production
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index a387c1e443e..c9d2f8dc32d 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -34,7 +34,7 @@ Set up your project's merge request settings:
- Set up the merge request method (merge commit, [fast-forward merge](../merge_requests/fast_forward_merge.html)).
- Merge request [description templates](../description_templates.md#description-templates).
-- Enable [merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html#merge-request-approvals), _available in [GitLab Starter](https://about.gitlab.com/products/)_.
+- Enable [merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html#merge-request-approvals). **[STARTER]**
- Enable [merge only of pipeline succeeds](../merge_requests/merge_when_pipeline_succeeds.md).
- Enable [merge only when all discussions are resolved](../../discussions/index.md#only-allow-merge-requests-to-be-merged-if-all-discussions-are-resolved).
diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md
index 4b0d2c65642..b7064b83c4e 100644
--- a/doc/user/project/web_ide/index.md
+++ b/doc/user/project/web_ide/index.md
@@ -1,7 +1,7 @@
# Web IDE
-> Introduced in [GitLab Ultimate][ee] 10.4.
-> Brought to [GitLab CE][ce] in 10.7.
+> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ee/issues/4539) [GitLab Ultimate][ee] 10.4.
+> [Brought to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/issues/44157) in 10.7.
The Web IDE makes it faster and easier to contribute changes to your projects
by providing an advanced editor with commit staging.
@@ -30,5 +30,4 @@ list.
An additional review mode is available when you open a merge request, which
shows you a preview of the merge request diff if you commit your changes.
-[ee]: https://about.gitlab.com/products/
-[ce]: https://about.gitlab.com/products/
+[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index c4095ee0f69..f1501c81b27 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -86,6 +86,7 @@ In most of the below cases, the notification will be sent to:
| Close issue | |
| Reassign issue | The above, plus the old assignee |
| Reopen issue | |
+| Due issue | Participants and Custom notification level with this event selected |
| New merge request | |
| Push to merge request | Participants and Custom notification level with this event selected |
| Reassign merge request | The above, plus the old assignee |
@@ -96,15 +97,14 @@ In most of the below cases, the notification will be sent to:
| Failed pipeline | The author of the pipeline |
| Successful pipeline | The author of the pipeline, if they have the custom notification setting for successful pipelines set |
-
In addition, if the title or description of an Issue or Merge Request is
changed, notifications will be sent to any **new** mentions by `@username` as
if they had been mentioned in the original text.
-You won't receive notifications for Issues, Merge Requests or Milestones
-created by yourself. You will only receive automatic notifications when
-somebody else comments or adds changes to the ones that you've created or
-mentions you.
+You won't receive notifications for Issues, Merge Requests or Milestones created
+by yourself (except when an issue is due). You will only receive automatic
+notifications when somebody else comments or adds changes to the ones that
+you've created or mentions you.
### Email Headers
@@ -122,7 +122,7 @@ Notification emails include headers that provide extra content about the notific
| X-GitLab-NotificationReason | The reason for being notified. "mentioned", "assigned", etc |
#### X-GitLab-NotificationReason
-This header holds the reason for the notification to have been sent out,
+This header holds the reason for the notification to have been sent out,
where reason can be `mentioned`, `assigned`, `own_activity`, etc.
Only one reason is sent out according to its priority:
- `own_activity`
@@ -130,7 +130,7 @@ Only one reason is sent out according to its priority:
- `mentioned`
The reason in this header will also be shown in the footer of the notification email. For example an email with the
-reason `assigned` will have this sentence in the footer:
+reason `assigned` will have this sentence in the footer:
`"You are receiving this email because you have been assigned an item on {configured GitLab hostname}"`
**Note: Only reasons listed above have been implemented so far**
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index e612646cfbc..f13d29884d4 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -92,9 +92,9 @@ corresponding **Done** button, and it will disappear from your Todo list.
![A Todo in the Todos dashboard](img/todo_list_item.png)
A Todo can also be marked as done from the issue or merge request sidebar using
-the "Mark done" button.
+the "Mark todo as done" button.
-![Mark Done from the issuable sidebar](img/todos_mark_done_sidebar.png)
+![Mark todo as done from the issuable sidebar](img/todos_mark_done_sidebar.png)
You can mark all your Todos as done at once by clicking on the **Mark all as
done** button.
diff --git a/features/project/builds/permissions.feature b/features/project/builds/permissions.feature
deleted file mode 100644
index db15968db06..00000000000
--- a/features/project/builds/permissions.feature
+++ /dev/null
@@ -1,54 +0,0 @@
-Feature: Project Builds Permissions
- Background:
- Given I sign in as a user
- And project exists in some group namespace
- And project has CI enabled
- And project has a recent build
-
- Scenario: I try to visit build details as guest
- Given I am member of a project with a guest role
- When I visit recent build details page
- Then page status code should be 404
-
- Scenario: I try to visit project builds page as guest
- Given I am member of a project with a guest role
- When I visit project builds page
- Then page status code should be 404
-
- Scenario: I try to visit build details of internal project without access to builds
- Given The project is internal
- And public access for builds is disabled
- When I visit recent build details page
- Then page status code should be 404
-
- Scenario: I try to visit internal project builds page without access to builds
- Given The project is internal
- And public access for builds is disabled
- When I visit project builds page
- Then page status code should be 404
-
- @javascript
- Scenario: I try to visit build details of internal project with access to builds
- Given The project is internal
- And public access for builds is enabled
- When I visit recent build details page
- Then I see details of a build
- And I see build trace
-
- Scenario: I try to visit internal project builds page with access to builds
- Given The project is internal
- And public access for builds is enabled
- When I visit project builds page
- Then I see the build
-
- Scenario: I try to download build artifacts as guest
- Given I am member of a project with a guest role
- And recent build has artifacts available
- When I access artifacts download page
- Then page status code should be 404
-
- Scenario: I try to download build artifacts as reporter
- Given I am member of a project with a reporter role
- And recent build has artifacts available
- When I access artifacts download page
- Then download of build artifacts archive starts
diff --git a/features/steps/project/builds/permissions.rb b/features/steps/project/builds/permissions.rb
deleted file mode 100644
index 6e9d6504fd5..00000000000
--- a/features/steps/project/builds/permissions.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class Spinach::Features::ProjectBuildsPermissions < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedBuilds
- include SharedPaths
- include RepoHelpers
-end
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
index f5950145348..c2197584d8d 100644
--- a/features/steps/shared/builds.rb
+++ b/features/steps/shared/builds.rb
@@ -30,10 +30,6 @@ module SharedBuilds
visit project_job_path(@project, @build)
end
- step 'I visit project builds page' do
- visit project_jobs_path(@project)
- end
-
step 'recent build has artifacts available' do
artifacts = Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
archive = fixture_file_upload(artifacts, 'application/zip')
@@ -54,25 +50,4 @@ module SharedBuilds
expect(page.response_headers['Content-Type']).to eq 'application/zip'
expect(page.response_headers['Content-Transfer-Encoding']).to eq 'binary'
end
-
- step 'I access artifacts download page' do
- visit download_project_job_artifacts_path(@project, @build)
- end
-
- step 'I see details of a build' do
- expect(page).to have_content "Job ##{@build.id}"
- end
-
- step 'I see build trace' do
- expect(page).to have_css '#build-trace'
- end
-
- step 'I see the build' do
- page.within('.build') do
- expect(page).to have_content "##{@build.id}"
- expect(page).to have_content @build.sha[0..7]
- expect(page).to have_content @build.ref
- expect(page).to have_content @build.name
- end
- end
end
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index 3b4c839bcef..d16c127f6e6 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -435,12 +435,4 @@ module SharedPaths
mr = MergeRequest.find_by(title: title)
project_merge_request_path(mr.target_project, mr)
end
-
- # ----------------------------------------
- # Errors
- # ----------------------------------------
-
- step 'page status code should be 404' do
- expect(status_code).to eq 404
- end
end
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index 09969a6473f..a1945cf5f3d 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -13,11 +13,6 @@ module SharedProject
@project.add_master(@user)
end
- step "project exists in some group namespace" do
- @group = create(:group, name: 'some group')
- @project = create(:project, :repository, namespace: @group, public_builds: false)
- end
-
# Create a specific project called "Shop"
step 'I own project "Shop"' do
@project = Project.find_by(name: "Shop")
@@ -30,18 +25,6 @@ module SharedProject
end
# ----------------------------------------
- # Project permissions
- # ----------------------------------------
-
- step 'I am member of a project with a guest role' do
- @project.add_guest(@user)
- end
-
- step 'I am member of a project with a reporter role' do
- @project.add_reporter(@user)
- end
-
- # ----------------------------------------
# Visibility of archived project
# ----------------------------------------
@@ -140,18 +123,6 @@ module SharedProject
create(:label, project: project, title: 'enhancement')
end
- step 'The project is internal' do
- @project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- end
-
- step 'public access for builds is enabled' do
- @project.update(public_builds: true)
- end
-
- step 'public access for builds is disabled' do
- @project.update(public_builds: false)
- end
-
def user_owns_project(user_name:, project_name:, visibility: :private)
user = user_exists(user_name, username: user_name.gsub(/\s/, '').underscore)
project = Project.find_by(name: project_name)
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
index 6abd575b6ad..7975f35ab1e 100644
--- a/lib/api/discussions.rb
+++ b/lib/api/discussions.rb
@@ -25,7 +25,7 @@ module API
get ":id/#{noteables_str}/:noteable_id/discussions" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
- return not_found!("Discussions") unless can?(current_user, noteable_read_ability_name(noteable), noteable)
+ break not_found!("Discussions") unless can?(current_user, noteable_read_ability_name(noteable), noteable)
notes = noteable.notes
.inc_relations_for_view
@@ -50,7 +50,7 @@ module API
notes = readable_discussion_notes(noteable, params[:discussion_id])
if notes.empty? || !can?(current_user, noteable_read_ability_name(noteable), noteable)
- return not_found!("Discussion")
+ break not_found!("Discussion")
end
discussion = Discussion.build(notes, noteable)
@@ -98,7 +98,7 @@ module API
notes = readable_discussion_notes(noteable, params[:discussion_id])
if notes.empty? || !can?(current_user, noteable_read_ability_name(noteable), noteable)
- return not_found!("Notes")
+ break not_found!("Notes")
end
present notes, with: Entities::Note
@@ -117,8 +117,8 @@ module API
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
- return not_found!("Discussion") if notes.empty?
- return bad_request!("Discussion is an individual note.") unless notes.first.part_of_discussion?
+ break not_found!("Discussion") if notes.empty?
+ break bad_request!("Discussion is an individual note.") unless notes.first.part_of_discussion?
opts = {
note: params[:body],
diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb
index 92800ce6450..55d5c7f1606 100644
--- a/lib/api/group_variables.rb
+++ b/lib/api/group_variables.rb
@@ -31,7 +31,7 @@ module API
key = params[:key]
variable = user_group.variables.find_by(key: key)
- return not_found!('GroupVariable') unless variable
+ break not_found!('GroupVariable') unless variable
present variable, with: Entities::Variable
end
@@ -67,7 +67,7 @@ module API
put ':id/variables/:key' do
variable = user_group.variables.find_by(key: params[:key])
- return not_found!('GroupVariable') unless variable
+ break not_found!('GroupVariable') unless variable
variable_params = declared_params(include_missing: false).except(:key)
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index fcbc248fc3b..6b72caea8fd 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -50,7 +50,7 @@ module API
access_checker.check(params[:action], params[:changes])
@project ||= access_checker.project
rescue Gitlab::GitAccess::UnauthorizedError, Gitlab::GitAccess::NotFoundError => e
- return { status: false, message: e.message }
+ break { status: false, message: e.message }
end
log_user_activity(actor)
@@ -142,21 +142,21 @@ module API
if key
key.update_last_used_at
else
- return { 'success' => false, 'message' => 'Could not find the given key' }
+ break { 'success' => false, 'message' => 'Could not find the given key' }
end
if key.is_a?(DeployKey)
- return { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' }
+ break { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' }
end
user = key.user
unless user
- return { success: false, message: 'Could not find a user for the given key' }
+ break { success: false, message: 'Could not find a user for the given key' }
end
unless user.two_factor_enabled?
- return { success: false, message: 'Two-factor authentication is not enabled for this user' }
+ break { success: false, message: 'Two-factor authentication is not enabled for this user' }
end
codes = nil
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 88e7f46c92c..12ff2a1398b 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -310,7 +310,7 @@ module API
issue = find_project_issue(params[:issue_iid])
- return not_found!('UserAgentDetail') unless issue.user_agent_detail
+ break not_found!('UserAgentDetail') unless issue.user_agent_detail
present issue.user_agent_detail, with: Entities::UserAgentDetail
end
diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb
index b1adef49d46..32379d7c8ab 100644
--- a/lib/api/job_artifacts.rb
+++ b/lib/api/job_artifacts.rb
@@ -77,7 +77,7 @@ module API
build = find_build!(params[:job_id])
authorize!(:update_build, build)
- return not_found!(build) unless build.artifacts?
+ break not_found!(build) unless build.artifacts?
build.keep_artifacts!
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 60911c8d733..54d1acbd412 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -120,7 +120,7 @@ module API
build = find_build!(params[:job_id])
authorize!(:update_build, build)
- return forbidden!('Job is not retryable') unless build.retryable?
+ break forbidden!('Job is not retryable') unless build.retryable?
build = Ci::Build.retry(build, current_user)
@@ -138,7 +138,7 @@ module API
build = find_build!(params[:job_id])
authorize!(:erase_build, build)
- return forbidden!('Job is not erasable!') unless build.erasable?
+ break forbidden!('Job is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
present build, with: Entities::Job
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index 39c03c40bab..1de5551fee9 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -145,7 +145,7 @@ module API
snippet = Snippet.find_by!(id: params[:snippet_id], project_id: params[:id])
- return not_found!('UserAgentDetail') unless snippet.user_agent_detail
+ break not_found!('UserAgentDetail') unless snippet.user_agent_detail
present snippet.user_agent_detail, with: Entities::UserAgentDetail
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 3ae6fbd1fa9..51b3b0459f3 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -402,7 +402,7 @@ module API
end
unless user_project.allowed_to_share_with_group?
- return render_api_error!("The project sharing with group is disabled", 400)
+ break render_api_error!("The project sharing with group is disabled", 400)
end
link = user_project.project_group_links.new(declared_params(include_missing: false))
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 2396dc73f0e..bb3fa99af38 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -111,8 +111,8 @@ module API
end
params do
use :pagination
- optional :order_by, type: String, values: %w[email name commits], default: nil, desc: 'Return contributors ordered by `name` or `email` or `commits`'
- optional :sort, type: String, values: %w[asc desc], default: nil, desc: 'Sort by asc (ascending) or desc (descending)'
+ optional :order_by, type: String, values: %w[email name commits], default: 'commits', desc: 'Return contributors ordered by `name` or `email` or `commits`'
+ optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
end
get ':id/repository/contributors' do
begin
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 60aeb69e10a..4d4fbe50f9f 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -29,7 +29,7 @@ module API
project.runners.create(attributes)
end
- return forbidden! unless runner
+ break forbidden! unless runner
if runner.id
present runner, with: Entities::RunnerRegistrationDetails
@@ -83,7 +83,7 @@ module API
if current_runner.runner_queue_value_latest?(params[:last_update])
header 'X-GitLab-Last-Update', params[:last_update]
Gitlab::Metrics.add_event(:build_not_found_cached)
- return no_content!
+ break no_content!
end
new_update = current_runner.ensure_runner_queue_value
@@ -152,7 +152,7 @@ module API
stream_size = job.trace.append(request.body.read, content_range[0].to_i)
if stream_size < 0
- return error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{-stream_size}" })
+ break error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{-stream_size}" })
end
status 202
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index c736cc32021..b30305b4bc9 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -94,7 +94,7 @@ module API
end
put ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
- return not_found!('Snippet') unless snippet
+ break not_found!('Snippet') unless snippet
authorize! :update_personal_snippet, snippet
@@ -120,7 +120,7 @@ module API
end
delete ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
- return not_found!('Snippet') unless snippet
+ break not_found!('Snippet') unless snippet
authorize! :destroy_personal_snippet, snippet
@@ -135,7 +135,7 @@ module API
end
get ":id/raw" do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
- return not_found!('Snippet') unless snippet
+ break not_found!('Snippet') unless snippet
env['api.format'] = :txt
content_type 'text/plain'
@@ -153,7 +153,7 @@ module API
snippet = Snippet.find_by!(id: params[:id])
- return not_found!('UserAgentDetail') unless snippet.user_agent_detail
+ break not_found!('UserAgentDetail') unless snippet.user_agent_detail
present snippet.user_agent_detail, with: Entities::UserAgentDetail
end
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index b3709455bc3..b29e660c6e0 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -62,7 +62,7 @@ module API
authorize! :admin_build, user_project
trigger = user_project.triggers.find(params.delete(:trigger_id))
- return not_found!('Trigger') unless trigger
+ break not_found!('Trigger') unless trigger
present trigger, with: Entities::Trigger
end
@@ -99,7 +99,7 @@ module API
authorize! :admin_build, user_project
trigger = user_project.triggers.find(params.delete(:trigger_id))
- return not_found!('Trigger') unless trigger
+ break not_found!('Trigger') unless trigger
if trigger.update(declared_params(include_missing: false))
present trigger, with: Entities::Trigger
@@ -119,7 +119,7 @@ module API
authorize! :admin_build, user_project
trigger = user_project.triggers.find(params.delete(:trigger_id))
- return not_found!('Trigger') unless trigger
+ break not_found!('Trigger') unless trigger
if trigger.update(owner: current_user)
status :ok
@@ -140,7 +140,7 @@ module API
authorize! :admin_build, user_project
trigger = user_project.triggers.find(params.delete(:trigger_id))
- return not_found!('Trigger') unless trigger
+ break not_found!('Trigger') unless trigger
destroy_conditionally!(trigger)
end
diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb
index 683b9c993cb..b49448e1e67 100644
--- a/lib/api/v3/builds.rb
+++ b/lib/api/v3/builds.rb
@@ -51,7 +51,7 @@ module API
get ':id/repository/commits/:sha/builds' do
authorize_read_builds!
- return not_found! unless user_project.commit(params[:sha])
+ break not_found! unless user_project.commit(params[:sha])
pipelines = user_project.pipelines.where(sha: params[:sha])
builds = user_project.builds.where(pipeline: pipelines).order('id DESC')
@@ -153,7 +153,7 @@ module API
build = get_build!(params[:build_id])
authorize!(:update_build, build)
- return forbidden!('Build is not retryable') unless build.retryable?
+ break forbidden!('Build is not retryable') unless build.retryable?
build = Ci::Build.retry(build, current_user)
@@ -171,7 +171,7 @@ module API
build = get_build!(params[:build_id])
authorize!(:erase_build, build)
- return forbidden!('Build is not erasable!') unless build.erasable?
+ break forbidden!('Build is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
present build, with: ::API::V3::Entities::Build
@@ -188,7 +188,7 @@ module API
build = get_build!(params[:build_id])
authorize!(:update_build, build)
- return not_found!(build) unless build.artifacts?
+ break not_found!(build) unless build.artifacts?
build.keep_artifacts!
diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb
index a2df969d819..eb3dd113524 100644
--- a/lib/api/v3/projects.rb
+++ b/lib/api/v3/projects.rb
@@ -423,7 +423,7 @@ module API
end
unless user_project.allowed_to_share_with_group?
- return render_api_error!("The project sharing with group is disabled", 400)
+ break render_api_error!("The project sharing with group is disabled", 400)
end
link = user_project.project_group_links.new(declared_params(include_missing: false))
diff --git a/lib/api/v3/snippets.rb b/lib/api/v3/snippets.rb
index 85613c8ed84..1df8a20e74a 100644
--- a/lib/api/v3/snippets.rb
+++ b/lib/api/v3/snippets.rb
@@ -90,7 +90,7 @@ module API
end
put ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
- return not_found!('Snippet') unless snippet
+ break not_found!('Snippet') unless snippet
authorize! :update_personal_snippet, snippet
@@ -114,7 +114,7 @@ module API
end
delete ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
- return not_found!('Snippet') unless snippet
+ break not_found!('Snippet') unless snippet
authorize! :destroy_personal_snippet, snippet
snippet.destroy
@@ -129,7 +129,7 @@ module API
end
get ":id/raw" do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
- return not_found!('Snippet') unless snippet
+ break not_found!('Snippet') unless snippet
env['api.format'] = :txt
content_type 'text/plain'
diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb
index 34f07dfb486..969bb2a05de 100644
--- a/lib/api/v3/triggers.rb
+++ b/lib/api/v3/triggers.rb
@@ -72,7 +72,7 @@ module API
authorize! :admin_build, user_project
trigger = user_project.triggers.find_by(token: params[:token].to_s)
- return not_found!('Trigger') unless trigger
+ break not_found!('Trigger') unless trigger
present trigger, with: ::API::V3::Entities::Trigger
end
@@ -100,7 +100,7 @@ module API
authorize! :admin_build, user_project
trigger = user_project.triggers.find_by(token: params[:token].to_s)
- return not_found!('Trigger') unless trigger
+ break not_found!('Trigger') unless trigger
trigger.destroy
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index d08876ae1b9..a34de9410e8 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -31,7 +31,7 @@ module API
key = params[:key]
variable = user_project.variables.find_by(key: key)
- return not_found!('Variable') unless variable
+ break not_found!('Variable') unless variable
present variable, with: Entities::Variable
end
@@ -67,7 +67,7 @@ module API
put ':id/variables/:key' do
variable = user_project.variables.find_by(key: params[:key])
- return not_found!('Variable') unless variable
+ break not_found!('Variable') unless variable
variable_params = declared_params(include_missing: false).except(:key)
diff --git a/lib/banzai/renderer/common_mark/html.rb b/lib/banzai/renderer/common_mark/html.rb
index c7a54629f31..46b609c36b0 100644
--- a/lib/banzai/renderer/common_mark/html.rb
+++ b/lib/banzai/renderer/common_mark/html.rb
@@ -9,7 +9,7 @@ module Banzai
lang_attr = lang.present? ? %Q{ lang="#{lang}"} : ''
result =
"<pre>" \
- "<code#{lang_attr}>#{html_escape(code)}</code>" \
+ "<code#{lang_attr}>#{ERB::Util.html_escape(code)}</code>" \
"</pre>"
out(result)
diff --git a/lib/banzai/renderer/redcarpet/html.rb b/lib/banzai/renderer/redcarpet/html.rb
index 94df5d8b1e1..30e815f1224 100644
--- a/lib/banzai/renderer/redcarpet/html.rb
+++ b/lib/banzai/renderer/redcarpet/html.rb
@@ -6,7 +6,7 @@ module Banzai
lang_attr = lang ? %Q{ lang="#{lang}"} : ''
"\n<pre>" \
- "<code#{lang_attr}>#{html_escape(code)}</code>" \
+ "<code#{lang_attr}>#{ERB::Util.html_escape(code)}</code>" \
"</pre>"
end
end
diff --git a/lib/declarative_policy/runner.rb b/lib/declarative_policy/runner.rb
index 77c91817382..87f14b3b0d2 100644
--- a/lib/declarative_policy/runner.rb
+++ b/lib/declarative_policy/runner.rb
@@ -77,7 +77,7 @@ module DeclarativePolicy
@state = State.new
steps_by_score do |step, score|
- return if !debug && @state.prevented?
+ break if !debug && @state.prevented?
passed = nil
case step.action
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index 2526a870976..f6629982512 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -10,11 +10,15 @@ module Gitlab
Gitlab.config.gitlab.url == COM_URL || gl_subdomain?
end
+ def self.org?
+ Gitlab.config.gitlab.url == 'https://dev.gitlab.org'
+ end
+
def self.gl_subdomain?
SUBDOMAIN_REGEX === Gitlab.config.gitlab.url
end
def self.dev_env_or_com?
- Rails.env.test? || Rails.env.development? || com?
+ Rails.env.test? || Rails.env.development? || org? || com?
end
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 2a44e11efb6..8e5a985edd7 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -51,7 +51,7 @@ module Gitlab
Gitlab::Auth::UniqueIpsLimiter.limit_user! do
user = User.by_login(login)
- return if user && !user.active?
+ break if user && !user.active?
authenticators = []
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index cedf4171ab1..47b67930c6d 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -45,7 +45,7 @@ module Gitlab
def append(data, offset)
write do |stream|
current_length = stream.size
- return -current_length unless current_length == offset
+ break -current_length unless current_length == offset
data = job.hide_secrets(data)
stream.append(data, offset)
diff --git a/lib/gitlab/ci/trace/http_io.rb b/lib/gitlab/ci/trace/http_io.rb
index ac4308f4e2c..cff924e27ef 100644
--- a/lib/gitlab/ci/trace/http_io.rb
+++ b/lib/gitlab/ci/trace/http_io.rb
@@ -75,18 +75,28 @@ module Gitlab
end
end
- def read(length = nil)
+ def read(length = nil, outbuf = "")
out = ""
- until eof? || (length && out.length >= length)
+ length ||= size - tell
+
+ until length <= 0 || eof?
data = get_chunk
break if data.empty?
- out << data
- @tell += data.bytesize
+ chunk_bytes = [BUFFER_SIZE - chunk_offset, length].min
+ chunk_data = data.byteslice(0, chunk_bytes)
+
+ out << chunk_data
+ @tell += chunk_data.bytesize
+ length -= chunk_data.bytesize
end
- out = out[0, length] if length && out.length > length
+ # If outbuf is passed, we put the output into the buffer. This supports IO.copy_stream functionality
+ if outbuf
+ outbuf.slice!(0, outbuf.bytesize)
+ outbuf << out
+ end
out
end
@@ -158,7 +168,7 @@ module Gitlab
# Provider: GCS
# - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
# - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPOK 200
- @chunk_range ||= (chunk_start...(chunk_start + @chunk.length))
+ @chunk_range ||= (chunk_start...(chunk_start + @chunk.bytesize))
end
@chunk[chunk_offset..BUFFER_SIZE]
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index 54894a46077..187ad8b833a 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -10,7 +10,9 @@ module Gitlab
delegate :close, :tell, :seek, :size, :url, :truncate, to: :stream, allow_nil: true
- delegate :valid?, to: :stream, as: :present?, allow_nil: true
+ delegate :valid?, to: :stream, allow_nil: true
+
+ alias_method :present?, :valid?
def initialize
@stream = yield
@@ -85,7 +87,7 @@ module Gitlab
match = matches.flatten.last
coverage = match.gsub(/\d+(\.\d+)?/).first
- return coverage if coverage.present?
+ return coverage if coverage.present? # rubocop:disable Cop/AvoidReturnFromBlocks
end
nil
diff --git a/lib/gitlab/daemon.rb b/lib/gitlab/daemon.rb
index 633de9f9776..bd14c7eece3 100644
--- a/lib/gitlab/daemon.rb
+++ b/lib/gitlab/daemon.rb
@@ -30,7 +30,7 @@ module Gitlab
return unless enabled?
@mutex.synchronize do
- return thread if thread?
+ break thread if thread?
@thread = Thread.new { start_working }
end
@@ -38,7 +38,7 @@ module Gitlab
def stop
@mutex.synchronize do
- return unless thread?
+ break unless thread?
stop_working
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index 269016daac2..5c1baa19b66 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -33,10 +33,7 @@ module Gitlab
# match the blob, which is a bug. But we shouldn't fail to render
# completely in that case, even though we want to report the error.
rescue RangeError => e
- if Gitlab::Sentry.enabled?
- Gitlab::Sentry.context
- Raven.capture_exception(e)
- end
+ Gitlab::Sentry.track_exception(e, issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/45441')
end
end
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index 5fdd5dcd374..8cf59fa8e28 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -5,7 +5,7 @@ module Gitlab
CANONICAL_CE_PROJECT_URL = 'https://gitlab.com/gitlab-org/gitlab-ce'.freeze
CANONICAL_EE_REPO_URL = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
CHECK_DIR = Rails.root.join('ee_compat_check')
- IGNORED_FILES_REGEX = %r{VERSION|CHANGELOG\.md|db/schema\.rb}i.freeze
+ IGNORED_FILES_REGEX = %r{VERSION|CHANGELOG\.md|db/schema\.rb|locale/gitlab\.pot}i.freeze
PLEASE_READ_THIS_BANNER = %Q{
============================================================
===================== PLEASE READ THIS =====================
diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb
index 1d6f5bb5e1c..d5d35dbd97f 100644
--- a/lib/gitlab/etag_caching/middleware.rb
+++ b/lib/gitlab/etag_caching/middleware.rb
@@ -50,7 +50,7 @@ module Gitlab
status_code = Gitlab::PollingInterval.polling_enabled? ? 304 : 429
- [status_code, { 'ETag' => etag }, []]
+ [status_code, { 'ETag' => etag, 'X-Gitlab-From-Cache' => 'true' }, []]
end
def track_cache_miss(if_none_match, cached_value_present, route)
diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb
index 1b74f735679..b6eeb5d9a2b 100644
--- a/lib/gitlab/gfm/uploads_rewriter.rb
+++ b/lib/gitlab/gfm/uploads_rewriter.rb
@@ -21,7 +21,7 @@ module Gitlab
@text.gsub(@pattern) do |markdown|
file = find_file(@source_project, $~[:secret], $~[:file])
- return markdown unless file.try(:exists?)
+ break markdown unless file.try(:exists?)
new_uploader = FileUploader.new(target_project)
with_link_in_tmp_dir(file.file) do |open_tmp_file|
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 0fb82441bf8..fabcd46c8e9 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -486,6 +486,8 @@ module Gitlab
end
def tree_entry(path)
+ return unless path.present?
+
@repository.gitaly_migrate(:commit_tree_entry) do |is_migrated|
if is_migrated
gitaly_tree_entry(path)
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index a203587aec1..b58296375ef 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -249,7 +249,7 @@ module Gitlab
if size >= SIZE_LIMIT
too_large!
- return true
+ return true # rubocop:disable Cop/AvoidReturnFromBlocks
end
end
end
diff --git a/lib/gitlab/git/popen.rb b/lib/gitlab/git/popen.rb
index c1767046ff0..f9f24ecc48d 100644
--- a/lib/gitlab/git/popen.rb
+++ b/lib/gitlab/git/popen.rb
@@ -25,7 +25,9 @@ module Gitlab
stdin.close
if lazy_block
- return [lazy_block.call(stdout.lazy), 0]
+ cmd_output = lazy_block.call(stdout.lazy)
+ cmd_status = 0
+ break
else
cmd_output << stdout.read
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 11a421cb430..3124c426f97 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -9,6 +9,7 @@ module Gitlab
include Gitlab::Git::RepositoryMirroring
include Gitlab::Git::Popen
include Gitlab::EncodingHelper
+ include Gitlab::Utils::StrongMemoize
ALLOWED_OBJECT_DIRECTORIES_VARIABLES = %w[
GIT_OBJECT_DIRECTORY
@@ -231,13 +232,13 @@ module Gitlab
end
end
+ def expire_has_local_branches_cache
+ clear_memoization(:has_local_branches)
+ end
+
def has_local_branches?
- gitaly_migrate(:has_local_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_repository_client.has_local_branches?
- else
- has_local_branches_rugged?
- end
+ strong_memoize(:has_local_branches) do
+ uncached_has_local_branches?
end
end
@@ -299,7 +300,8 @@ module Gitlab
#
# Ref names must start with `refs/`.
def ref_exists?(ref_name)
- gitaly_migrate(:ref_exists) do |is_enabled|
+ gitaly_migrate(:ref_exists,
+ status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_ref_exists?(ref_name)
else
@@ -1559,6 +1561,16 @@ module Gitlab
private
+ def uncached_has_local_branches?
+ gitaly_migrate(:has_local_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
+ if is_enabled
+ gitaly_repository_client.has_local_branches?
+ else
+ has_local_branches_rugged?
+ end
+ end
+ end
+
def local_write_ref(ref_path, ref, old_ref: nil, shell: true)
if shell
shell_write_ref(ref_path, ref, old_ref)
diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb
index dc424a433fb..8a01f92e2af 100644
--- a/lib/gitlab/git/repository_mirroring.rb
+++ b/lib/gitlab/git/repository_mirroring.rb
@@ -26,7 +26,7 @@ module Gitlab
# When the remote repo does not have tags.
if target.nil? || path.nil?
Rails.logger.info "Empty or invalid list of tags for remote: #{remote}. Output: #{output}"
- return []
+ break []
end
name = path.split('/', 3).last
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index 8d82820915d..821436911ab 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -2,10 +2,11 @@ module Gitlab
module Git
class Wiki
DuplicatePageError = Class.new(StandardError)
+ OperationError = Class.new(StandardError)
- CommitDetails = Struct.new(:name, :email, :message) do
+ CommitDetails = Struct.new(:user_id, :username, :name, :email, :message) do
def to_h
- { name: name, email: email, message: message }
+ { user_id: user_id, username: username, name: name, email: email, message: message }
end
end
PageBlob = Struct.new(:name)
@@ -140,6 +141,10 @@ module Gitlab
end
end
+ def gollum_wiki
+ @gollum_wiki ||= Gollum::Wiki.new(@repository.path)
+ end
+
private
# options:
@@ -158,10 +163,6 @@ module Gitlab
offset: options[:offset])
end
- def gollum_wiki
- @gollum_wiki ||= Gollum::Wiki.new(@repository.path)
- end
-
def gollum_page_by_path(page_path)
page_name = Gollum::Page.canonicalize_filename(page_path)
page_dir = File.split(page_path).first
@@ -201,12 +202,12 @@ module Gitlab
assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails)
- filename = File.basename(name)
- dir = (tmp_dir = File.dirname(name)) == '.' ? '' : tmp_dir
-
- gollum_wiki.write_page(filename, format, content, commit_details.to_h, dir)
+ with_committer_with_hooks(commit_details) do |committer|
+ filename = File.basename(name)
+ dir = (tmp_dir = File.dirname(name)) == '.' ? '' : tmp_dir
- nil
+ gollum_wiki.write_page(filename, format, content, { committer: committer }, dir)
+ end
rescue Gollum::DuplicatePageError => e
raise Gitlab::Git::Wiki::DuplicatePageError, e.message
end
@@ -214,24 +215,23 @@ module Gitlab
def gollum_delete_page(page_path, commit_details)
assert_type!(commit_details, CommitDetails)
- gollum_wiki.delete_page(gollum_page_by_path(page_path), commit_details.to_h)
- nil
+ with_committer_with_hooks(commit_details) do |committer|
+ gollum_wiki.delete_page(gollum_page_by_path(page_path), committer: committer)
+ end
end
def gollum_update_page(page_path, title, format, content, commit_details)
assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails)
- page = gollum_page_by_path(page_path)
- committer = Gollum::Committer.new(page.wiki, commit_details.to_h)
-
- # Instead of performing two renames if the title has changed,
- # the update_page will only update the format and content and
- # the rename_page will do anything related to moving/renaming
- gollum_wiki.update_page(page, page.name, format, content, committer: committer)
- gollum_wiki.rename_page(page, title, committer: committer)
- committer.commit
- nil
+ with_committer_with_hooks(commit_details) do |committer|
+ page = gollum_page_by_path(page_path)
+ # Instead of performing two renames if the title has changed,
+ # the update_page will only update the format and content and
+ # the rename_page will do anything related to moving/renaming
+ gollum_wiki.update_page(page, page.name, format, content, committer: committer)
+ gollum_wiki.rename_page(page, title, committer: committer)
+ end
end
def gollum_find_page(title:, version: nil, dir: nil)
@@ -288,6 +288,20 @@ module Gitlab
Gitlab::Git::WikiPage.new(wiki_page, version)
end
end
+
+ def committer_with_hooks(commit_details)
+ Gitlab::Wiki::CommitterWithHooks.new(self, commit_details.to_h)
+ end
+
+ def with_committer_with_hooks(commit_details, &block)
+ committer = committer_with_hooks(commit_details)
+
+ yield committer
+
+ committer.commit
+
+ nil
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb
index 7a698e4b3f3..2dfe055a496 100644
--- a/lib/gitlab/gitaly_client/wiki_service.rb
+++ b/lib/gitlab/gitaly_client/wiki_service.rb
@@ -200,6 +200,8 @@ module Gitlab
def gitaly_commit_details(commit_details)
Gitaly::WikiCommitDetails.new(
+ user_id: commit_details.user_id,
+ user_name: encode_binary(commit_details.username),
name: encode_binary(commit_details.name),
email: encode_binary(commit_details.email),
message: encode_binary(commit_details.message)
diff --git a/lib/gitlab/gl_id.rb b/lib/gitlab/gl_id.rb
index 624fd00367e..a53d156b41f 100644
--- a/lib/gitlab/gl_id.rb
+++ b/lib/gitlab/gl_id.rb
@@ -2,10 +2,14 @@ module Gitlab
module GlId
def self.gl_id(user)
if user.present?
- "user-#{user.id}"
+ gl_id_from_id_value(user.id)
else
- ""
+ ''
end
end
+
+ def self.gl_id_from_id_value(id)
+ "user-#{id}"
+ end
end
end
diff --git a/lib/gitlab/optimistic_locking.rb b/lib/gitlab/optimistic_locking.rb
index 1d9a5d1a20a..d09bce642b0 100644
--- a/lib/gitlab/optimistic_locking.rb
+++ b/lib/gitlab/optimistic_locking.rb
@@ -3,18 +3,15 @@ module Gitlab
module_function
def retry_lock(subject, retries = 100, &block)
- loop do
- begin
- ActiveRecord::Base.transaction do
- return yield(subject)
- end
- rescue ActiveRecord::StaleObjectError
- retries -= 1
- raise unless retries >= 0
-
- subject.reload
- end
+ ActiveRecord::Base.transaction do
+ yield(subject)
end
+ rescue ActiveRecord::StaleObjectError
+ retries -= 1
+ raise unless retries >= 0
+
+ subject.reload
+ retry
end
alias_method :retry_optimistic_lock, :retry_lock
diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb
index 4a22fc80f75..6381e94c1d2 100644
--- a/lib/gitlab/sentry.rb
+++ b/lib/gitlab/sentry.rb
@@ -18,6 +18,25 @@ module Gitlab
end
end
+ # This can be used for investigating exceptions that can be recovered from in
+ # code. The exception will still be raised in development and test
+ # environments.
+ #
+ # That way we can track down these exceptions with as much information as we
+ # need to resolve them.
+ #
+ # Provide an issue URL for follow up.
+ def self.track_exception(exception, issue_url: nil, extra: {})
+ if enabled?
+ extra[:issue_url] = issue_url if issue_url
+ context # Make sure we've set everything we know in the context
+
+ Raven.capture_exception(exception, extra: extra)
+ end
+
+ raise exception if should_raise?
+ end
+
def self.program_context
if Sidekiq.server?
'sidekiq'
@@ -25,5 +44,9 @@ module Gitlab
'rails'
end
end
+
+ def self.should_raise?
+ Rails.env.development? || Rails.env.test?
+ end
end
end
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 67407b651a5..ac4ac537a8a 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -340,7 +340,7 @@ module Gitlab
if enabled
gitaly_namespace_client(storage).rename(old_name, new_name)
else
- return false if exists?(storage, new_name) || !exists?(storage, old_name)
+ break false if exists?(storage, new_name) || !exists?(storage, old_name)
FileUtils.mv(full_path(storage, old_name), full_path(storage, new_name))
end
diff --git a/lib/gitlab/sidekiq_middleware/shutdown.rb b/lib/gitlab/sidekiq_middleware/shutdown.rb
index c2b8d6de66e..b232ac4da33 100644
--- a/lib/gitlab/sidekiq_middleware/shutdown.rb
+++ b/lib/gitlab/sidekiq_middleware/shutdown.rb
@@ -25,7 +25,7 @@ module Gitlab
# can be only one shutdown thread in the process.
def self.create_shutdown_thread
mu_synchronize do
- return unless @shutdown_thread.nil?
+ break unless @shutdown_thread.nil?
@shutdown_thread = Thread.new { yield }
end
diff --git a/lib/gitlab/view/presenter/base.rb b/lib/gitlab/view/presenter/base.rb
index 841fb681435..36162faa1eb 100644
--- a/lib/gitlab/view/presenter/base.rb
+++ b/lib/gitlab/view/presenter/base.rb
@@ -20,6 +20,10 @@ module Gitlab
subject
end
+ def present(**attributes)
+ self
+ end
+
class_methods do
def presenter?
true
diff --git a/lib/gitlab/wiki/committer_with_hooks.rb b/lib/gitlab/wiki/committer_with_hooks.rb
new file mode 100644
index 00000000000..19f0b3814fd
--- /dev/null
+++ b/lib/gitlab/wiki/committer_with_hooks.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module Wiki
+ class CommitterWithHooks < Gollum::Committer
+ attr_reader :gl_wiki
+
+ def initialize(gl_wiki, options = {})
+ @gl_wiki = gl_wiki
+ super(gl_wiki.gollum_wiki, options)
+ end
+
+ def commit
+ result = Gitlab::Git::OperationService.new(git_user, gl_wiki.repository).with_branch(
+ @wiki.ref,
+ start_branch_name: @wiki.ref
+ ) do |start_commit|
+ super(false)
+ end
+
+ result[:newrev]
+ rescue Gitlab::Git::HooksService::PreReceiveError => e
+ message = "Custom Hook failed: #{e.message}"
+ raise Gitlab::Git::Wiki::OperationError, message
+ end
+
+ private
+
+ def git_user
+ @git_user ||= Gitlab::Git::User.new(@options[:username],
+ @options[:name],
+ @options[:email],
+ gitlab_id)
+ end
+
+ def gitlab_id
+ Gitlab::GlId.gl_id_from_id_value(@options[:user_id])
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake
index 1d903c81358..f71e69987cb 100644
--- a/lib/tasks/gitlab/setup.rake
+++ b/lib/tasks/gitlab/setup.rake
@@ -1,9 +1,20 @@
namespace :gitlab do
desc "GitLab | Setup production application"
task setup: :gitlab_environment do
+ check_gitaly_connection
setup_db
end
+ def check_gitaly_connection
+ Gitlab.config.repositories.storages.each do |name, _details|
+ Gitlab::GitalyClient::ServerService.new(name).info
+ end
+ rescue GRPC::Unavailable => ex
+ puts "Failed to connect to Gitaly...".color(:red)
+ puts "Error: #{ex}"
+ exit 1
+ end
+
def setup_db
warn_user_is_not_gitlab
diff --git a/lib/tasks/gitlab/storage.rake b/lib/tasks/gitlab/storage.rake
index 8ac73bc8ff2..6e8bd9078c8 100644
--- a/lib/tasks/gitlab/storage.rake
+++ b/lib/tasks/gitlab/storage.rake
@@ -111,7 +111,7 @@ namespace :gitlab do
puts " - #{project.full_path} (id: #{project.id})".color(:red)
- return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator
+ return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator, Cop/AvoidReturnFromBlocks
end
end
end
@@ -132,7 +132,7 @@ namespace :gitlab do
puts " - #{upload.path} (id: #{upload.id})".color(:red)
- return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator
+ return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator, Cop/AvoidReturnFromBlocks
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d7eb123b48b..0eec9793391 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-04-04 18:02+0200\n"
-"PO-Revision-Date: 2018-04-04 18:02+0200\n"
+"POT-Creation-Date: 2018-04-17 11:44+0200\n"
+"PO-Revision-Date: 2018-04-17 11:44+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -241,6 +241,9 @@ msgstr ""
msgid "Allow edits from maintainers."
msgstr ""
+msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
+msgstr ""
+
msgid "Allow requests to the local network from hooks and services."
msgstr ""
@@ -322,7 +325,7 @@ msgstr ""
msgid "April"
msgstr ""
-msgid "Archived project! Repository is read-only"
+msgid "Archived project! Repository and other project resources are read-only"
msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
@@ -433,9 +436,81 @@ msgstr ""
msgid "Background jobs"
msgstr ""
+msgid "Badges"
+msgstr ""
+
+msgid "Badges|A new badge was added."
+msgstr ""
+
+msgid "Badges|Add badge"
+msgstr ""
+
+msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
+msgstr ""
+
+msgid "Badges|Badge image URL"
+msgstr ""
+
+msgid "Badges|Badge image preview"
+msgstr ""
+
+msgid "Badges|Delete badge"
+msgstr ""
+
+msgid "Badges|Delete badge?"
+msgstr ""
+
+msgid "Badges|Deleting the badge failed, please try again."
+msgstr ""
+
+msgid "Badges|Group Badge"
+msgstr ""
+
+msgid "Badges|Link"
+msgstr ""
+
+msgid "Badges|No badge image"
+msgstr ""
+
+msgid "Badges|No image to preview"
+msgstr ""
+
+msgid "Badges|Project Badge"
+msgstr ""
+
+msgid "Badges|Reload badge image"
+msgstr ""
+
+msgid "Badges|Save changes"
+msgstr ""
+
+msgid "Badges|Saving the badge failed, please check the entered URLs and try again."
+msgstr ""
+
+msgid "Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}"
+msgstr ""
+
+msgid "Badges|The badge was deleted."
+msgstr ""
+
+msgid "Badges|The badge was saved."
+msgstr ""
+
+msgid "Badges|This group has no badges"
+msgstr ""
+
+msgid "Badges|This project has no badges"
+msgstr ""
+
+msgid "Badges|Your badges"
+msgstr ""
+
msgid "Begin with the selected commit"
msgstr ""
+msgid "Blame"
+msgstr ""
+
msgid "Branch (%{branch_count})"
msgid_plural "Branches (%{branch_count})"
msgstr[0] ""
@@ -600,12 +675,18 @@ msgstr ""
msgid "Cancel"
msgstr ""
+msgid "Cancel this job"
+msgstr ""
+
msgid "Cannot be merged automatically"
msgstr ""
msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
+msgid "Change this value to influence how frequently the GitLab UI polls for updates."
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr ""
@@ -744,6 +825,12 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
+msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
+msgstr ""
+
+msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
+msgstr ""
+
msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
@@ -816,7 +903,7 @@ msgstr ""
msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr ""
-msgid "ClusterIntegration|Create on GKE"
+msgid "ClusterIntegration|Create on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
@@ -1147,6 +1234,9 @@ msgstr ""
msgid "Confidentiality"
msgstr ""
+msgid "Configure Gitaly timeouts."
+msgstr ""
+
msgid "Configure Sidekiq job throttling."
msgstr ""
@@ -1213,6 +1303,9 @@ msgstr ""
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
msgstr ""
+msgid "ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images."
+msgstr ""
+
msgid "Continuous Integration and Deployment"
msgstr ""
@@ -1386,6 +1479,78 @@ msgstr[1] ""
msgid "Deploy Keys"
msgstr ""
+msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
+msgstr ""
+
+msgid "DeployTokens|Add a deploy token"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the registry images"
+msgstr ""
+
+msgid "DeployTokens|Allows read-only access to the repository"
+msgstr ""
+
+msgid "DeployTokens|Copy deploy token to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Copy username to clipboard"
+msgstr ""
+
+msgid "DeployTokens|Create deploy token"
+msgstr ""
+
+msgid "DeployTokens|Created"
+msgstr ""
+
+msgid "DeployTokens|Deploy Tokens"
+msgstr ""
+
+msgid "DeployTokens|Deploy tokens allow read-only access to your repository and registry images."
+msgstr ""
+
+msgid "DeployTokens|Expires"
+msgstr ""
+
+msgid "DeployTokens|Name"
+msgstr ""
+
+msgid "DeployTokens|Pick a name for the application, and we'll give you a unique deploy token."
+msgstr ""
+
+msgid "DeployTokens|Revoke"
+msgstr ""
+
+msgid "DeployTokens|Revoke %{name}"
+msgstr ""
+
+msgid "DeployTokens|Scopes"
+msgstr ""
+
+msgid "DeployTokens|This action cannot be undone."
+msgstr ""
+
+msgid "DeployTokens|This project has no active Deploy Tokens."
+msgstr ""
+
+msgid "DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again."
+msgstr ""
+
+msgid "DeployTokens|Use this username as a login."
+msgstr ""
+
+msgid "DeployTokens|Username"
+msgstr ""
+
+msgid "DeployTokens|You are about to revoke"
+msgstr ""
+
+msgid "DeployTokens|Your New Deploy Token"
+msgstr ""
+
+msgid "DeployTokens|Your new project deploy token has been created."
+msgstr ""
+
msgid "Description"
msgstr ""
@@ -1455,9 +1620,15 @@ msgstr ""
msgid "Editing"
msgstr ""
+msgid "Email"
+msgstr ""
+
msgid "Emails"
msgstr ""
+msgid "Embed"
+msgstr ""
+
msgid "Enable Auto DevOps"
msgstr ""
@@ -1470,6 +1641,9 @@ msgstr ""
msgid "Enable and configure Prometheus metrics."
msgstr ""
+msgid "Enable or disable version check and usage ping."
+msgstr ""
+
msgid "Enable reCAPTCHA or Akismet and set IP limits."
msgstr ""
@@ -1703,6 +1877,9 @@ msgstr ""
msgid "GitLab Runner section"
msgstr ""
+msgid "Gitaly"
+msgstr ""
+
msgid "Gitaly Servers"
msgstr ""
@@ -1885,6 +2062,9 @@ msgstr ""
msgid "January"
msgstr ""
+msgid "Job has been erased"
+msgstr ""
+
msgid "Jobs"
msgstr ""
@@ -1900,6 +2080,9 @@ msgstr ""
msgid "June"
msgstr ""
+msgid "Koding"
+msgstr ""
+
msgid "Kubernetes"
msgstr ""
@@ -2321,6 +2504,9 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr ""
+msgid "Online IDE integration settings."
+msgstr ""
+
msgid "Only project members can comment."
msgstr ""
@@ -2369,6 +2555,12 @@ msgstr ""
msgid "Pending"
msgstr ""
+msgid "Performance optimization"
+msgstr ""
+
+msgid "Permalink"
+msgstr ""
+
msgid "Personal Access Token"
msgstr ""
@@ -2510,12 +2702,18 @@ msgstr ""
msgid "Pipeline|with stages"
msgstr ""
+msgid "PlantUML"
+msgstr ""
+
msgid "Play"
msgstr ""
msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
msgstr ""
+msgid "Please select at least one filter to see results"
+msgstr ""
+
msgid "Please solve the reCAPTCHA"
msgstr ""
@@ -2540,6 +2738,12 @@ msgstr ""
msgid "Profiles|Account scheduled for removal."
msgstr ""
+msgid "Profiles|Change username"
+msgstr ""
+
+msgid "Profiles|Current path: %{path}"
+msgstr ""
+
msgid "Profiles|Delete Account"
msgstr ""
@@ -2558,9 +2762,21 @@ msgstr ""
msgid "Profiles|Invalid username"
msgstr ""
+msgid "Profiles|Path"
+msgstr ""
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr ""
+msgid "Profiles|Update username"
+msgstr ""
+
+msgid "Profiles|Username change failed - %{message}"
+msgstr ""
+
+msgid "Profiles|Username successfully changed"
+msgstr ""
+
msgid "Profiles|You don't have access to delete this user."
msgstr ""
@@ -2591,6 +2807,9 @@ msgstr ""
msgid "Project '%{project_name}' was successfully updated."
msgstr ""
+msgid "Project Badges"
+msgstr ""
+
msgid "Project access must be granted explicitly to each user."
msgstr ""
@@ -2618,15 +2837,6 @@ msgstr ""
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
-msgid "ProjectFeature|Disabled"
-msgstr ""
-
-msgid "ProjectFeature|Everyone with access"
-msgstr ""
-
-msgid "ProjectFeature|Only team members"
-msgstr ""
-
msgid "ProjectFileTree|Name"
msgstr ""
@@ -2663,6 +2873,9 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "PrometheusDashboard|Time"
+msgstr ""
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
@@ -2729,6 +2942,9 @@ msgstr ""
msgid "Promote"
msgstr ""
+msgid "Promote these project milestones into a group milestone."
+msgstr ""
+
msgid "Promote to Group Label"
msgstr ""
@@ -2756,12 +2972,18 @@ msgstr ""
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
+msgid "Raw"
+msgstr ""
+
msgid "Read more"
msgstr ""
msgid "Readme"
msgstr ""
+msgid "Real-time features"
+msgstr ""
+
msgid "RefSwitcher|Branches"
msgstr ""
@@ -2828,6 +3050,12 @@ msgstr ""
msgid "Resolve discussion"
msgstr ""
+msgid "Retry this job"
+msgstr ""
+
+msgid "Retry verification"
+msgstr ""
+
msgid "Reveal value"
msgid_plural "Reveal values"
msgstr[0] ""
@@ -2839,6 +3067,9 @@ msgstr ""
msgid "Revert this merge request"
msgstr ""
+msgid "Review"
+msgstr ""
+
msgid "Reviewing"
msgstr ""
@@ -2941,6 +3172,9 @@ msgstr ""
msgid "Set default and restrict visibility levels. Configure import sources and git access protocol."
msgstr ""
+msgid "Set max session time for web terminal."
+msgstr ""
+
msgid "Set notification email for abuse reports."
msgstr ""
@@ -2962,6 +3196,9 @@ msgstr ""
msgid "Setup a specific Runner automatically"
msgstr ""
+msgid "Share"
+msgstr ""
+
msgid "Show command"
msgstr ""
@@ -3141,6 +3378,9 @@ msgstr ""
msgid "Status"
msgstr ""
+msgid "Stop this environment"
+msgstr ""
+
msgid "Stopped"
msgstr ""
@@ -3371,6 +3611,15 @@ msgstr ""
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
+msgid "This job does not have a trace."
+msgstr ""
+
+msgid "This job has been canceled"
+msgstr ""
+
+msgid "This job has been skipped"
+msgstr ""
+
msgid "This job has not been triggered yet"
msgstr ""
@@ -3392,6 +3641,9 @@ msgstr ""
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
+msgid "This page will be removed in a future release."
+msgstr ""
+
msgid "This project"
msgstr ""
@@ -3615,6 +3867,9 @@ msgstr ""
msgid "Unstar"
msgstr ""
+msgid "Unverified"
+msgstr ""
+
msgid "Up to date"
msgstr ""
@@ -3633,6 +3888,12 @@ msgstr ""
msgid "Upvotes"
msgstr ""
+msgid "Usage statistics"
+msgstr ""
+
+msgid "Use group milestones to manage issues from multiple projects in the same milestone."
+msgstr ""
+
msgid "Use the following registration token during setup:"
msgstr ""
@@ -3645,6 +3906,18 @@ msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
msgstr ""
+msgid "Various container registry settings."
+msgstr ""
+
+msgid "Various email settings."
+msgstr ""
+
+msgid "Various settings that affect GitLab performance."
+msgstr ""
+
+msgid "Verified"
+msgstr ""
+
msgid "View and edit lines"
msgstr ""
@@ -3696,6 +3969,9 @@ msgstr ""
msgid "Web IDE"
msgstr ""
+msgid "Web terminal"
+msgstr ""
+
msgid "Wiki"
msgstr ""
@@ -3938,6 +4214,9 @@ msgid_plural "days"
msgstr[0] ""
msgstr[1] ""
+msgid "deploy token"
+msgstr ""
+
msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
@@ -3988,6 +4267,9 @@ msgstr ""
msgid "mrWidget|Closes"
msgstr ""
+msgid "mrWidget|Create an issue to resolve them later"
+msgstr ""
+
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
@@ -4081,6 +4363,9 @@ msgstr ""
msgid "mrWidget|There are merge conflicts"
msgstr ""
+msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
+msgstr ""
+
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr ""
diff --git a/qa/qa.rb b/qa/qa.rb
index 56a99c32b26..fff99a1d31b 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -31,6 +31,7 @@ module QA
autoload :Project, 'qa/factory/resource/project'
autoload :MergeRequest, 'qa/factory/resource/merge_request'
autoload :DeployKey, 'qa/factory/resource/deploy_key'
+ autoload :Branch, 'qa/factory/resource/branch'
autoload :SecretVariable, 'qa/factory/resource/secret_variable'
autoload :Runner, 'qa/factory/resource/runner'
autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token'
@@ -132,6 +133,7 @@ module QA
autoload :Repository, 'qa/page/project/settings/repository'
autoload :CICD, 'qa/page/project/settings/ci_cd'
autoload :DeployKeys, 'qa/page/project/settings/deploy_keys'
+ autoload :ProtectedBranches, 'qa/page/project/settings/protected_branches'
autoload :SecretVariables, 'qa/page/project/settings/secret_variables'
autoload :Runners, 'qa/page/project/settings/runners'
autoload :MergeRequest, 'qa/page/project/settings/merge_request'
diff --git a/qa/qa/factory/base.rb b/qa/qa/factory/base.rb
index afaa96b4541..7a532ce534b 100644
--- a/qa/qa/factory/base.rb
+++ b/qa/qa/factory/base.rb
@@ -22,7 +22,7 @@ module QA
factory.fabricate!(*args)
- return Factory::Product.populate!(factory)
+ break Factory::Product.populate!(factory)
end
end
diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb
new file mode 100644
index 00000000000..d0ef142e90d
--- /dev/null
+++ b/qa/qa/factory/resource/branch.rb
@@ -0,0 +1,73 @@
+module QA
+ module Factory
+ module Resource
+ class Branch < Factory::Base
+ attr_accessor :project, :branch_name, :allow_to_push, :protected
+
+ dependency Factory::Resource::Project, as: :project do |project|
+ project.name = 'protected-branch-project'
+ end
+
+ product :name do
+ Page::Project::Settings::Repository.act do
+ expand_protected_branches(&:last_branch_name)
+ end
+ end
+
+ product :push_allowance do
+ Page::Project::Settings::Repository.act do
+ expand_protected_branches(&:last_push_allowance)
+ end
+ end
+
+ def initialize
+ @branch_name = 'test/branch'
+ @allow_to_push = true
+ @protected = false
+ end
+
+ def fabricate!
+ project.visit!
+
+ Factory::Repository::Push.fabricate! do |resource|
+ resource.project = project
+ resource.file_name = 'kick-off.txt'
+ resource.commit_message = 'First commit'
+ end
+
+ branch = Factory::Repository::Push.fabricate! do |resource|
+ resource.project = project
+ resource.file_name = 'README.md'
+ resource.commit_message = 'Add readme'
+ resource.branch_name = "master:#{@branch_name}"
+ end
+
+ Page::Project::Show.act { wait_for_push }
+
+ # The upcoming process will make it access the Protected Branches page,
+ # select the already created branch and protect it according
+ # to `allow_to_push` variable.
+ return branch unless @protected
+
+ Page::Menu::Side.act do
+ click_repository_settings
+ end
+
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_protected_branches do |page|
+ page.select_branch(branch_name)
+
+ if allow_to_push
+ page.allow_devs_and_masters_to_push
+ else
+ page.allow_no_one_to_push
+ end
+
+ page.protect_branch
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index b3150e8f3fa..2f9f06ba277 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -1,11 +1,14 @@
require 'cgi'
require 'uri'
+require 'open3'
module QA
module Git
class Repository
include Scenario::Actable
+ attr_reader :push_error
+
def self.perform(*args)
Dir.mktmpdir do |dir|
Dir.chdir(dir) { super }
@@ -65,7 +68,8 @@ module QA
end
def push_changes(branch = 'master')
- `git push #{@uri.to_s} #{branch} #{suppress_output}`
+ # capture3 returns stdout, stderr and status.
+ _, @push_error, _ = Open3.capture3("git push #{@uri} #{branch} #{suppress_output}")
end
def commits
diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb
index d215518d316..89125bd2e59 100644
--- a/qa/qa/page/group/show.rb
+++ b/qa/qa/page/group/show.rb
@@ -29,7 +29,7 @@ module QA
filter_by_name(name)
wait(reload: false) do
- return false if page.has_content?('Sorry, no groups or projects matched your search')
+ break false if page.has_content?('Sorry, no groups or projects matched your search')
page.has_link?(name)
end
diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb
index b183552d46c..ec61c47b3bb 100644
--- a/qa/qa/page/project/pipeline/show.rb
+++ b/qa/qa/page/project/pipeline/show.rb
@@ -20,14 +20,14 @@ module QA::Page
def running?
within('.ci-header-container') do
- return page.has_content?('running')
+ page.has_content?('running')
end
end
def has_build?(name, status: :success)
within('.pipeline-graph') do
within('.ci-job-component', text: name) do
- return has_selector?(".ci-status-icon-#{status}")
+ has_selector?(".ci-status-icon-#{status}")
end
end
end
diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb
new file mode 100644
index 00000000000..f3563401124
--- /dev/null
+++ b/qa/qa/page/project/settings/protected_branches.rb
@@ -0,0 +1,69 @@
+module QA
+ module Page
+ module Project
+ module Settings
+ class ProtectedBranches < Page::Base
+ view 'app/views/projects/protected_branches/shared/_dropdown.html.haml' do
+ element :protected_branch_select
+ element :protected_branch_dropdown
+ end
+
+ view 'app/views/projects/protected_branches/_create_protected_branch.html.haml' do
+ element :allowed_to_push_select
+ element :allowed_to_push_dropdown
+ end
+
+ view 'app/views/projects/protected_branches/shared/_branches_list.html.haml' do
+ element :protected_branches_list
+ end
+
+ view 'app/views/projects/protected_branches/shared/_protected_branch.html.haml' do
+ element :protected_branch_name
+ end
+
+ def select_branch(branch_name)
+ click_element :protected_branch_select
+
+ within_element(:protected_branch_dropdown) do
+ click_on branch_name
+ end
+ end
+
+ def allow_no_one_to_push
+ allow_to_push('No one')
+ end
+
+ def allow_devs_and_masters_to_push
+ allow_to_push('Developers + Masters')
+ end
+
+ def protect_branch
+ click_on 'Protect'
+ end
+
+ def last_branch_name
+ within_element(:protected_branches_list) do
+ all('.qa-protected-branch-name').last
+ end
+ end
+
+ def last_push_allowance
+ within_element(:protected_branches_list) do
+ all('.qa-allowed-to-push').last
+ end
+ end
+
+ private
+
+ def allow_to_push(text)
+ click_element :allowed_to_push_select
+
+ within_element(:allowed_to_push_dropdown) do
+ click_on text
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb
index 22362164a1a..30900e74e90 100644
--- a/qa/qa/page/project/settings/repository.rb
+++ b/qa/qa/page/project/settings/repository.rb
@@ -14,6 +14,12 @@ module QA
DeployKeys.perform(&block)
end
end
+
+ def expand_protected_branches(&block)
+ expand_section('Protected Branches') do
+ ProtectedBranches.perform(&block)
+ end
+ end
end
end
end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 0c7ad46d36b..c7e7ece792d 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -21,6 +21,11 @@ module QA
element :new_issue_link, "link_to 'New issue', new_project_issue_path(@project)"
end
+ view 'app/views/shared/_ref_switcher.html.haml' do
+ element :branches_select
+ element :branches_dropdown
+ end
+
def choose_repository_clone_http
choose_repository_clone('HTTP', 'http')
end
@@ -44,6 +49,18 @@ module QA
find('.qa-project-name').text
end
+ def switch_to_branch(branch_name)
+ find_element(:branches_select).click
+
+ within_element(:branches_dropdown) do
+ click_on branch_name
+ end
+ end
+
+ def last_commit_content
+ find_element(:commit_content).text
+ end
+
def new_merge_request
wait(reload: true) do
has_css?(element_selector_css(:create_merge_request))
diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb
index 341998af160..d21a9d52997 100644
--- a/qa/qa/scenario/template.rb
+++ b/qa/qa/scenario/template.rb
@@ -4,7 +4,7 @@ module QA
def self.perform(*args)
new.tap do |scenario|
yield scenario if block_given?
- return scenario.perform(*args)
+ break scenario.perform(*args)
end
end
diff --git a/qa/qa/specs/features/repository/protected_branches_spec.rb b/qa/qa/specs/features/repository/protected_branches_spec.rb
new file mode 100644
index 00000000000..88fa4994e32
--- /dev/null
+++ b/qa/qa/specs/features/repository/protected_branches_spec.rb
@@ -0,0 +1,63 @@
+module QA
+ feature 'branch protection support', :core do
+ given(:branch_name) { 'protected-branch' }
+ given(:commit_message) { 'Protected push commit message' }
+ given(:project) do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'protected-branch-project'
+ end
+ end
+ given(:location) do
+ Page::Project::Show.act do
+ choose_repository_clone_http
+ repository_location
+ end
+ end
+
+ before do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+ end
+
+ scenario 'user is able to protect a branch' do
+ protected_branch = Factory::Resource::Branch.fabricate! do |resource|
+ resource.branch_name = branch_name
+ resource.project = project
+ resource.allow_to_push = true
+ resource.protected = true
+ end
+
+ expect(protected_branch.name).to have_content(branch_name)
+ expect(protected_branch.push_allowance).to have_content('Developers + Masters')
+ end
+
+ scenario 'users without authorization cannot push to protected branch' do
+ Factory::Resource::Branch.fabricate! do |resource|
+ resource.branch_name = branch_name
+ resource.project = project
+ resource.allow_to_push = false
+ resource.protected = true
+ end
+
+ project.visit!
+
+ Git::Repository.perform do |repository|
+ repository.location = location
+ repository.use_default_credentials
+
+ repository.act do
+ clone
+ configure_identity('GitLab QA', 'root@gitlab.com')
+ checkout('protected-branch')
+ commit_file('README.md', 'readme content', 'Add a readme')
+ push_changes('protected-branch')
+ end
+
+ expect(repository.push_error)
+ .to match(/remote\: GitLab\: You are not allowed to push code to protected branches on this project/)
+ expect(repository.push_error)
+ .to match(/\[remote rejected\] #{branch_name} -> #{branch_name} \(pre-receive hook declined\)/)
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/avoid_break_from_strong_memoize.rb b/rubocop/cop/avoid_break_from_strong_memoize.rb
new file mode 100644
index 00000000000..9b436118db3
--- /dev/null
+++ b/rubocop/cop/avoid_break_from_strong_memoize.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ # Checks for break inside strong_memoize blocks.
+ # For more information see: https://gitlab.com/gitlab-org/gitlab-ce/issues/42889
+ #
+ # @example
+ # # bad
+ # strong_memoize(:result) do
+ # break if something
+ #
+ # do_an_heavy_calculation
+ # end
+ #
+ # # good
+ # strong_memoize(:result) do
+ # next if something
+ #
+ # do_an_heavy_calculation
+ # end
+ #
+ class AvoidBreakFromStrongMemoize < RuboCop::Cop::Cop
+ MSG = 'Do not use break inside strong_memoize, use next instead.'
+
+ def on_block(node)
+ block_body = node.body
+
+ return unless block_body
+ return unless node.method_name == :strong_memoize
+
+ block_body.each_node(:break) do |break_node|
+ next if container_block_for(break_node) != node
+
+ add_offense(break_node)
+ end
+ end
+
+ private
+
+ def container_block_for(current_node)
+ current_node = current_node.parent until current_node.type == :block && current_node.method_name == :strong_memoize
+
+ current_node
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/avoid_return_from_blocks.rb b/rubocop/cop/avoid_return_from_blocks.rb
new file mode 100644
index 00000000000..40b2aed019f
--- /dev/null
+++ b/rubocop/cop/avoid_return_from_blocks.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ # Checks for return inside blocks.
+ # For more information see: https://gitlab.com/gitlab-org/gitlab-ce/issues/42889
+ #
+ # @example
+ # # bad
+ # call do
+ # return if something
+ #
+ # do_something_else
+ # end
+ #
+ # # good
+ # call do
+ # break if something
+ #
+ # do_something_else
+ # end
+ #
+ class AvoidReturnFromBlocks < RuboCop::Cop::Cop
+ MSG = 'Do not return from a block, use next or break instead.'
+ DEF_METHODS = %i[define_method lambda].freeze
+ WHITELISTED_METHODS = %i[each each_filename times loop].freeze
+
+ def on_block(node)
+ block_body = node.body
+
+ return unless block_body
+ return unless top_block?(node)
+
+ block_body.each_node(:return) do |return_node|
+ next if parent_blocks(node, return_node).all?(&method(:whitelisted?))
+
+ add_offense(return_node)
+ end
+ end
+
+ private
+
+ def top_block?(node)
+ current_node = node
+ top_block = nil
+
+ while current_node && current_node.type != :def
+ top_block = current_node if current_node.type == :block
+ current_node = current_node.parent
+ end
+
+ top_block == node
+ end
+
+ def parent_blocks(node, current_node)
+ blocks = []
+
+ until node == current_node || def?(current_node)
+ blocks << current_node if current_node.type == :block
+ current_node = current_node.parent
+ end
+
+ blocks << node if node == current_node && !def?(node)
+ blocks
+ end
+
+ def def?(node)
+ node.type == :def || node.type == :defs ||
+ (node.type == :block && DEF_METHODS.include?(node.method_name))
+ end
+
+ def whitelisted?(block_node)
+ WHITELISTED_METHODS.include?(block_node.method_name)
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/gitlab/has_many_through_scope.rb b/rubocop/cop/gitlab/has_many_through_scope.rb
deleted file mode 100644
index 770a2a0529f..00000000000
--- a/rubocop/cop/gitlab/has_many_through_scope.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-require 'gitlab/styles/rubocop/model_helpers'
-
-module RuboCop
- module Cop
- module Gitlab
- class HasManyThroughScope < RuboCop::Cop::Cop
- include ::Gitlab::Styles::Rubocop::ModelHelpers
-
- MSG = 'Always provide an explicit scope calling auto_include(false) when using has_many :through'.freeze
-
- def_node_search :through?, <<~PATTERN
- (pair (sym :through) _)
- PATTERN
-
- def_node_matcher :has_many_through?, <<~PATTERN
- (send nil? :has_many ... #through?)
- PATTERN
-
- def_node_search :disables_auto_include?, <<~PATTERN
- (send _ :auto_include false)
- PATTERN
-
- def_node_matcher :scope_disables_auto_include?, <<~PATTERN
- (block (send nil? :lambda) _ #disables_auto_include?)
- PATTERN
-
- def on_send(node)
- return unless in_model?(node)
- return unless has_many_through?(node)
-
- target = node
- scope_argument = node.children[3]
-
- if scope_argument.children[0].children.last == :lambda
- return if scope_disables_auto_include?(scope_argument)
-
- target = scope_argument
- end
-
- add_offense(target, location: :expression)
- end
- end
- end
- end
-end
diff --git a/rubocop/cop/migration/safer_boolean_column.rb b/rubocop/cop/migration/safer_boolean_column.rb
index dc5c55df6fb..a7d922c752f 100644
--- a/rubocop/cop/migration/safer_boolean_column.rb
+++ b/rubocop/cop/migration/safer_boolean_column.rb
@@ -61,7 +61,7 @@ module RuboCop
return true unless opts
each_hash_node_pair(opts) do |key, value|
- return value == 'nil' if key == :default
+ break value == 'nil' if key == :default
end
end
@@ -69,7 +69,7 @@ module RuboCop
return true unless opts
each_hash_node_pair(opts) do |key, value|
- return value != 'false' if key == :null
+ break value != 'false' if key == :null
end
end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index c2254332e7d..f05990232ab 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,9 +1,10 @@
# rubocop:disable Naming/FileName
-require_relative 'cop/gitlab/has_many_through_scope'
-require_relative 'cop/gitlab/httparty'
require_relative 'cop/gitlab/module_with_instance_variables'
require_relative 'cop/gitlab/predicate_memoization'
+require_relative 'cop/gitlab/httparty'
require_relative 'cop/include_sidekiq_worker'
+require_relative 'cop/avoid_return_from_blocks'
+require_relative 'cop/avoid_break_from_strong_memoize'
require_relative 'cop/line_break_around_conditional_block'
require_relative 'cop/migration/add_column'
require_relative 'cop/migration/add_concurrent_foreign_key'
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index b9a979044fe..f677cec3408 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -190,7 +190,10 @@ describe Projects::JobsController do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq job.id
expect(json_response['status']).to eq job.status
- expect(json_response['html']).to be_nil
+ end
+
+ it 'returns no job log message' do
+ expect(json_response['html']).to eq('No job log')
end
end
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index c3b71458e38..a102a3a3c8c 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -40,6 +40,30 @@ describe Projects::RepositoriesController do
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
end
+ it 'handles legacy queries with the ref specified as ref in params' do
+ get :archive, namespace_id: project.namespace, project_id: project, ref: 'feature', format: 'zip'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(assigns(:ref)).to eq('feature')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
+ end
+
+ it 'handles legacy queries with the ref specified as id in params' do
+ get :archive, namespace_id: project.namespace, project_id: project, id: 'feature', format: 'zip'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(assigns(:ref)).to eq('feature')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
+ end
+
+ it 'prioritizes the id param over the ref param when both are specified' do
+ get :archive, namespace_id: project.namespace, project_id: project, id: 'feature', ref: 'feature_conflict', format: 'zip'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(assigns(:ref)).to eq('feature')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
+ end
+
context "when the service raises an error" do
before do
allow(Gitlab::Workhorse).to receive(:send_git_archive).and_raise("Archive failed")
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index e1fafa71d5c..4acc008ed38 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -62,6 +62,7 @@ FactoryBot.define do
end
trait :pending do
+ queued_at 'Di 29. Okt 09:50:59 CET 2013'
status 'pending'
end
@@ -242,5 +243,10 @@ FactoryBot.define do
failed
failure_reason 1
end
+
+ trait :api_failure do
+ failed
+ failure_reason 2
+ end
end
end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index d4c44c1adf9..4d31123a699 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -237,6 +237,22 @@ describe 'Issue Boards', :js do
end
context 'labels' do
+ it 'shows current labels when editing' do
+ click_card(card)
+
+ page.within('.labels') do
+ click_link 'Edit'
+
+ wait_for_requests
+
+ page.within('.value') do
+ expect(page).to have_selector('.label', count: 2)
+ expect(page).to have_content(development.title)
+ expect(page).to have_content(stretch.title)
+ end
+ end
+ end
+
it 'adds a single label' do
click_card(card)
@@ -296,7 +312,9 @@ describe 'Issue Boards', :js do
wait_for_requests
- click_link stretch.title
+ within('.dropdown-menu-labels') do
+ click_link stretch.title
+ end
wait_for_requests
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 27551bb70ee..830c794376d 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -5,9 +5,9 @@ feature 'Issue Sidebar' do
let(:group) { create(:group, :nested) }
let(:project) { create(:project, :public, namespace: group) }
- let(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
let!(:label) { create(:label, project: project, title: 'bug') }
+ let(:issue) { create(:labeled_issue, project: project, labels: [label]) }
let!(:xss_label) { create(:label, project: project, title: '&lt;script&gt;alert("xss");&lt;&#x2F;script&gt;') }
before do
@@ -112,11 +112,18 @@ feature 'Issue Sidebar' do
context 'editing issue labels', :js do
before do
+ issue.update_attributes(labels: [label])
page.within('.block.labels') do
find('.edit-link').click
end
end
+ it 'shows the current set of labels' do
+ page.within('.issuable-show-labels') do
+ expect(page).to have_content label.title
+ end
+ end
+
it 'shows option to create a project label' do
page.within('.block.labels') do
expect(page).to have_content 'Create project'
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index 8e6493bbd93..4a44ec302fc 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -14,7 +14,7 @@ feature 'Manually create a todo item from issue', :js do
it 'creates todo when clicking button' do
page.within '.issuable-sidebar' do
click_button 'Add todo'
- expect(page).to have_content 'Mark done'
+ expect(page).to have_content 'Mark todo as done'
end
page.within '.header-content .todos-count' do
@@ -31,7 +31,7 @@ feature 'Manually create a todo item from issue', :js do
it 'marks a todo as done' do
page.within '.issuable-sidebar' do
click_button 'Add todo'
- click_button 'Mark done'
+ click_button 'Mark todo as done'
end
expect(page).to have_selector('.todos-count', visible: false)
diff --git a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
index 565e375600b..3b6fffb7abd 100644
--- a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
+++ b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
@@ -27,6 +27,23 @@ describe 'Merge request > User scrolls to note on load', :js do
expect(fragment_position_top).to be < (page_scroll_y + page_height)
end
+ it 'renders un-collapsed notes with diff' do
+ page.current_window.resize_to(1000, 1000)
+
+ visit "#{project_merge_request_path(project, merge_request)}#{fragment_id}"
+
+ page.execute_script "window.scrollTo(0,0)"
+
+ note_element = find(fragment_id)
+ note_container = note_element.ancestor('.js-toggle-container')
+
+ expect(note_element.visible?).to eq true
+
+ page.within note_container do
+ expect(page).not_to have_selector('.js-error-lazy-load-diff')
+ end
+ end
+
it 'expands collapsed notes' do
visit "#{project_merge_request_path(project, merge_request)}#{collapsed_fragment_id}"
note_element = find(collapsed_fragment_id)
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index 4d47cdb500c..dfe8e02dce0 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -33,7 +33,7 @@ feature 'Gcp Cluster', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
- click_link 'Create on GKE'
+ click_link 'Create on Google Kubernetes Engine'
end
context 'when user filled form with valid parameters' do
@@ -139,7 +139,7 @@ feature 'Gcp Cluster', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
- click_link 'Create on GKE'
+ click_link 'Create on Google Kubernetes Engine'
fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
fill_in 'cluster_name', with: 'dev-cluster'
@@ -159,7 +159,7 @@ feature 'Gcp Cluster', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
- click_link 'Create on GKE'
+ click_link 'Create on Google Kubernetes Engine'
fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
fill_in 'cluster_name', with: 'dev-cluster'
@@ -177,7 +177,7 @@ feature 'Gcp Cluster', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
- click_link 'Create on GKE'
+ click_link 'Create on Google Kubernetes Engine'
end
it 'user sees a login page' do
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index bd9f7745cf8..a251a2f4e52 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -83,7 +83,7 @@ feature 'Clusters', :js do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
- click_link 'Create on GKE'
+ click_link 'Create on Google Kubernetes Engine'
end
it 'user sees a login page' do
diff --git a/spec/features/projects/files/user_uploads_files_spec.rb b/spec/features/projects/files/user_uploads_files_spec.rb
index 7a1e3a8bcce..8b212faa29d 100644
--- a/spec/features/projects/files/user_uploads_files_spec.rb
+++ b/spec/features/projects/files/user_uploads_files_spec.rb
@@ -7,11 +7,11 @@ describe 'Projects > Files > User uploads files' do
"You're not allowed to make changes to this project directly. "\
"A fork of this project has been created that you can make changes in, so you can submit a merge request."
end
- let(:project) { create(:project, :repository, name: 'Shop') }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository, name: 'Shop', creator: user) }
let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') }
let(:project_tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) }
let(:project2_tree_path_root_ref) { project_tree_path(project2, project2.repository.root_ref) }
- let(:user) { project.creator }
before do
project.add_master(user)
diff --git a/spec/features/projects/jobs/permissions_spec.rb b/spec/features/projects/jobs/permissions_spec.rb
new file mode 100644
index 00000000000..31abadf9bd6
--- /dev/null
+++ b/spec/features/projects/jobs/permissions_spec.rb
@@ -0,0 +1,130 @@
+require 'spec_helper'
+
+describe 'Project Jobs Permissions' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group, name: 'some group') }
+ let(:project) { create(:project, :repository, namespace: group) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') }
+ let!(:job) { create(:ci_build, :running, :coverage, :trace_artifact, pipeline: pipeline) }
+
+ before do
+ sign_in(user)
+
+ project.enable_ci
+ end
+
+ describe 'jobs pages' do
+ shared_examples 'recent job page details responds with status' do |status|
+ before do
+ visit project_job_path(project, job)
+ end
+
+ it { expect(status_code).to eq(status) }
+ end
+
+ shared_examples 'project jobs page responds with status' do |status|
+ before do
+ visit project_jobs_path(project)
+ end
+
+ it { expect(status_code).to eq(status) }
+ end
+
+ context 'when public access for jobs is disabled' do
+ before do
+ project.update(public_builds: false)
+ end
+
+ context 'when user is a guest' do
+ before do
+ project.add_guest(user)
+ end
+
+ it_behaves_like 'recent job page details responds with status', 404
+ it_behaves_like 'project jobs page responds with status', 404
+ end
+
+ context 'when project is internal' do
+ before do
+ project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it_behaves_like 'recent job page details responds with status', 404
+ it_behaves_like 'project jobs page responds with status', 404
+ end
+ end
+
+ context 'when public access for jobs is enabled' do
+ before do
+ project.update(public_builds: true)
+ end
+
+ context 'when project is internal' do
+ before do
+ project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it_behaves_like 'recent job page details responds with status', 200 do
+ it 'renders job details', :js do
+ expect(page).to have_content "Job ##{job.id}"
+ expect(page).to have_css '#build-trace'
+ end
+ end
+
+ it_behaves_like 'project jobs page responds with status', 200 do
+ it 'renders job' do
+ page.within('.build') do
+ expect(page).to have_content("##{job.id}")
+ .and have_content(job.sha[0..7])
+ .and have_content(job.ref)
+ .and have_content(job.name)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe 'artifacts page' do
+ context 'when recent job has artifacts available' do
+ before do
+ artifacts = Rails.root.join('spec/fixtures/ci_build_artifacts.zip')
+ archive = fixture_file_upload(artifacts, 'application/zip')
+
+ job.update_attributes(legacy_artifacts_file: archive)
+ end
+
+ context 'when public access for jobs is disabled' do
+ before do
+ project.update(public_builds: false)
+ end
+
+ context 'when user with guest role' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'responds with 404 status' do
+ visit download_project_job_artifacts_path(project, job)
+
+ expect(status_code).to eq(404)
+ end
+ end
+
+ context 'when user with reporter role' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'starts download artifact' do
+ visit download_project_job_artifacts_path(project, job)
+
+ expect(status_code).to eq(200)
+ expect(page.response_headers['Content-Type']).to eq 'application/zip'
+ expect(page.response_headers['Content-Transfer-Encoding']).to eq 'binary'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index a460024542c..a00db6dd161 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -491,16 +491,18 @@ feature 'Jobs' do
end
end
- describe "POST /:project/jobs/:id/retry" do
+ describe "POST /:project/jobs/:id/retry", :js do
context "Job from project", :js do
before do
job.run!
+ job.cancel!
visit project_job_path(project, job)
- find('.js-cancel-job').click()
+ wait_for_requests
+
find('.js-retry-button').click
end
- it 'shows the right status and buttons', :js do
+ it 'shows the right status and buttons' do
page.within('aside.right-sidebar') do
expect(page).to have_content 'Cancel'
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 6e63e0f0b49..705ba78a0b7 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -517,7 +517,7 @@ describe 'Pipelines', :js do
end
it 'creates a new pipeline' do
- expect { click_on 'Create pipeline' }
+ expect { click_on 'Run pipeline' }
.to change { Ci::Pipeline.count }.by(1)
expect(Ci::Pipeline.last).to be_web
@@ -526,7 +526,7 @@ describe 'Pipelines', :js do
context 'without gitlab-ci.yml' do
before do
- click_on 'Create pipeline'
+ click_on 'Run pipeline'
end
it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
@@ -539,7 +539,7 @@ describe 'Pipelines', :js do
click_link 'master'
end
- expect { click_on 'Create pipeline' }
+ expect { click_on 'Run pipeline' }
.to change { Ci::Pipeline.count }.by(1)
end
end
@@ -557,7 +557,7 @@ describe 'Pipelines', :js do
it 'has field to add a new pipeline' do
expect(page).to have_selector('.js-branch-select')
expect(find('.js-branch-select')).to have_content project.default_branch
- expect(page).to have_content('Create for')
+ expect(page).to have_content('Run on')
end
end
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
index d96c7e655ba..b242e41df1c 100644
--- a/spec/features/projects/tree/create_directory_spec.rb
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -44,6 +44,8 @@ feature 'Multi-file editor new directory', :js do
wait_for_requests
+ click_button 'Stage all'
+
fill_in('commit-message', with: 'commit message ide')
click_button('Commit')
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
index a4cbd5cf766..7d65456e049 100644
--- a/spec/features/projects/tree/create_file_spec.rb
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -34,6 +34,8 @@ feature 'Multi-file editor new file', :js do
wait_for_requests
+ click_button 'Stage all'
+
fill_in('commit-message', with: 'commit message ide')
click_button('Commit')
diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb
index 375bcc9087e..796d40cb625 100644
--- a/spec/finders/group_descendants_finder_spec.rb
+++ b/spec/finders/group_descendants_finder_spec.rb
@@ -35,15 +35,6 @@ describe GroupDescendantsFinder do
expect(finder.execute).to contain_exactly(project)
end
- it 'does not include projects shared with the group' do
- project = create(:project, namespace: group)
- other_project = create(:project)
- other_project.project_group_links.create(group: group,
- group_access: ProjectGroupLink::MASTER)
-
- expect(finder.execute).to contain_exactly(project)
- end
-
context 'when archived is `true`' do
let(:params) { { archived: 'true' } }
diff --git a/spec/fixtures/trace/sample_trace b/spec/fixtures/trace/sample_trace
index 55fcb9d2756..c65cf05d5ca 100644
--- a/spec/fixtures/trace/sample_trace
+++ b/spec/fixtures/trace/sample_trace
@@ -1,24 +1,24 @@
-Running with gitlab-runner 10.4.0 (857480b6)
- on docker-auto-scale-com (9a6801bd)
-Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6 ...
-Starting service postgres:9.2 ...
-Pulling docker image postgres:9.2 ...
-Using docker image postgres:9.2 ID=sha256:18cdbca56093c841d28e629eb8acd4224afe0aa4c57c839351fc181888b8a470 for postgres service...
+Running with gitlab-runner 10.6.0 (a3543a27)
+ on docker-auto-scale-com 30d62d59
+Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.16-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6 ...
+Starting service mysql:latest ...
+Pulling docker image mysql:latest ...
+Using docker image sha256:5195076672a7e30525705a18f7d352c920bbd07a5ae72b30e374081fe660a011 for mysql:latest ...
Starting service redis:alpine ...
Pulling docker image redis:alpine ...
-Using docker image redis:alpine ID=sha256:cb1ec54b370d4a91dff57d00f91fd880dc710160a58440adaa133e0f84ae999d for redis service...
+Using docker image sha256:98bd7cfc43b8ef0ff130465e3d5427c0771002c2f35a6a9b62cb2d04602bed0a for redis:alpine ...
Waiting for services to be up and running...
-Using docker image sha256:3006a02a5a6f0a116358a13bbc46ee46fb2471175efd5b7f9b1c22345ec2a8e9 for predefined container...
-Pulling docker image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6 ...
-Using docker image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6 ID=sha256:1f59be408f12738509ffe4177d65e9de6391f32461de83d9d45f58517b30af99 for build container...
-section_start:1517486886:prepare_script
-Running on runner-9a6801bd-project-13083-concurrent-0 via runner-9a6801bd-gsrm-1517484168-a8449153...
-section_end:1517486887:prepare_script
-section_start:1517486887:get_sources
-Fetching changes for 42624-gitaly-bundle-isolation-not-working-in-ci with git depth set to 20...
+Pulling docker image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.16-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6 ...
+Using docker image sha256:1b06077bb03d9d42d801b53f45701bb6a7e862ca02e1e75f30ca7fcf1270eb02 for dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.16-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6 ...
+section_start:1522927103:prepare_script
+Running on runner-30d62d59-project-13083-concurrent-0 via runner-30d62d59-prm-1522922015-ddc29478...
+section_end:1522927104:prepare_script
+section_start:1522927104:get_sources
+Fetching changes for master with git depth set to 20...
Removing .gitlab_shell_secret
Removing .gitlab_workhorse_secret
Removing .yarn-cache/
+Removing builds/2018_04/
Removing config/database.yml
Removing config/gitlab.yml
Removing config/redis.cache.yml
@@ -26,1160 +26,3420 @@ Removing config/redis.queues.yml
Removing config/redis.shared_state.yml
Removing config/resque.yml
Removing config/secrets.yml
-Removing coverage/
-Removing knapsack/
Removing log/api_json.log
Removing log/application.log
Removing log/gitaly-test.log
-Removing log/githost.log
Removing log/grpc.log
Removing log/test_json.log
-Removing node_modules/
-Removing public/assets/
-Removing rspec_flaky/
-Removing shared/tmp/
Removing tmp/tests/
Removing vendor/ruby/
-HEAD is now at 4cea24f Converted todos.js to axios
+HEAD is now at b7cbff3d Add `direct_upload` setting for artifacts
From https://gitlab.com/gitlab-org/gitlab-ce
- * [new branch] 42624-gitaly-bundle-isolation-not-working-in-ci -> origin/42624-gitaly-bundle-isolation-not-working-in-ci
-Checking out f42a5e24 as 42624-gitaly-bundle-isolation-not-working-in-ci...
+ 2dbcb9cb..641bb13b master -> origin/master
+Checking out 21488c74 as master...
Skipping Git submodules setup
-section_end:1517486896:get_sources
-section_start:1517486896:restore_cache
+section_end:1522927113:get_sources
+section_start:1522927113:restore_cache
Checking cache for ruby-2.3.6-with-yarn...
Downloading cache.zip from http://runners-cache-5-internal.gitlab.com:444/runner/project/13083/ruby-2.3.6-with-yarn
Successfully extracted cache
-section_end:1517486919:restore_cache
-section_start:1517486919:download_artifacts
-Downloading artifacts for retrieve-tests-metadata (50551658)...
-Downloading artifacts from coordinator... ok  id=50551658 responseStatus=200 OK token=HhF7y_1X
-Downloading artifacts for compile-assets (50551659)...
-Downloading artifacts from coordinator... ok  id=50551659 responseStatus=200 OK token=wTz6JrCP
-Downloading artifacts for setup-test-env (50551660)...
-Downloading artifacts from coordinator... ok  id=50551660 responseStatus=200 OK token=DTGgeVF5
+section_end:1522927128:restore_cache
+section_start:1522927128:download_artifacts
+Downloading artifacts for retrieve-tests-metadata (61303215)...
+Downloading artifacts from coordinator... ok  id=61303215 responseStatus=200 OK token=AdWPNg2R
+Downloading artifacts for compile-assets (61303216)...
+Downloading artifacts from coordinator... ok  id=61303216 responseStatus=200 OK token=iy2yYbq8
+Downloading artifacts for setup-test-env (61303217)...
+Downloading artifacts from coordinator... ok  id=61303217 responseStatus=200 OK token=ur1g79-4
WARNING: tmp/tests/gitlab-shell/.gitlab_shell_secret: chmod tmp/tests/gitlab-shell/.gitlab_shell_secret: no such file or directory (suppressing repeats)
-section_end:1517486934:download_artifacts
-section_start:1517486934:build_script
+section_end:1522927141:download_artifacts
+section_start:1522927141:build_script
$ bundle --version
Bundler version 1.16.1
+$ date
+Thu Apr 5 11:19:01 UTC 2018
$ source scripts/utils.sh
+$ date
+Thu Apr 5 11:19:01 UTC 2018
$ source scripts/prepare_build.sh
The Gemfile's dependencies are satisfied
-Successfully installed knapsack-1.15.0
+Successfully installed knapsack-1.16.0
1 gem installed
-NOTICE: database "gitlabhq_test" does not exist, skipping
-DROP DATABASE
-CREATE DATABASE
-CREATE ROLE
-GRANT
-- enable_extension("plpgsql")
- -> 0.0156s
+ -> 0.0010s
-- enable_extension("pg_trgm")
- -> 0.0156s
+ -> 0.0000s
-- create_table("abuse_reports", {:force=>:cascade})
- -> 0.0119s
+ -> 0.0401s
-- create_table("appearances", {:force=>:cascade})
- -> 0.0065s
+ -> 0.1035s
-- create_table("application_settings", {:force=>:cascade})
- -> 0.0382s
+ -> 0.0871s
-- create_table("audit_events", {:force=>:cascade})
- -> 0.0056s
+ -> 0.0539s
-- add_index("audit_events", ["entity_id", "entity_type"], {:name=>"index_audit_events_on_entity_id_and_entity_type", :using=>:btree})
- -> 0.0040s
+ -> 0.0647s
-- create_table("award_emoji", {:force=>:cascade})
- -> 0.0058s
+ -> 0.0134s
-- add_index("award_emoji", ["awardable_type", "awardable_id"], {:name=>"index_award_emoji_on_awardable_type_and_awardable_id", :using=>:btree})
- -> 0.0068s
+ -> 0.0074s
-- add_index("award_emoji", ["user_id", "name"], {:name=>"index_award_emoji_on_user_id_and_name", :using=>:btree})
- -> 0.0043s
+ -> 0.0072s
+-- create_table("badges", {:force=>:cascade})
+ -> 0.0122s
+-- add_index("badges", ["group_id"], {:name=>"index_badges_on_group_id", :using=>:btree})
+ -> 0.0086s
+-- add_index("badges", ["project_id"], {:name=>"index_badges_on_project_id", :using=>:btree})
+ -> 0.0069s
-- create_table("boards", {:force=>:cascade})
- -> 0.0049s
+ -> 0.0075s
+-- add_index("boards", ["group_id"], {:name=>"index_boards_on_group_id", :using=>:btree})
+ -> 0.0050s
-- add_index("boards", ["project_id"], {:name=>"index_boards_on_project_id", :using=>:btree})
- -> 0.0056s
+ -> 0.0051s
-- create_table("broadcast_messages", {:force=>:cascade})
- -> 0.0056s
+ -> 0.0082s
-- add_index("broadcast_messages", ["starts_at", "ends_at", "id"], {:name=>"index_broadcast_messages_on_starts_at_and_ends_at_and_id", :using=>:btree})
- -> 0.0041s
+ -> 0.0063s
-- create_table("chat_names", {:force=>:cascade})
- -> 0.0056s
+ -> 0.0084s
-- add_index("chat_names", ["service_id", "team_id", "chat_id"], {:name=>"index_chat_names_on_service_id_and_team_id_and_chat_id", :unique=>true, :using=>:btree})
- -> 0.0039s
+ -> 0.0088s
-- add_index("chat_names", ["user_id", "service_id"], {:name=>"index_chat_names_on_user_id_and_service_id", :unique=>true, :using=>:btree})
- -> 0.0036s
+ -> 0.0077s
-- create_table("chat_teams", {:force=>:cascade})
- -> 0.0068s
+ -> 0.0120s
-- add_index("chat_teams", ["namespace_id"], {:name=>"index_chat_teams_on_namespace_id", :unique=>true, :using=>:btree})
- -> 0.0098s
+ -> 0.0135s
-- create_table("ci_build_trace_section_names", {:force=>:cascade})
- -> 0.0048s
+ -> 0.0125s
-- add_index("ci_build_trace_section_names", ["project_id", "name"], {:name=>"index_ci_build_trace_section_names_on_project_id_and_name", :unique=>true, :using=>:btree})
- -> 0.0035s
+ -> 0.0087s
-- create_table("ci_build_trace_sections", {:force=>:cascade})
- -> 0.0040s
+ -> 0.0094s
-- add_index("ci_build_trace_sections", ["build_id", "section_name_id"], {:name=>"index_ci_build_trace_sections_on_build_id_and_section_name_id", :unique=>true, :using=>:btree})
- -> 0.0035s
+ -> 0.0916s
-- add_index("ci_build_trace_sections", ["project_id"], {:name=>"index_ci_build_trace_sections_on_project_id", :using=>:btree})
- -> 0.0033s
+ -> 0.0089s
+-- add_index("ci_build_trace_sections", ["section_name_id"], {:name=>"index_ci_build_trace_sections_on_section_name_id", :using=>:btree})
+ -> 0.0132s
-- create_table("ci_builds", {:force=>:cascade})
- -> 0.0062s
+ -> 0.0140s
+-- add_index("ci_builds", ["artifacts_expire_at"], {:name=>"index_ci_builds_on_artifacts_expire_at", :where=>"(artifacts_file <> ''::text)", :using=>:btree})
+ -> 0.0325s
-- add_index("ci_builds", ["auto_canceled_by_id"], {:name=>"index_ci_builds_on_auto_canceled_by_id", :using=>:btree})
- -> 0.0035s
+ -> 0.0081s
-- add_index("ci_builds", ["commit_id", "stage_idx", "created_at"], {:name=>"index_ci_builds_on_commit_id_and_stage_idx_and_created_at", :using=>:btree})
- -> 0.0032s
+ -> 0.0114s
-- add_index("ci_builds", ["commit_id", "status", "type"], {:name=>"index_ci_builds_on_commit_id_and_status_and_type", :using=>:btree})
- -> 0.0032s
+ -> 0.0119s
-- add_index("ci_builds", ["commit_id", "type", "name", "ref"], {:name=>"index_ci_builds_on_commit_id_and_type_and_name_and_ref", :using=>:btree})
- -> 0.0035s
+ -> 0.0116s
-- add_index("ci_builds", ["commit_id", "type", "ref"], {:name=>"index_ci_builds_on_commit_id_and_type_and_ref", :using=>:btree})
- -> 0.0042s
+ -> 0.0144s
-- add_index("ci_builds", ["project_id", "id"], {:name=>"index_ci_builds_on_project_id_and_id", :using=>:btree})
- -> 0.0031s
+ -> 0.0136s
-- add_index("ci_builds", ["protected"], {:name=>"index_ci_builds_on_protected", :using=>:btree})
- -> 0.0031s
+ -> 0.0113s
-- add_index("ci_builds", ["runner_id"], {:name=>"index_ci_builds_on_runner_id", :using=>:btree})
- -> 0.0033s
+ -> 0.0082s
-- add_index("ci_builds", ["stage_id"], {:name=>"index_ci_builds_on_stage_id", :using=>:btree})
- -> 0.0035s
+ -> 0.0086s
-- add_index("ci_builds", ["status", "type", "runner_id"], {:name=>"index_ci_builds_on_status_and_type_and_runner_id", :using=>:btree})
- -> 0.0031s
+ -> 0.0091s
-- add_index("ci_builds", ["status"], {:name=>"index_ci_builds_on_status", :using=>:btree})
- -> 0.0032s
+ -> 0.0081s
-- add_index("ci_builds", ["token"], {:name=>"index_ci_builds_on_token", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0103s
-- add_index("ci_builds", ["updated_at"], {:name=>"index_ci_builds_on_updated_at", :using=>:btree})
- -> 0.0047s
+ -> 0.0149s
-- add_index("ci_builds", ["user_id"], {:name=>"index_ci_builds_on_user_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0156s
+-- create_table("ci_builds_metadata", {:force=>:cascade})
+ -> 0.0134s
+-- add_index("ci_builds_metadata", ["build_id"], {:name=>"index_ci_builds_metadata_on_build_id", :unique=>true, :using=>:btree})
+ -> 0.0067s
+-- add_index("ci_builds_metadata", ["project_id"], {:name=>"index_ci_builds_metadata_on_project_id", :using=>:btree})
+ -> 0.0061s
-- create_table("ci_group_variables", {:force=>:cascade})
- -> 0.0055s
+ -> 0.0088s
-- add_index("ci_group_variables", ["group_id", "key"], {:name=>"index_ci_group_variables_on_group_id_and_key", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0073s
-- create_table("ci_job_artifacts", {:force=>:cascade})
- -> 0.0048s
+ -> 0.0089s
+-- add_index("ci_job_artifacts", ["expire_at", "job_id"], {:name=>"index_ci_job_artifacts_on_expire_at_and_job_id", :using=>:btree})
+ -> 0.0061s
-- add_index("ci_job_artifacts", ["job_id", "file_type"], {:name=>"index_ci_job_artifacts_on_job_id_and_file_type", :unique=>true, :using=>:btree})
- -> 0.0027s
+ -> 0.0077s
-- add_index("ci_job_artifacts", ["project_id"], {:name=>"index_ci_job_artifacts_on_project_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0071s
-- create_table("ci_pipeline_schedule_variables", {:force=>:cascade})
- -> 0.0044s
+ -> 0.0512s
-- add_index("ci_pipeline_schedule_variables", ["pipeline_schedule_id", "key"], {:name=>"index_ci_pipeline_schedule_variables_on_schedule_id_and_key", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0144s
-- create_table("ci_pipeline_schedules", {:force=>:cascade})
- -> 0.0047s
+ -> 0.0603s
-- add_index("ci_pipeline_schedules", ["next_run_at", "active"], {:name=>"index_ci_pipeline_schedules_on_next_run_at_and_active", :using=>:btree})
- -> 0.0029s
+ -> 0.0247s
-- add_index("ci_pipeline_schedules", ["project_id"], {:name=>"index_ci_pipeline_schedules_on_project_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0082s
-- create_table("ci_pipeline_variables", {:force=>:cascade})
- -> 0.0045s
+ -> 0.0112s
-- add_index("ci_pipeline_variables", ["pipeline_id", "key"], {:name=>"index_ci_pipeline_variables_on_pipeline_id_and_key", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0075s
-- create_table("ci_pipelines", {:force=>:cascade})
- -> 0.0057s
+ -> 0.0111s
-- add_index("ci_pipelines", ["auto_canceled_by_id"], {:name=>"index_ci_pipelines_on_auto_canceled_by_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0074s
-- add_index("ci_pipelines", ["pipeline_schedule_id"], {:name=>"index_ci_pipelines_on_pipeline_schedule_id", :using=>:btree})
- -> 0.0031s
+ -> 0.0086s
-- add_index("ci_pipelines", ["project_id", "ref", "status", "id"], {:name=>"index_ci_pipelines_on_project_id_and_ref_and_status_and_id", :using=>:btree})
- -> 0.0032s
+ -> 0.0104s
-- add_index("ci_pipelines", ["project_id", "sha"], {:name=>"index_ci_pipelines_on_project_id_and_sha", :using=>:btree})
- -> 0.0032s
+ -> 0.0107s
-- add_index("ci_pipelines", ["project_id"], {:name=>"index_ci_pipelines_on_project_id", :using=>:btree})
- -> 0.0035s
+ -> 0.0084s
-- add_index("ci_pipelines", ["status"], {:name=>"index_ci_pipelines_on_status", :using=>:btree})
- -> 0.0032s
+ -> 0.0065s
-- add_index("ci_pipelines", ["user_id"], {:name=>"index_ci_pipelines_on_user_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0071s
-- create_table("ci_runner_projects", {:force=>:cascade})
- -> 0.0035s
+ -> 0.0077s
-- add_index("ci_runner_projects", ["project_id"], {:name=>"index_ci_runner_projects_on_project_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0072s
-- add_index("ci_runner_projects", ["runner_id"], {:name=>"index_ci_runner_projects_on_runner_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0064s
-- create_table("ci_runners", {:force=>:cascade})
- -> 0.0059s
+ -> 0.0090s
-- add_index("ci_runners", ["contacted_at"], {:name=>"index_ci_runners_on_contacted_at", :using=>:btree})
- -> 0.0030s
+ -> 0.0078s
-- add_index("ci_runners", ["is_shared"], {:name=>"index_ci_runners_on_is_shared", :using=>:btree})
- -> 0.0030s
+ -> 0.0054s
-- add_index("ci_runners", ["locked"], {:name=>"index_ci_runners_on_locked", :using=>:btree})
- -> 0.0030s
+ -> 0.0052s
-- add_index("ci_runners", ["token"], {:name=>"index_ci_runners_on_token", :using=>:btree})
- -> 0.0029s
+ -> 0.0057s
-- create_table("ci_stages", {:force=>:cascade})
- -> 0.0046s
--- add_index("ci_stages", ["pipeline_id", "name"], {:name=>"index_ci_stages_on_pipeline_id_and_name", :using=>:btree})
- -> 0.0031s
+ -> 0.0059s
+-- add_index("ci_stages", ["pipeline_id", "name"], {:name=>"index_ci_stages_on_pipeline_id_and_name", :unique=>true, :using=>:btree})
+ -> 0.0054s
-- add_index("ci_stages", ["pipeline_id"], {:name=>"index_ci_stages_on_pipeline_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0045s
-- add_index("ci_stages", ["project_id"], {:name=>"index_ci_stages_on_project_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0053s
-- create_table("ci_trigger_requests", {:force=>:cascade})
- -> 0.0058s
+ -> 0.0079s
-- add_index("ci_trigger_requests", ["commit_id"], {:name=>"index_ci_trigger_requests_on_commit_id", :using=>:btree})
- -> 0.0031s
+ -> 0.0059s
-- create_table("ci_triggers", {:force=>:cascade})
- -> 0.0043s
+ -> 0.0100s
-- add_index("ci_triggers", ["project_id"], {:name=>"index_ci_triggers_on_project_id", :using=>:btree})
- -> 0.0033s
--- create_table("ci_variables", {:force=>:cascade})
-> 0.0059s
+-- create_table("ci_variables", {:force=>:cascade})
+ -> 0.0110s
-- add_index("ci_variables", ["project_id", "key", "environment_scope"], {:name=>"index_ci_variables_on_project_id_and_key_and_environment_scope", :unique=>true, :using=>:btree})
- -> 0.0031s
+ -> 0.0066s
-- create_table("cluster_platforms_kubernetes", {:force=>:cascade})
- -> 0.0053s
+ -> 0.0082s
-- add_index("cluster_platforms_kubernetes", ["cluster_id"], {:name=>"index_cluster_platforms_kubernetes_on_cluster_id", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0047s
-- create_table("cluster_projects", {:force=>:cascade})
- -> 0.0032s
+ -> 0.0079s
-- add_index("cluster_projects", ["cluster_id"], {:name=>"index_cluster_projects_on_cluster_id", :using=>:btree})
- -> 0.0035s
+ -> 0.0045s
-- add_index("cluster_projects", ["project_id"], {:name=>"index_cluster_projects_on_project_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0044s
-- create_table("cluster_providers_gcp", {:force=>:cascade})
- -> 0.0051s
+ -> 0.0247s
-- add_index("cluster_providers_gcp", ["cluster_id"], {:name=>"index_cluster_providers_gcp_on_cluster_id", :unique=>true, :using=>:btree})
- -> 0.0034s
+ -> 0.0088s
-- create_table("clusters", {:force=>:cascade})
- -> 0.0052s
+ -> 0.0767s
-- add_index("clusters", ["enabled"], {:name=>"index_clusters_on_enabled", :using=>:btree})
- -> 0.0031s
+ -> 0.0162s
-- add_index("clusters", ["user_id"], {:name=>"index_clusters_on_user_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0216s
-- create_table("clusters_applications_helm", {:force=>:cascade})
- -> 0.0045s
+ -> 0.0379s
-- create_table("clusters_applications_ingress", {:force=>:cascade})
- -> 0.0044s
+ -> 0.0409s
-- create_table("clusters_applications_prometheus", {:force=>:cascade})
- -> 0.0047s
+ -> 0.0178s
+-- create_table("clusters_applications_runners", {:force=>:cascade})
+ -> 0.0471s
+-- add_index("clusters_applications_runners", ["cluster_id"], {:name=>"index_clusters_applications_runners_on_cluster_id", :unique=>true, :using=>:btree})
+ -> 0.0487s
+-- add_index("clusters_applications_runners", ["runner_id"], {:name=>"index_clusters_applications_runners_on_runner_id", :using=>:btree})
+ -> 0.0094s
-- create_table("container_repositories", {:force=>:cascade})
- -> 0.0050s
+ -> 0.0142s
-- add_index("container_repositories", ["project_id", "name"], {:name=>"index_container_repositories_on_project_id_and_name", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0080s
-- add_index("container_repositories", ["project_id"], {:name=>"index_container_repositories_on_project_id", :using=>:btree})
- -> 0.0032s
+ -> 0.0070s
-- create_table("conversational_development_index_metrics", {:force=>:cascade})
- -> 0.0076s
+ -> 0.0204s
-- create_table("deploy_keys_projects", {:force=>:cascade})
- -> 0.0037s
+ -> 0.0154s
-- add_index("deploy_keys_projects", ["project_id"], {:name=>"index_deploy_keys_projects_on_project_id", :using=>:btree})
- -> 0.0032s
+ -> 0.0471s
-- create_table("deployments", {:force=>:cascade})
- -> 0.0049s
+ -> 0.0191s
-- add_index("deployments", ["created_at"], {:name=>"index_deployments_on_created_at", :using=>:btree})
- -> 0.0034s
+ -> 0.0552s
-- add_index("deployments", ["environment_id", "id"], {:name=>"index_deployments_on_environment_id_and_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0294s
-- add_index("deployments", ["environment_id", "iid", "project_id"], {:name=>"index_deployments_on_environment_id_and_iid_and_project_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0408s
-- add_index("deployments", ["project_id", "iid"], {:name=>"index_deployments_on_project_id_and_iid", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0094s
-- create_table("emails", {:force=>:cascade})
- -> 0.0046s
+ -> 0.0127s
-- add_index("emails", ["confirmation_token"], {:name=>"index_emails_on_confirmation_token", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0082s
-- add_index("emails", ["email"], {:name=>"index_emails_on_email", :unique=>true, :using=>:btree})
- -> 0.0035s
+ -> 0.0110s
-- add_index("emails", ["user_id"], {:name=>"index_emails_on_user_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0079s
-- create_table("environments", {:force=>:cascade})
- -> 0.0052s
+ -> 0.0106s
-- add_index("environments", ["project_id", "name"], {:name=>"index_environments_on_project_id_and_name", :unique=>true, :using=>:btree})
- -> 0.0031s
+ -> 0.0086s
-- add_index("environments", ["project_id", "slug"], {:name=>"index_environments_on_project_id_and_slug", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0076s
-- create_table("events", {:force=>:cascade})
- -> 0.0046s
+ -> 0.0122s
-- add_index("events", ["action"], {:name=>"index_events_on_action", :using=>:btree})
- -> 0.0032s
--- add_index("events", ["author_id"], {:name=>"index_events_on_author_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0068s
+-- add_index("events", ["author_id", "project_id"], {:name=>"index_events_on_author_id_and_project_id", :using=>:btree})
+ -> 0.0081s
-- add_index("events", ["project_id", "id"], {:name=>"index_events_on_project_id_and_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0064s
-- add_index("events", ["target_type", "target_id"], {:name=>"index_events_on_target_type_and_target_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0087s
-- create_table("feature_gates", {:force=>:cascade})
- -> 0.0046s
+ -> 0.0105s
-- add_index("feature_gates", ["feature_key", "key", "value"], {:name=>"index_feature_gates_on_feature_key_and_key_and_value", :unique=>true, :using=>:btree})
- -> 0.0031s
+ -> 0.0080s
-- create_table("features", {:force=>:cascade})
- -> 0.0041s
+ -> 0.0086s
-- add_index("features", ["key"], {:name=>"index_features_on_key", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0058s
-- create_table("fork_network_members", {:force=>:cascade})
- -> 0.0033s
+ -> 0.0081s
-- add_index("fork_network_members", ["fork_network_id"], {:name=>"index_fork_network_members_on_fork_network_id", :using=>:btree})
- -> 0.0033s
+ -> 0.0056s
-- add_index("fork_network_members", ["project_id"], {:name=>"index_fork_network_members_on_project_id", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0053s
-- create_table("fork_networks", {:force=>:cascade})
- -> 0.0049s
+ -> 0.0081s
-- add_index("fork_networks", ["root_project_id"], {:name=>"index_fork_networks_on_root_project_id", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0051s
-- create_table("forked_project_links", {:force=>:cascade})
- -> 0.0032s
+ -> 0.0070s
-- add_index("forked_project_links", ["forked_to_project_id"], {:name=>"index_forked_project_links_on_forked_to_project_id", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0061s
-- create_table("gcp_clusters", {:force=>:cascade})
- -> 0.0074s
+ -> 0.0090s
-- add_index("gcp_clusters", ["project_id"], {:name=>"index_gcp_clusters_on_project_id", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0073s
-- create_table("gpg_key_subkeys", {:force=>:cascade})
- -> 0.0042s
+ -> 0.0092s
-- add_index("gpg_key_subkeys", ["fingerprint"], {:name=>"index_gpg_key_subkeys_on_fingerprint", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0063s
-- add_index("gpg_key_subkeys", ["gpg_key_id"], {:name=>"index_gpg_key_subkeys_on_gpg_key_id", :using=>:btree})
- -> 0.0032s
+ -> 0.0603s
-- add_index("gpg_key_subkeys", ["keyid"], {:name=>"index_gpg_key_subkeys_on_keyid", :unique=>true, :using=>:btree})
- -> 0.0027s
+ -> 0.0705s
-- create_table("gpg_keys", {:force=>:cascade})
- -> 0.0042s
+ -> 0.0235s
-- add_index("gpg_keys", ["fingerprint"], {:name=>"index_gpg_keys_on_fingerprint", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0220s
-- add_index("gpg_keys", ["primary_keyid"], {:name=>"index_gpg_keys_on_primary_keyid", :unique=>true, :using=>:btree})
- -> 0.0026s
+ -> 0.0329s
-- add_index("gpg_keys", ["user_id"], {:name=>"index_gpg_keys_on_user_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0087s
-- create_table("gpg_signatures", {:force=>:cascade})
- -> 0.0054s
+ -> 0.0126s
-- add_index("gpg_signatures", ["commit_sha"], {:name=>"index_gpg_signatures_on_commit_sha", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0105s
-- add_index("gpg_signatures", ["gpg_key_id"], {:name=>"index_gpg_signatures_on_gpg_key_id", :using=>:btree})
- -> 0.0026s
+ -> 0.0094s
-- add_index("gpg_signatures", ["gpg_key_primary_keyid"], {:name=>"index_gpg_signatures_on_gpg_key_primary_keyid", :using=>:btree})
- -> 0.0029s
+ -> 0.0100s
-- add_index("gpg_signatures", ["gpg_key_subkey_id"], {:name=>"index_gpg_signatures_on_gpg_key_subkey_id", :using=>:btree})
- -> 0.0032s
+ -> 0.0079s
-- add_index("gpg_signatures", ["project_id"], {:name=>"index_gpg_signatures_on_project_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0081s
-- create_table("group_custom_attributes", {:force=>:cascade})
- -> 0.0044s
+ -> 0.0092s
-- add_index("group_custom_attributes", ["group_id", "key"], {:name=>"index_group_custom_attributes_on_group_id_and_key", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0086s
-- add_index("group_custom_attributes", ["key", "value"], {:name=>"index_group_custom_attributes_on_key_and_value", :using=>:btree})
- -> 0.0028s
+ -> 0.0071s
-- create_table("identities", {:force=>:cascade})
- -> 0.0043s
+ -> 0.0114s
-- add_index("identities", ["user_id"], {:name=>"index_identities_on_user_id", :using=>:btree})
- -> 0.0034s
+ -> 0.0064s
+-- create_table("internal_ids", {:id=>:bigserial, :force=>:cascade})
+ -> 0.0097s
+-- add_index("internal_ids", ["usage", "project_id"], {:name=>"index_internal_ids_on_usage_and_project_id", :unique=>true, :using=>:btree})
+ -> 0.0073s
-- create_table("issue_assignees", {:id=>false, :force=>:cascade})
- -> 0.0013s
+ -> 0.0127s
-- add_index("issue_assignees", ["issue_id", "user_id"], {:name=>"index_issue_assignees_on_issue_id_and_user_id", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0110s
-- add_index("issue_assignees", ["user_id"], {:name=>"index_issue_assignees_on_user_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0079s
-- create_table("issue_metrics", {:force=>:cascade})
- -> 0.0032s
+ -> 0.0098s
-- add_index("issue_metrics", ["issue_id"], {:name=>"index_issue_metrics", :using=>:btree})
- -> 0.0029s
+ -> 0.0053s
-- create_table("issues", {:force=>:cascade})
- -> 0.0051s
+ -> 0.0090s
-- add_index("issues", ["author_id"], {:name=>"index_issues_on_author_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0056s
-- add_index("issues", ["confidential"], {:name=>"index_issues_on_confidential", :using=>:btree})
- -> 0.0029s
+ -> 0.0055s
-- add_index("issues", ["description"], {:name=>"index_issues_on_description_trigram", :using=>:gin, :opclasses=>{"description"=>"gin_trgm_ops"}})
- -> 0.0022s
+ -> 0.0006s
-- add_index("issues", ["milestone_id"], {:name=>"index_issues_on_milestone_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0061s
-- add_index("issues", ["moved_to_id"], {:name=>"index_issues_on_moved_to_id", :where=>"(moved_to_id IS NOT NULL)", :using=>:btree})
- -> 0.0030s
+ -> 0.0051s
-- add_index("issues", ["project_id", "created_at", "id", "state"], {:name=>"index_issues_on_project_id_and_created_at_and_id_and_state", :using=>:btree})
- -> 0.0039s
+ -> 0.0069s
-- add_index("issues", ["project_id", "due_date", "id", "state"], {:name=>"idx_issues_on_project_id_and_due_date_and_id_and_state_partial", :where=>"(due_date IS NOT NULL)", :using=>:btree})
- -> 0.0031s
+ -> 0.0073s
-- add_index("issues", ["project_id", "iid"], {:name=>"index_issues_on_project_id_and_iid", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0060s
-- add_index("issues", ["project_id", "updated_at", "id", "state"], {:name=>"index_issues_on_project_id_and_updated_at_and_id_and_state", :using=>:btree})
- -> 0.0035s
+ -> 0.0094s
-- add_index("issues", ["relative_position"], {:name=>"index_issues_on_relative_position", :using=>:btree})
- -> 0.0030s
+ -> 0.0070s
-- add_index("issues", ["state"], {:name=>"index_issues_on_state", :using=>:btree})
- -> 0.0027s
+ -> 0.0078s
-- add_index("issues", ["title"], {:name=>"index_issues_on_title_trigram", :using=>:gin, :opclasses=>{"title"=>"gin_trgm_ops"}})
- -> 0.0021s
+ -> 0.0007s
-- add_index("issues", ["updated_at"], {:name=>"index_issues_on_updated_at", :using=>:btree})
- -> 0.0030s
+ -> 0.0068s
-- add_index("issues", ["updated_by_id"], {:name=>"index_issues_on_updated_by_id", :where=>"(updated_by_id IS NOT NULL)", :using=>:btree})
- -> 0.0028s
+ -> 0.0066s
-- create_table("keys", {:force=>:cascade})
- -> 0.0048s
+ -> 0.0087s
-- add_index("keys", ["fingerprint"], {:name=>"index_keys_on_fingerprint", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0066s
-- add_index("keys", ["user_id"], {:name=>"index_keys_on_user_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0063s
-- create_table("label_links", {:force=>:cascade})
- -> 0.0041s
+ -> 0.0073s
-- add_index("label_links", ["label_id"], {:name=>"index_label_links_on_label_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0050s
-- add_index("label_links", ["target_id", "target_type"], {:name=>"index_label_links_on_target_id_and_target_type", :using=>:btree})
- -> 0.0028s
+ -> 0.0062s
-- create_table("label_priorities", {:force=>:cascade})
- -> 0.0031s
+ -> 0.0073s
-- add_index("label_priorities", ["priority"], {:name=>"index_label_priorities_on_priority", :using=>:btree})
- -> 0.0028s
+ -> 0.0058s
-- add_index("label_priorities", ["project_id", "label_id"], {:name=>"index_label_priorities_on_project_id_and_label_id", :unique=>true, :using=>:btree})
- -> 0.0027s
+ -> 0.0056s
-- create_table("labels", {:force=>:cascade})
- -> 0.0046s
+ -> 0.0087s
-- add_index("labels", ["group_id", "project_id", "title"], {:name=>"index_labels_on_group_id_and_project_id_and_title", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0074s
-- add_index("labels", ["project_id"], {:name=>"index_labels_on_project_id", :using=>:btree})
- -> 0.0032s
+ -> 0.0061s
-- add_index("labels", ["template"], {:name=>"index_labels_on_template", :where=>"template", :using=>:btree})
- -> 0.0027s
+ -> 0.0060s
-- add_index("labels", ["title"], {:name=>"index_labels_on_title", :using=>:btree})
- -> 0.0030s
+ -> 0.0076s
-- add_index("labels", ["type", "project_id"], {:name=>"index_labels_on_type_and_project_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0061s
+-- create_table("lfs_file_locks", {:force=>:cascade})
+ -> 0.0078s
+-- add_index("lfs_file_locks", ["project_id", "path"], {:name=>"index_lfs_file_locks_on_project_id_and_path", :unique=>true, :using=>:btree})
+ -> 0.0067s
+-- add_index("lfs_file_locks", ["user_id"], {:name=>"index_lfs_file_locks_on_user_id", :using=>:btree})
+ -> 0.0060s
-- create_table("lfs_objects", {:force=>:cascade})
- -> 0.0040s
+ -> 0.0109s
-- add_index("lfs_objects", ["oid"], {:name=>"index_lfs_objects_on_oid", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0059s
-- create_table("lfs_objects_projects", {:force=>:cascade})
- -> 0.0035s
+ -> 0.0091s
-- add_index("lfs_objects_projects", ["project_id"], {:name=>"index_lfs_objects_projects_on_project_id", :using=>:btree})
- -> 0.0025s
+ -> 0.0060s
-- create_table("lists", {:force=>:cascade})
- -> 0.0033s
+ -> 0.0115s
-- add_index("lists", ["board_id", "label_id"], {:name=>"index_lists_on_board_id_and_label_id", :unique=>true, :using=>:btree})
- -> 0.0026s
+ -> 0.0055s
-- add_index("lists", ["label_id"], {:name=>"index_lists_on_label_id", :using=>:btree})
- -> 0.0026s
+ -> 0.0055s
-- create_table("members", {:force=>:cascade})
- -> 0.0046s
+ -> 0.0140s
-- add_index("members", ["access_level"], {:name=>"index_members_on_access_level", :using=>:btree})
- -> 0.0028s
+ -> 0.0067s
-- add_index("members", ["invite_token"], {:name=>"index_members_on_invite_token", :unique=>true, :using=>:btree})
- -> 0.0027s
+ -> 0.0069s
-- add_index("members", ["requested_at"], {:name=>"index_members_on_requested_at", :using=>:btree})
- -> 0.0025s
+ -> 0.0057s
-- add_index("members", ["source_id", "source_type"], {:name=>"index_members_on_source_id_and_source_type", :using=>:btree})
- -> 0.0027s
+ -> 0.0057s
-- add_index("members", ["user_id"], {:name=>"index_members_on_user_id", :using=>:btree})
- -> 0.0026s
+ -> 0.0073s
-- create_table("merge_request_diff_commits", {:id=>false, :force=>:cascade})
- -> 0.0027s
+ -> 0.0087s
-- add_index("merge_request_diff_commits", ["merge_request_diff_id", "relative_order"], {:name=>"index_merge_request_diff_commits_on_mr_diff_id_and_order", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0151s
-- add_index("merge_request_diff_commits", ["sha"], {:name=>"index_merge_request_diff_commits_on_sha", :using=>:btree})
- -> 0.0029s
+ -> 0.0057s
-- create_table("merge_request_diff_files", {:id=>false, :force=>:cascade})
- -> 0.0027s
+ -> 0.0094s
-- add_index("merge_request_diff_files", ["merge_request_diff_id", "relative_order"], {:name=>"index_merge_request_diff_files_on_mr_diff_id_and_order", :unique=>true, :using=>:btree})
- -> 0.0027s
+ -> 0.0138s
-- create_table("merge_request_diffs", {:force=>:cascade})
- -> 0.0042s
+ -> 0.0077s
-- add_index("merge_request_diffs", ["merge_request_id", "id"], {:name=>"index_merge_request_diffs_on_merge_request_id_and_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0060s
-- create_table("merge_request_metrics", {:force=>:cascade})
- -> 0.0034s
+ -> 0.0098s
-- add_index("merge_request_metrics", ["first_deployed_to_production_at"], {:name=>"index_merge_request_metrics_on_first_deployed_to_production_at", :using=>:btree})
- -> 0.0028s
+ -> 0.0060s
-- add_index("merge_request_metrics", ["merge_request_id"], {:name=>"index_merge_request_metrics", :using=>:btree})
- -> 0.0025s
+ -> 0.0050s
-- add_index("merge_request_metrics", ["pipeline_id"], {:name=>"index_merge_request_metrics_on_pipeline_id", :using=>:btree})
- -> 0.0026s
+ -> 0.0045s
-- create_table("merge_requests", {:force=>:cascade})
-> 0.0066s
-- add_index("merge_requests", ["assignee_id"], {:name=>"index_merge_requests_on_assignee_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0072s
-- add_index("merge_requests", ["author_id"], {:name=>"index_merge_requests_on_author_id", :using=>:btree})
- -> 0.0026s
+ -> 0.0050s
-- add_index("merge_requests", ["created_at"], {:name=>"index_merge_requests_on_created_at", :using=>:btree})
- -> 0.0026s
+ -> 0.0053s
-- add_index("merge_requests", ["description"], {:name=>"index_merge_requests_on_description_trigram", :using=>:gin, :opclasses=>{"description"=>"gin_trgm_ops"}})
- -> 0.0020s
+ -> 0.0008s
-- add_index("merge_requests", ["head_pipeline_id"], {:name=>"index_merge_requests_on_head_pipeline_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0053s
-- add_index("merge_requests", ["latest_merge_request_diff_id"], {:name=>"index_merge_requests_on_latest_merge_request_diff_id", :using=>:btree})
- -> 0.0025s
+ -> 0.0048s
-- add_index("merge_requests", ["merge_user_id"], {:name=>"index_merge_requests_on_merge_user_id", :where=>"(merge_user_id IS NOT NULL)", :using=>:btree})
- -> 0.0029s
+ -> 0.0051s
-- add_index("merge_requests", ["milestone_id"], {:name=>"index_merge_requests_on_milestone_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0055s
-- add_index("merge_requests", ["source_branch"], {:name=>"index_merge_requests_on_source_branch", :using=>:btree})
- -> 0.0026s
+ -> 0.0055s
-- add_index("merge_requests", ["source_project_id", "source_branch"], {:name=>"index_merge_requests_on_source_project_and_branch_state_opened", :where=>"((state)::text = 'opened'::text)", :using=>:btree})
- -> 0.0029s
+ -> 0.0061s
-- add_index("merge_requests", ["source_project_id", "source_branch"], {:name=>"index_merge_requests_on_source_project_id_and_source_branch", :using=>:btree})
- -> 0.0031s
+ -> 0.0068s
-- add_index("merge_requests", ["target_branch"], {:name=>"index_merge_requests_on_target_branch", :using=>:btree})
- -> 0.0028s
+ -> 0.0054s
-- add_index("merge_requests", ["target_project_id", "iid"], {:name=>"index_merge_requests_on_target_project_id_and_iid", :unique=>true, :using=>:btree})
- -> 0.0027s
+ -> 0.0061s
-- add_index("merge_requests", ["target_project_id", "merge_commit_sha", "id"], {:name=>"index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0077s
-- add_index("merge_requests", ["title"], {:name=>"index_merge_requests_on_title", :using=>:btree})
- -> 0.0026s
+ -> 0.0105s
-- add_index("merge_requests", ["title"], {:name=>"index_merge_requests_on_title_trigram", :using=>:gin, :opclasses=>{"title"=>"gin_trgm_ops"}})
- -> 0.0020s
+ -> 0.0008s
-- add_index("merge_requests", ["updated_by_id"], {:name=>"index_merge_requests_on_updated_by_id", :where=>"(updated_by_id IS NOT NULL)", :using=>:btree})
- -> 0.0029s
+ -> 0.0074s
-- create_table("merge_requests_closing_issues", {:force=>:cascade})
- -> 0.0031s
+ -> 0.0125s
-- add_index("merge_requests_closing_issues", ["issue_id"], {:name=>"index_merge_requests_closing_issues_on_issue_id", :using=>:btree})
- -> 0.0026s
+ -> 0.0064s
-- add_index("merge_requests_closing_issues", ["merge_request_id"], {:name=>"index_merge_requests_closing_issues_on_merge_request_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0061s
-- create_table("milestones", {:force=>:cascade})
- -> 0.0044s
+ -> 0.0064s
-- add_index("milestones", ["description"], {:name=>"index_milestones_on_description_trigram", :using=>:gin, :opclasses=>{"description"=>"gin_trgm_ops"}})
- -> 0.0022s
+ -> 0.0007s
-- add_index("milestones", ["due_date"], {:name=>"index_milestones_on_due_date", :using=>:btree})
- -> 0.0033s
+ -> 0.0053s
-- add_index("milestones", ["group_id"], {:name=>"index_milestones_on_group_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0068s
-- add_index("milestones", ["project_id", "iid"], {:name=>"index_milestones_on_project_id_and_iid", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0057s
-- add_index("milestones", ["title"], {:name=>"index_milestones_on_title", :using=>:btree})
- -> 0.0026s
+ -> 0.0051s
-- add_index("milestones", ["title"], {:name=>"index_milestones_on_title_trigram", :using=>:gin, :opclasses=>{"title"=>"gin_trgm_ops"}})
- -> 0.0021s
+ -> 0.0006s
-- create_table("namespaces", {:force=>:cascade})
- -> 0.0068s
+ -> 0.0083s
-- add_index("namespaces", ["created_at"], {:name=>"index_namespaces_on_created_at", :using=>:btree})
- -> 0.0030s
+ -> 0.0061s
-- add_index("namespaces", ["name", "parent_id"], {:name=>"index_namespaces_on_name_and_parent_id", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0062s
-- add_index("namespaces", ["name"], {:name=>"index_namespaces_on_name_trigram", :using=>:gin, :opclasses=>{"name"=>"gin_trgm_ops"}})
- -> 0.0020s
+ -> 0.0006s
-- add_index("namespaces", ["owner_id"], {:name=>"index_namespaces_on_owner_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0061s
-- add_index("namespaces", ["parent_id", "id"], {:name=>"index_namespaces_on_parent_id_and_id", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0072s
-- add_index("namespaces", ["path"], {:name=>"index_namespaces_on_path", :using=>:btree})
- -> 0.0031s
+ -> 0.0056s
-- add_index("namespaces", ["path"], {:name=>"index_namespaces_on_path_trigram", :using=>:gin, :opclasses=>{"path"=>"gin_trgm_ops"}})
- -> 0.0019s
+ -> 0.0006s
-- add_index("namespaces", ["require_two_factor_authentication"], {:name=>"index_namespaces_on_require_two_factor_authentication", :using=>:btree})
- -> 0.0029s
+ -> 0.0061s
-- add_index("namespaces", ["type"], {:name=>"index_namespaces_on_type", :using=>:btree})
- -> 0.0032s
--- create_table("notes", {:force=>:cascade})
-> 0.0055s
+-- create_table("notes", {:force=>:cascade})
+ -> 0.0092s
-- add_index("notes", ["author_id"], {:name=>"index_notes_on_author_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0072s
-- add_index("notes", ["commit_id"], {:name=>"index_notes_on_commit_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0057s
-- add_index("notes", ["created_at"], {:name=>"index_notes_on_created_at", :using=>:btree})
- -> 0.0029s
+ -> 0.0065s
-- add_index("notes", ["discussion_id"], {:name=>"index_notes_on_discussion_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0064s
-- add_index("notes", ["line_code"], {:name=>"index_notes_on_line_code", :using=>:btree})
- -> 0.0029s
+ -> 0.0078s
-- add_index("notes", ["note"], {:name=>"index_notes_on_note_trigram", :using=>:gin, :opclasses=>{"note"=>"gin_trgm_ops"}})
- -> 0.0024s
+ -> 0.0006s
-- add_index("notes", ["noteable_id", "noteable_type"], {:name=>"index_notes_on_noteable_id_and_noteable_type", :using=>:btree})
- -> 0.0029s
+ -> 0.0102s
-- add_index("notes", ["noteable_type"], {:name=>"index_notes_on_noteable_type", :using=>:btree})
- -> 0.0030s
+ -> 0.0092s
-- add_index("notes", ["project_id", "noteable_type"], {:name=>"index_notes_on_project_id_and_noteable_type", :using=>:btree})
- -> 0.0027s
+ -> 0.0082s
-- add_index("notes", ["updated_at"], {:name=>"index_notes_on_updated_at", :using=>:btree})
- -> 0.0026s
+ -> 0.0062s
-- create_table("notification_settings", {:force=>:cascade})
- -> 0.0053s
+ -> 0.0088s
-- add_index("notification_settings", ["source_id", "source_type"], {:name=>"index_notification_settings_on_source_id_and_source_type", :using=>:btree})
- -> 0.0028s
+ -> 0.0405s
-- add_index("notification_settings", ["user_id", "source_id", "source_type"], {:name=>"index_notifications_on_user_id_and_source_id_and_source_type", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0677s
-- add_index("notification_settings", ["user_id"], {:name=>"index_notification_settings_on_user_id", :using=>:btree})
- -> 0.0031s
+ -> 0.1199s
-- create_table("oauth_access_grants", {:force=>:cascade})
- -> 0.0042s
+ -> 0.0140s
-- add_index("oauth_access_grants", ["token"], {:name=>"index_oauth_access_grants_on_token", :unique=>true, :using=>:btree})
- -> 0.0031s
+ -> 0.0076s
-- create_table("oauth_access_tokens", {:force=>:cascade})
- -> 0.0051s
+ -> 0.0167s
-- add_index("oauth_access_tokens", ["refresh_token"], {:name=>"index_oauth_access_tokens_on_refresh_token", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0098s
-- add_index("oauth_access_tokens", ["resource_owner_id"], {:name=>"index_oauth_access_tokens_on_resource_owner_id", :using=>:btree})
- -> 0.0025s
+ -> 0.0074s
-- add_index("oauth_access_tokens", ["token"], {:name=>"index_oauth_access_tokens_on_token", :unique=>true, :using=>:btree})
- -> 0.0026s
+ -> 0.0078s
-- create_table("oauth_applications", {:force=>:cascade})
- -> 0.0049s
+ -> 0.0112s
-- add_index("oauth_applications", ["owner_id", "owner_type"], {:name=>"index_oauth_applications_on_owner_id_and_owner_type", :using=>:btree})
- -> 0.0030s
+ -> 0.0079s
-- add_index("oauth_applications", ["uid"], {:name=>"index_oauth_applications_on_uid", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0114s
-- create_table("oauth_openid_requests", {:force=>:cascade})
- -> 0.0048s
+ -> 0.0102s
-- create_table("pages_domains", {:force=>:cascade})
- -> 0.0052s
+ -> 0.0102s
-- add_index("pages_domains", ["domain"], {:name=>"index_pages_domains_on_domain", :unique=>true, :using=>:btree})
- -> 0.0027s
+ -> 0.0067s
+-- add_index("pages_domains", ["project_id", "enabled_until"], {:name=>"index_pages_domains_on_project_id_and_enabled_until", :using=>:btree})
+ -> 0.0114s
-- add_index("pages_domains", ["project_id"], {:name=>"index_pages_domains_on_project_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0066s
+-- add_index("pages_domains", ["verified_at", "enabled_until"], {:name=>"index_pages_domains_on_verified_at_and_enabled_until", :using=>:btree})
+ -> 0.0073s
+-- add_index("pages_domains", ["verified_at"], {:name=>"index_pages_domains_on_verified_at", :using=>:btree})
+ -> 0.0063s
-- create_table("personal_access_tokens", {:force=>:cascade})
- -> 0.0056s
+ -> 0.0084s
-- add_index("personal_access_tokens", ["token"], {:name=>"index_personal_access_tokens_on_token", :unique=>true, :using=>:btree})
- -> 0.0032s
+ -> 0.0075s
-- add_index("personal_access_tokens", ["user_id"], {:name=>"index_personal_access_tokens_on_user_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0066s
-- create_table("project_authorizations", {:id=>false, :force=>:cascade})
- -> 0.0018s
+ -> 0.0087s
-- add_index("project_authorizations", ["project_id"], {:name=>"index_project_authorizations_on_project_id", :using=>:btree})
- -> 0.0033s
+ -> 0.0056s
-- add_index("project_authorizations", ["user_id", "project_id", "access_level"], {:name=>"index_project_authorizations_on_user_id_project_id_access_level", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0075s
-- create_table("project_auto_devops", {:force=>:cascade})
- -> 0.0043s
+ -> 0.0079s
-- add_index("project_auto_devops", ["project_id"], {:name=>"index_project_auto_devops_on_project_id", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0067s
-- create_table("project_custom_attributes", {:force=>:cascade})
- -> 0.0047s
+ -> 0.0071s
-- add_index("project_custom_attributes", ["key", "value"], {:name=>"index_project_custom_attributes_on_key_and_value", :using=>:btree})
- -> 0.0030s
+ -> 0.0060s
-- add_index("project_custom_attributes", ["project_id", "key"], {:name=>"index_project_custom_attributes_on_project_id_and_key", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0069s
-- create_table("project_features", {:force=>:cascade})
- -> 0.0038s
+ -> 0.0100s
-- add_index("project_features", ["project_id"], {:name=>"index_project_features_on_project_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0069s
-- create_table("project_group_links", {:force=>:cascade})
- -> 0.0036s
+ -> 0.0117s
-- add_index("project_group_links", ["group_id"], {:name=>"index_project_group_links_on_group_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0121s
-- add_index("project_group_links", ["project_id"], {:name=>"index_project_group_links_on_project_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0076s
-- create_table("project_import_data", {:force=>:cascade})
- -> 0.0049s
+ -> 0.0084s
-- add_index("project_import_data", ["project_id"], {:name=>"index_project_import_data_on_project_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0058s
-- create_table("project_statistics", {:force=>:cascade})
- -> 0.0046s
+ -> 0.0075s
-- add_index("project_statistics", ["namespace_id"], {:name=>"index_project_statistics_on_namespace_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0054s
-- add_index("project_statistics", ["project_id"], {:name=>"index_project_statistics_on_project_id", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0054s
-- create_table("projects", {:force=>:cascade})
- -> 0.0090s
+ -> 0.0077s
-- add_index("projects", ["ci_id"], {:name=>"index_projects_on_ci_id", :using=>:btree})
- -> 0.0033s
+ -> 0.0070s
-- add_index("projects", ["created_at"], {:name=>"index_projects_on_created_at", :using=>:btree})
- -> 0.0030s
+ -> 0.0060s
-- add_index("projects", ["creator_id"], {:name=>"index_projects_on_creator_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0071s
-- add_index("projects", ["description"], {:name=>"index_projects_on_description_trigram", :using=>:gin, :opclasses=>{"description"=>"gin_trgm_ops"}})
- -> 0.0022s
+ -> 0.0009s
+-- add_index("projects", ["id"], {:name=>"index_projects_on_id_partial_for_visibility", :unique=>true, :where=>"(visibility_level = ANY (ARRAY[10, 20]))", :using=>:btree})
+ -> 0.0062s
-- add_index("projects", ["last_activity_at"], {:name=>"index_projects_on_last_activity_at", :using=>:btree})
- -> 0.0032s
+ -> 0.0060s
-- add_index("projects", ["last_repository_check_failed"], {:name=>"index_projects_on_last_repository_check_failed", :using=>:btree})
- -> 0.0030s
+ -> 0.0063s
-- add_index("projects", ["last_repository_updated_at"], {:name=>"index_projects_on_last_repository_updated_at", :using=>:btree})
- -> 0.0031s
+ -> 0.0633s
-- add_index("projects", ["name"], {:name=>"index_projects_on_name_trigram", :using=>:gin, :opclasses=>{"name"=>"gin_trgm_ops"}})
- -> 0.0022s
+ -> 0.0012s
-- add_index("projects", ["namespace_id"], {:name=>"index_projects_on_namespace_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0167s
-- add_index("projects", ["path"], {:name=>"index_projects_on_path", :using=>:btree})
- -> 0.0028s
+ -> 0.0222s
-- add_index("projects", ["path"], {:name=>"index_projects_on_path_trigram", :using=>:gin, :opclasses=>{"path"=>"gin_trgm_ops"}})
- -> 0.0023s
+ -> 0.0010s
-- add_index("projects", ["pending_delete"], {:name=>"index_projects_on_pending_delete", :using=>:btree})
- -> 0.0029s
+ -> 0.0229s
-- add_index("projects", ["repository_storage"], {:name=>"index_projects_on_repository_storage", :using=>:btree})
- -> 0.0026s
+ -> 0.0173s
-- add_index("projects", ["runners_token"], {:name=>"index_projects_on_runners_token", :using=>:btree})
- -> 0.0034s
+ -> 0.0167s
-- add_index("projects", ["star_count"], {:name=>"index_projects_on_star_count", :using=>:btree})
- -> 0.0028s
+ -> 0.0491s
-- add_index("projects", ["visibility_level"], {:name=>"index_projects_on_visibility_level", :using=>:btree})
- -> 0.0027s
+ -> 0.0598s
-- create_table("protected_branch_merge_access_levels", {:force=>:cascade})
- -> 0.0042s
+ -> 0.1964s
-- add_index("protected_branch_merge_access_levels", ["protected_branch_id"], {:name=>"index_protected_branch_merge_access", :using=>:btree})
- -> 0.0029s
+ -> 0.1112s
-- create_table("protected_branch_push_access_levels", {:force=>:cascade})
- -> 0.0037s
+ -> 0.0195s
-- add_index("protected_branch_push_access_levels", ["protected_branch_id"], {:name=>"index_protected_branch_push_access", :using=>:btree})
- -> 0.0030s
+ -> 0.0069s
-- create_table("protected_branches", {:force=>:cascade})
- -> 0.0048s
+ -> 0.0113s
-- add_index("protected_branches", ["project_id"], {:name=>"index_protected_branches_on_project_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0071s
-- create_table("protected_tag_create_access_levels", {:force=>:cascade})
- -> 0.0037s
+ -> 0.0180s
-- add_index("protected_tag_create_access_levels", ["protected_tag_id"], {:name=>"index_protected_tag_create_access", :using=>:btree})
- -> 0.0029s
+ -> 0.0068s
-- add_index("protected_tag_create_access_levels", ["user_id"], {:name=>"index_protected_tag_create_access_levels_on_user_id", :using=>:btree})
- -> 0.0029s
+ -> 0.0077s
-- create_table("protected_tags", {:force=>:cascade})
- -> 0.0051s
+ -> 0.0115s
-- add_index("protected_tags", ["project_id"], {:name=>"index_protected_tags_on_project_id", :using=>:btree})
- -> 0.0034s
+ -> 0.0081s
-- create_table("push_event_payloads", {:id=>false, :force=>:cascade})
- -> 0.0030s
+ -> 0.0108s
-- add_index("push_event_payloads", ["event_id"], {:name=>"index_push_event_payloads_on_event_id", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0189s
-- create_table("redirect_routes", {:force=>:cascade})
- -> 0.0049s
+ -> 0.0106s
-- add_index("redirect_routes", ["path"], {:name=>"index_redirect_routes_on_path", :unique=>true, :using=>:btree})
- -> 0.0031s
+ -> 0.0075s
-- add_index("redirect_routes", ["source_type", "source_id"], {:name=>"index_redirect_routes_on_source_type_and_source_id", :using=>:btree})
- -> 0.0034s
+ -> 0.0099s
-- create_table("releases", {:force=>:cascade})
- -> 0.0043s
+ -> 0.0126s
-- add_index("releases", ["project_id", "tag"], {:name=>"index_releases_on_project_id_and_tag", :using=>:btree})
- -> 0.0032s
+ -> 0.0066s
-- add_index("releases", ["project_id"], {:name=>"index_releases_on_project_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0060s
-- create_table("routes", {:force=>:cascade})
- -> 0.0055s
+ -> 0.0091s
-- add_index("routes", ["path"], {:name=>"index_routes_on_path", :unique=>true, :using=>:btree})
- -> 0.0028s
+ -> 0.0073s
-- add_index("routes", ["path"], {:name=>"index_routes_on_path_text_pattern_ops", :using=>:btree, :opclasses=>{"path"=>"varchar_pattern_ops"}})
- -> 0.0026s
+ -> 0.0004s
-- add_index("routes", ["source_type", "source_id"], {:name=>"index_routes_on_source_type_and_source_id", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0111s
-- create_table("sent_notifications", {:force=>:cascade})
- -> 0.0048s
+ -> 0.0093s
-- add_index("sent_notifications", ["reply_key"], {:name=>"index_sent_notifications_on_reply_key", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0060s
-- create_table("services", {:force=>:cascade})
- -> 0.0091s
+ -> 0.0099s
-- add_index("services", ["project_id"], {:name=>"index_services_on_project_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0068s
-- add_index("services", ["template"], {:name=>"index_services_on_template", :using=>:btree})
- -> 0.0031s
+ -> 0.0076s
-- create_table("snippets", {:force=>:cascade})
- -> 0.0050s
+ -> 0.0073s
-- add_index("snippets", ["author_id"], {:name=>"index_snippets_on_author_id", :using=>:btree})
- -> 0.0030s
+ -> 0.0055s
-- add_index("snippets", ["file_name"], {:name=>"index_snippets_on_file_name_trigram", :using=>:gin, :opclasses=>{"file_name"=>"gin_trgm_ops"}})
- -> 0.0020s
+ -> 0.0006s
-- add_index("snippets", ["project_id"], {:name=>"index_snippets_on_project_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0058s
-- add_index("snippets", ["title"], {:name=>"index_snippets_on_title_trigram", :using=>:gin, :opclasses=>{"title"=>"gin_trgm_ops"}})
- -> 0.0020s
+ -> 0.0005s
-- add_index("snippets", ["updated_at"], {:name=>"index_snippets_on_updated_at", :using=>:btree})
- -> 0.0026s
+ -> 0.0100s
-- add_index("snippets", ["visibility_level"], {:name=>"index_snippets_on_visibility_level", :using=>:btree})
- -> 0.0026s
+ -> 0.0091s
-- create_table("spam_logs", {:force=>:cascade})
- -> 0.0048s
+ -> 0.0129s
-- create_table("subscriptions", {:force=>:cascade})
- -> 0.0041s
+ -> 0.0094s
-- add_index("subscriptions", ["subscribable_id", "subscribable_type", "user_id", "project_id"], {:name=>"index_subscriptions_on_subscribable_and_user_id_and_project_id", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0107s
-- create_table("system_note_metadata", {:force=>:cascade})
- -> 0.0040s
+ -> 0.0138s
-- add_index("system_note_metadata", ["note_id"], {:name=>"index_system_note_metadata_on_note_id", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0060s
-- create_table("taggings", {:force=>:cascade})
- -> 0.0047s
+ -> 0.0121s
-- add_index("taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], {:name=>"taggings_idx", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0078s
+-- add_index("taggings", ["tag_id"], {:name=>"index_taggings_on_tag_id", :using=>:btree})
+ -> 0.0058s
-- add_index("taggings", ["taggable_id", "taggable_type", "context"], {:name=>"index_taggings_on_taggable_id_and_taggable_type_and_context", :using=>:btree})
- -> 0.0025s
+ -> 0.0059s
+-- add_index("taggings", ["taggable_id", "taggable_type"], {:name=>"index_taggings_on_taggable_id_and_taggable_type", :using=>:btree})
+ -> 0.0056s
-- create_table("tags", {:force=>:cascade})
- -> 0.0044s
+ -> 0.0063s
-- add_index("tags", ["name"], {:name=>"index_tags_on_name", :unique=>true, :using=>:btree})
- -> 0.0026s
+ -> 0.0055s
-- create_table("timelogs", {:force=>:cascade})
- -> 0.0033s
+ -> 0.0061s
-- add_index("timelogs", ["issue_id"], {:name=>"index_timelogs_on_issue_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0063s
-- add_index("timelogs", ["merge_request_id"], {:name=>"index_timelogs_on_merge_request_id", :using=>:btree})
- -> 0.0033s
+ -> 0.0052s
-- add_index("timelogs", ["user_id"], {:name=>"index_timelogs_on_user_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0055s
-- create_table("todos", {:force=>:cascade})
- -> 0.0043s
+ -> 0.0065s
-- add_index("todos", ["author_id"], {:name=>"index_todos_on_author_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0081s
-- add_index("todos", ["commit_id"], {:name=>"index_todos_on_commit_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0085s
-- add_index("todos", ["note_id"], {:name=>"index_todos_on_note_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0083s
-- add_index("todos", ["project_id"], {:name=>"index_todos_on_project_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0094s
-- add_index("todos", ["target_type", "target_id"], {:name=>"index_todos_on_target_type_and_target_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0070s
+-- add_index("todos", ["user_id", "id"], {:name=>"index_todos_on_user_id_and_id_done", :where=>"((state)::text = 'done'::text)", :using=>:btree})
+ -> 0.0099s
+-- add_index("todos", ["user_id", "id"], {:name=>"index_todos_on_user_id_and_id_pending", :where=>"((state)::text = 'pending'::text)", :using=>:btree})
+ -> 0.0080s
-- add_index("todos", ["user_id"], {:name=>"index_todos_on_user_id", :using=>:btree})
- -> 0.0026s
+ -> 0.0061s
-- create_table("trending_projects", {:force=>:cascade})
- -> 0.0030s
--- add_index("trending_projects", ["project_id"], {:name=>"index_trending_projects_on_project_id", :using=>:btree})
- -> 0.0027s
+ -> 0.0081s
+-- add_index("trending_projects", ["project_id"], {:name=>"index_trending_projects_on_project_id", :unique=>true, :using=>:btree})
+ -> 0.0046s
-- create_table("u2f_registrations", {:force=>:cascade})
- -> 0.0048s
+ -> 0.0063s
-- add_index("u2f_registrations", ["key_handle"], {:name=>"index_u2f_registrations_on_key_handle", :using=>:btree})
- -> 0.0029s
+ -> 0.0052s
-- add_index("u2f_registrations", ["user_id"], {:name=>"index_u2f_registrations_on_user_id", :using=>:btree})
- -> 0.0028s
+ -> 0.0072s
-- create_table("uploads", {:force=>:cascade})
- -> 0.0044s
+ -> 0.0067s
-- add_index("uploads", ["checksum"], {:name=>"index_uploads_on_checksum", :using=>:btree})
- -> 0.0028s
+ -> 0.0046s
-- add_index("uploads", ["model_id", "model_type"], {:name=>"index_uploads_on_model_id_and_model_type", :using=>:btree})
- -> 0.0027s
--- add_index("uploads", ["path"], {:name=>"index_uploads_on_path", :using=>:btree})
- -> 0.0028s
+ -> 0.0049s
+-- add_index("uploads", ["uploader", "path"], {:name=>"index_uploads_on_uploader_and_path", :using=>:btree})
+ -> 0.0052s
-- create_table("user_agent_details", {:force=>:cascade})
- -> 0.0051s
+ -> 0.0059s
-- add_index("user_agent_details", ["subject_id", "subject_type"], {:name=>"index_user_agent_details_on_subject_id_and_subject_type", :using=>:btree})
- -> 0.0028s
+ -> 0.0052s
+-- create_table("user_callouts", {:force=>:cascade})
+ -> 0.0059s
+-- add_index("user_callouts", ["user_id", "feature_name"], {:name=>"index_user_callouts_on_user_id_and_feature_name", :unique=>true, :using=>:btree})
+ -> 0.0094s
+-- add_index("user_callouts", ["user_id"], {:name=>"index_user_callouts_on_user_id", :using=>:btree})
+ -> 0.0064s
-- create_table("user_custom_attributes", {:force=>:cascade})
- -> 0.0044s
+ -> 0.0086s
-- add_index("user_custom_attributes", ["key", "value"], {:name=>"index_user_custom_attributes_on_key_and_value", :using=>:btree})
- -> 0.0027s
+ -> 0.0080s
-- add_index("user_custom_attributes", ["user_id", "key"], {:name=>"index_user_custom_attributes_on_user_id_and_key", :unique=>true, :using=>:btree})
- -> 0.0026s
--- create_table("user_synced_attributes_metadata", {:force=>:cascade})
+ -> 0.0066s
+-- create_table("user_interacted_projects", {:id=>false, :force=>:cascade})
+ -> 0.0108s
+-- add_index("user_interacted_projects", ["project_id", "user_id"], {:name=>"index_user_interacted_projects_on_project_id_and_user_id", :unique=>true, :using=>:btree})
+ -> 0.0114s
+-- add_index("user_interacted_projects", ["user_id"], {:name=>"index_user_interacted_projects_on_user_id", :using=>:btree})
-> 0.0056s
+-- create_table("user_synced_attributes_metadata", {:force=>:cascade})
+ -> 0.0115s
-- add_index("user_synced_attributes_metadata", ["user_id"], {:name=>"index_user_synced_attributes_metadata_on_user_id", :unique=>true, :using=>:btree})
- -> 0.0027s
+ -> 0.0054s
-- create_table("users", {:force=>:cascade})
- -> 0.0134s
+ -> 0.0111s
-- add_index("users", ["admin"], {:name=>"index_users_on_admin", :using=>:btree})
- -> 0.0030s
+ -> 0.0065s
-- add_index("users", ["confirmation_token"], {:name=>"index_users_on_confirmation_token", :unique=>true, :using=>:btree})
- -> 0.0029s
+ -> 0.0065s
-- add_index("users", ["created_at"], {:name=>"index_users_on_created_at", :using=>:btree})
- -> 0.0034s
+ -> 0.0068s
-- add_index("users", ["email"], {:name=>"index_users_on_email", :unique=>true, :using=>:btree})
- -> 0.0030s
+ -> 0.0066s
-- add_index("users", ["email"], {:name=>"index_users_on_email_trigram", :using=>:gin, :opclasses=>{"email"=>"gin_trgm_ops"}})
- -> 0.0431s
+ -> 0.0011s
-- add_index("users", ["ghost"], {:name=>"index_users_on_ghost", :using=>:btree})
- -> 0.0051s
+ -> 0.0063s
-- add_index("users", ["incoming_email_token"], {:name=>"index_users_on_incoming_email_token", :using=>:btree})
- -> 0.0044s
+ -> 0.0057s
-- add_index("users", ["name"], {:name=>"index_users_on_name", :using=>:btree})
- -> 0.0044s
+ -> 0.0056s
-- add_index("users", ["name"], {:name=>"index_users_on_name_trigram", :using=>:gin, :opclasses=>{"name"=>"gin_trgm_ops"}})
- -> 0.0034s
+ -> 0.0011s
-- add_index("users", ["reset_password_token"], {:name=>"index_users_on_reset_password_token", :unique=>true, :using=>:btree})
- -> 0.0044s
+ -> 0.0055s
-- add_index("users", ["rss_token"], {:name=>"index_users_on_rss_token", :using=>:btree})
- -> 0.0046s
+ -> 0.0068s
-- add_index("users", ["state"], {:name=>"index_users_on_state", :using=>:btree})
- -> 0.0040s
+ -> 0.0067s
-- add_index("users", ["username"], {:name=>"index_users_on_username", :using=>:btree})
- -> 0.0046s
+ -> 0.0072s
-- add_index("users", ["username"], {:name=>"index_users_on_username_trigram", :using=>:gin, :opclasses=>{"username"=>"gin_trgm_ops"}})
- -> 0.0044s
+ -> 0.0012s
-- create_table("users_star_projects", {:force=>:cascade})
- -> 0.0055s
+ -> 0.0100s
-- add_index("users_star_projects", ["project_id"], {:name=>"index_users_star_projects_on_project_id", :using=>:btree})
- -> 0.0037s
+ -> 0.0061s
-- add_index("users_star_projects", ["user_id", "project_id"], {:name=>"index_users_star_projects_on_user_id_and_project_id", :unique=>true, :using=>:btree})
- -> 0.0044s
+ -> 0.0068s
-- create_table("web_hook_logs", {:force=>:cascade})
- -> 0.0060s
+ -> 0.0097s
-- add_index("web_hook_logs", ["web_hook_id"], {:name=>"index_web_hook_logs_on_web_hook_id", :using=>:btree})
- -> 0.0034s
+ -> 0.0057s
-- create_table("web_hooks", {:force=>:cascade})
- -> 0.0120s
+ -> 0.0080s
-- add_index("web_hooks", ["project_id"], {:name=>"index_web_hooks_on_project_id", :using=>:btree})
- -> 0.0038s
+ -> 0.0062s
-- add_index("web_hooks", ["type"], {:name=>"index_web_hooks_on_type", :using=>:btree})
- -> 0.0036s
+ -> 0.0065s
+-- add_foreign_key("badges", "namespaces", {:column=>"group_id", :on_delete=>:cascade})
+ -> 0.0158s
+-- add_foreign_key("badges", "projects", {:on_delete=>:cascade})
+ -> 0.0140s
+-- add_foreign_key("boards", "namespaces", {:column=>"group_id", :on_delete=>:cascade})
+ -> 0.0138s
-- add_foreign_key("boards", "projects", {:name=>"fk_f15266b5f9", :on_delete=>:cascade})
- -> 0.0030s
+ -> 0.0118s
-- add_foreign_key("chat_teams", "namespaces", {:on_delete=>:cascade})
- -> 0.0021s
+ -> 0.0130s
-- add_foreign_key("ci_build_trace_section_names", "projects", {:on_delete=>:cascade})
- -> 0.0022s
+ -> 0.0131s
-- add_foreign_key("ci_build_trace_sections", "ci_build_trace_section_names", {:column=>"section_name_id", :name=>"fk_264e112c66", :on_delete=>:cascade})
- -> 0.0018s
+ -> 0.0210s
-- add_foreign_key("ci_build_trace_sections", "ci_builds", {:column=>"build_id", :name=>"fk_4ebe41f502", :on_delete=>:cascade})
- -> 0.0024s
+ -> 0.0823s
-- add_foreign_key("ci_build_trace_sections", "projects", {:on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0942s
-- add_foreign_key("ci_builds", "ci_pipelines", {:column=>"auto_canceled_by_id", :name=>"fk_a2141b1522", :on_delete=>:nullify})
- -> 0.0023s
+ -> 0.1346s
-- add_foreign_key("ci_builds", "ci_stages", {:column=>"stage_id", :name=>"fk_3a9eaa254d", :on_delete=>:cascade})
- -> 0.0020s
+ -> 0.0506s
-- add_foreign_key("ci_builds", "projects", {:name=>"fk_befce0568a", :on_delete=>:cascade})
- -> 0.0024s
+ -> 0.0403s
+-- add_foreign_key("ci_builds_metadata", "ci_builds", {:column=>"build_id", :on_delete=>:cascade})
+ -> 0.0160s
+-- add_foreign_key("ci_builds_metadata", "projects", {:on_delete=>:cascade})
+ -> 0.0165s
-- add_foreign_key("ci_group_variables", "namespaces", {:column=>"group_id", :name=>"fk_33ae4d58d8", :on_delete=>:cascade})
- -> 0.0024s
+ -> 0.0153s
-- add_foreign_key("ci_job_artifacts", "ci_builds", {:column=>"job_id", :on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0160s
-- add_foreign_key("ci_job_artifacts", "projects", {:on_delete=>:cascade})
- -> 0.0020s
+ -> 0.0278s
-- add_foreign_key("ci_pipeline_schedule_variables", "ci_pipeline_schedules", {:column=>"pipeline_schedule_id", :name=>"fk_41c35fda51", :on_delete=>:cascade})
- -> 0.0027s
+ -> 0.0193s
-- add_foreign_key("ci_pipeline_schedules", "projects", {:name=>"fk_8ead60fcc4", :on_delete=>:cascade})
- -> 0.0022s
+ -> 0.0184s
-- add_foreign_key("ci_pipeline_schedules", "users", {:column=>"owner_id", :name=>"fk_9ea99f58d2", :on_delete=>:nullify})
- -> 0.0025s
+ -> 0.0158s
-- add_foreign_key("ci_pipeline_variables", "ci_pipelines", {:column=>"pipeline_id", :name=>"fk_f29c5f4380", :on_delete=>:cascade})
- -> 0.0018s
+ -> 0.0097s
-- add_foreign_key("ci_pipelines", "ci_pipeline_schedules", {:column=>"pipeline_schedule_id", :name=>"fk_3d34ab2e06", :on_delete=>:nullify})
- -> 0.0019s
+ -> 0.0693s
-- add_foreign_key("ci_pipelines", "ci_pipelines", {:column=>"auto_canceled_by_id", :name=>"fk_262d4c2d19", :on_delete=>:nullify})
- -> 0.0029s
+ -> 0.1599s
-- add_foreign_key("ci_pipelines", "projects", {:name=>"fk_86635dbd80", :on_delete=>:cascade})
- -> 0.0023s
+ -> 0.1505s
-- add_foreign_key("ci_runner_projects", "projects", {:name=>"fk_4478a6f1e4", :on_delete=>:cascade})
- -> 0.0036s
+ -> 0.0984s
-- add_foreign_key("ci_stages", "ci_pipelines", {:column=>"pipeline_id", :name=>"fk_fb57e6cc56", :on_delete=>:cascade})
- -> 0.0017s
+ -> 0.1152s
-- add_foreign_key("ci_stages", "projects", {:name=>"fk_2360681d1d", :on_delete=>:cascade})
- -> 0.0020s
+ -> 0.1062s
-- add_foreign_key("ci_trigger_requests", "ci_triggers", {:column=>"trigger_id", :name=>"fk_b8ec8b7245", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0455s
-- add_foreign_key("ci_triggers", "projects", {:name=>"fk_e3e63f966e", :on_delete=>:cascade})
- -> 0.0021s
+ -> 0.0725s
-- add_foreign_key("ci_triggers", "users", {:column=>"owner_id", :name=>"fk_e8e10d1964", :on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0774s
-- add_foreign_key("ci_variables", "projects", {:name=>"fk_ada5eb64b3", :on_delete=>:cascade})
- -> 0.0021s
+ -> 0.0626s
-- add_foreign_key("cluster_platforms_kubernetes", "clusters", {:on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0529s
-- add_foreign_key("cluster_projects", "clusters", {:on_delete=>:cascade})
- -> 0.0018s
+ -> 0.0678s
-- add_foreign_key("cluster_projects", "projects", {:on_delete=>:cascade})
- -> 0.0020s
+ -> 0.0391s
-- add_foreign_key("cluster_providers_gcp", "clusters", {:on_delete=>:cascade})
- -> 0.0017s
+ -> 0.0328s
-- add_foreign_key("clusters", "users", {:on_delete=>:nullify})
- -> 0.0018s
+ -> 0.1266s
-- add_foreign_key("clusters_applications_helm", "clusters", {:on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0489s
+-- add_foreign_key("clusters_applications_ingress", "clusters", {:name=>"fk_753a7b41c1", :on_delete=>:cascade})
+ -> 0.0565s
+-- add_foreign_key("clusters_applications_prometheus", "clusters", {:name=>"fk_557e773639", :on_delete=>:cascade})
+ -> 0.0174s
+-- add_foreign_key("clusters_applications_runners", "ci_runners", {:column=>"runner_id", :name=>"fk_02de2ded36", :on_delete=>:nullify})
+ -> 0.0182s
+-- add_foreign_key("clusters_applications_runners", "clusters", {:on_delete=>:cascade})
+ -> 0.0208s
-- add_foreign_key("container_repositories", "projects")
- -> 0.0020s
+ -> 0.0186s
-- add_foreign_key("deploy_keys_projects", "projects", {:name=>"fk_58a901ca7e", :on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0140s
-- add_foreign_key("deployments", "projects", {:name=>"fk_b9a3851b82", :on_delete=>:cascade})
- -> 0.0021s
+ -> 0.0328s
-- add_foreign_key("environments", "projects", {:name=>"fk_d1c8c1da6a", :on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0221s
-- add_foreign_key("events", "projects", {:on_delete=>:cascade})
- -> 0.0020s
+ -> 0.0212s
-- add_foreign_key("events", "users", {:column=>"author_id", :name=>"fk_edfd187b6f", :on_delete=>:cascade})
- -> 0.0020s
+ -> 0.0150s
-- add_foreign_key("fork_network_members", "fork_networks", {:on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0134s
-- add_foreign_key("fork_network_members", "projects", {:column=>"forked_from_project_id", :name=>"fk_b01280dae4", :on_delete=>:nullify})
- -> 0.0019s
+ -> 0.0200s
-- add_foreign_key("fork_network_members", "projects", {:on_delete=>:cascade})
- -> 0.0018s
+ -> 0.0162s
-- add_foreign_key("fork_networks", "projects", {:column=>"root_project_id", :name=>"fk_e7b436b2b5", :on_delete=>:nullify})
- -> 0.0018s
+ -> 0.0138s
-- add_foreign_key("forked_project_links", "projects", {:column=>"forked_to_project_id", :name=>"fk_434510edb0", :on_delete=>:cascade})
- -> 0.0018s
+ -> 0.0137s
-- add_foreign_key("gcp_clusters", "projects", {:on_delete=>:cascade})
- -> 0.0029s
+ -> 0.0148s
-- add_foreign_key("gcp_clusters", "services", {:on_delete=>:nullify})
- -> 0.0022s
+ -> 0.0216s
-- add_foreign_key("gcp_clusters", "users", {:on_delete=>:nullify})
- -> 0.0019s
+ -> 0.0156s
-- add_foreign_key("gpg_key_subkeys", "gpg_keys", {:on_delete=>:cascade})
- -> 0.0017s
+ -> 0.0139s
-- add_foreign_key("gpg_keys", "users", {:on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0142s
-- add_foreign_key("gpg_signatures", "gpg_key_subkeys", {:on_delete=>:nullify})
- -> 0.0016s
+ -> 0.0216s
-- add_foreign_key("gpg_signatures", "gpg_keys", {:on_delete=>:nullify})
- -> 0.0016s
+ -> 0.0211s
-- add_foreign_key("gpg_signatures", "projects", {:on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0215s
-- add_foreign_key("group_custom_attributes", "namespaces", {:column=>"group_id", :on_delete=>:cascade})
- -> 0.0014s
+ -> 0.0174s
+-- add_foreign_key("internal_ids", "projects", {:on_delete=>:cascade})
+ -> 0.0143s
-- add_foreign_key("issue_assignees", "issues", {:name=>"fk_b7d881734a", :on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0139s
-- add_foreign_key("issue_assignees", "users", {:name=>"fk_5e0c8d9154", :on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0138s
-- add_foreign_key("issue_metrics", "issues", {:on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0106s
-- add_foreign_key("issues", "issues", {:column=>"moved_to_id", :name=>"fk_a194299be1", :on_delete=>:nullify})
- -> 0.0014s
+ -> 0.0366s
-- add_foreign_key("issues", "milestones", {:name=>"fk_96b1dd429c", :on_delete=>:nullify})
- -> 0.0016s
+ -> 0.0309s
-- add_foreign_key("issues", "projects", {:name=>"fk_899c8f3231", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0314s
-- add_foreign_key("issues", "users", {:column=>"author_id", :name=>"fk_05f1e72feb", :on_delete=>:nullify})
- -> 0.0015s
+ -> 0.0504s
+-- add_foreign_key("issues", "users", {:column=>"closed_by_id", :name=>"fk_c63cbf6c25", :on_delete=>:nullify})
+ -> 0.0428s
-- add_foreign_key("issues", "users", {:column=>"updated_by_id", :name=>"fk_ffed080f01", :on_delete=>:nullify})
- -> 0.0017s
+ -> 0.0333s
-- add_foreign_key("label_priorities", "labels", {:on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0143s
-- add_foreign_key("label_priorities", "projects", {:on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0160s
-- add_foreign_key("labels", "namespaces", {:column=>"group_id", :on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0176s
-- add_foreign_key("labels", "projects", {:name=>"fk_7de4989a69", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0216s
+-- add_foreign_key("lfs_file_locks", "projects", {:on_delete=>:cascade})
+ -> 0.0144s
+-- add_foreign_key("lfs_file_locks", "users", {:on_delete=>:cascade})
+ -> 0.0178s
-- add_foreign_key("lists", "boards", {:name=>"fk_0d3f677137", :on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0161s
-- add_foreign_key("lists", "labels", {:name=>"fk_7a5553d60f", :on_delete=>:cascade})
- -> 0.0014s
+ -> 0.0137s
-- add_foreign_key("members", "users", {:name=>"fk_2e88fb7ce9", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0171s
-- add_foreign_key("merge_request_diff_commits", "merge_request_diffs", {:on_delete=>:cascade})
- -> 0.0014s
+ -> 0.0143s
-- add_foreign_key("merge_request_diff_files", "merge_request_diffs", {:on_delete=>:cascade})
- -> 0.0014s
+ -> 0.0106s
-- add_foreign_key("merge_request_diffs", "merge_requests", {:name=>"fk_8483f3258f", :on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0119s
-- add_foreign_key("merge_request_metrics", "ci_pipelines", {:column=>"pipeline_id", :on_delete=>:cascade})
- -> 0.0017s
+ -> 0.0163s
-- add_foreign_key("merge_request_metrics", "merge_requests", {:on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0204s
-- add_foreign_key("merge_request_metrics", "users", {:column=>"latest_closed_by_id", :name=>"fk_ae440388cc", :on_delete=>:nullify})
- -> 0.0015s
+ -> 0.0196s
-- add_foreign_key("merge_request_metrics", "users", {:column=>"merged_by_id", :name=>"fk_7f28d925f3", :on_delete=>:nullify})
- -> 0.0015s
+ -> 0.0202s
-- add_foreign_key("merge_requests", "ci_pipelines", {:column=>"head_pipeline_id", :name=>"fk_fd82eae0b9", :on_delete=>:nullify})
- -> 0.0014s
+ -> 0.0394s
-- add_foreign_key("merge_requests", "merge_request_diffs", {:column=>"latest_merge_request_diff_id", :name=>"fk_06067f5644", :on_delete=>:nullify})
- -> 0.0014s
+ -> 0.0532s
-- add_foreign_key("merge_requests", "milestones", {:name=>"fk_6a5165a692", :on_delete=>:nullify})
- -> 0.0015s
+ -> 0.0291s
-- add_foreign_key("merge_requests", "projects", {:column=>"source_project_id", :name=>"fk_3308fe130c", :on_delete=>:nullify})
- -> 0.0017s
+ -> 0.0278s
-- add_foreign_key("merge_requests", "projects", {:column=>"target_project_id", :name=>"fk_a6963e8447", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0367s
-- add_foreign_key("merge_requests", "users", {:column=>"assignee_id", :name=>"fk_6149611a04", :on_delete=>:nullify})
- -> 0.0016s
+ -> 0.0327s
-- add_foreign_key("merge_requests", "users", {:column=>"author_id", :name=>"fk_e719a85f8a", :on_delete=>:nullify})
- -> 0.0017s
+ -> 0.0337s
-- add_foreign_key("merge_requests", "users", {:column=>"merge_user_id", :name=>"fk_ad525e1f87", :on_delete=>:nullify})
- -> 0.0018s
+ -> 0.0517s
-- add_foreign_key("merge_requests", "users", {:column=>"updated_by_id", :name=>"fk_641731faff", :on_delete=>:nullify})
- -> 0.0017s
+ -> 0.0335s
-- add_foreign_key("merge_requests_closing_issues", "issues", {:on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0167s
-- add_foreign_key("merge_requests_closing_issues", "merge_requests", {:on_delete=>:cascade})
- -> 0.0014s
+ -> 0.0191s
-- add_foreign_key("milestones", "namespaces", {:column=>"group_id", :name=>"fk_95650a40d4", :on_delete=>:cascade})
- -> 0.0014s
+ -> 0.0206s
-- add_foreign_key("milestones", "projects", {:name=>"fk_9bd0a0c791", :on_delete=>:cascade})
- -> 0.0017s
+ -> 0.0221s
-- add_foreign_key("notes", "projects", {:name=>"fk_99e097b079", :on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0332s
-- add_foreign_key("oauth_openid_requests", "oauth_access_grants", {:column=>"access_grant_id", :name=>"fk_oauth_openid_requests_oauth_access_grants_access_grant_id"})
- -> 0.0014s
+ -> 0.0128s
-- add_foreign_key("pages_domains", "projects", {:name=>"fk_ea2f6dfc6f", :on_delete=>:cascade})
- -> 0.0021s
+ -> 0.0220s
-- add_foreign_key("personal_access_tokens", "users")
- -> 0.0016s
+ -> 0.0187s
-- add_foreign_key("project_authorizations", "projects", {:on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0149s
-- add_foreign_key("project_authorizations", "users", {:on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0167s
-- add_foreign_key("project_auto_devops", "projects", {:on_delete=>:cascade})
- -> 0.0026s
+ -> 0.0142s
-- add_foreign_key("project_custom_attributes", "projects", {:on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0218s
-- add_foreign_key("project_features", "projects", {:name=>"fk_18513d9b92", :on_delete=>:cascade})
- -> 0.0020s
+ -> 0.0204s
-- add_foreign_key("project_group_links", "projects", {:name=>"fk_daa8cee94c", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0174s
-- add_foreign_key("project_import_data", "projects", {:name=>"fk_ffb9ee3a10", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0138s
-- add_foreign_key("project_statistics", "projects", {:on_delete=>:cascade})
- -> 0.0021s
+ -> 0.0125s
-- add_foreign_key("protected_branch_merge_access_levels", "protected_branches", {:name=>"fk_8a3072ccb3", :on_delete=>:cascade})
- -> 0.0014s
+ -> 0.0157s
-- add_foreign_key("protected_branch_push_access_levels", "protected_branches", {:name=>"fk_9ffc86a3d9", :on_delete=>:cascade})
- -> 0.0014s
+ -> 0.0112s
-- add_foreign_key("protected_branches", "projects", {:name=>"fk_7a9c6d93e7", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0122s
-- add_foreign_key("protected_tag_create_access_levels", "namespaces", {:column=>"group_id"})
- -> 0.0016s
+ -> 0.0131s
-- add_foreign_key("protected_tag_create_access_levels", "protected_tags", {:name=>"fk_f7dfda8c51", :on_delete=>:cascade})
- -> 0.0013s
+ -> 0.0168s
-- add_foreign_key("protected_tag_create_access_levels", "users")
- -> 0.0018s
+ -> 0.0221s
-- add_foreign_key("protected_tags", "projects", {:name=>"fk_8e4af87648", :on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0135s
-- add_foreign_key("push_event_payloads", "events", {:name=>"fk_36c74129da", :on_delete=>:cascade})
- -> 0.0013s
+ -> 0.0107s
-- add_foreign_key("releases", "projects", {:name=>"fk_47fe2a0596", :on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0131s
-- add_foreign_key("services", "projects", {:name=>"fk_71cce407f9", :on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0142s
-- add_foreign_key("snippets", "projects", {:name=>"fk_be41fd4bb7", :on_delete=>:cascade})
- -> 0.0017s
+ -> 0.0178s
-- add_foreign_key("subscriptions", "projects", {:on_delete=>:cascade})
- -> 0.0018s
+ -> 0.0160s
-- add_foreign_key("system_note_metadata", "notes", {:name=>"fk_d83a918cb1", :on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0156s
-- add_foreign_key("timelogs", "issues", {:name=>"fk_timelogs_issues_issue_id", :on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0183s
-- add_foreign_key("timelogs", "merge_requests", {:name=>"fk_timelogs_merge_requests_merge_request_id", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0198s
+-- add_foreign_key("todos", "notes", {:name=>"fk_91d1f47b13", :on_delete=>:cascade})
+ -> 0.0276s
-- add_foreign_key("todos", "projects", {:name=>"fk_45054f9c45", :on_delete=>:cascade})
- -> 0.0018s
+ -> 0.0175s
+-- add_foreign_key("todos", "users", {:column=>"author_id", :name=>"fk_ccf0373936", :on_delete=>:cascade})
+ -> 0.0182s
+-- add_foreign_key("todos", "users", {:name=>"fk_d94154aa95", :on_delete=>:cascade})
+ -> 0.0184s
-- add_foreign_key("trending_projects", "projects", {:on_delete=>:cascade})
- -> 0.0015s
+ -> 0.0338s
-- add_foreign_key("u2f_registrations", "users")
- -> 0.0017s
+ -> 0.0176s
+-- add_foreign_key("user_callouts", "users", {:on_delete=>:cascade})
+ -> 0.0160s
-- add_foreign_key("user_custom_attributes", "users", {:on_delete=>:cascade})
- -> 0.0019s
+ -> 0.0191s
+-- add_foreign_key("user_interacted_projects", "projects", {:name=>"fk_722ceba4f7", :on_delete=>:cascade})
+ -> 0.0171s
+-- add_foreign_key("user_interacted_projects", "users", {:name=>"fk_0894651f08", :on_delete=>:cascade})
+ -> 0.0155s
-- add_foreign_key("user_synced_attributes_metadata", "users", {:on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0164s
-- add_foreign_key("users_star_projects", "projects", {:name=>"fk_22cd27ddfc", :on_delete=>:cascade})
- -> 0.0016s
+ -> 0.0180s
-- add_foreign_key("web_hook_logs", "web_hooks", {:on_delete=>:cascade})
- -> 0.0014s
+ -> 0.0164s
-- add_foreign_key("web_hooks", "projects", {:name=>"fk_0c8ca6d9d1", :on_delete=>:cascade})
- -> 0.0017s
+ -> 0.0172s
-- initialize_schema_migrations_table()
- -> 0.0112s
+ -> 0.0212s
+Adding limits to schema.rb for mysql
+-- column_exists?(:merge_request_diffs, :st_commits)
+ -> 0.0010s
+-- column_exists?(:merge_request_diffs, :st_diffs)
+ -> 0.0006s
+-- change_column(:snippets, :content, :text, {:limit=>2147483647})
+ -> 0.0308s
+-- change_column(:notes, :st_diff, :text, {:limit=>2147483647})
+ -> 0.0366s
+-- change_column(:snippets, :content_html, :text, {:limit=>2147483647})
+ -> 0.0272s
+-- change_column(:merge_request_diff_files, :diff, :text, {:limit=>2147483647})
+ -> 0.0170s
+$ date
+Thu Apr 5 11:19:41 UTC 2018
$ JOB_NAME=( $CI_JOB_NAME )
$ export CI_NODE_INDEX=${JOB_NAME[-2]}
$ export CI_NODE_TOTAL=${JOB_NAME[-1]}
$ export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
$ export KNAPSACK_GENERATE_REPORT=true
+$ export SUITE_FLAKY_RSPEC_REPORT_PATH=${FLAKY_RSPEC_SUITE_REPORT_PATH}
+$ export FLAKY_RSPEC_REPORT_PATH=rspec_flaky/all_${JOB_NAME[0]}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+$ export NEW_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/new_${JOB_NAME[0]}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+$ export FLAKY_RSPEC_GENERATE_REPORT=true
$ export CACHE_CLASSES=true
-$ cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
+$ cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
+$ [[ -f $FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_REPORT_PATH}
+$ [[ -f $NEW_FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${NEW_FLAKY_RSPEC_REPORT_PATH}
$ scripts/gitaly-test-spawn
-Gem.path: ["/root/.gem/ruby/2.3.0", "/usr/local/lib/ruby/gems/2.3.0", "/usr/local/bundle"]
-ENV['BUNDLE_GEMFILE']: nil
-ENV['RUBYOPT']: nil
-bundle config in /builds/gitlab-org/gitlab-ce
-scripts/gitaly-test-spawn:10:in `<main>': undefined local variable or method `gitaly_dir' for main:Object (NameError)
-Did you mean? gitaly_dir
-Settings are listed in order of priority. The top value will be used.
-retry
-Set for your local app (/usr/local/bundle/config): 3
+59
+$ knapsack rspec "--color --format documentation"
+
+Report specs:
+spec/services/todo_service_spec.rb
+spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
+spec/controllers/projects/merge_requests_controller_spec.rb
+spec/controllers/groups_controller_spec.rb
+spec/features/projects/import_export/import_file_spec.rb
+spec/lib/gitlab/middleware/go_spec.rb
+spec/services/groups/transfer_service_spec.rb
+spec/features/projects/blobs/edit_spec.rb
+spec/services/boards/lists/move_service_spec.rb
+spec/services/create_deployment_service_spec.rb
+spec/controllers/groups/milestones_controller_spec.rb
+spec/helpers/groups_helper_spec.rb
+spec/requests/api/v3/todos_spec.rb
+spec/models/project_services/teamcity_service_spec.rb
+spec/lib/gitlab/conflict/file_spec.rb
+spec/lib/banzai/filter/snippet_reference_filter_spec.rb
+spec/finders/autocomplete_users_finder_spec.rb
+spec/models/service_spec.rb
+spec/services/test_hooks/project_service_spec.rb
+spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb
+spec/finders/runner_jobs_finder_spec.rb
+spec/features/projects/snippets_spec.rb
+spec/requests/api/v3/environments_spec.rb
+spec/requests/api/namespaces_spec.rb
+spec/services/merge_requests/get_urls_service_spec.rb
+spec/models/lfs_file_lock_spec.rb
+spec/lib/gitlab/ci/config/entry/boolean_spec.rb
+
+Leftover specs:
+
+Knapsack report generator started!
+
+==> Setting up GitLab Shell...
+ GitLab Shell setup in 0.307428917 seconds...
+
+==> Setting up Gitaly...
+ Gitaly setup in 0.000135767 seconds...
+
+TodoService
+ updates cached counts when a todo is created
+ Issues
+ #new_issue
+ creates a todo if assigned
+ does not create a todo if unassigned
+ creates a todo if assignee is the current user
+ creates a todo for each valid mentioned user
+ creates a directly addressed todo for each valid addressed user
+ creates correct todos for each valid user based on the type of mention
+ does not create todo if user can not see the issue when issue is confidential
+ does not create directly addressed todo if user cannot see the issue when issue is confidential
+ when a private group is mentioned
+ creates a todo for group members
+ #update_issue
+ creates a todo for each valid mentioned user not included in skip_users
+ creates a todo for each valid user not included in skip_users based on the type of mention
+ creates a directly addressed todo for each valid addressed user not included in skip_users
+ does not create a todo if user was already mentioned and todo is pending
+ does not create a todo if user was already mentioned and todo is done
+ does not create a directly addressed todo if user was already mentioned or addressed and todo is pending
+ does not create a directly addressed todo if user was already mentioned or addressed and todo is done
+ does not create todo if user can not see the issue when issue is confidential
+ does not create a directly addressed todo if user can not see the issue when issue is confidential
+ issues with a task list
+ does not create todo when tasks are marked as completed
+ does not create directly addressed todo when tasks are marked as completed
+ does not raise an error when description not change
+ #close_issue
+ marks related pending todos to the target for the user as done
+ #destroy_target
+ refreshes the todos count cache for users with todos on the target
+ does not refresh the todos count cache for users with only done todos on the target
+ yields the target to the caller
+ #reassigned_issue
+ creates a pending todo for new assignee
+ does not create a todo if unassigned
+ creates a todo if new assignee is the current user
+ #mark_pending_todos_as_done
+ marks related pending todos to the target for the user as done
+ cached counts
+ updates when todos change
+ #mark_todos_as_done
+ behaves like updating todos state
+ updates related todos for the user with the new_state
+ returns the updated ids
+ cached counts
+ updates when todos change
+ #mark_todos_as_done_by_ids
+ behaves like updating todos state
+ updates related todos for the user with the new_state
+ returns the updated ids
+ cached counts
+ updates when todos change
+ #mark_todos_as_pending
+ behaves like updating todos state
+ updates related todos for the user with the new_state
+ returns the updated ids
+ cached counts
+ updates when todos change
+ #mark_todos_as_pending_by_ids
+ behaves like updating todos state
+ updates related todos for the user with the new_state
+ returns the updated ids
+ cached counts
+ updates when todos change
+ #new_note
+ mark related pending todos to the noteable for the note author as done
+ does not mark related pending todos it is a system note
+ creates a todo for each valid mentioned user
+ creates a todo for each valid user based on the type of mention
+ creates a directly addressed todo for each valid addressed user
+ does not create todo if user can not see the issue when leaving a note on a confidential issue
+ does not create a directly addressed todo if user can not see the issue when leaving a note on a confidential issue
+ does not create todo when leaving a note on snippet
+ on commit
+ creates a todo for each valid mentioned user when leaving a note on commit
+ creates a directly addressed todo for each valid mentioned user when leaving a note on commit
+ #mark_todo
+ creates a todo from a issue
+ #todo_exists?
+ returns false when no todo exist for the given issuable
+ returns true when a todo exist for the given issuable
+ Merge Requests
+ #new_merge_request
+ creates a pending todo if assigned
+ does not create a todo if unassigned
+ does not create a todo if assignee is the current user
+ creates a todo for each valid mentioned user
+ creates a todo for each valid user based on the type of mention
+ creates a directly addressed todo for each valid addressed user
+ #update_merge_request
+ creates a todo for each valid mentioned user not included in skip_users
+ creates a todo for each valid user not included in skip_users based on the type of mention
+ creates a directly addressed todo for each valid addressed user not included in skip_users
+ does not create a todo if user was already mentioned and todo is pending
+ does not create a todo if user was already mentioned and todo is done
+ does not create a directly addressed todo if user was already mentioned or addressed and todo is pending
+ does not create a directly addressed todo if user was already mentioned or addressed and todo is done
+ with a task list
+ does not create todo when tasks are marked as completed
+ does not create directly addressed todo when tasks are marked as completed
+ does not raise an error when description not change
+ #close_merge_request
+ marks related pending todos to the target for the user as done
+ #reassigned_merge_request
+ creates a pending todo for new assignee
+ does not create a todo if unassigned
+ creates a todo if new assignee is the current user
+ does not create a todo for guests
+ does not create a directly addressed todo for guests
+ #merge_merge_request
+ marks related pending todos to the target for the user as done
+ does not create todo for guests
+ does not create directly addressed todo for guests
+ #new_award_emoji
+ marks related pending todos to the target for the user as done
+ #merge_request_build_failed
+ creates a pending todo for the merge request author
+ creates a pending todo for merge_user
+ #merge_request_push
+ marks related pending todos to the target for the user as done
+ #merge_request_became_unmergeable
+ creates a pending todo for a merge_user
+ #mark_todo
+ creates a todo from a merge request
+ #new_note
+ creates a todo for mentioned user on new diff note
+ creates a directly addressed todo for addressed user on new diff note
+ creates a todo for mentioned user on legacy diff note
+ does not create todo for guests
+ #update_note
+ creates a todo for each valid mentioned user not included in skip_users
+ creates a todo for each valid user not included in skip_users based on the type of mention
+ creates a directly addressed todo for each valid addressed user not included in skip_users
+ does not create a todo if user was already mentioned and todo is pending
+ does not create a todo if user was already mentioned and todo is done
+ does not create a directly addressed todo if user was already mentioned or addressed and todo is pending
+ does not create a directly addressed todo if user was already mentioned or addressed and todo is done
+ #mark_todos_as_done
+ marks a relation of todos as done
+ marks an array of todos as done
+ returns the ids of updated todos
+ when some of the todos are done already
+ returns the ids of those still pending
+ returns an empty array if all are done
+ #mark_todos_as_done_by_ids
+ marks an array of todo ids as done
+ marks a single todo id as done
+ caches the number of todos of a user
+
+Gitlab::ImportExport::ProjectTreeSaver
+ saves the project tree into a json object
+ saves project successfully
+ JSON
+ saves the correct json
+ has milestones
+ has merge requests
+ has merge request's milestones
+ has merge request's source branch SHA
+ has merge request's target branch SHA
+ has events
+ has snippets
+ has snippet notes
+ has releases
+ has issues
+ has issue comments
+ has issue assignees
+ has author on issue comments
+ has project members
+ has merge requests diffs
+ has merge request diff files
+ has merge request diff commits
+ has merge requests comments
+ has author on merge requests comments
+ has pipeline stages
+ has pipeline statuses
+ has pipeline builds
+ has no when YML attributes but only the DB column
+ has pipeline commits
+ has ci pipeline notes
+ has labels with no associations
+ has labels associated to records
+ has project and group labels
+ has priorities associated to labels
+ saves the correct service type
+ saves the properties for a service
+ has project feature
+ has custom attributes
+ has badges
+ does not complain about non UTF-8 characters in MR diff files
+ with description override
+ overrides the project description
+ group members
+ does not export group members if it has no permission
+ does not export group members as master
+ exports group members as group owner
+ as admin
+ exports group members as admin
+ exports group members as project members
+ project attributes
+ contains the html description
+ does not contain the runners token
+
+Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits
+ #perform
+ when the diff IDs passed do not exist
+ does not raise
+ when the merge request diff has no serialised commits or diffs
+ does not raise
+ processing multiple merge request diffs
+ when BUFFER_ROWS is exceeded
+ inserts commit rows in chunks of BUFFER_ROWS
+ inserts diff rows in chunks of DIFF_FILE_BUFFER_ROWS
+ when BUFFER_ROWS is not exceeded
+ only updates once
+ when some rows were already inserted due to a previous failure
+ does not raise
+ logs a message
+ ends up with the correct rows
+ when the merge request diff update fails
+ raises an error
+ logs the error
+ still adds diff commits
+ still adds diff files
+ when the merge request diff has valid commits and diffs
+ creates correct entries in the merge_request_diff_commits table
+ creates correct entries in the merge_request_diff_files table
+ sets the st_commits and st_diffs columns to nil
+ when the merge request diff has diffs but no commits
+ creates correct entries in the merge_request_diff_commits table
+ creates correct entries in the merge_request_diff_files table
+ sets the st_commits and st_diffs columns to nil
+ when the merge request diffs do not have too_large set
+ creates correct entries in the merge_request_diff_commits table
+ creates correct entries in the merge_request_diff_files table
+ sets the st_commits and st_diffs columns to nil
+ when the merge request diffs do not have a_mode and b_mode set
+ creates correct entries in the merge_request_diff_commits table
+ creates correct entries in the merge_request_diff_files table
+ sets the st_commits and st_diffs columns to nil
+ when the merge request diffs have binary content
+ creates correct entries in the merge_request_diff_commits table
+ creates correct entries in the merge_request_diff_files table
+ sets the st_commits and st_diffs columns to nil
+ when the merge request diff has commits, but no diffs
+ creates correct entries in the merge_request_diff_commits table
+ creates correct entries in the merge_request_diff_files table
+ sets the st_commits and st_diffs columns to nil
+ when the merge request diffs have invalid content
+ creates correct entries in the merge_request_diff_commits table
+ creates correct entries in the merge_request_diff_files table
+ sets the st_commits and st_diffs columns to nil
+ when the merge request diffs are Rugged::Patch instances
+ creates correct entries in the merge_request_diff_commits table
+ creates correct entries in the merge_request_diff_files table
+ sets the st_commits and st_diffs columns to nil
+ when the merge request diffs are Rugged::Diff::Delta instances
+ creates correct entries in the merge_request_diff_commits table
+ creates correct entries in the merge_request_diff_files table
+ sets the st_commits and st_diffs columns to nil
+
+Projects::MergeRequestsController
+ GET commit_change_content
+ renders commit_change_content template
+ GET show
+ behaves like loads labels
+ loads labels into the @labels variable
+ as html
+ renders merge request page
+ loads notes
+ with special_role FIRST_TIME_CONTRIBUTOR
+ as json
+ with basic serializer param
+ renders basic MR entity as json
+ with widget serializer param
+ renders widget MR entity as json
+ when no serialiser was passed
+ renders widget MR entity as json
+ as diff
+ triggers workhorse to serve the request
+ as patch
+ triggers workhorse to serve the request
+ GET index
+ behaves like issuables list meta-data
+ creates indexed meta-data object for issuable notes and votes count
+ when given empty collection
+ doesn't execute any queries with false conditions
+ when page param
+ redirects to last_page if page number is larger than number of pages
+ redirects to specified page
+ does not redirect to external sites when provided a host field
+ when filtering by opened state
+ with opened merge requests
+ lists those merge requests
+ with reopened merge requests
+ lists those merge requests
+ PUT update
+ changing the assignee
+ limits the attributes exposed on the assignee
+ when user does not have access to update issue
+ responds with 404
+ there is no source project
+ closes MR without errors
+ allows editing of a closed merge request
+ does not allow to update target branch closed merge request
+ behaves like update invalid issuable
+ when updating causes conflicts
+ renders edit when format is html
+ renders json error message when format is json
+ when updating an invalid issuable
+ renders edit when merge request is invalid
+ POST merge
+ when user cannot access
+ returns 404
+ when the merge request is not mergeable
+ returns :failed
+ when the sha parameter does not match the source SHA
+ returns :sha_mismatch
+ when the sha parameter matches the source SHA
+ returns :success
+ starts the merge immediately
+ when the pipeline succeeds is passed
+ returns :merge_when_pipeline_succeeds
+ sets the MR to merge when the pipeline succeeds
+ when project.only_allow_merge_if_pipeline_succeeds? is true
+ returns :merge_when_pipeline_succeeds
+ and head pipeline is not the current one
+ returns :failed
+ only_allow_merge_if_all_discussions_are_resolved? setting
+ when enabled
+ with unresolved discussion
+ returns :failed
+ with all discussions resolved
+ returns :success
+ when disabled
+ with unresolved discussion
+ returns :success
+ with all discussions resolved
+ returns :success
+ DELETE destroy
+ denies access to users unless they're admin or project owner
+ when the user is owner
+ deletes the merge request
+ delegates the update of the todos count cache to TodoService
+ GET commits
+ renders the commits template to a string
+ GET pipelines
+ responds with serialized pipelines
+ POST remove_wip
+ removes the wip status
+ renders MergeRequest as JSON
+ POST cancel_merge_when_pipeline_succeeds
+ calls MergeRequests::MergeWhenPipelineSucceedsService
+ should respond with numeric status code success
+ renders MergeRequest as JSON
+ POST assign_related_issues
+ shows a flash message on success
+ correctly pluralizes flash message on success
+ calls MergeRequests::AssignIssuesService
+ is skipped when not signed in
+ GET ci_environments_status
+ the environment is from a forked project
+ links to the environment on that project
+ GET pipeline_status.json
+ when head_pipeline exists
+ return a detailed head_pipeline status in json
+ when head_pipeline does not exist
+ return empty
+ POST #rebase
+ successfully
+ enqeues a RebaseWorker
+ with a forked project
+ user cannot push to source branch
+ returns 404
+ user can push to source branch
+ returns 200
+
+GroupsController
+ GET #show
+ as html
+ assigns whether or not a group has children
+ as atom
+ assigns events for all the projects in the group
+ GET #new
+ when creating subgroups
+ and can_create_group is true
+ and logged in as Admin
+ behaves like member with ability to create subgroups
+ renders the new page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Owner
+ behaves like member with ability to create subgroups
+ renders the new page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Guest
+ behaves like member without ability to create subgroups
+ renders the 404 page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Developer
+ behaves like member without ability to create subgroups
+ renders the 404 page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Master
+ behaves like member without ability to create subgroups
+ renders the 404 page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and can_create_group is false
+ and logged in as Admin
+ behaves like member with ability to create subgroups
+ renders the new page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Owner
+ behaves like member with ability to create subgroups
+ renders the new page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Guest
+ behaves like member without ability to create subgroups
+ renders the 404 page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Developer
+ behaves like member without ability to create subgroups
+ renders the 404 page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Master
+ behaves like member without ability to create subgroups
+ renders the 404 page (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ GET #activity
+ as json
+ includes all projects in event feed
+ POST #create
+ when creating subgroups
+ and can_create_group is true
+ and logged in as Owner
+ creates the subgroup (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Developer
+ renders the new template (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and can_create_group is false
+ and logged in as Owner
+ creates the subgroup (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ and logged in as Developer
+ renders the new template (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ when creating a top level group
+ and can_create_group is enabled
+ creates the Group
+ and can_create_group is disabled
+ does not create the Group
+ GET #index
+ as a user
+ redirects to Groups Dashboard
+ as a guest
+ redirects to Explore Groups
+ GET #issues
+ sorting by votes
+ sorts most popular issues
+ sorts least popular issues
+ GET #merge_requests
+ sorting by votes
+ sorts most popular merge requests
+ sorts least popular merge requests
+ DELETE #destroy
+ as another user
+ returns 404
+ as the group owner
+ schedules a group destroy
+ redirects to the root path
+ PUT update
+ updates the path successfully
+ does not update the path on error
+ #ensure_canonical_path
+ for a GET request
+ when requesting groups at the root path
+ when requesting the canonical path with different casing
+ redirects to the correct casing
+ when requesting a redirected path
+ redirects to the canonical path
+ when the old group path is a substring of the scheme or host
+ does not modify the requested host
+ when the old group path is substring of groups
+ does not modify the /groups part of the path
+ when requesting groups under the /groups path
+ when requesting the canonical path
+ non-show path
+ with exactly matching casing
+ does not redirect
+ with different casing
+ redirects to the correct casing
+ show path
+ with exactly matching casing
+ does not redirect
+ with different casing
+ redirects to the correct casing at the root path
+ when requesting a redirected path
+ redirects to the canonical path
+ when the old group path is a substring of the scheme or host
+ does not modify the requested host
+ when the old group path is substring of groups
+ does not modify the /groups part of the path
+ when the old group path is substring of groups plus the new path
+ does not modify the /groups part of the path
+ for a POST request
+ when requesting the canonical path with different casing
+ does not 404
+ does not redirect to the correct casing
+ when requesting a redirected path
+ returns not found
+ for a DELETE request
+ when requesting the canonical path with different casing
+ does not 404
+ does not redirect to the correct casing
+ when requesting a redirected path
+ returns not found
+ PUT transfer
+ when transfering to a subgroup goes right
+ should return a notice (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should redirect to the new path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when converting to a root group goes right
+ should return a notice (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should redirect to the new path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ When the transfer goes wrong
+ should return an alert (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should redirect to the current path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the user is not allowed to transfer the group
+ should be denied (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+
+Import/Export - project import integration test
+Starting the Capybara driver server...
+ invalid project
+ when selecting the namespace
+ prefilled the path
+ user imports an exported project successfully
+ path is not prefilled
+ user imports an exported project successfully
+
+Gitlab::Middleware::Go
+ #call
+ when go-get=0
+ skips go-import generation
+ when go-get=1
+ with SSH disabled
+ with simple 2-segment project path
+ with subpackages
+ returns the full project path
+ without subpackages
+ returns the full project path
+ with a nested project path
+ with subpackages
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ with a subpackage that is not a valid project path
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ without subpackages
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ with a bogus path
+ skips go-import generation
+ with HTTP disabled
+ with simple 2-segment project path
+ with subpackages
+ returns the full project path
+ without subpackages
+ returns the full project path
+ with a nested project path
+ with subpackages
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ with a subpackage that is not a valid project path
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ without subpackages
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ with a bogus path
+ skips go-import generation
+ with nothing disabled
+ with simple 2-segment project path
+ with subpackages
+ returns the full project path
+ without subpackages
+ returns the full project path
+ with a nested project path
+ with subpackages
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ with a subpackage that is not a valid project path
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ without subpackages
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ with a bogus path
+ skips go-import generation
+ with nothing disabled (blank string)
+ with simple 2-segment project path
+ with subpackages
+ returns the full project path
+ without subpackages
+ returns the full project path
+ with a nested project path
+ with subpackages
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ with a subpackage that is not a valid project path
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ without subpackages
+ behaves like a nested project
+ when the project is public
+ returns the full project path
+ when the project is private
+ when not authenticated
+ behaves like unauthorized
+ returns the 2-segment group path
+ when authenticated
+ using warden
+ when active
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ when blocked
+ behaves like unauthorized
+ returns the 2-segment group path
+ using a personal access token
+ with api scope
+ behaves like authenticated
+ with access to the project
+ returns the full project path
+ without access to the project
+ behaves like unauthorized
+ returns the 2-segment group path
+ with read_user scope
+ behaves like unauthorized
+ returns the 2-segment group path
+ with a bogus path
+ skips go-import generation
+
+Groups::TransferService
+ #execute
+ when transforming a group into a root group
+ behaves like ensuring allowed transfer for a group
+ with other database than PostgreSQL
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when there's an exception on Gitlab shell directories
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the group is already a root group
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the user does not have the right policies
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when there is a group with the same path
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the group is a subgroup and the transfer is valid
+ should update group attributes (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should update group children path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should update group projects path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when transferring a subgroup into another group
+ behaves like ensuring allowed transfer for a group
+ with other database than PostgreSQL
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when there's an exception on Gitlab shell directories
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the new parent group is the same as the previous parent group
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the user does not have the right policies
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the parent has a group with the same path
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the parent group has a project with the same path
+ should return false (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should add an error on group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the group is allowed to be transferred
+ should update visibility for the group based on the parent group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should update parent group to the new parent (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should return the group as children of the new parent (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should create a redirect for the group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the group has a lower visibility than the parent group
+ should not update the visibility for the group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the group has a higher visibility than the parent group
+ should update visibility level based on the parent group (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when transferring a group with group descendants
+ should update subgroups path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should create redirects for the subgroups (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the new parent has a higher visibility than the children
+ should not update the children visibility (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the new parent has a lower visibility than the children
+ should update children visibility to match the new parent (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when transferring a group with project descendants
+ should update projects path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should create permanent redirects for the projects (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the new parent has a higher visibility than the projects
+ should not update projects visibility (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when the new parent has a lower visibility than the projects
+ should update projects visibility to match the new parent (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when transferring a group with subgroups & projects descendants
+ should update subgroups path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should update projects path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should create redirect for the subgroups and projects (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when transfering a group with nested groups and projects
+ should update subgroups path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should update projects path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ should create redirect for the subgroups and projects (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+ when updating the group goes wrong
+ should restore group and projects visibility (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
+
+Editing file blob
+ as a developer
+ from MR diff
+ returns me to the mr
+ from blob file path
+ updates content
+ previews content
+ visit blob edit
+ redirects to sign in and returns
+ as developer
+ redirects to sign in and returns
+ as guest
+ redirects to sign in and returns
+ as developer
+ on some branch
+ shows blob editor with same branch
+ with protected branch
+ shows blob editor with patch branch
+ as master
+ shows blob editor with same branch
+
+Boards::Lists::MoveService
+ #execute
+ when board parent is a project
+ behaves like lists move service
+ keeps position of lists when list type is closed
+ when list type is set to label
+ keeps position of lists when new position is nil
+ keeps position of lists when new positon is equal to old position
+ keeps position of lists when new positon is negative
+ keeps position of lists when new positon is equal to number of labels lists
+ keeps position of lists when new positon is greater than number of labels lists
+ increments position of intermediate lists when new positon is equal to first position
+ decrements position of intermediate lists when new positon is equal to last position
+ decrements position of intermediate lists when new position is greater than old position
+ increments position of intermediate lists when new position is lower than old position
+ when board parent is a group
+ behaves like lists move service
+ keeps position of lists when list type is closed
+ when list type is set to label
+ keeps position of lists when new position is nil
+ keeps position of lists when new positon is equal to old position
+ keeps position of lists when new positon is negative
+ keeps position of lists when new positon is equal to number of labels lists
+ keeps position of lists when new positon is greater than number of labels lists
+ increments position of intermediate lists when new positon is equal to first position
+ decrements position of intermediate lists when new positon is equal to last position
+ decrements position of intermediate lists when new position is greater than old position
+ increments position of intermediate lists when new position is lower than old position
+
+CreateDeploymentService
+ #execute
+ when environment exists
+ creates a deployment
+ when environment does not exist
+ does not create a deployment
+ when start action is defined
+ and environment is stopped
+ makes environment available
+ creates a deployment
+ when stop action is defined
+ and environment is available
+ makes environment stopped
+ does not create a deployment
+ when variables are used
+ creates a new deployment
+ does not create a new environment
+ updates external url
+ when project was removed
+ does not create deployment or environment
+ #expanded_environment_url
+ when yaml environment uses $CI_COMMIT_REF_NAME
+ should eq "http://review/master"
+ when yaml environment uses $CI_ENVIRONMENT_SLUG
+ should eq "http://review/prod-slug"
+ when yaml environment uses yaml_variables containing symbol keys
+ should eq "http://review/host"
+ when yaml environment does not have url
+ returns the external_url from persisted environment
+ processing of builds
+ without environment specified
+ behaves like does not create deployment
+ does not create a new deployment
+ does not call a service
+ when environment is specified
+ when job succeeds
+ behaves like creates deployment
+ creates a new deployment
+ calls a service
+ is set as deployable
+ updates environment URL
+ when job fails
+ behaves like does not create deployment
+ does not create a new deployment
+ does not call a service
+ when job is retried
+ behaves like creates deployment
+ creates a new deployment
+ calls a service
+ is set as deployable
+ updates environment URL
+ merge request metrics
+ while updating the 'first_deployed_to_production_at' time
+ for merge requests merged before the current deploy
+ sets the time if the deploy's environment is 'production'
+ doesn't set the time if the deploy's environment is not 'production'
+ does not raise errors if the merge request does not have a metrics record
+ for merge requests merged before the previous deploy
+ if the 'first_deployed_to_production_at' time is already set
+ does not overwrite the older 'first_deployed_to_production_at' time
+ if the 'first_deployed_to_production_at' time is not already set
+ does not overwrite the older 'first_deployed_to_production_at' time
+
+Groups::MilestonesController
+ #index
+ shows group milestones page
+ as JSON
+ lists legacy group milestones and group milestones
+ #show
+ when there is a title parameter
+ searchs for a legacy group milestone
+ when there is not a title parameter
+ searchs for a group milestone
+ behaves like milestone tabs
+ #merge_requests
+ as html
+ redirects to milestone#show
+ as json
+ renders the merge requests tab template to a string
+ #participants
+ as html
+ redirects to milestone#show
+ as json
+ renders the participants tab template to a string
+ #labels
+ as html
+ redirects to milestone#show
+ as json
+ renders the labels tab template to a string
+ #create
+ creates group milestone with Chinese title
+ #update
+ updates group milestone
+ legacy group milestones
+ updates only group milestones state
+ #ensure_canonical_path
+ for a GET request
+ when requesting the canonical path
+ non-show path
+ with exactly matching casing
+ does not redirect
+ with different casing
+ redirects to the correct casing
+ show path
+ with exactly matching casing
+ does not redirect
+ with different casing
+ redirects to the correct casing
+ when requesting a redirected path
+ redirects to the canonical path
+ when the old group path is a substring of the scheme or host
+ does not modify the requested host
+ when the old group path is substring of groups
+ does not modify the /groups part of the path
+ when the old group path is substring of groups plus the new path
+ does not modify the /groups part of the path
+ for a non-GET request
+ when requesting the canonical path with different casing
+ does not 404
+ does not redirect to the correct casing
+ when requesting a redirected path
+ returns not found
+
+GroupsHelper
+ group_icon
+ returns an url for the avatar
+ group_icon_url
+ returns an url for the avatar
+ gives default avatar_icon when no avatar is present
+ group_lfs_status
+ only one project in group
+ returns all projects as enabled
+ returns all projects as disabled
+ more than one project in group
+ LFS enabled in group
+ returns both projects as enabled
+ returns only one as enabled
+ LFS disabled in group
+ returns both projects as disabled
+ returns only one as disabled
+ group_title
+ outputs the groups in the correct order (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ #share_with_group_lock_help_text
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :root_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :sub_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :sub_sub_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :root_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :sub_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :sub_sub_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :root_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :subgroup
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :sub_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :subgroup
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :sub_sub_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :subgroup
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :root_owner, help_text: :ancestor_locked_but_you_can_override, linked_ancestor: :subgroup
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :sub_owner, help_text: :ancestor_locked_but_you_can_override, linked_ancestor: :subgroup
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :sub_sub_owner, help_text: :ancestor_locked_so_ask_the_owner, linked_ancestor: :subgroup
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :root_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :sub_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :sub_sub_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :root_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :sub_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :sub_sub_owner, help_text: :default_help, linked_ancestor: nil
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :root_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :root_group
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :sub_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :root_group
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :sub_sub_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :root_group
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :root_owner, help_text: :ancestor_locked_but_you_can_override, linked_ancestor: :root_group
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :sub_owner, help_text: :ancestor_locked_so_ask_the_owner, linked_ancestor: :root_group
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :sub_sub_owner, help_text: :ancestor_locked_so_ask_the_owner, linked_ancestor: :root_group
+ has the correct help text with correct ancestor links (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ #group_sidebar_links
+ returns all the expected links
+ includes settings when the user can admin the group
+ excludes cross project features when the user cannot read cross project
+
+API::V3::Todos
+ DELETE /todos/:id
+ when unauthenticated
+ returns authentication error
+ when authenticated
+ marks a todo as done
+ updates todos cache
+ returns 404 if the todo does not belong to the current user
+ DELETE /todos
+ when unauthenticated
+ returns authentication error
+ when authenticated
+ marks all todos as done
+ updates todos cache
+
+TeamcityService
+ Associations
+ should belong to project
+ should have one service_hook
+ Validations
+ when service is active
+ should validate that :build_type cannot be empty/falsy
+ should validate that :teamcity_url cannot be empty/falsy
+ behaves like issue tracker service URL attribute
+ should allow :teamcity_url to be ‹"https://example.com"›
+ should not allow :teamcity_url to be ‹"example.com"›
+ should not allow :teamcity_url to be ‹"ftp://example.com"›
+ should not allow :teamcity_url to be ‹"herp-and-derp"›
+ #username
+ does not validate the presence of username if password is nil
+ validates the presence of username if password is present
+ #password
+ does not validate the presence of password if username is nil
+ validates the presence of password if username is present
+ when service is inactive
+ should not validate that :build_type cannot be empty/falsy
+ should not validate that :teamcity_url cannot be empty/falsy
+ should not validate that :username cannot be empty/falsy
+ should not validate that :password cannot be empty/falsy
+ Callbacks
+ before_update :reset_password
+ saves password if new url is set together with password when no password was previously set
+ when a password was previously set
+ resets password if url changed
+ does not reset password if username changed
+ does not reset password if new url is set together with password, even if it's the same password
+ #build_page
+ returns the contents of the reactive cache
+ #commit_status
+ returns the contents of the reactive cache
+ #calculate_reactive_cache
+ build_page
+ returns a specific URL when status is 500
+ returns a build URL when teamcity_url has no trailing slash
+ teamcity_url has trailing slash
+ returns a build URL
+ commit_status
+ sets commit status to :error when status is 500
+ sets commit status to "pending" when status is 404
+ sets commit status to "success" when build status contains SUCCESS
+ sets commit status to "failed" when build status contains FAILURE
+ sets commit status to "pending" when build status contains Pending
+ sets commit status to :error when build status is unknown
+
+Gitlab::Conflict::File
+ #resolve_lines
+ raises ResolutionError when passed a hash without resolutions for all sections
+ when resolving everything to the same side
+ has the correct number of lines
+ has content matching the chosen lines
+ with mixed resolutions
+ has the correct number of lines
+ returns a file containing only the chosen parts of the resolved sections
+ #highlight_lines!
+ modifies the existing lines
+ is called implicitly when rich_text is accessed on a line
+ sets the rich_text of the lines matching the text content
+ highlights the lines correctly
+ #sections
+ only inserts match lines when there is a gap between sections
+ sets conflict to false for sections with only unchanged lines
+ only includes a maximum of CONTEXT_LINES (plus an optional match line) in context sections
+ sets conflict to true for sections with only changed lines
+ adds unique IDs to conflict sections, and not to other sections
+ with an example file
+ sets the correct match line headers
+ does not add match lines where they are not needed
+ creates context sections of the correct length
+ #as_json
+ includes the blob path for the file
+ includes the blob icon for the file
+ with the full_content option passed
+ includes the full content of the conflict
+ includes the detected language of the conflict file
+
+Banzai::Filter::SnippetReferenceFilter
+ requires project context
+ ignores valid references contained inside 'pre' element
+ ignores valid references contained inside 'code' element
+ ignores valid references contained inside 'a' element
+ ignores valid references contained inside 'style' element
+ internal reference
+ links to a valid reference
+ links with adjacent text
+ ignores invalid snippet IDs
+ includes a title attribute
+ escapes the title attribute
+ includes default classes
+ includes a data-project attribute
+ includes a data-snippet attribute
+ supports an :only_path context
+ cross-project / cross-namespace complete reference
+ links to a valid reference
+ link has valid text
+ has valid text
+ ignores invalid snippet IDs on the referenced project
+ cross-project / same-namespace complete reference
+ links to a valid reference
+ link has valid text
+ has valid text
+ ignores invalid snippet IDs on the referenced project
+ cross-project shorthand reference
+ links to a valid reference
+ link has valid text
+ has valid text
+ ignores invalid snippet IDs on the referenced project
+ cross-project URL reference
+ links to a valid reference
+ links with adjacent text
+ ignores invalid snippet IDs on the referenced project
+ group context
+ links to a valid reference
+
+AutocompleteUsersFinder
+ #execute
+ should contain exactly #<User id:2126 @johndoe>, #<User id:2128 @user2119>, #<User id:2129 @user2120>, and #<User id:2130 @user2121>
+ when current_user not passed or nil
+ should contain exactly
+ when project passed
+ should contain exactly #<User id:2140 @user2127>
+ when author_id passed
+ should contain exactly #<User id:2146 @user2131> and #<User id:2142 @notsorandom>
+ when group passed and project not passed
+ should contain exactly #<User id:2147 @johndoe>
+ when passed a subgroup
+ includes users from parent groups as well (PENDING: around hook at ./spec/spec_helper.rb:186 did not execute the example)
+ when filtered by search
+ should contain exactly #<User id:2152 @johndoe>
+ when filtered by skip_users
+ should contain exactly #<User id:2157 @johndoe> and #<User id:2159 @user2138>
+ when todos exist
+ when filtered by todo_filter without todo_state_filter
+ should contain exactly
+ when filtered by todo_filter with pending todo_state_filter
+ should contain exactly #<User id:2175 @johndoe>
+ when filtered by todo_filter with done todo_state_filter
+ should contain exactly #<User id:2190 @user2163>
+ when filtered by current_user
+ should contain exactly #<User id:2202 @notsorandom>, #<User id:2201 @johndoe>, #<User id:2203 @user2174>, and #<User id:2204 @user2175>
+ when filtered by author_id
+ should contain exactly #<User id:2206 @notsorandom>, #<User id:2205 @johndoe>, #<User id:2207 @user2176>, #<User id:2208 @user2177>, and #<User id:2209 @user2178>
+
+Service
+ Associations
+ should belong to project
+ should have one service_hook
+ Validations
+ should validate that :type cannot be empty/falsy
+ Scopes
+ .confidential_note_hooks
+ includes services where confidential_note_events is true
+ excludes services where confidential_note_events is false
+ Test Button
+ #can_test?
+ when repository is not empty
+ returns true
+ when repository is empty
+ returns true
+ #test
+ when repository is not empty
+ test runs execute
+ when repository is empty
+ test runs execute
+ Template
+ .build_from_template
+ when template is invalid
+ sets service template to inactive when template is invalid
+ for pushover service
+ is prefilled for projects pushover service
+ has all fields prefilled
+ {property}_changed?
+ returns false when the property has not been assigned a new value
+ returns true when the property has been assigned a different value
+ returns true when the property has been assigned a different value twice
+ returns false when the property has been re-assigned the same value
+ returns false when the property has been assigned a new value then saved
+ {property}_touched?
+ returns false when the property has not been assigned a new value
+ returns true when the property has been assigned a different value
+ returns true when the property has been assigned a different value twice
+ returns true when the property has been re-assigned the same value
+ returns false when the property has been assigned a new value then saved
+ {property}_was
+ returns nil when the property has not been assigned a new value
+ returns the previous value when the property has been assigned a different value
+ returns initial value when the property has been re-assigned the same value
+ returns initial value when the property has been assigned multiple values
+ returns nil when the property has been assigned a new value then saved
+ initialize service with no properties
+ does not raise error
+ creates the properties
+ callbacks
+ on create
+ updates the has_external_issue_tracker boolean
+ on update
+ updates the has_external_issue_tracker boolean
+ #deprecated?
+ should return false by default
+ #deprecation_message
+ should be empty by default
+ .find_by_template
+ returns service template
+ #api_field_names
+ filters out sensitive fields
+
+TestHooks::ProjectService
+ #execute
+ hook with not implemented test
+ returns error message
+ push_events
+ returns error message if not enough data
+ executes hook
+ tag_push_events
+ returns error message if not enough data
+ executes hook
+ note_events
+ returns error message if not enough data
+ executes hook
+ issues_events
+ returns error message if not enough data
+ executes hook
+ confidential_issues_events
+ returns error message if not enough data
+ executes hook
+ merge_requests_events
+ returns error message if not enough data
+ executes hook
+ job_events
+ returns error message if not enough data
+ executes hook
+ pipeline_events
+ returns error message if not enough data
+ executes hook
+ wiki_page_events
+ returns error message if wiki disabled
+ returns error message if not enough data
+ executes hook
+
+User views an open merge request
+ when a merge request does not have repository
+ renders both the title and the description
+ when a merge request has repository
+ when rendering description preview
+ renders empty description preview
+ renders description preview
+ when the branch is rebased on the target
+ does not show diverged commits count
+ when the branch is diverged on the target
+ shows diverged commits count
+
+RunnerJobsFinder
+ #execute
+ when params is empty
+ returns all jobs assigned to Runner
+ when params contains status
+ when status is created
+ returns matched job
+ when status is pending
+ returns matched job
+ when status is running
+ returns matched job
+ when status is success
+ returns matched job
+ when status is failed
+ returns matched job
+ when status is canceled
+ returns matched job
+ when status is skipped
+ returns matched job
+ when status is manual
+ returns matched job
+
+Project snippets
+ when the project has snippets
+ pagination
+ behaves like paginated snippets
+ is limited to 20 items per page
+ clicking on the link to the second page
+ shows the remaining snippets
+ list content
+ contains all project snippets
+ when submitting a note
+ should have autocomplete
+ should have zen mode
+
+API::V3::Environments
+ GET /projects/:id/environments
+ as member of the project
+ returns project environments
+ behaves like a paginated resources
+ has pagination headers
+ as non member
+ returns a 404 status code
+ POST /projects/:id/environments
+ as a member
+ creates a environment with valid params
+ requires name to be passed
+ returns a 400 if environment already exists
+ returns a 400 if slug is specified
+ a non member
+ rejects the request
+ returns a 400 when the required params are missing
+ PUT /projects/:id/environments/:environment_id
+ returns a 200 if name and external_url are changed
+ won't allow slug to be changed
+ won't update the external_url if only the name is passed
+ returns a 404 if the environment does not exist
+ DELETE /projects/:id/environments/:environment_id
+ as a master
+ returns a 200 for an existing environment
+ returns a 404 for non existing id
+ a non member
+ rejects the request
+
+API::Namespaces
+ GET /namespaces
+ when unauthenticated
+ returns authentication error
+ when authenticated as admin
+ returns correct attributes
+ admin: returns an array of all namespaces
+ admin: returns an array of matched namespaces
+ when authenticated as a regular user
+ returns correct attributes when user can admin group
+ returns correct attributes when user cannot admin group
+ user: returns an array of namespaces
+ admin: returns an array of matched namespaces
+ GET /namespaces/:id
+ when unauthenticated
+ returns authentication error
+ when authenticated as regular user
+ when requested namespace is not owned by user
+ when requesting group
+ returns not-found
+ when requesting personal namespace
+ returns not-found
+ when requested namespace is owned by user
+ behaves like namespace reader
+ when namespace exists
+ when requested by ID
+ when requesting group
+ behaves like can access namespace
+ returns namespace details
+ when requesting personal namespace
+ behaves like can access namespace
+ returns namespace details
+ when requested by path
+ when requesting group
+ behaves like can access namespace
+ returns namespace details
+ when requesting personal namespace
+ behaves like can access namespace
+ returns namespace details
+ when namespace doesn't exist
+ returns not-found
+ when authenticated as admin
+ when requested namespace is not owned by user
+ when requesting group
+ behaves like can access namespace
+ returns namespace details
+ when requesting personal namespace
+ behaves like can access namespace
+ returns namespace details
+ when requested namespace is owned by user
+ behaves like namespace reader
+ when namespace exists
+ when requested by ID
+ when requesting group
+ behaves like can access namespace
+ returns namespace details
+ when requesting personal namespace
+ behaves like can access namespace
+ returns namespace details
+ when requested by path
+ when requesting group
+ behaves like can access namespace
+ returns namespace details
+ when requesting personal namespace
+ behaves like can access namespace
+ returns namespace details
+ when namespace doesn't exist
+ returns not-found
+
+MergeRequests::GetUrlsService
+ #execute
+ pushing to default branch
+ behaves like no_merge_request_url
+ returns no URL
+ pushing to project with MRs disabled
+ behaves like no_merge_request_url
+ returns no URL
+ pushing one completely new branch
+ behaves like new_merge_request_link
+ returns url to create new merge request
+ pushing to existing branch but no merge request
+ behaves like new_merge_request_link
+ returns url to create new merge request
+ pushing to deleted branch
+ behaves like no_merge_request_url
+ returns no URL
+ pushing to existing branch and merge request opened
+ behaves like show_merge_request_url
+ returns url to view merge request
+ pushing to existing branch and merge request is reopened
+ behaves like show_merge_request_url
+ returns url to view merge request
+ pushing to existing branch from forked project
+ behaves like show_merge_request_url
+ returns url to view merge request
+ pushing to existing branch and merge request is closed
+ behaves like new_merge_request_link
+ returns url to create new merge request
+ pushing to existing branch and merge request is merged
+ behaves like new_merge_request_link
+ returns url to create new merge request
+ pushing new branch and existing branch (with merge request created) at once
+ returns 2 urls for both creating new and showing merge request
+ when printing_merge_request_link_enabled is false
+ returns empty array
+
+LfsFileLock
+ should belong to project
+ should belong to user
+ should validate that :project_id cannot be empty/falsy
+ should validate that :user_id cannot be empty/falsy
+ should validate that :path cannot be empty/falsy
+ #can_be_unlocked_by?
+ when it's forced
+ can be unlocked by the author
+ can be unlocked by a master
+ can't be unlocked by other user
+ when it isn't forced
+ can be unlocked by the author
+ can't be unlocked by a master
+ can't be unlocked by other user
+
+Gitlab::Ci::Config::Entry::Boolean
+ validations
+ when entry config value is valid
+ #value
+ returns key value
+ #valid?
+ is valid
+ when entry value is not valid
+ #errors
+ saves errors
+Knapsack report was generated. Preview:
+{
+ "spec/services/todo_service_spec.rb": 53.71851348876953,
+ "spec/lib/gitlab/import_export/project_tree_saver_spec.rb": 48.39624857902527,
+ "spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb": 35.17360734939575,
+ "spec/controllers/projects/merge_requests_controller_spec.rb": 25.50887441635132,
+ "spec/controllers/groups_controller_spec.rb": 13.007296323776245,
+ "spec/features/projects/import_export/import_file_spec.rb": 16.827879428863525,
+ "spec/lib/gitlab/middleware/go_spec.rb": 12.497276306152344,
+ "spec/features/projects/blobs/edit_spec.rb": 11.511932134628296,
+ "spec/services/boards/lists/move_service_spec.rb": 8.695446491241455,
+ "spec/services/create_deployment_service_spec.rb": 6.754847526550293,
+ "spec/controllers/groups/milestones_controller_spec.rb": 6.8740551471710205,
+ "spec/helpers/groups_helper_spec.rb": 0.9002459049224854,
+ "spec/requests/api/v3/todos_spec.rb": 6.5924904346466064,
+ "spec/models/project_services/teamcity_service_spec.rb": 2.9881808757781982,
+ "spec/lib/gitlab/conflict/file_spec.rb": 5.294132709503174,
+ "spec/lib/banzai/filter/snippet_reference_filter_spec.rb": 4.118850469589233,
+ "spec/finders/autocomplete_users_finder_spec.rb": 3.864232063293457,
+ "spec/models/service_spec.rb": 3.1697962284088135,
+ "spec/services/test_hooks/project_service_spec.rb": 4.167759656906128,
+ "spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb": 4.707003355026245,
+ "spec/finders/runner_jobs_finder_spec.rb": 3.2137575149536133,
+ "spec/features/projects/snippets_spec.rb": 3.631467580795288,
+ "spec/requests/api/v3/environments_spec.rb": 2.314746856689453,
+ "spec/requests/api/namespaces_spec.rb": 2.352935314178467,
+ "spec/services/merge_requests/get_urls_service_spec.rb": 2.8039824962615967,
+ "spec/models/lfs_file_lock_spec.rb": 0.7295050621032715,
+ "spec/lib/gitlab/ci/config/entry/boolean_spec.rb": 0.007024049758911133
+}
+
+Knapsack global time execution for tests: 04m 49s
+
+Pending: (Failures listed here are expected and do not affect your suite's status)
+
+ 1) GroupsController GET #new when creating subgroups and can_create_group is true and logged in as Admin behaves like member with ability to create subgroups renders the new page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:15
+
+ 2) GroupsController GET #new when creating subgroups and can_create_group is true and logged in as Owner behaves like member with ability to create subgroups renders the new page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:15
+
+ 3) GroupsController GET #new when creating subgroups and can_create_group is true and logged in as Guest behaves like member without ability to create subgroups renders the 404 page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:25
+
+ 4) GroupsController GET #new when creating subgroups and can_create_group is true and logged in as Developer behaves like member without ability to create subgroups renders the 404 page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:25
+
+ 5) GroupsController GET #new when creating subgroups and can_create_group is true and logged in as Master behaves like member without ability to create subgroups renders the 404 page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:25
+
+ 6) GroupsController GET #new when creating subgroups and can_create_group is false and logged in as Admin behaves like member with ability to create subgroups renders the new page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:15
+
+ 7) GroupsController GET #new when creating subgroups and can_create_group is false and logged in as Owner behaves like member with ability to create subgroups renders the new page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:15
+
+ 8) GroupsController GET #new when creating subgroups and can_create_group is false and logged in as Guest behaves like member without ability to create subgroups renders the 404 page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:25
+
+ 9) GroupsController GET #new when creating subgroups and can_create_group is false and logged in as Developer behaves like member without ability to create subgroups renders the 404 page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:25
+
+ 10) GroupsController GET #new when creating subgroups and can_create_group is false and logged in as Master behaves like member without ability to create subgroups renders the 404 page
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:25
+
+ 11) GroupsController POST #create when creating subgroups and can_create_group is true and logged in as Owner creates the subgroup
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:117
+
+ 12) GroupsController POST #create when creating subgroups and can_create_group is true and logged in as Developer renders the new template
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:129
+
+ 13) GroupsController POST #create when creating subgroups and can_create_group is false and logged in as Owner creates the subgroup
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:117
+
+ 14) GroupsController POST #create when creating subgroups and can_create_group is false and logged in as Developer renders the new template
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:129
+
+ 15) GroupsController PUT transfer when transfering to a subgroup goes right should return a notice
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:516
+
+ 16) GroupsController PUT transfer when transfering to a subgroup goes right should redirect to the new path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:520
+
+ 17) GroupsController PUT transfer when converting to a root group goes right should return a notice
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:535
+
+ 18) GroupsController PUT transfer when converting to a root group goes right should redirect to the new path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:539
+
+ 19) GroupsController PUT transfer When the transfer goes wrong should return an alert
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:557
+
+ 20) GroupsController PUT transfer When the transfer goes wrong should redirect to the current path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:561
+
+ 21) GroupsController PUT transfer when the user is not allowed to transfer the group should be denied
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/controllers/groups_controller_spec.rb:577
+
+ 22) Groups::TransferService#execute when transforming a group into a root group behaves like ensuring allowed transfer for a group with other database than PostgreSQL should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:15
+
+ 23) Groups::TransferService#execute when transforming a group into a root group behaves like ensuring allowed transfer for a group with other database than PostgreSQL should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:19
+
+ 24) Groups::TransferService#execute when transforming a group into a root group behaves like ensuring allowed transfer for a group when there's an exception on Gitlab shell directories should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:33
+
+ 25) Groups::TransferService#execute when transforming a group into a root group behaves like ensuring allowed transfer for a group when there's an exception on Gitlab shell directories should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:37
+
+ 26) Groups::TransferService#execute when transforming a group into a root group when the group is already a root group should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:53
+
+ 27) Groups::TransferService#execute when transforming a group into a root group when the user does not have the right policies should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:62
+
+ 28) Groups::TransferService#execute when transforming a group into a root group when the user does not have the right policies should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:66
+
+ 29) Groups::TransferService#execute when transforming a group into a root group when there is a group with the same path should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:79
+
+ 30) Groups::TransferService#execute when transforming a group into a root group when there is a group with the same path should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:83
+
+ 31) Groups::TransferService#execute when transforming a group into a root group when the group is a subgroup and the transfer is valid should update group attributes
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:99
+
+ 32) Groups::TransferService#execute when transforming a group into a root group when the group is a subgroup and the transfer is valid should update group children path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:103
+
+ 33) Groups::TransferService#execute when transforming a group into a root group when the group is a subgroup and the transfer is valid should update group projects path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:109
+
+ 34) Groups::TransferService#execute when transferring a subgroup into another group behaves like ensuring allowed transfer for a group with other database than PostgreSQL should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:15
+
+ 35) Groups::TransferService#execute when transferring a subgroup into another group behaves like ensuring allowed transfer for a group with other database than PostgreSQL should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:19
+
+ 36) Groups::TransferService#execute when transferring a subgroup into another group behaves like ensuring allowed transfer for a group when there's an exception on Gitlab shell directories should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:33
+
+ 37) Groups::TransferService#execute when transferring a subgroup into another group behaves like ensuring allowed transfer for a group when there's an exception on Gitlab shell directories should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:37
+
+ 38) Groups::TransferService#execute when transferring a subgroup into another group when the new parent group is the same as the previous parent group should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:125
+
+ 39) Groups::TransferService#execute when transferring a subgroup into another group when the new parent group is the same as the previous parent group should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:129
+
+ 40) Groups::TransferService#execute when transferring a subgroup into another group when the user does not have the right policies should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:138
+
+ 41) Groups::TransferService#execute when transferring a subgroup into another group when the user does not have the right policies should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:142
+
+ 42) Groups::TransferService#execute when transferring a subgroup into another group when the parent has a group with the same path should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:155
+
+ 43) Groups::TransferService#execute when transferring a subgroup into another group when the parent has a group with the same path should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:159
+
+ 44) Groups::TransferService#execute when transferring a subgroup into another group when the parent group has a project with the same path should return false
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:174
+
+ 45) Groups::TransferService#execute when transferring a subgroup into another group when the parent group has a project with the same path should add an error on group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:178
+
+ 46) Groups::TransferService#execute when transferring a subgroup into another group when the group is allowed to be transferred should update visibility for the group based on the parent group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:212
+
+ 47) Groups::TransferService#execute when transferring a subgroup into another group when the group is allowed to be transferred should update parent group to the new parent
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:216
+
+ 48) Groups::TransferService#execute when transferring a subgroup into another group when the group is allowed to be transferred should return the group as children of the new parent
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:220
+
+ 49) Groups::TransferService#execute when transferring a subgroup into another group when the group is allowed to be transferred should create a redirect for the group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:225
+
+ 50) Groups::TransferService#execute when transferring a subgroup into another group when the group is allowed to be transferred when the group has a lower visibility than the parent group should not update the visibility for the group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:194
+
+ 51) Groups::TransferService#execute when transferring a subgroup into another group when the group is allowed to be transferred when the group has a higher visibility than the parent group should update visibility level based on the parent group
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:205
+
+ 52) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with group descendants should update subgroups path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:239
+
+ 53) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with group descendants should create redirects for the subgroups
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:246
+
+ 54) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with group descendants when the new parent has a higher visibility than the children should not update the children visibility
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:253
+
+ 55) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with group descendants when the new parent has a lower visibility than the children should update children visibility to match the new parent
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:264
+
+ 56) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with project descendants should update projects path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:282
+
+ 57) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with project descendants should create permanent redirects for the projects
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:289
+
+ 58) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with project descendants when the new parent has a higher visibility than the projects should not update projects visibility
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:296
+
+ 59) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with project descendants when the new parent has a lower visibility than the projects should update projects visibility to match the new parent
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:307
+
+ 60) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with subgroups & projects descendants should update subgroups path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:327
+
+ 61) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with subgroups & projects descendants should update projects path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:334
+
+ 62) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with subgroups & projects descendants should create redirect for the subgroups and projects
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:341
+
+ 63) Groups::TransferService#execute when transferring a subgroup into another group when transfering a group with nested groups and projects should update subgroups path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:363
+
+ 64) Groups::TransferService#execute when transferring a subgroup into another group when transfering a group with nested groups and projects should update projects path
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:375
+
+ 65) Groups::TransferService#execute when transferring a subgroup into another group when transfering a group with nested groups and projects should create redirect for the subgroups and projects
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:383
+
+ 66) Groups::TransferService#execute when transferring a subgroup into another group when updating the group goes wrong should restore group and projects visibility
+ # around hook at ./spec/spec_helper.rb:190 did not execute the example
+ # ./spec/services/groups/transfer_service_spec.rb:405
+
+ 67) GroupsHelper group_title outputs the groups in the correct order
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:106
+
+ 68) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :root_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 69) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :sub_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 70) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :sub_sub_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 71) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :root_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 72) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :sub_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 73) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :sub_sub_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 74) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :root_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :subgroup has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 75) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :sub_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :subgroup has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 76) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :sub_sub_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :subgroup has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 77) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :root_owner, help_text: :ancestor_locked_but_you_can_override, linked_ancestor: :subgroup has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 78) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :sub_owner, help_text: :ancestor_locked_but_you_can_override, linked_ancestor: :subgroup has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 79) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: false, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :sub_sub_owner, help_text: :ancestor_locked_so_ask_the_owner, linked_ancestor: :subgroup has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 80) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :root_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 81) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :sub_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 82) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: false, current_user: :sub_sub_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 83) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :root_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
+
+ 84) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :sub_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
-path
-Set for your local app (/usr/local/bundle/config): "vendor"
-Set via BUNDLE_PATH: "/usr/local/bundle"
+ 85) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: false, sub_subgroup_share_with_group_locked: true, current_user: :sub_sub_owner, help_text: :default_help, linked_ancestor: nil has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
-jobs
-Set for your local app (/usr/local/bundle/config): "2"
+ 86) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :root_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :root_group has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
-clean
-Set for your local app (/usr/local/bundle/config): "true"
+ 87) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :sub_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :root_group has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
-without
-Set for your local app (/usr/local/bundle/config): [:production]
+ 88) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: false, current_user: :sub_sub_owner, help_text: :ancestor_locked_and_has_been_overridden, linked_ancestor: :root_group has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
-silence_root_warning
-Set via BUNDLE_SILENCE_ROOT_WARNING: true
+ 89) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :root_owner, help_text: :ancestor_locked_but_you_can_override, linked_ancestor: :root_group has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
-app_config
-Set via BUNDLE_APP_CONFIG: "/usr/local/bundle"
+ 90) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :sub_owner, help_text: :ancestor_locked_so_ask_the_owner, linked_ancestor: :root_group has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
-install_flags
-Set via BUNDLE_INSTALL_FLAGS: "--without=production --jobs=2 --path=vendor --retry=3 --quiet"
+ 91) GroupsHelper#share_with_group_lock_help_text root_share_with_group_locked: true, subgroup_share_with_group_locked: true, sub_subgroup_share_with_group_locked: true, current_user: :sub_sub_owner, help_text: :ancestor_locked_so_ask_the_owner, linked_ancestor: :root_group has the correct help text with correct ancestor links
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/helpers/groups_helper_spec.rb:198
-bin
-Set via BUNDLE_BIN: "/usr/local/bundle/bin"
+ 92) AutocompleteUsersFinder#execute when passed a subgroup includes users from parent groups as well
+ # around hook at ./spec/spec_helper.rb:186 did not execute the example
+ # ./spec/finders/autocomplete_users_finder_spec.rb:55
-gemfile
-Set via BUNDLE_GEMFILE: "/builds/gitlab-org/gitlab-ce/Gemfile"
+Finished in 5 minutes 7 seconds (files took 16.6 seconds to load)
+819 examples, 0 failures, 92 pending
-section_end:1517486961:build_script
-section_start:1517486961:after_script
-section_end:1517486962:after_script
-section_start:1517486962:upload_artifacts
+section_end:1522927514:build_script
+section_start:1522927514:after_script
+Running after script...
+$ date
+Thu Apr 5 11:25:14 UTC 2018
+section_end:1522927515:after_script
+section_start:1522927515:archive_cache
+Not uploading cache ruby-2.3.6-with-yarn due to policy
+section_end:1522927516:archive_cache
+section_start:1522927516:upload_artifacts
Uploading artifacts...
-WARNING: coverage/: no matching files 
+coverage/: found 5 matching files 
knapsack/: found 5 matching files 
+rspec_flaky/: found 4 matching files 
WARNING: tmp/capybara/: no matching files 
-Uploading artifacts to coordinator... ok  id=50551722 responseStatus=201 Created token=XkN753rp
-section_end:1517486963:upload_artifacts
-ERROR: Job failed: exit code 1
- \ No newline at end of file
+Uploading artifacts to coordinator... ok  id=61303283 responseStatus=201 Created token=rusBKvxM
+section_end:1522927520:upload_artifacts
+Job succeeded
+
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 4224cea4652..7b59fde999d 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -22,11 +22,15 @@ describe IssuablesHelper do
end
describe '#issuable_labels_tooltip' do
- it 'returns label text' do
+ it 'returns label text with no labels' do
+ expect(issuable_labels_tooltip([])).to eq("Labels")
+ end
+
+ it 'returns label text with labels within max limit' do
expect(issuable_labels_tooltip([label])).to eq(label.title)
end
- it 'returns label text' do
+ it 'returns label text with labels exceeding max limit' do
expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more")
end
end
diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb
index 70b4a89cb86..f5185cb2857 100644
--- a/spec/helpers/milestones_helper_spec.rb
+++ b/spec/helpers/milestones_helper_spec.rb
@@ -83,58 +83,4 @@ describe MilestonesHelper do
end
end
end
-
- describe '#milestone_remaining_days' do
- around do |example|
- Timecop.freeze(Time.utc(2017, 3, 17)) { example.run }
- end
-
- context 'when less than 31 days remaining' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) }
-
- it 'returns days remaining' do
- expect(milestone_remaining).to eq("<strong>12</strong> days remaining")
- end
- end
-
- context 'when less than 1 year and more than 30 days remaining' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) }
-
- it 'returns months remaining' do
- expect(milestone_remaining).to eq("<strong>2</strong> months remaining")
- end
- end
-
- context 'when more than 1 year remaining' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) }
-
- it 'returns years remaining' do
- expect(milestone_remaining).to eq("<strong>1</strong> year remaining")
- end
- end
-
- context 'when milestone is expired' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.days.ago.utc)) }
-
- it 'returns "Past due"' do
- expect(milestone_remaining).to eq("<strong>Past due</strong>")
- end
- end
-
- context 'when milestone has start_date in the future' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) }
-
- it 'returns "Upcoming"' do
- expect(milestone_remaining).to eq("<strong>Upcoming</strong>")
- end
- end
-
- context 'when milestone has start_date in the past' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.ago.utc)) }
-
- it 'returns days elapsed' do
- expect(milestone_remaining).to eq("<strong>2</strong> days elapsed")
- end
- end
- end
end
diff --git a/spec/javascripts/branches/branches_delete_modal_spec.js b/spec/javascripts/branches/branches_delete_modal_spec.js
new file mode 100644
index 00000000000..b223b8e2c0a
--- /dev/null
+++ b/spec/javascripts/branches/branches_delete_modal_spec.js
@@ -0,0 +1,40 @@
+import $ from 'jquery';
+import DeleteModal from '~/branches/branches_delete_modal';
+
+describe('branches delete modal', () => {
+ describe('setDisableDeleteButton', () => {
+ let submitSpy;
+ let $deleteButton;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div id="modal-delete-branch">
+ <form>
+ <button type="submit" class="js-delete-branch">Delete</button>
+ </form>
+ </div>
+ `);
+ $deleteButton = $('.js-delete-branch');
+ submitSpy = jasmine.createSpy('submit').and.callFake(event => event.preventDefault());
+ $('#modal-delete-branch form').on('submit', submitSpy);
+ // eslint-disable-next-line no-new
+ new DeleteModal();
+ });
+
+ it('does not submit if button is disabled', () => {
+ $deleteButton.attr('disabled', true);
+
+ $deleteButton.click();
+
+ expect(submitSpy).not.toHaveBeenCalled();
+ });
+
+ it('submits if button is not disabled', () => {
+ $deleteButton.attr('disabled', false);
+
+ $deleteButton.click();
+
+ expect(submitSpy).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js
index 2abf52a1676..8427e8a0ba7 100644
--- a/spec/javascripts/collapsed_sidebar_todo_spec.js
+++ b/spec/javascripts/collapsed_sidebar_todo_spec.js
@@ -85,7 +85,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
setTimeout(() => {
expect(
document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
- ).toBe('Mark done');
+ ).toBe('Mark todo as done');
done();
});
@@ -97,7 +97,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
setTimeout(() => {
expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('data-original-title'),
- ).toBe('Mark done');
+ ).toBe('Mark todo as done');
done();
});
@@ -128,13 +128,13 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
.catch(done.fail);
});
- it('updates aria-label to mark done', (done) => {
+ it('updates aria-label to mark todo as done', (done) => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
setTimeout(() => {
expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
- ).toBe('Mark done');
+ ).toBe('Mark todo as done');
done();
});
@@ -147,7 +147,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
.then(() => {
expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
- ).toBe('Mark done');
+ ).toBe('Mark todo as done');
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
})
diff --git a/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js
new file mode 100644
index 00000000000..b80d08de7b1
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js
@@ -0,0 +1,95 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import emptyState from '~/ide/components/commit_sidebar/empty_state.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { resetStore } from '../../helpers';
+
+describe('IDE commit panel empty state', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(emptyState);
+
+ vm = createComponentWithStore(Component, store, {
+ noChangesStateSvgPath: 'no-changes',
+ committedStateSvgPath: 'committed-state',
+ });
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ describe('statusSvg', () => {
+ it('uses noChangesStateSvgPath when commit message is empty', () => {
+ expect(vm.statusSvg).toBe('no-changes');
+ expect(vm.$el.querySelector('img').getAttribute('src')).toBe(
+ 'no-changes',
+ );
+ });
+
+ it('uses committedStateSvgPath when commit message exists', done => {
+ vm.$store.state.lastCommitMsg = 'testing';
+
+ Vue.nextTick(() => {
+ expect(vm.statusSvg).toBe('committed-state');
+ expect(vm.$el.querySelector('img').getAttribute('src')).toBe(
+ 'committed-state',
+ );
+
+ done();
+ });
+ });
+ });
+
+ it('renders no changes text when last commit message is empty', () => {
+ expect(vm.$el.textContent).toContain('No changes');
+ });
+
+ it('renders last commit message when it exists', done => {
+ vm.$store.state.lastCommitMsg = 'testing commit message';
+
+ Vue.nextTick(() => {
+ expect(vm.$el.textContent).toContain('testing commit message');
+
+ done();
+ });
+ });
+
+ describe('toggle button', () => {
+ it('calls store action', () => {
+ spyOn(vm, 'toggleRightPanelCollapsed');
+
+ vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
+
+ expect(vm.toggleRightPanelCollapsed).toHaveBeenCalled();
+ });
+
+ it('renders collapsed class', done => {
+ vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
+
+ done();
+ });
+ });
+ });
+
+ describe('collapsed state', () => {
+ beforeEach(done => {
+ vm.$store.state.rightPanelCollapsed = true;
+
+ Vue.nextTick(done);
+ });
+
+ it('does not render text & svg', () => {
+ expect(vm.$el.querySelector('img')).toBeNull();
+ expect(vm.$el.textContent).not.toContain('No changes');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
index 32dbc3bf72e..9af3c15a4e3 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
@@ -11,10 +11,17 @@ describe('Multi-file editor commit sidebar list collapsed', () => {
beforeEach(() => {
const Component = Vue.extend(listCollapsed);
- vm = createComponentWithStore(Component, store);
-
- vm.$store.state.changedFiles.push(file('file1'), file('file2'));
- vm.$store.state.changedFiles[0].tempFile = true;
+ vm = createComponentWithStore(Component, store, {
+ files: [
+ {
+ ...file('file1'),
+ tempFile: true,
+ },
+ file('file2'),
+ ],
+ iconName: 'staged',
+ title: 'Staged',
+ });
vm.$mount();
});
@@ -26,4 +33,40 @@ describe('Multi-file editor commit sidebar list collapsed', () => {
it('renders added & modified files count', () => {
expect(removeWhitespace(vm.$el.textContent).trim()).toBe('1 1');
});
+
+ describe('addedFilesLength', () => {
+ it('returns an length of temp files', () => {
+ expect(vm.addedFilesLength).toBe(1);
+ });
+ });
+
+ describe('modifiedFilesLength', () => {
+ it('returns an length of modified files', () => {
+ expect(vm.modifiedFilesLength).toBe(1);
+ });
+ });
+
+ describe('addedFilesIconClass', () => {
+ it('includes multi-file-addition when addedFiles is not empty', () => {
+ expect(vm.addedFilesIconClass).toContain('multi-file-addition');
+ });
+
+ it('excludes multi-file-addition when addedFiles is empty', () => {
+ vm.files = [];
+
+ expect(vm.addedFilesIconClass).not.toContain('multi-file-addition');
+ });
+ });
+
+ describe('modifiedFilesClass', () => {
+ it('includes multi-file-modified when addedFiles is not empty', () => {
+ expect(vm.modifiedFilesClass).toContain('multi-file-modified');
+ });
+
+ it('excludes multi-file-modified when addedFiles is empty', () => {
+ vm.files = [];
+
+ expect(vm.modifiedFilesClass).not.toContain('multi-file-modified');
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
index 509434e4300..cc7e0a3f26d 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
+import store from '~/ide/stores';
import listItem from '~/ide/components/commit_sidebar/list_item.vue';
import router from '~/ide/ide_router';
-import store from '~/ide/stores';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file, resetStore } from '../../helpers';
@@ -18,6 +18,7 @@ describe('Multi-file editor commit sidebar list item', () => {
vm = createComponentWithStore(Component, store, {
file: f,
+ actionComponent: 'stage-button',
}).$mount();
});
@@ -31,22 +32,18 @@ describe('Multi-file editor commit sidebar list item', () => {
expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path);
});
- it('calls discardFileChanges when clicking discard button', () => {
- spyOn(vm, 'discardFileChanges');
-
- vm.$el.querySelector('.multi-file-discard-btn').click();
-
- expect(vm.discardFileChanges).toHaveBeenCalled();
+ it('renders actionn button', () => {
+ expect(vm.$el.querySelector('.multi-file-discard-btn')).not.toBeNull();
});
it('opens a closed file in the editor when clicking the file path', done => {
- spyOn(vm, 'openFileInEditor').and.callThrough();
+ spyOn(vm, 'openPendingTab').and.callThrough();
spyOn(router, 'push');
vm.$el.querySelector('.multi-file-commit-list-path').click();
setTimeout(() => {
- expect(vm.openFileInEditor).toHaveBeenCalled();
+ expect(vm.openPendingTab).toHaveBeenCalled();
expect(router.push).toHaveBeenCalled();
done();
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_spec.js
index a62c0a28340..62fc3f90ad1 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import store from '~/ide/stores';
import commitSidebarList from '~/ide/components/commit_sidebar/list.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import { file } from '../../helpers';
+import { file, resetStore } from '../../helpers';
describe('Multi-file editor commit sidebar list', () => {
let vm;
@@ -13,6 +13,10 @@ describe('Multi-file editor commit sidebar list', () => {
vm = createComponentWithStore(Component, store, {
title: 'Staged',
fileList: [],
+ iconName: 'staged',
+ action: 'stageAllChanges',
+ actionBtnText: 'stage all',
+ itemActionComponent: 'stage-button',
});
vm.$store.state.rightPanelCollapsed = false;
@@ -22,6 +26,8 @@ describe('Multi-file editor commit sidebar list', () => {
afterEach(() => {
vm.$destroy();
+
+ resetStore(vm.$store);
});
describe('with a list of files', () => {
@@ -38,6 +44,12 @@ describe('Multi-file editor commit sidebar list', () => {
});
});
+ describe('empty files array', () => {
+ it('renders no changes text when empty', () => {
+ expect(vm.$el.textContent).toContain('No changes');
+ });
+ });
+
describe('collapsed', () => {
beforeEach(done => {
vm.$store.state.rightPanelCollapsed = true;
@@ -50,4 +62,32 @@ describe('Multi-file editor commit sidebar list', () => {
expect(vm.$el.querySelector('.help-block')).toBeNull();
});
});
+
+ describe('with toggle', () => {
+ beforeEach(done => {
+ spyOn(vm, 'toggleRightPanelCollapsed');
+
+ vm.showToggle = true;
+
+ Vue.nextTick(done);
+ });
+
+ it('calls setPanelCollapsedStatus when clickin toggle', () => {
+ vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
+
+ expect(vm.toggleRightPanelCollapsed).toHaveBeenCalled();
+ });
+ });
+
+ describe('action button', () => {
+ beforeEach(() => {
+ spyOn(vm, 'stageAllChanges');
+ });
+
+ it('calls store action when clicked', () => {
+ vm.$el.querySelector('.ide-staged-action-btn').click();
+
+ expect(vm.stageAllChanges).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js b/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js
new file mode 100644
index 00000000000..d62d58101d6
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js
@@ -0,0 +1,174 @@
+import Vue from 'vue';
+import CommitMessageField from '~/ide/components/commit_sidebar/message_field.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('IDE commit message field', () => {
+ const Component = Vue.extend(CommitMessageField);
+ let vm;
+
+ beforeEach(() => {
+ setFixtures('<div id="app"></div>');
+
+ vm = createComponent(
+ Component,
+ {
+ text: '',
+ },
+ '#app',
+ );
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('adds is-focused class on focus', done => {
+ vm.$el.querySelector('textarea').focus();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.is-focused')).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('removed is-focused class on blur', done => {
+ vm.$el.querySelector('textarea').focus();
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.is-focused')).not.toBeNull();
+
+ vm.$el.querySelector('textarea').blur();
+
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(vm.$el.querySelector('.is-focused')).toBeNull();
+
+ done();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('emits input event on input', () => {
+ spyOn(vm, '$emit');
+
+ const textarea = vm.$el.querySelector('textarea');
+ textarea.value = 'testing';
+
+ textarea.dispatchEvent(new Event('input'));
+
+ expect(vm.$emit).toHaveBeenCalledWith('input', 'testing');
+ });
+
+ describe('highlights', () => {
+ describe('subject line', () => {
+ it('does not highlight less than 50 characters', done => {
+ vm.text = 'text less than 50 chars';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.highlights span').textContent).toContain(
+ 'text less than 50 chars',
+ );
+ expect(vm.$el.querySelector('mark').style.display).toBe('none');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('highlights characters over 50 length', done => {
+ vm.text =
+ 'text less than 50 chars that should not highlighted. text more than 50 should be highlighted';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.highlights span').textContent).toContain(
+ 'text less than 50 chars that should not highlighte',
+ );
+ expect(vm.$el.querySelector('mark').style.display).not.toBe('none');
+ expect(vm.$el.querySelector('mark').textContent).toBe(
+ 'd. text more than 50 should be highlighted',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('body text', () => {
+ it('does not highlight body text less tan 72 characters', done => {
+ vm.text = 'subject line\nbody content';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
+ expect(vm.$el.querySelectorAll('mark')[1].style.display).toBe('none');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('highlights body text more than 72 characters', done => {
+ vm.text =
+ 'subject line\nbody content that will be highlighted when it is more than 72 characters in length';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
+ expect(vm.$el.querySelectorAll('mark')[1].style.display).not.toBe('none');
+ expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('highlights body text & subject line', done => {
+ vm.text =
+ 'text less than 50 chars that should not highlighted\nbody content that will be highlighted when it is more than 72 characters in length';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
+ expect(vm.$el.querySelectorAll('mark').length).toBe(2);
+
+ expect(vm.$el.querySelectorAll('mark')[0].textContent).toContain('d');
+ expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('scrolling textarea', () => {
+ it('updates transform of highlights', done => {
+ vm.text = 'subject line\n\n\n\n\n\n\n\n\n\n\nbody content';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ vm.$el.querySelector('textarea').scrollTo(0, 50);
+
+ vm.handleScroll();
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.scrollTop).toBe(50);
+ expect(vm.$el.querySelector('.highlights').style.transform).toBe(
+ 'translate3d(0px, -50px, 0px)',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
index 4e8243439f3..21bfe4be52f 100644
--- a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
@@ -69,19 +69,6 @@ describe('IDE commit sidebar radio group', () => {
});
});
- it('renders helpText tooltip', done => {
- vm.helpText = 'help text';
-
- Vue.nextTick(() => {
- const help = vm.$el.querySelector('.help-block');
-
- expect(help).not.toBeNull();
- expect(help.getAttribute('data-original-title')).toBe('help text');
-
- done();
- });
- });
-
describe('with input', () => {
beforeEach(done => {
vm.$destroy();
diff --git a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js
new file mode 100644
index 00000000000..6bf8710bda7
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js
@@ -0,0 +1,46 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import stageButton from '~/ide/components/commit_sidebar/stage_button.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { file, resetStore } from '../../helpers';
+
+describe('IDE stage file button', () => {
+ let vm;
+ let f;
+
+ beforeEach(() => {
+ const Component = Vue.extend(stageButton);
+ f = file();
+
+ vm = createComponentWithStore(Component, store, {
+ path: f.path,
+ });
+
+ spyOn(vm, 'stageChange');
+ spyOn(vm, 'discardFileChanges');
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders button to discard & stage', () => {
+ expect(vm.$el.querySelectorAll('.btn').length).toBe(2);
+ });
+
+ it('calls store with stage button', () => {
+ vm.$el.querySelectorAll('.btn')[0].click();
+
+ expect(vm.stageChange).toHaveBeenCalledWith(f.path);
+ });
+
+ it('calls store with discard button', () => {
+ vm.$el.querySelectorAll('.btn')[1].click();
+
+ expect(vm.discardFileChanges).toHaveBeenCalledWith(f.path);
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js
new file mode 100644
index 00000000000..917bbb9fb46
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import unstageButton from '~/ide/components/commit_sidebar/unstage_button.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { file, resetStore } from '../../helpers';
+
+describe('IDE unstage file button', () => {
+ let vm;
+ let f;
+
+ beforeEach(() => {
+ const Component = Vue.extend(unstageButton);
+ f = file();
+
+ vm = createComponentWithStore(Component, store, {
+ path: f.path,
+ });
+
+ spyOn(vm, 'unstageChange');
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders button to unstage', () => {
+ expect(vm.$el.querySelectorAll('.btn').length).toBe(1);
+ });
+
+ it('calls store with unnstage button', () => {
+ vm.$el.querySelector('.btn').click();
+
+ expect(vm.unstageChange).toHaveBeenCalledWith(f.path);
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js
index 113ade269e9..768f6e99bf1 100644
--- a/spec/javascripts/ide/components/repo_commit_section_spec.js
+++ b/spec/javascripts/ide/components/repo_commit_section_spec.js
@@ -28,16 +28,34 @@ describe('RepoCommitSection', () => {
},
};
+ const files = [file('file1'), file('file2')].map(f =>
+ Object.assign(f, {
+ type: 'blob',
+ }),
+ );
+
vm.$store.state.rightPanelCollapsed = false;
vm.$store.state.currentBranch = 'master';
- vm.$store.state.changedFiles = [file('file1'), file('file2')];
+ vm.$store.state.changedFiles = [...files];
vm.$store.state.changedFiles.forEach(f =>
Object.assign(f, {
changed: true,
+ content: 'changedFile testing',
+ }),
+ );
+
+ vm.$store.state.stagedFiles = [{ ...files[0] }, { ...files[1] }];
+ vm.$store.state.stagedFiles.forEach(f =>
+ Object.assign(f, {
+ changed: true,
content: 'testing',
}),
);
+ vm.$store.state.changedFiles.forEach(f => {
+ vm.$store.state.entries[f.path] = f;
+ });
+
return vm.$mount();
}
@@ -94,20 +112,93 @@ describe('RepoCommitSection', () => {
...vm.$el.querySelectorAll('.multi-file-commit-list li'),
];
const submitCommit = vm.$el.querySelector('form .btn');
+ const allFiles = vm.$store.state.changedFiles.concat(
+ vm.$store.state.stagedFiles,
+ );
expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull();
- expect(changedFileElements.length).toEqual(2);
+ expect(changedFileElements.length).toEqual(4);
changedFileElements.forEach((changedFile, i) => {
- expect(changedFile.textContent.trim()).toContain(
- vm.$store.state.changedFiles[i].path,
- );
+ expect(changedFile.textContent.trim()).toContain(allFiles[i].path);
});
expect(submitCommit.disabled).toBeTruthy();
expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull();
});
+ it('adds changed files into staged files', done => {
+ vm.$el.querySelector('.ide-staged-action-btn').click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.ide-commit-list-container').textContent,
+ ).toContain('No changes');
+
+ done();
+ });
+ });
+
+ it('stages a single file', done => {
+ vm.$el.querySelector('.multi-file-discard-btn .btn').click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el
+ .querySelector('.ide-commit-list-container')
+ .querySelectorAll('li').length,
+ ).toBe(1);
+
+ done();
+ });
+ });
+
+ it('discards a single file', done => {
+ vm.$el.querySelectorAll('.multi-file-discard-btn .btn')[1].click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.ide-commit-list-container').textContent,
+ ).not.toContain('file1');
+ expect(
+ vm.$el
+ .querySelector('.ide-commit-list-container')
+ .querySelectorAll('li').length,
+ ).toBe(1);
+
+ done();
+ });
+ });
+
+ it('removes all staged files', done => {
+ vm.$el.querySelectorAll('.ide-staged-action-btn')[1].click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelectorAll('.ide-commit-list-container')[1].textContent,
+ ).toContain('No changes');
+
+ done();
+ });
+ });
+
+ it('unstages a single file', done => {
+ vm.$el
+ .querySelectorAll('.multi-file-discard-btn')[2]
+ .querySelector('.btn')
+ .click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el
+ .querySelectorAll('.ide-commit-list-container')[1]
+ .querySelectorAll('li').length,
+ ).toBe(1);
+
+ done();
+ });
+ });
+
it('updates commitMessage in store on input', done => {
const textarea = vm.$el.querySelector('textarea');
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
index 310d222377f..b06a6c62a1c 100644
--- a/spec/javascripts/ide/components/repo_editor_spec.js
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -200,7 +200,7 @@ describe('RepoEditor', () => {
vm.setupEditor();
- expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file);
+ expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file, null);
expect(vm.model).not.toBeNull();
});
@@ -222,7 +222,7 @@ describe('RepoEditor', () => {
vm.setupEditor();
expect(vm.editor.onPositionChange).toHaveBeenCalled();
- expect(vm.model.events.size).toBe(1);
+ expect(vm.model.events.size).toBe(2);
});
it('updates state when model content changed', done => {
@@ -234,6 +234,20 @@ describe('RepoEditor', () => {
done();
});
});
+
+ it('sets head model as staged file', () => {
+ spyOn(vm.editor, 'createModel').and.callThrough();
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.$store.state.stagedFiles.push({ ...vm.file, key: 'staged' });
+ vm.file.staged = true;
+ vm.file.key = `unstaged-${vm.file.key}`;
+
+ vm.setupEditor();
+
+ expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file, vm.$store.state.stagedFiles[0]);
+ });
});
describe('editor updateDimensions', () => {
diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js
index 8fc2fccb64c..7a6c22b6d27 100644
--- a/spec/javascripts/ide/lib/common/model_spec.js
+++ b/spec/javascripts/ide/lib/common/model_spec.js
@@ -30,6 +30,19 @@ describe('Multi-file editor library model', () => {
expect(model.baseModel).not.toBeNull();
});
+ it('creates model with head file to compare against', () => {
+ const f = file('path');
+ model.dispose();
+
+ model = new Model(monaco, f, {
+ ...f,
+ content: '123 testing',
+ });
+
+ expect(model.head).not.toBeNull();
+ expect(model.getOriginalModel().getValue()).toBe('123 testing');
+ });
+
it('adds eventHub listener', () => {
expect(eventHub.$on).toHaveBeenCalledWith(
`editor.update.model.dispose.${model.file.key}`,
@@ -70,13 +83,6 @@ describe('Multi-file editor library model', () => {
});
describe('onChange', () => {
- it('caches event by path', () => {
- model.onChange(() => {});
-
- expect(model.events.size).toBe(1);
- expect(model.events.keys().next().value).toBe(model.file.key);
- });
-
it('calls callback on change', done => {
const spy = jasmine.createSpy();
model.onChange(spy);
@@ -119,5 +125,15 @@ describe('Multi-file editor library model', () => {
jasmine.anything(),
);
});
+
+ it('calls onDispose callback', () => {
+ const disposeSpy = jasmine.createSpy();
+
+ model.onDispose(disposeSpy);
+
+ model.dispose();
+
+ expect(disposeSpy).toHaveBeenCalled();
+ });
});
});
diff --git a/spec/javascripts/ide/lib/decorations/controller_spec.js b/spec/javascripts/ide/lib/decorations/controller_spec.js
index aec325e26a9..e1c4ca570b6 100644
--- a/spec/javascripts/ide/lib/decorations/controller_spec.js
+++ b/spec/javascripts/ide/lib/decorations/controller_spec.js
@@ -117,4 +117,33 @@ describe('Multi-file editor library decorations controller', () => {
expect(controller.editorDecorations.size).toBe(0);
});
});
+
+ describe('hasDecorations', () => {
+ it('returns true when decorations are cached', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ expect(controller.hasDecorations(model)).toBe(true);
+ });
+
+ it('returns false when no model decorations exist', () => {
+ expect(controller.hasDecorations(model)).toBe(false);
+ });
+ });
+
+ describe('removeDecorations', () => {
+ beforeEach(() => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+ controller.decorate(model);
+ });
+
+ it('removes cached decorations', () => {
+ expect(controller.decorations.size).not.toBe(0);
+ expect(controller.editorDecorations.size).not.toBe(0);
+
+ controller.removeDecorations(model);
+
+ expect(controller.decorations.size).toBe(0);
+ expect(controller.editorDecorations.size).toBe(0);
+ });
+ });
});
diff --git a/spec/javascripts/ide/lib/diff/controller_spec.js b/spec/javascripts/ide/lib/diff/controller_spec.js
index ff73240734e..fd8ab3b4f1d 100644
--- a/spec/javascripts/ide/lib/diff/controller_spec.js
+++ b/spec/javascripts/ide/lib/diff/controller_spec.js
@@ -3,10 +3,7 @@ import monacoLoader from '~/ide/monaco_loader';
import editor from '~/ide/lib/editor';
import ModelManager from '~/ide/lib/common/model_manager';
import DecorationsController from '~/ide/lib/decorations/controller';
-import DirtyDiffController, {
- getDiffChangeType,
- getDecorator,
-} from '~/ide/lib/diff/controller';
+import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller';
import { computeDiff } from '~/ide/lib/diff/diff';
import { file } from '../../helpers';
@@ -90,6 +87,14 @@ describe('Multi-file editor library dirty diff controller', () => {
expect(model.onChange).toHaveBeenCalled();
});
+ it('adds dispose event callback', () => {
+ spyOn(model, 'onDispose');
+
+ controller.attachModel(model);
+
+ expect(model.onDispose).toHaveBeenCalled();
+ });
+
it('calls throttledComputeDiff on change', () => {
spyOn(controller, 'throttledComputeDiff');
@@ -99,6 +104,12 @@ describe('Multi-file editor library dirty diff controller', () => {
expect(controller.throttledComputeDiff).toHaveBeenCalled();
});
+
+ it('caches model', () => {
+ controller.attachModel(model);
+
+ expect(controller.models.has(model.url)).toBe(true);
+ });
});
describe('computeDiff', () => {
@@ -116,14 +127,22 @@ describe('Multi-file editor library dirty diff controller', () => {
});
describe('reDecorate', () => {
- it('calls decorations controller decorate', () => {
+ it('calls computeDiff when no decorations are cached', () => {
+ spyOn(controller, 'computeDiff');
+
+ controller.reDecorate(model);
+
+ expect(controller.computeDiff).toHaveBeenCalledWith(model);
+ });
+
+ it('calls decorate when decorations are cached', () => {
spyOn(controller.decorationsController, 'decorate');
+ controller.decorationsController.decorations.set(model.url, 'test');
+
controller.reDecorate(model);
- expect(controller.decorationsController.decorate).toHaveBeenCalledWith(
- model,
- );
+ expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model);
});
});
@@ -133,16 +152,15 @@ describe('Multi-file editor library dirty diff controller', () => {
controller.decorate({ data: { changes: [], path: model.path } });
- expect(
- controller.decorationsController.addDecorations,
- ).toHaveBeenCalledWith(model, 'dirtyDiff', jasmine.anything());
+ expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith(
+ model,
+ 'dirtyDiff',
+ jasmine.anything(),
+ );
});
it('adds decorations into editor', () => {
- const spy = spyOn(
- controller.decorationsController.editor.instance,
- 'deltaDecorations',
- );
+ const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations');
controller.decorate({
data: { changes: computeDiff('123', '1234'), path: model.path },
@@ -181,16 +199,22 @@ describe('Multi-file editor library dirty diff controller', () => {
});
it('removes worker event listener', () => {
- spyOn(
- controller.dirtyDiffWorker,
- 'removeEventListener',
- ).and.callThrough();
+ spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough();
controller.dispose();
- expect(
- controller.dirtyDiffWorker.removeEventListener,
- ).toHaveBeenCalledWith('message', jasmine.anything());
+ expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith(
+ 'message',
+ jasmine.anything(),
+ );
+ });
+
+ it('clears cached models', () => {
+ controller.attachModel(model);
+
+ model.dispose();
+
+ expect(controller.models.size).toBe(0);
});
});
});
diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js
index 75e6f0f54ec..530bdfa2759 100644
--- a/spec/javascripts/ide/lib/editor_spec.js
+++ b/spec/javascripts/ide/lib/editor_spec.js
@@ -88,7 +88,7 @@ describe('Multi-file editor library', () => {
instance.createModel('FILE');
- expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE');
+ expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE', null);
});
});
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 479ed7ce49e..ce5c525bed7 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -1,9 +1,12 @@
import Vue from 'vue';
import store from '~/ide/stores';
+import * as actions from '~/ide/stores/actions/file';
+import * as types from '~/ide/stores/mutation_types';
import service from '~/ide/services';
import router from '~/ide/ide_router';
import eventHub from '~/ide/eventhub';
import { file, resetStore } from '../../helpers';
+import testAction from '../../../helpers/vuex_action_helper';
describe('IDE store file actions', () => {
beforeEach(() => {
@@ -402,6 +405,7 @@ describe('IDE store file actions', () => {
beforeEach(() => {
spyOn(eventHub, '$on');
+ spyOn(eventHub, '$emit');
tmpFile = file();
tmpFile.content = 'testing';
@@ -460,6 +464,57 @@ describe('IDE store file actions', () => {
})
.catch(done.fail);
});
+
+ it('pushes route for active file', done => {
+ tmpFile.active = true;
+ store.state.openFiles.push(tmpFile);
+
+ store
+ .dispatch('discardFileChanges', tmpFile.path)
+ .then(() => {
+ expect(router.push).toHaveBeenCalledWith(`/project${tmpFile.url}`);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('emits eventHub event to dispose cached model', done => {
+ store
+ .dispatch('discardFileChanges', tmpFile.path)
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalled();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('stageChange', () => {
+ it('calls STAGE_CHANGE with file path', done => {
+ testAction(
+ actions.stageChange,
+ 'path',
+ store.state,
+ [{ type: types.STAGE_CHANGE, payload: 'path' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('unstageChange', () => {
+ it('calls UNSTAGE_CHANGE with file path', done => {
+ testAction(
+ actions.unstageChange,
+ 'path',
+ store.state,
+ [{ type: types.UNSTAGE_CHANGE, payload: 'path' }],
+ [],
+ done,
+ );
+ });
});
describe('openPendingTab', () => {
@@ -476,7 +531,7 @@ describe('IDE store file actions', () => {
it('makes file pending in openFiles', done => {
store
- .dispatch('openPendingTab', f)
+ .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
.then(() => {
expect(store.state.openFiles[0].pending).toBe(true);
})
@@ -486,7 +541,7 @@ describe('IDE store file actions', () => {
it('returns true when opened', done => {
store
- .dispatch('openPendingTab', f)
+ .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
.then(added => {
expect(added).toBe(true);
})
@@ -498,7 +553,7 @@ describe('IDE store file actions', () => {
store.state.currentBranchId = 'master';
store
- .dispatch('openPendingTab', f)
+ .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
.then(() => {
expect(router.push).toHaveBeenCalledWith('/project/123/tree/master/');
})
@@ -512,7 +567,7 @@ describe('IDE store file actions', () => {
store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line
store
- .dispatch('openPendingTab', f)
+ .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
.then(() => {
expect(scrollToTabSpy).toHaveBeenCalled();
store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line
@@ -527,7 +582,7 @@ describe('IDE store file actions', () => {
store.state.viewer = 'diff';
store
- .dispatch('openPendingTab', f)
+ .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
.then(added => {
expect(added).toBe(false);
})
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index cec572f4507..22a7441ba92 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -1,7 +1,10 @@
import * as urlUtils from '~/lib/utils/url_utility';
import store from '~/ide/stores';
+import * as actions from '~/ide/stores/actions';
+import * as types from '~/ide/stores/mutation_types';
import router from '~/ide/ide_router';
import { resetStore, file } from '../helpers';
+import testAction from '../../helpers/vuex_action_helper';
describe('Multi-file store actions', () => {
beforeEach(() => {
@@ -191,9 +194,7 @@ describe('Multi-file store actions', () => {
})
.then(f => {
expect(f.tempFile).toBeTruthy();
- expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(
- 1,
- );
+ expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1);
done();
})
@@ -292,6 +293,42 @@ describe('Multi-file store actions', () => {
});
});
+ describe('stageAllChanges', () => {
+ it('adds all files from changedFiles to stagedFiles', done => {
+ store.state.changedFiles.push(file(), file('new'));
+
+ testAction(
+ actions.stageAllChanges,
+ null,
+ store.state,
+ [
+ { type: types.STAGE_CHANGE, payload: store.state.changedFiles[0].path },
+ { type: types.STAGE_CHANGE, payload: store.state.changedFiles[1].path },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('unstageAllChanges', () => {
+ it('removes all files from stagedFiles after unstaging', done => {
+ store.state.stagedFiles.push(file(), file('new'));
+
+ testAction(
+ actions.unstageAllChanges,
+ null,
+ store.state,
+ [
+ { type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[0].path },
+ { type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[1].path },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
describe('updateViewer', () => {
it('updates viewer state', done => {
store
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
index 33733b97dff..8d04b83928c 100644
--- a/spec/javascripts/ide/stores/getters_spec.js
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -37,19 +37,11 @@ describe('IDE store getters', () => {
expect(modifiedFiles.length).toBe(1);
expect(modifiedFiles[0].name).toBe('changed');
});
- });
- describe('addedFiles', () => {
- it('returns a list of added files', () => {
- localState.openFiles.push(file());
- localState.changedFiles.push(file('added'));
- localState.changedFiles[0].changed = true;
- localState.changedFiles[0].tempFile = true;
+ it('returns angle left when collapsed', () => {
+ localState.rightPanelCollapsed = true;
- const modifiedFiles = getters.addedFiles(localState);
-
- expect(modifiedFiles.length).toBe(1);
- expect(modifiedFiles[0].name).toBe('added');
+ expect(getters.collapseButtonIcon(localState)).toBe('angle-double-left');
});
});
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
index 1946a0c547c..116967208e0 100644
--- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -209,14 +209,14 @@ describe('IDE commit module actions', () => {
},
},
};
- store.state.changedFiles.push(f, {
+ store.state.stagedFiles.push(f, {
...file('changedFile2'),
changed: true,
});
- store.state.openFiles = store.state.changedFiles;
+ store.state.openFiles = store.state.stagedFiles;
- store.state.changedFiles.forEach(changedFile => {
- store.state.entries[changedFile.path] = changedFile;
+ store.state.stagedFiles.forEach(stagedFile => {
+ store.state.entries[stagedFile.path] = stagedFile;
});
});
@@ -248,19 +248,6 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
- it('removes all changed files', done => {
- store
- .dispatch('commit/updateFilesAfterCommit', {
- data,
- branch,
- })
- .then(() => {
- expect(store.state.changedFiles.length).toBe(0);
- })
- .then(done)
- .catch(done.fail);
- });
-
it('sets files commit data', done => {
store
.dispatch('commit/updateFilesAfterCommit', {
@@ -294,10 +281,10 @@ describe('IDE commit module actions', () => {
branch,
})
.then(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith(
- `editor.update.model.content.${f.path}`,
- f.content,
- );
+ expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.content.${f.key}`, {
+ content: f.content,
+ changed: false,
+ });
})
.then(done)
.catch(done.fail);
@@ -335,12 +322,22 @@ describe('IDE commit module actions', () => {
},
},
};
- store.state.changedFiles.push(file('changed'));
- store.state.changedFiles[0].active = true;
+
+ const f = {
+ ...file('changed'),
+ type: 'blob',
+ active: true,
+ };
+ store.state.stagedFiles.push(f);
+ store.state.changedFiles = [
+ {
+ ...f,
+ },
+ ];
store.state.openFiles = store.state.changedFiles;
- store.state.openFiles.forEach(f => {
- store.state.entries[f.path] = f;
+ store.state.openFiles.forEach(localF => {
+ store.state.entries[localF.path] = localF;
});
store.state.commit.commitAction = '2';
@@ -420,11 +417,13 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
- it('adds commit data to changed files', done => {
+ it('adds commit data to files', done => {
store
.dispatch('commit/commitChanges')
.then(() => {
- expect(store.state.openFiles[0].lastCommit.message).toBe('test message');
+ expect(store.state.entries[store.state.openFiles[0].path].lastCommit.message).toBe(
+ 'test message',
+ );
done();
})
@@ -443,6 +442,16 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
+ it('removes all staged files', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(store.state.stagedFiles.length).toBe(0);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
describe('merge request', () => {
it('redirects to new merge request page', done => {
spyOn(eventHub, '$on');
@@ -471,7 +480,7 @@ describe('IDE commit module actions', () => {
store
.dispatch('commit/commitChanges')
.then(() => {
- expect(store.state.changedFiles.length).toBe(0);
+ expect(store.state.stagedFiles.length).toBe(0);
done();
})
diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
index e396284ec2c..55580f046ad 100644
--- a/spec/javascripts/ide/stores/modules/commit/getters_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
@@ -34,17 +34,17 @@ describe('IDE commit module getters', () => {
discardDraftButtonDisabled: false,
};
const rootState = {
- changedFiles: ['a'],
+ stagedFiles: ['a'],
};
- it('returns false when discardDraftButtonDisabled is false & changedFiles is not empty', () => {
+ it('returns false when discardDraftButtonDisabled is false & stagedFiles is not empty', () => {
expect(
getters.commitButtonDisabled(state, localGetters, rootState),
).toBeFalsy();
});
- it('returns true when discardDraftButtonDisabled is false & changedFiles is empty', () => {
- rootState.changedFiles.length = 0;
+ it('returns true when discardDraftButtonDisabled is false & stagedFiles is empty', () => {
+ rootState.stagedFiles.length = 0;
expect(
getters.commitButtonDisabled(state, localGetters, rootState),
@@ -61,7 +61,7 @@ describe('IDE commit module getters', () => {
it('returns true when discardDraftButtonDisabled is false & changedFiles is not empty', () => {
localGetters.discardDraftButtonDisabled = false;
- rootState.changedFiles.length = 0;
+ rootState.stagedFiles.length = 0;
expect(
getters.commitButtonDisabled(state, localGetters, rootState),
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
index bf9d5166d0a..6fba934810d 100644
--- a/spec/javascripts/ide/stores/mutations/file_spec.js
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -8,7 +8,10 @@ describe('IDE store file mutations', () => {
beforeEach(() => {
localState = state();
- localFile = file();
+ localFile = {
+ ...file(),
+ type: 'blob',
+ };
localState.entries[localFile.path] = localFile;
});
@@ -183,6 +186,49 @@ describe('IDE store file mutations', () => {
});
});
+ describe('STAGE_CHANGE', () => {
+ it('adds file into stagedFiles array', () => {
+ mutations.STAGE_CHANGE(localState, localFile.path);
+
+ expect(localState.stagedFiles.length).toBe(1);
+ expect(localState.stagedFiles[0]).toEqual(localFile);
+ });
+
+ it('updates stagedFile if it is already staged', () => {
+ mutations.STAGE_CHANGE(localState, localFile.path);
+
+ localFile.raw = 'testing 123';
+
+ mutations.STAGE_CHANGE(localState, localFile.path);
+
+ expect(localState.stagedFiles.length).toBe(1);
+ expect(localState.stagedFiles[0].raw).toEqual('testing 123');
+ });
+ });
+
+ describe('UNSTAGE_CHANGE', () => {
+ let f;
+
+ beforeEach(() => {
+ f = {
+ ...file(),
+ type: 'blob',
+ staged: true,
+ };
+
+ localState.stagedFiles.push(f);
+ localState.changedFiles.push(f);
+ localState.entries[f.path] = f;
+ });
+
+ it('removes from stagedFiles array', () => {
+ mutations.UNSTAGE_CHANGE(localState, f.path);
+
+ expect(localState.stagedFiles.length).toBe(0);
+ expect(localState.changedFiles.length).toBe(1);
+ });
+ });
+
describe('TOGGLE_FILE_CHANGED', () => {
it('updates file changed status', () => {
mutations.TOGGLE_FILE_CHANGED(localState, {
diff --git a/spec/javascripts/ide/stores/mutations/tree_spec.js b/spec/javascripts/ide/stores/mutations/tree_spec.js
index e6c085eaff6..67e9f7509da 100644
--- a/spec/javascripts/ide/stores/mutations/tree_spec.js
+++ b/spec/javascripts/ide/stores/mutations/tree_spec.js
@@ -55,6 +55,16 @@ describe('Multi-file store tree mutations', () => {
expect(tree.tree[1].name).toBe('submodule');
expect(tree.tree[2].name).toBe('blob');
});
+
+ it('keeps loading state', () => {
+ mutations.CREATE_TREE(localState, { treePath: 'project/master' });
+ mutations.SET_DIRECTORY_DATA(localState, {
+ data,
+ treePath: 'project/master',
+ });
+
+ expect(localState.trees['project/master'].loading).toBe(true);
+ });
});
describe('REMOVE_ALL_CHANGES_FILES', () => {
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
index 38162a470ad..26e7ed4535e 100644
--- a/spec/javascripts/ide/stores/mutations_spec.js
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -69,6 +69,16 @@ describe('Multi-file store mutations', () => {
});
});
+ describe('CLEAR_STAGED_CHANGES', () => {
+ it('clears stagedFiles array', () => {
+ localState.stagedFiles.push('a');
+
+ mutations.CLEAR_STAGED_CHANGES(localState);
+
+ expect(localState.stagedFiles.length).toBe(0);
+ });
+ });
+
describe('UPDATE_VIEWER', () => {
it('sets viewer state', () => {
mutations.UPDATE_VIEWER(localState, 'diff');
diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js
index 0961605ce5c..4f861c39d3f 100644
--- a/spec/javascripts/jobs/header_spec.js
+++ b/spec/javascripts/jobs/header_spec.js
@@ -36,14 +36,28 @@ describe('Job details header', () => {
},
isLoading: false,
};
-
- vm = mountComponent(HeaderComponent, props);
});
afterEach(() => {
vm.$destroy();
});
+ describe('job reason', () => {
+ it('should not render the reason when reason is absent', () => {
+ vm = mountComponent(HeaderComponent, props);
+
+ expect(vm.shouldRenderReason).toBe(false);
+ });
+
+ it('should render the reason when reason is present', () => {
+ props.job.callout_message = 'There is an unknown failure, please try again';
+
+ vm = mountComponent(HeaderComponent, props);
+
+ expect(vm.shouldRenderReason).toBe(true);
+ });
+ });
+
describe('triggered job', () => {
beforeEach(() => {
vm = mountComponent(HeaderComponent, props);
@@ -51,14 +65,17 @@ describe('Job details header', () => {
it('should render provided job information', () => {
expect(
- vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(),
+ vm.$el
+ .querySelector('.header-main-content')
+ .textContent.replace(/\s+/g, ' ')
+ .trim(),
).toEqual('failed Job #123 triggered 3 weeks ago by Foo');
});
it('should render new issue link', () => {
- expect(
- vm.$el.querySelector('.js-new-issue').getAttribute('href'),
- ).toEqual(props.job.new_issue_path);
+ expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(
+ props.job.new_issue_path,
+ );
});
});
@@ -68,7 +85,10 @@ describe('Job details header', () => {
vm = mountComponent(HeaderComponent, props);
expect(
- vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(),
+ vm.$el
+ .querySelector('.header-main-content')
+ .textContent.replace(/\s+/g, ' ')
+ .trim(),
).toEqual('failed Job #123 created 3 weeks ago by Foo');
});
});
diff --git a/spec/javascripts/jobs/sidebar_details_block_spec.js b/spec/javascripts/jobs/sidebar_details_block_spec.js
index 602dae514b1..6b397c22fb9 100644
--- a/spec/javascripts/jobs/sidebar_details_block_spec.js
+++ b/spec/javascripts/jobs/sidebar_details_block_spec.js
@@ -31,10 +31,25 @@ describe('Sidebar details block', () => {
});
});
+ describe("when user can't retry", () => {
+ it('should not render a retry button', () => {
+ vm = new SidebarComponent({
+ propsData: {
+ job: {},
+ canUserRetry: false,
+ isLoading: true,
+ },
+ }).$mount();
+
+ expect(vm.$el.querySelector('.js-retry-job')).toBeNull();
+ });
+ });
+
beforeEach(() => {
vm = new SidebarComponent({
propsData: {
job,
+ canUserRetry: true,
isLoading: false,
},
}).$mount();
@@ -42,7 +57,9 @@ describe('Sidebar details block', () => {
describe('actions', () => {
it('should render link to new issue', () => {
- expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(job.new_issue_path);
+ expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(
+ job.new_issue_path,
+ );
expect(vm.$el.querySelector('.js-new-issue').textContent.trim()).toEqual('New issue');
});
@@ -57,43 +74,35 @@ describe('Sidebar details block', () => {
describe('information', () => {
it('should render merge request link', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-mr')),
- ).toEqual('Merge Request: !2');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-mr'))).toEqual('Merge Request: !2');
- expect(
- vm.$el.querySelector('.js-job-mr a').getAttribute('href'),
- ).toEqual(job.merge_request.path);
+ expect(vm.$el.querySelector('.js-job-mr a').getAttribute('href')).toEqual(
+ job.merge_request.path,
+ );
});
it('should render job duration', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-duration')),
- ).toEqual('Duration: 6 seconds');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-duration'))).toEqual(
+ 'Duration: 6 seconds',
+ );
});
it('should render erased date', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-erased')),
- ).toEqual('Erased: 3 weeks ago');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-erased'))).toEqual('Erased: 3 weeks ago');
});
it('should render finished date', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-finished')),
- ).toEqual('Finished: 3 weeks ago');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-finished'))).toEqual(
+ 'Finished: 3 weeks ago',
+ );
});
it('should render queued date', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-queued')),
- ).toEqual('Queued: 9 seconds');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-queued'))).toEqual('Queued: 9 seconds');
});
it('should render runner ID', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-runner')),
- ).toEqual('Runner: #1');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual('Runner: #1');
});
it('should render timeout information', () => {
@@ -103,15 +112,11 @@ describe('Sidebar details block', () => {
});
it('should render coverage', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-coverage')),
- ).toEqual('Coverage: 20%');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-coverage'))).toEqual('Coverage: 20%');
});
it('should render tags', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-tags')),
- ).toEqual('Tags: tag');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-tags'))).toEqual('Tags: tag');
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
index 78bac1c61a5..5573d7c5c93 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
@@ -1,16 +1,19 @@
import Vue from 'vue';
-import pipelineFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_failed';
+import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue';
+import { removeBreakLine } from 'spec/helpers/vue_component_helper';
-describe('MRWidgetPipelineFailed', () => {
+describe('PipelineFailed', () => {
describe('template', () => {
- const Component = Vue.extend(pipelineFailedComponent);
+ const Component = Vue.extend(PipelineFailed);
const vm = new Component({
el: document.createElement('div'),
});
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
- expect(vm.$el.innerText).toContain('The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure');
+ expect(
+ removeBreakLine(vm.$el.innerText).trim(),
+ ).toContain('The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure');
});
});
});
diff --git a/spec/javascripts/vue_shared/components/callout_spec.js b/spec/javascripts/vue_shared/components/callout_spec.js
new file mode 100644
index 00000000000..e62bd86f4ca
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/callout_spec.js
@@ -0,0 +1,45 @@
+import Vue from 'vue';
+import callout from '~/vue_shared/components/callout.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('Callout Component', () => {
+ let CalloutComponent;
+ let vm;
+ const exampleMessage = 'This is a callout message!';
+
+ beforeEach(() => {
+ CalloutComponent = Vue.extend(callout);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render the appropriate variant of callout', () => {
+ vm = createComponent(CalloutComponent, {
+ category: 'info',
+ message: exampleMessage,
+ });
+
+ expect(vm.$el.getAttribute('class')).toEqual('bs-callout bs-callout-info');
+
+ expect(vm.$el.tagName).toEqual('DIV');
+ });
+
+ it('should render accessibility attributes', () => {
+ vm = createComponent(CalloutComponent, {
+ message: exampleMessage,
+ });
+
+ expect(vm.$el.getAttribute('role')).toEqual('alert');
+ expect(vm.$el.getAttribute('aria-live')).toEqual('assertive');
+ });
+
+ it('should render the provided message', () => {
+ vm = createComponent(CalloutComponent, {
+ message: exampleMessage,
+ });
+
+ expect(vm.$el.innerHTML.trim()).toEqual(exampleMessage);
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js
index fdead874209..ed66361bfc3 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import commitComp from '~/vue_shared/components/commit.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Commit component', () => {
let props;
@@ -10,25 +11,28 @@ describe('Commit component', () => {
CommitComponent = Vue.extend(commitComp);
});
+ afterEach(() => {
+ component.$destroy();
+ });
+
it('should render a fork icon if it does not represent a tag', () => {
- component = new CommitComponent({
- propsData: {
- tag: false,
- commitRef: {
- name: 'master',
- ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
- },
- commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
- shortSha: 'b7836edd',
- title: 'Commit message',
- author: {
- avatar_url: 'https://gitlab.com/uploads/-/system/user/avatar/300478/avatar.png',
- web_url: 'https://gitlab.com/jschatz1',
- path: '/jschatz1',
- username: 'jschatz1',
- },
+ component = mountComponent(CommitComponent, {
+ tag: false,
+ commitRef: {
+ name: 'master',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
},
- }).$mount();
+ commitUrl:
+ 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ shortSha: 'b7836edd',
+ title: 'Commit message',
+ author: {
+ avatar_url: 'https://gitlab.com/uploads/-/system/user/avatar/300478/avatar.png',
+ web_url: 'https://gitlab.com/jschatz1',
+ path: '/jschatz1',
+ username: 'jschatz1',
+ },
+ });
expect(component.$el.querySelector('.icon-container').children).toContain('svg');
});
@@ -41,7 +45,8 @@ describe('Commit component', () => {
name: 'master',
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
},
- commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ commitUrl:
+ 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
shortSha: 'b7836edd',
title: 'Commit message',
author: {
@@ -53,9 +58,7 @@ describe('Commit component', () => {
commitIconSvg: '<svg></svg>',
};
- component = new CommitComponent({
- propsData: props,
- }).$mount();
+ component = mountComponent(CommitComponent, props);
});
it('should render a tag icon if it represents a tag', () => {
@@ -63,7 +66,9 @@ describe('Commit component', () => {
});
it('should render a link to the ref url', () => {
- expect(component.$el.querySelector('.ref-name').getAttribute('href')).toEqual(props.commitRef.ref_url);
+ expect(component.$el.querySelector('.ref-name').getAttribute('href')).toEqual(
+ props.commitRef.ref_url,
+ );
});
it('should render the ref name', () => {
@@ -71,7 +76,9 @@ describe('Commit component', () => {
});
it('should render the commit short sha with a link to the commit url', () => {
- expect(component.$el.querySelector('.commit-sha').getAttribute('href')).toEqual(props.commitUrl);
+ expect(component.$el.querySelector('.commit-sha').getAttribute('href')).toEqual(
+ props.commitUrl,
+ );
expect(component.$el.querySelector('.commit-sha').textContent).toContain(props.shortSha);
});
@@ -88,21 +95,25 @@ describe('Commit component', () => {
it('Should render the author avatar with title and alt attributes', () => {
expect(
- component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('data-original-title'),
+ component.$el
+ .querySelector('.commit-title .avatar-image-container img')
+ .getAttribute('data-original-title'),
).toContain(props.author.username);
expect(
- component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt'),
+ component.$el
+ .querySelector('.commit-title .avatar-image-container img')
+ .getAttribute('alt'),
).toContain(`${props.author.username}'s avatar`);
});
});
it('should render the commit title', () => {
- expect(
- component.$el.querySelector('a.commit-row-message').getAttribute('href'),
- ).toEqual(props.commitUrl);
- expect(
- component.$el.querySelector('a.commit-row-message').textContent,
- ).toContain(props.title);
+ expect(component.$el.querySelector('a.commit-row-message').getAttribute('href')).toEqual(
+ props.commitUrl,
+ );
+ expect(component.$el.querySelector('a.commit-row-message').textContent).toContain(
+ props.title,
+ );
});
});
@@ -114,19 +125,18 @@ describe('Commit component', () => {
name: 'master',
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
},
- commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ commitUrl:
+ 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
shortSha: 'b7836edd',
title: null,
author: {},
};
- component = new CommitComponent({
- propsData: props,
- }).$mount();
+ component = mountComponent(CommitComponent, props);
- expect(
- component.$el.querySelector('.commit-title span').textContent,
- ).toContain('Cant find HEAD commit for this branch');
+ expect(component.$el.querySelector('.commit-title span').textContent).toContain(
+ "Can't find HEAD commit for this branch",
+ );
});
});
});
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
index 1fe034ae9a2..209a547c3b3 100644
--- a/spec/lib/banzai/object_renderer_spec.rb
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -11,7 +11,7 @@ describe Banzai::ObjectRenderer do
)
end
- let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_VERSION) }
+ let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
describe '#render' do
context 'with cache' do
diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb
index 3a9371ed2e8..6a9c6442282 100644
--- a/spec/lib/gitlab/ci/trace_spec.rb
+++ b/spec/lib/gitlab/ci/trace_spec.rb
@@ -458,7 +458,7 @@ describe Gitlab::Ci::Trace do
context 'when job does not have trace artifact' do
context 'when trace file stored in default path' do
let!(:build) { create(:ci_build, :success, :trace_live) }
- let!(:src_path) { trace.read { |s| return s.path } }
+ let!(:src_path) { trace.read { |s| s.path } }
let!(:src_checksum) { Digest::SHA256.file(src_path).hexdigest }
it_behaves_like 'archive trace file'
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index 73d60c021c8..7c9e8c8d04e 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -79,6 +79,8 @@ describe Gitlab::Diff::Highlight do
end
it 'keeps the original rich line' do
+ allow(Gitlab::Sentry).to receive(:track_exception)
+
code = %q{+ raise RuntimeError, "System commands must be given as an array of strings"}
expect(subject[5].text).to eq(code)
@@ -86,12 +88,9 @@ describe Gitlab::Diff::Highlight do
end
it 'reports to Sentry if configured' do
- allow(Gitlab::Sentry).to receive(:enabled?).and_return(true)
-
- expect(Gitlab::Sentry).to receive(:context)
- expect(Raven).to receive(:capture_exception)
+ expect(Gitlab::Sentry).to receive(:track_exception).and_call_original
- subject
+ expect { subject }. to raise_exception(RangeError)
end
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 1e00e8d2739..5acf40ea5ce 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -470,9 +470,20 @@ describe Gitlab::Git::Repository, seed_helper: true do
FileUtils.rm_rf(heads_dir)
FileUtils.mkdir_p(heads_dir)
+ repository.expire_has_local_branches_cache
expect(repository.has_local_branches?).to eq(false)
end
end
+
+ context 'memoizes the value' do
+ it 'returns true' do
+ expect(repository).to receive(:uncached_has_local_branches?).once.and_call_original
+
+ 2.times do
+ expect(repository.has_local_branches?).to eq(true)
+ end
+ end
+ end
end
context 'with gitaly' do
diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb
index 761f7732036..722d697c28e 100644
--- a/spec/lib/gitlab/git/wiki_spec.rb
+++ b/spec/lib/gitlab/git/wiki_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::Git::Wiki do
end
def commit_details(name)
- Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "created page #{name}")
+ Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "created page #{name}")
end
def destroy_page(title, dir = '')
diff --git a/spec/lib/gitlab/sentry_spec.rb b/spec/lib/gitlab/sentry_spec.rb
index 8c211d1c63f..499757da061 100644
--- a/spec/lib/gitlab/sentry_spec.rb
+++ b/spec/lib/gitlab/sentry_spec.rb
@@ -7,7 +7,49 @@ describe Gitlab::Sentry do
described_class.context(nil)
- expect(Raven.tags_context[:locale]).to eq(I18n.locale.to_s)
+ expect(Raven.tags_context[:locale].to_s).to eq(I18n.locale.to_s)
+ end
+ end
+
+ describe '.track_exception' do
+ let(:exception) { RuntimeError.new('boom') }
+
+ before do
+ allow(described_class).to receive(:enabled?).and_return(true)
+ end
+
+ it 'raises the exception if it should' do
+ expect(described_class).to receive(:should_raise?).and_return(true)
+ expect { described_class.track_exception(exception) }
+ .to raise_error(RuntimeError)
+ end
+
+ context 'when exceptions should not be raised' do
+ before do
+ allow(described_class).to receive(:should_raise?).and_return(false)
+ end
+
+ it 'logs the exception with all attributes passed' do
+ expected_extras = {
+ some_other_info: 'info',
+ issue_url: 'http://gitlab.com/gitlab-org/gitlab-ce/issues/1'
+ }
+
+ expect(Raven).to receive(:capture_exception)
+ .with(exception, extra: a_hash_including(expected_extras))
+
+ described_class.track_exception(
+ exception,
+ issue_url: 'http://gitlab.com/gitlab-org/gitlab-ce/issues/1',
+ extra: { some_other_info: 'info' }
+ )
+ end
+
+ it 'sets the context' do
+ expect(described_class).to receive(:context)
+
+ described_class.track_exception(exception)
+ end
end
end
end
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index 7ff2c0639ec..7f579df1c36 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -727,7 +727,7 @@ describe Gitlab::Shell do
def find_in_authorized_keys_file(key_id)
gitlab_shell.batch_read_key_ids do |ids|
- return true if ids.include?(key_id)
+ return true if ids.include?(key_id) # rubocop:disable Cop/AvoidReturnFromBlocks
end
false
diff --git a/spec/lib/gitlab/view/presenter/base_spec.rb b/spec/lib/gitlab/view/presenter/base_spec.rb
index 32a946ca034..4eca53032a2 100644
--- a/spec/lib/gitlab/view/presenter/base_spec.rb
+++ b/spec/lib/gitlab/view/presenter/base_spec.rb
@@ -48,4 +48,11 @@ describe Gitlab::View::Presenter::Base do
end
end
end
+
+ describe '#present' do
+ it 'returns self' do
+ presenter = presenter_class.new(build_stubbed(:project))
+ expect(presenter.present).to eq(presenter)
+ end
+ end
end
diff --git a/spec/lib/gitlab/wiki/committer_with_hooks_spec.rb b/spec/lib/gitlab/wiki/committer_with_hooks_spec.rb
new file mode 100644
index 00000000000..830fb8a8598
--- /dev/null
+++ b/spec/lib/gitlab/wiki/committer_with_hooks_spec.rb
@@ -0,0 +1,154 @@
+require 'spec_helper'
+
+describe Gitlab::Wiki::CommitterWithHooks, seed_helper: true do
+ shared_examples 'calling wiki hooks' do
+ let(:project) { create(:project) }
+ let(:user) { project.owner }
+ let(:project_wiki) { ProjectWiki.new(project, user) }
+ let(:wiki) { project_wiki.wiki }
+ let(:options) do
+ {
+ id: user.id,
+ username: user.username,
+ name: user.name,
+ email: user.email,
+ message: 'commit message'
+ }
+ end
+
+ subject { described_class.new(wiki, options) }
+
+ before do
+ project_wiki.create_page('home', 'test content')
+ end
+
+ shared_examples 'failing pre-receive hook' do
+ before do
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([false, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('update')
+ expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive')
+ end
+
+ it 'raises exception' do
+ expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
+ end
+
+ it 'does not create a new commit inside the repository' do
+ current_rev = find_current_rev
+
+ expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
+
+ expect(current_rev).to eq find_current_rev
+ end
+ end
+
+ shared_examples 'failing update hook' do
+ before do
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([false, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive')
+ end
+
+ it 'raises exception' do
+ expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
+ end
+
+ it 'does not create a new commit inside the repository' do
+ current_rev = find_current_rev
+
+ expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
+
+ expect(current_rev).to eq find_current_rev
+ end
+ end
+
+ shared_examples 'failing post-receive hook' do
+ before do
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([true, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('post-receive').and_return([false, ''])
+ end
+
+ it 'does not raise exception' do
+ expect { subject.commit }.not_to raise_error
+ end
+
+ it 'creates the commit' do
+ current_rev = find_current_rev
+
+ subject.commit
+
+ expect(current_rev).not_to eq find_current_rev
+ end
+ end
+
+ shared_examples 'when hooks call succceeds' do
+ let(:hook) { double(:hook) }
+
+ it 'calls the three hooks' do
+ expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
+ expect(hook).to receive(:trigger).exactly(3).times.and_return([true, nil])
+
+ subject.commit
+ end
+
+ it 'creates the commit' do
+ current_rev = find_current_rev
+
+ subject.commit
+
+ expect(current_rev).not_to eq find_current_rev
+ end
+ end
+
+ context 'when creating a page' do
+ before do
+ project_wiki.create_page('index', 'test content')
+ end
+
+ it_behaves_like 'failing pre-receive hook'
+ it_behaves_like 'failing update hook'
+ it_behaves_like 'failing post-receive hook'
+ it_behaves_like 'when hooks call succceeds'
+ end
+
+ context 'when updating a page' do
+ before do
+ project_wiki.update_page(find_page('home'), content: 'some other content', format: :markdown)
+ end
+
+ it_behaves_like 'failing pre-receive hook'
+ it_behaves_like 'failing update hook'
+ it_behaves_like 'failing post-receive hook'
+ it_behaves_like 'when hooks call succceeds'
+ end
+
+ context 'when deleting a page' do
+ before do
+ project_wiki.delete_page(find_page('home'))
+ end
+
+ it_behaves_like 'failing pre-receive hook'
+ it_behaves_like 'failing update hook'
+ it_behaves_like 'failing post-receive hook'
+ it_behaves_like 'when hooks call succceeds'
+ end
+
+ def find_current_rev
+ wiki.gollum_wiki.repo.commits.first&.sha
+ end
+
+ def find_page(name)
+ wiki.page(title: name)
+ end
+ end
+
+ # TODO: Uncomment once Gitaly updates the ruby vendor code
+ # context 'when Gitaly is enabled' do
+ # it_behaves_like 'calling wiki hooks'
+ # end
+
+ context 'when Gitaly is disabled', :skip_gitaly_mock do
+ it_behaves_like 'calling wiki hooks'
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index a12717835b0..fcdc31c8984 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1384,29 +1384,51 @@ describe Ci::Build do
end
end
- describe '#update_project_statistics' do
- let!(:build) { create(:ci_build, artifacts_size: 23) }
-
- it 'updates project statistics when the artifact size changes' do
- expect(ProjectCacheWorker).to receive(:perform_async)
- .with(build.project_id, [], [:build_artifacts_size])
+ context 'when updating the build' do
+ let(:build) { create(:ci_build, artifacts_size: 23) }
+ it 'updates project statistics' do
build.artifacts_size = 42
- build.save!
+
+ expect(build).to receive(:update_project_statistics_after_save).and_call_original
+
+ expect { build.save! }
+ .to change { build.project.statistics.reload.build_artifacts_size }
+ .by(19)
end
- it 'does not update project statistics when the artifact size stays the same' do
- expect(ProjectCacheWorker).not_to receive(:perform_async)
+ context 'when the artifact size stays the same' do
+ it 'does not update project statistics' do
+ build.name = 'changed'
- build.name = 'changed'
- build.save!
+ expect(build).not_to receive(:update_project_statistics_after_save)
+
+ build.save!
+ end
end
+ end
- it 'updates project statistics when the build is destroyed' do
- expect(ProjectCacheWorker).to receive(:perform_async)
- .with(build.project_id, [], [:build_artifacts_size])
+ context 'when destroying the build' do
+ let!(:build) { create(:ci_build, artifacts_size: 23) }
- build.destroy
+ it 'updates project statistics' do
+ expect(ProjectStatistics)
+ .to receive(:increment_statistic)
+ .and_call_original
+
+ expect { build.destroy! }
+ .to change { build.project.statistics.reload.build_artifacts_size }
+ .by(-23)
+ end
+
+ context 'when the build is destroyed due to the project being destroyed' do
+ it 'does not update the project statistics' do
+ expect(ProjectStatistics)
+ .not_to receive(:increment_statistic)
+
+ build.project.update_attributes(pending_delete: true)
+ build.project.destroy!
+ end
end
end
@@ -1472,7 +1494,7 @@ describe Ci::Build do
{ key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false },
{ key: 'CI', value: 'true', public: true },
{ key: 'GITLAB_CI', value: 'true', public: true },
- { key: 'GITLAB_FEATURES', value: project.namespace.features.join(','), public: true },
+ { key: 'GITLAB_FEATURES', value: project.licensed_features.join(','), public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
{ key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
{ key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
@@ -2140,10 +2162,6 @@ describe Ci::Build do
it "doesn't save timeout_source" do
expect { run_job_without_exception }.not_to change { job.reload.ensure_metadata.timeout_source }
end
-
- it 'raises an exception' do
- expect { job.run! }.to raise_error(StateMachines::InvalidTransition)
- end
end
end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index 1aa28434879..a3e119cbc27 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Ci::JobArtifact do
- set(:artifact) { create(:ci_job_artifact, :archive) }
+ let(:artifact) { create(:ci_job_artifact, :archive) }
describe "Associations" do
it { is_expected.to belong_to(:project) }
@@ -59,10 +59,32 @@ describe Ci::JobArtifact do
end
end
- describe '#set_size' do
- it 'sets the size' do
+ context 'creating the artifact' do
+ let(:project) { create(:project) }
+ let(:artifact) { create(:ci_job_artifact, :archive, project: project) }
+
+ it 'sets the size from the file size' do
expect(artifact.size).to eq(106365)
end
+
+ it 'updates the project statistics' do
+ expect { artifact }
+ .to change { project.statistics.reload.build_artifacts_size }
+ .by(106365)
+ end
+ end
+
+ context 'updating the artifact file' do
+ it 'updates the artifact size' do
+ artifact.update!(file: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png')))
+ expect(artifact.size).to eq(1062)
+ end
+
+ it 'updates the project statistics' do
+ expect { artifact.update!(file: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
+ .to change { artifact.project.statistics.reload.build_artifacts_size }
+ .by(1062 - 106365)
+ end
end
describe '#file' do
@@ -118,4 +140,71 @@ describe Ci::JobArtifact do
is_expected.to be_nil
end
end
+
+ context 'when destroying the artifact' do
+ let(:project) { create(:project, :repository) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let!(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ it 'updates the project statistics' do
+ artifact = build.job_artifacts.first
+
+ expect(ProjectStatistics)
+ .to receive(:increment_statistic)
+ .and_call_original
+
+ expect { artifact.destroy }
+ .to change { project.statistics.reload.build_artifacts_size }
+ .by(-106365)
+ end
+
+ context 'when it is destroyed from the project level' do
+ it 'does not update the project statistics' do
+ expect(ProjectStatistics)
+ .not_to receive(:increment_statistic)
+
+ project.update_attributes(pending_delete: true)
+ project.destroy!
+ end
+ end
+ end
+
+ describe 'file is being stored' do
+ subject { create(:ci_job_artifact, :archive) }
+
+ context 'when object has nil store' do
+ before do
+ subject.update_column(:file_store, nil)
+ subject.reload
+ end
+
+ it 'is stored locally' do
+ expect(subject.file_store).to be(nil)
+ expect(subject.file).to be_file_storage
+ expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+
+ context 'when existing object has local store' do
+ it 'is stored locally' do
+ expect(subject.file_store).to be(ObjectStorage::Store::LOCAL)
+ expect(subject.file).to be_file_storage
+ expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+
+ context 'when direct upload is enabled' do
+ before do
+ stub_artifacts_object_storage(direct_upload: true)
+ end
+
+ context 'when file is stored' do
+ it 'is stored remotely' do
+ expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(subject.file).not_to be_file_storage
+ expect(subject.file.object_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 959383ff0b7..4e6b037a720 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -450,6 +450,11 @@ eos
it "returns nil if the path doesn't exists" do
expect(commit.uri_type('this/path/doesnt/exist')).to be_nil
end
+
+ it 'is nil if the path is nil or empty' do
+ expect(commit.uri_type(nil)).to be_nil
+ expect(commit.uri_type("")).to be_nil
+ end
end
context 'when Gitaly commit_tree_entry feature is enabled' do
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index c536dab2681..2ed29052dc1 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -533,4 +533,36 @@ describe CommitStatus do
end
end
end
+
+ describe '#enqueue' do
+ let!(:current_time) { Time.new(2018, 4, 5, 14, 0, 0) }
+
+ before do
+ allow(Time).to receive(:now).and_return(current_time)
+ end
+
+ shared_examples 'commit status enqueued' do
+ it 'sets queued_at value when enqueued' do
+ expect { commit_status.enqueue }.to change { commit_status.reload.queued_at }.from(nil).to(current_time)
+ end
+ end
+
+ context 'when initial state is :created' do
+ let(:commit_status) { create(:commit_status, :created) }
+
+ it_behaves_like 'commit status enqueued'
+ end
+
+ context 'when initial state is :skipped' do
+ let(:commit_status) { create(:commit_status, :skipped) }
+
+ it_behaves_like 'commit status enqueued'
+ end
+
+ context 'when initial state is :manual' do
+ let(:commit_status) { create(:commit_status, :manual) }
+
+ it_behaves_like 'commit status enqueued'
+ end
+ end
end
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 3c7f578975b..b3797c1fb46 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -72,7 +72,7 @@ describe CacheMarkdownField do
let(:updated_markdown) { '`Bar`' }
let(:updated_html) { '<p dir="auto"><code>Bar</code></p>' }
- let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: CacheMarkdownField::CACHE_VERSION) }
+ let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
describe '.attributes' do
it 'excludes cache attributes' do
@@ -89,17 +89,24 @@ describe CacheMarkdownField do
it { expect(thing.foo).to eq(markdown) }
it { expect(thing.foo_html).to eq(html) }
it { expect(thing.foo_html_changed?).not_to be_truthy }
- it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
+ it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
end
context 'a changed markdown field' do
- before do
- thing.foo = updated_markdown
- thing.save
+ shared_examples 'with cache version' do |cache_version|
+ let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
+
+ before do
+ thing.foo = updated_markdown
+ thing.save
+ end
+
+ it { expect(thing.foo_html).to eq(updated_html) }
+ it { expect(thing.cached_markdown_version).to eq(cache_version) }
end
- it { expect(thing.foo_html).to eq(updated_html) }
- it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
+ it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
+ it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
end
context 'when a markdown field is set repeatedly to an empty string' do
@@ -123,15 +130,22 @@ describe CacheMarkdownField do
end
context 'a non-markdown field changed' do
- before do
- thing.bar = 'OK'
- thing.save
+ shared_examples 'with cache version' do |cache_version|
+ let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
+
+ before do
+ thing.bar = 'OK'
+ thing.save
+ end
+
+ it { expect(thing.bar).to eq('OK') }
+ it { expect(thing.foo).to eq(markdown) }
+ it { expect(thing.foo_html).to eq(html) }
+ it { expect(thing.cached_markdown_version).to eq(cache_version) }
end
- it { expect(thing.bar).to eq('OK') }
- it { expect(thing.foo).to eq(markdown) }
- it { expect(thing.foo_html).to eq(html) }
- it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
+ it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
+ it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
end
context 'version is out of date' do
@@ -142,59 +156,85 @@ describe CacheMarkdownField do
end
it { expect(thing.foo_html).to eq(updated_html) }
- it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
+ it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION) }
end
describe '#cached_html_up_to_date?' do
- subject { thing.cached_html_up_to_date?(:foo) }
+ shared_examples 'with cache version' do |cache_version|
+ let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
- it 'returns false when the version is absent' do
- thing.cached_markdown_version = nil
+ subject { thing.cached_html_up_to_date?(:foo) }
- is_expected.to be_falsy
- end
+ it 'returns false when the version is absent' do
+ thing.cached_markdown_version = nil
- it 'returns false when the version is too early' do
- thing.cached_markdown_version -= 1
+ is_expected.to be_falsy
+ end
- is_expected.to be_falsy
- end
+ it 'returns false when the version is too early' do
+ thing.cached_markdown_version -= 1
- it 'returns false when the version is too late' do
- thing.cached_markdown_version += 1
+ is_expected.to be_falsy
+ end
- is_expected.to be_falsy
- end
+ it 'returns false when the version is too late' do
+ thing.cached_markdown_version += 1
- it 'returns true when the version is just right' do
- thing.cached_markdown_version = CacheMarkdownField::CACHE_VERSION
+ is_expected.to be_falsy
+ end
- is_expected.to be_truthy
- end
+ it 'returns true when the version is just right' do
+ thing.cached_markdown_version = cache_version
- it 'returns false if markdown has been changed but html has not' do
- thing.foo = updated_html
+ is_expected.to be_truthy
+ end
- is_expected.to be_falsy
- end
+ it 'returns false if markdown has been changed but html has not' do
+ thing.foo = updated_html
- it 'returns true if markdown has not been changed but html has' do
- thing.foo_html = updated_html
+ is_expected.to be_falsy
+ end
+
+ it 'returns true if markdown has not been changed but html has' do
+ thing.foo_html = updated_html
- is_expected.to be_truthy
+ is_expected.to be_truthy
+ end
+
+ it 'returns true if markdown and html have both been changed' do
+ thing.foo = updated_markdown
+ thing.foo_html = updated_html
+
+ is_expected.to be_truthy
+ end
+
+ it 'returns false if the markdown field is set but the html is not' do
+ thing.foo_html = nil
+
+ is_expected.to be_falsy
+ end
end
- it 'returns true if markdown and html have both been changed' do
- thing.foo = updated_markdown
- thing.foo_html = updated_html
+ it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
+ it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
+ end
+
+ describe '#latest_cached_markdown_version' do
+ subject { thing.latest_cached_markdown_version }
- is_expected.to be_truthy
+ it 'returns redcarpet version' do
+ thing.cached_markdown_version = CacheMarkdownField::CACHE_COMMONMARK_VERSION_START - 1
+ is_expected.to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end
- it 'returns false if the markdown field is set but the html is not' do
- thing.foo_html = nil
+ it 'returns commonmark version' do
+ thing.cached_markdown_version = CacheMarkdownField::CACHE_COMMONMARK_VERSION_START + 1
+ is_expected.to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION)
+ end
- is_expected.to be_falsy
+ it 'returns default version when version is nil' do
+ thing.cached_markdown_version = nil
+ is_expected.to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end
end
@@ -221,37 +261,44 @@ describe CacheMarkdownField do
thing.cached_markdown_version = nil
thing.refresh_markdown_cache
- expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
+ expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end
end
describe '#refresh_markdown_cache!' do
- before do
- thing.foo = updated_markdown
- end
+ shared_examples 'with cache version' do |cache_version|
+ let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
- it 'fills all html fields' do
- thing.refresh_markdown_cache!
+ before do
+ thing.foo = updated_markdown
+ end
- expect(thing.foo_html).to eq(updated_html)
- expect(thing.foo_html_changed?).to be_truthy
- expect(thing.baz_html_changed?).to be_truthy
- end
+ it 'fills all html fields' do
+ thing.refresh_markdown_cache!
- it 'skips saving if not persisted' do
- expect(thing).to receive(:persisted?).and_return(false)
- expect(thing).not_to receive(:update_columns)
+ expect(thing.foo_html).to eq(updated_html)
+ expect(thing.foo_html_changed?).to be_truthy
+ expect(thing.baz_html_changed?).to be_truthy
+ end
- thing.refresh_markdown_cache!
- end
+ it 'skips saving if not persisted' do
+ expect(thing).to receive(:persisted?).and_return(false)
+ expect(thing).not_to receive(:update_columns)
- it 'saves the changes using #update_columns' do
- expect(thing).to receive(:persisted?).and_return(true)
- expect(thing).to receive(:update_columns)
- .with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION)
+ thing.refresh_markdown_cache!
+ end
- thing.refresh_markdown_cache!
+ it 'saves the changes using #update_columns' do
+ expect(thing).to receive(:persisted?).and_return(true)
+ expect(thing).to receive(:update_columns)
+ .with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => cache_version)
+
+ thing.refresh_markdown_cache!
+ end
end
+
+ it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
+ it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
end
describe '#banzai_render_context' do
@@ -299,7 +346,7 @@ describe CacheMarkdownField do
expect(thing.foo_html).to eq(updated_html)
expect(thing.baz_html).to eq(updated_html)
- expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
+ expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end
end
@@ -319,7 +366,7 @@ describe CacheMarkdownField do
expect(thing.foo_html).to eq(updated_html)
expect(thing.baz_html).to eq(updated_html)
- expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
+ expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end
end
end
diff --git a/spec/models/concerns/group_descendant_spec.rb b/spec/models/concerns/group_descendant_spec.rb
index c163fb01a81..28352d8c961 100644
--- a/spec/models/concerns/group_descendant_spec.rb
+++ b/spec/models/concerns/group_descendant_spec.rb
@@ -79,9 +79,24 @@ describe GroupDescendant, :nested_groups do
expect(described_class.build_hierarchy(groups)).to eq(expected_hierarchy)
end
+ it 'tracks the exception when a parent was not preloaded' do
+ expect(Gitlab::Sentry).to receive(:track_exception).and_call_original
+
+ expect { GroupDescendant.build_hierarchy([subsub_group]) }.to raise_error(ArgumentError)
+ end
+
+ it 'recovers if a parent was not reloaded by querying for the parent' do
+ expected_hierarchy = { parent => { subgroup => subsub_group } }
+
+ # this does not raise in production, so stubbing it here.
+ allow(Gitlab::Sentry).to receive(:track_exception)
+
+ expect(GroupDescendant.build_hierarchy([subsub_group])).to eq(expected_hierarchy)
+ end
+
it 'raises an error if not all elements were preloaded' do
expect { described_class.build_hierarchy([subsub_group]) }
- .to raise_error('parent was not preloaded')
+ .to raise_error(/was not preloaded/)
end
end
end
diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb
index a182116d637..ba06ff42d87 100644
--- a/spec/models/lfs_object_spec.rb
+++ b/spec/models/lfs_object_spec.rb
@@ -81,5 +81,44 @@ describe LfsObject do
end
end
end
+
+ describe 'file is being stored' do
+ let(:lfs_object) { create(:lfs_object, :with_file) }
+
+ context 'when object has nil store' do
+ before do
+ lfs_object.update_column(:file_store, nil)
+ lfs_object.reload
+ end
+
+ it 'is stored locally' do
+ expect(lfs_object.file_store).to be(nil)
+ expect(lfs_object.file).to be_file_storage
+ expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+
+ context 'when existing object has local store' do
+ it 'is stored locally' do
+ expect(lfs_object.file_store).to be(ObjectStorage::Store::LOCAL)
+ expect(lfs_object.file).to be_file_storage
+ expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+
+ context 'when direct upload is enabled' do
+ before do
+ stub_lfs_object_storage(direct_upload: true)
+ end
+
+ context 'when file is stored' do
+ it 'is stored remotely' do
+ expect(lfs_object.file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(lfs_object.file).not_to be_file_storage
+ expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 47f4a792e5c..c7460981a32 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -96,7 +96,9 @@ describe Milestone do
allow(milestone).to receive(:due_date).and_return(Date.today.prev_year)
end
- it { expect(milestone.expired?).to be_truthy }
+ it 'returns true when due_date is in the past' do
+ expect(milestone.expired?).to be_truthy
+ end
end
context "not expired" do
@@ -104,17 +106,19 @@ describe Milestone do
allow(milestone).to receive(:due_date).and_return(Date.today.next_year)
end
- it { expect(milestone.expired?).to be_falsey }
+ it 'returns false when due_date is in the future' do
+ expect(milestone.expired?).to be_falsey
+ end
end
end
describe '#upcoming?' do
- it 'returns true' do
+ it 'returns true when start_date is in the future' do
milestone = build(:milestone, start_date: Time.now + 1.month)
expect(milestone.upcoming?).to be_truthy
end
- it 'returns false' do
+ it 'returns false when start_date is in the past' do
milestone = build(:milestone, start_date: Date.today.prev_year)
expect(milestone.upcoming?).to be_falsey
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 86962cd8d61..6a6c71e6c82 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -91,6 +91,23 @@ describe Note do
it "keeps the commit around" do
expect(note.project.repository.kept_around?(commit.id)).to be_truthy
end
+
+ it 'does not generate N+1 queries for participants', :request_store do
+ def retrieve_participants
+ commit.notes_with_associations.map(&:participants).to_a
+ end
+
+ # Project authorization checks are cached, establish a baseline
+ retrieve_participants
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ retrieve_participants
+ end
+
+ create(:note_on_commit, project: note.project, note: 'another note', noteable_id: commit.id)
+
+ expect { retrieve_participants }.not_to exceed_query_limit(control_count)
+ end
end
describe 'authorization' do
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index 5cff2af4aca..38a3590ad12 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -4,26 +4,6 @@ describe ProjectStatistics do
let(:project) { create :project }
let(:statistics) { project.statistics }
- describe 'constants' do
- describe 'STORAGE_COLUMNS' do
- it 'is an array of symbols' do
- expect(described_class::STORAGE_COLUMNS).to be_kind_of Array
- expect(described_class::STORAGE_COLUMNS.map(&:class).uniq).to eq [Symbol]
- end
- end
-
- describe 'STATISTICS_COLUMNS' do
- it 'is an array of symbols' do
- expect(described_class::STATISTICS_COLUMNS).to be_kind_of Array
- expect(described_class::STATISTICS_COLUMNS.map(&:class).uniq).to eq [Symbol]
- end
-
- it 'includes all storage columns' do
- expect(described_class::STATISTICS_COLUMNS & described_class::STORAGE_COLUMNS).to eq described_class::STORAGE_COLUMNS
- end
- end
- end
-
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:namespace) }
@@ -63,7 +43,6 @@ describe ProjectStatistics do
allow(statistics).to receive(:update_commit_count)
allow(statistics).to receive(:update_repository_size)
allow(statistics).to receive(:update_lfs_objects_size)
- allow(statistics).to receive(:update_build_artifacts_size)
allow(statistics).to receive(:update_storage_size)
end
@@ -76,7 +55,6 @@ describe ProjectStatistics do
expect(statistics).to have_received(:update_commit_count)
expect(statistics).to have_received(:update_repository_size)
expect(statistics).to have_received(:update_lfs_objects_size)
- expect(statistics).to have_received(:update_build_artifacts_size)
end
end
@@ -89,7 +67,6 @@ describe ProjectStatistics do
expect(statistics).to have_received(:update_lfs_objects_size)
expect(statistics).not_to have_received(:update_commit_count)
expect(statistics).not_to have_received(:update_repository_size)
- expect(statistics).not_to have_received(:update_build_artifacts_size)
end
end
end
@@ -131,40 +108,6 @@ describe ProjectStatistics do
end
end
- describe '#update_build_artifacts_size' do
- let!(:pipeline) { create(:ci_pipeline, project: project) }
-
- context 'when new job artifacts are calculated' do
- let(:ci_build) { create(:ci_build, pipeline: pipeline) }
-
- before do
- create(:ci_job_artifact, :archive, project: pipeline.project, job: ci_build)
- end
-
- it "stores the size of related build artifacts" do
- statistics.update_build_artifacts_size
-
- expect(statistics.build_artifacts_size).to be(106365)
- end
-
- it 'calculates related build artifacts by project' do
- expect(Ci::JobArtifact).to receive(:artifacts_size_for).with(project) { 0 }
-
- statistics.update_build_artifacts_size
- end
- end
-
- context 'when legacy artifacts are used' do
- let!(:ci_build) { create(:ci_build, pipeline: pipeline, artifacts_size: 10.megabytes) }
-
- it "stores the size of related build artifacts" do
- statistics.update_build_artifacts_size
-
- expect(statistics.build_artifacts_size).to eq(10.megabytes)
- end
- end
- end
-
describe '#update_storage_size' do
it "sums all storage counters" do
statistics.update!(
@@ -177,4 +120,27 @@ describe ProjectStatistics do
expect(statistics.storage_size).to eq 5
end
end
+
+ describe '.increment_statistic' do
+ it 'increases the statistic by that amount' do
+ expect { described_class.increment_statistic(project.id, :build_artifacts_size, 13) }
+ .to change { statistics.reload.build_artifacts_size }
+ .by(13)
+ end
+
+ context 'when the amount is 0' do
+ it 'does not execute a query' do
+ project
+ expect { described_class.increment_statistic(project.id, :build_artifacts_size, 0) }
+ .not_to exceed_query_limit(0)
+ end
+ end
+
+ context 'when using an invalid column' do
+ it 'raises an error' do
+ expect { described_class.increment_statistic(project.id, :id, 13) }
+ .to raise_error(ArgumentError, "Cannot increment attribute: id")
+ end
+ end
+ end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 374a157bec0..4e83f4353cf 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -377,7 +377,7 @@ describe ProjectWiki do
end
def commit_details
- Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit")
+ Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "test commit")
end
def create_page(name, content)
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 60ab52565cb..e45fe7db1e7 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1437,6 +1437,12 @@ describe Repository do
repository.expire_emptiness_caches
end
+
+ it 'expires the memoized repository cache' do
+ allow(repository.raw_repository).to receive(:expire_has_local_branches_cache).and_call_original
+
+ repository.expire_emptiness_caches
+ end
end
describe 'skip_merges option' do
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index b2b7721674c..90b7e7715a8 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -561,7 +561,7 @@ describe WikiPage do
end
def commit_details
- Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit")
+ Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "test commit")
end
def create_page(name, content)
diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb
index cc16d0f156b..4bc005df2fc 100644
--- a/spec/presenters/ci/build_presenter_spec.rb
+++ b/spec/presenters/ci/build_presenter_spec.rb
@@ -217,4 +217,39 @@ describe Ci::BuildPresenter do
end
end
end
+
+ describe '#callout_failure_message' do
+ let(:build) { create(:ci_build, :failed, :script_failure) }
+
+ it 'returns a verbose failure reason' do
+ description = subject.callout_failure_message
+ expect(description).to eq('There has been a script failure. Check the job log for more information')
+ end
+ end
+
+ describe '#recoverable?' do
+ let(:build) { create(:ci_build, :failed, :script_failure) }
+
+ context 'when is a script or missing dependency failure' do
+ let(:failure_reasons) { %w(script_failure missing_dependency_failure) }
+
+ it 'should return false' do
+ failure_reasons.each do |failure_reason|
+ build.update_attribute(:failure_reason, failure_reason)
+ expect(presenter.recoverable?).to be_falsy
+ end
+ end
+ end
+
+ context 'when is any other failure type' do
+ let(:failure_reasons) { %w(unknown_failure api_failure stuck_or_timeout_failure runner_system_failure) }
+
+ it 'should return true' do
+ failure_reasons.each do |failure_reason|
+ build.update_attribute(:failure_reason, failure_reason)
+ expect(presenter.recoverable?).to be_truthy
+ end
+ end
+ end
+ end
end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 741800ff61d..9e6d69e3874 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -427,5 +427,20 @@ describe API::Repositories do
let(:request) { get api(route, guest) }
end
end
+
+ # Regression: https://gitlab.com/gitlab-org/gitlab-ce/issues/45363
+ describe 'Links header contains working URLs when no `order_by` nor `sort` is given' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+
+ it 'returns `Link` header that includes URLs with default value for `order_by` & `sort`' do
+ get api(route, current_user)
+
+ first_link_url = response.headers['Link'].split(';').first
+
+ expect(first_link_url).to include('order_by=commits')
+ expect(first_link_url).to include('sort=asc')
+ end
+ end
end
end
diff --git a/spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb b/spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb
new file mode 100644
index 00000000000..ac7b1575ec0
--- /dev/null
+++ b/spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../rubocop/cop/avoid_break_from_strong_memoize'
+
+describe RuboCop::Cop::AvoidBreakFromStrongMemoize do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ it 'flags violation for break inside strong_memoize' do
+ expect_offense(<<~RUBY)
+ strong_memoize(:result) do
+ break if something
+ ^^^^^ Do not use break inside strong_memoize, use next instead.
+
+ do_an_heavy_calculation
+ end
+ RUBY
+ end
+
+ it 'flags violation for break inside strong_memoize nested blocks' do
+ expect_offense(<<~RUBY)
+ strong_memoize do
+ items.each do |item|
+ break item
+ ^^^^^^^^^^ Do not use break inside strong_memoize, use next instead.
+ end
+ end
+ RUBY
+ end
+
+ it "doesn't flag violation for next inside strong_memoize" do
+ expect_no_offenses(<<~RUBY)
+ strong_memoize(:result) do
+ next if something
+
+ do_an_heavy_calculation
+ end
+ RUBY
+ end
+
+ it "doesn't flag violation for break inside blocks" do
+ expect_no_offenses(<<~RUBY)
+ call do
+ break if something
+
+ do_an_heavy_calculation
+ end
+ RUBY
+ end
+
+ it "doesn't call add_offense twice for nested blocks" do
+ source = <<~RUBY
+ call do
+ strong_memoize(:result) do
+ break if something
+
+ do_an_heavy_calculation
+ end
+ end
+ RUBY
+ expect_any_instance_of(described_class).to receive(:add_offense).once
+
+ inspect_source(source)
+ end
+
+ it "doesn't check when block is empty" do
+ expect_no_offenses(<<~RUBY)
+ strong_memoize(:result) do
+ end
+ RUBY
+ end
+end
diff --git a/spec/rubocop/cop/avoid_return_from_blocks_spec.rb b/spec/rubocop/cop/avoid_return_from_blocks_spec.rb
new file mode 100644
index 00000000000..a5c280a7adc
--- /dev/null
+++ b/spec/rubocop/cop/avoid_return_from_blocks_spec.rb
@@ -0,0 +1,127 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../rubocop/cop/avoid_return_from_blocks'
+
+describe RuboCop::Cop::AvoidReturnFromBlocks do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ it 'flags violation for return inside a block' do
+ expect_offense(<<~RUBY)
+ call do
+ do_something
+ return if something_else
+ ^^^^^^ Do not return from a block, use next or break instead.
+ end
+ RUBY
+ end
+
+ it "doesn't call add_offense twice for nested blocks" do
+ source = <<~RUBY
+ call do
+ call do
+ something
+ return if something_else
+ end
+ end
+ RUBY
+ expect_any_instance_of(described_class).to receive(:add_offense).once
+
+ inspect_source(source)
+ end
+
+ it 'flags violation for return inside included > def > block' do
+ expect_offense(<<~RUBY)
+ included do
+ def a_method
+ return if something
+
+ call do
+ return if something_else
+ ^^^^^^ Do not return from a block, use next or break instead.
+ end
+ end
+ end
+ RUBY
+ end
+
+ shared_examples 'examples with whitelisted method' do |whitelisted_method|
+ it "doesn't flag violation for return inside #{whitelisted_method}" do
+ expect_no_offenses(<<~RUBY)
+ items.#{whitelisted_method} do |item|
+ do_something
+ return if something_else
+ end
+ RUBY
+ end
+ end
+
+ %i[each each_filename times loop].each do |whitelisted_method|
+ it_behaves_like 'examples with whitelisted method', whitelisted_method
+ end
+
+ shared_examples 'examples with def methods' do |def_method|
+ it "doesn't flag violation for return inside #{def_method}" do
+ expect_no_offenses(<<~RUBY)
+ helpers do
+ #{def_method} do
+ return if something
+
+ do_something_more
+ end
+ end
+ RUBY
+ end
+ end
+
+ %i[define_method lambda].each do |def_method|
+ it_behaves_like 'examples with def methods', def_method
+ end
+
+ it "doesn't flag violation for return inside a lambda" do
+ expect_no_offenses(<<~RUBY)
+ lambda do
+ do_something
+ return if something_else
+ end
+ RUBY
+ end
+
+ it "doesn't flag violation for return used inside a method definition" do
+ expect_no_offenses(<<~RUBY)
+ describe Klass do
+ def a_method
+ do_something
+ return if something_else
+ end
+ end
+ RUBY
+ end
+
+ it "doesn't flag violation for next inside a block" do
+ expect_no_offenses(<<~RUBY)
+ call do
+ do_something
+ next if something_else
+ end
+ RUBY
+ end
+
+ it "doesn't flag violation for break inside a block" do
+ expect_no_offenses(<<~RUBY)
+ call do
+ do_something
+ break if something_else
+ end
+ RUBY
+ end
+
+ it "doesn't check when block is empty" do
+ expect_no_offenses(<<~RUBY)
+ call do
+ end
+ RUBY
+ end
+end
diff --git a/spec/rubocop/cop/gitlab/has_many_through_scope_spec.rb b/spec/rubocop/cop/gitlab/has_many_through_scope_spec.rb
deleted file mode 100644
index 6d769c8e6fd..00000000000
--- a/spec/rubocop/cop/gitlab/has_many_through_scope_spec.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-require 'spec_helper'
-
-require 'rubocop'
-require 'rubocop/rspec/support'
-
-require_relative '../../../../rubocop/cop/gitlab/has_many_through_scope'
-
-describe RuboCop::Cop::Gitlab::HasManyThroughScope do # rubocop:disable RSpec/FilePath
- include CopHelper
-
- subject(:cop) { described_class.new }
-
- context 'in a model file' do
- before do
- allow(cop).to receive(:in_model?).and_return(true)
- end
-
- context 'when the model does not use has_many :through' do
- it 'does not register an offense' do
- expect_no_offenses(<<-RUBY)
- class User < ActiveRecord::Base
- has_many :tags, source: 'UserTag'
- end
- RUBY
- end
- end
-
- context 'when the model uses has_many :through' do
- context 'when the association has no scope defined' do
- it 'registers an offense on the association' do
- expect_offense(<<-RUBY)
- class User < ActiveRecord::Base
- has_many :tags, through: :user_tags
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
- end
- RUBY
- end
- end
-
- context 'when the association has a scope defined' do
- context 'when the scope does not disable auto-loading' do
- it 'registers an offense on the scope' do
- expect_offense(<<-RUBY)
- class User < ActiveRecord::Base
- has_many :tags, -> { where(active: true) }, through: :user_tags
- ^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
- end
- RUBY
- end
- end
-
- context 'when the scope has auto_include(false)' do
- it 'does not register an offense' do
- expect_no_offenses(<<-RUBY)
- class User < ActiveRecord::Base
- has_many :tags, -> { where(active: true).auto_include(false).reorder(nil) }, through: :user_tags
- end
- RUBY
- end
- end
- end
- end
- end
-
- context 'outside of a migration spec file' do
- it 'does not register an offense' do
- expect_no_offenses(<<-RUBY)
- class User < ActiveRecord::Base
- has_many :tags, through: :user_tags
- end
- RUBY
- end
- end
-end
diff --git a/spec/serializers/entity_date_helper_spec.rb b/spec/serializers/entity_date_helper_spec.rb
index b9cc2f64831..36da8d33a44 100644
--- a/spec/serializers/entity_date_helper_spec.rb
+++ b/spec/serializers/entity_date_helper_spec.rb
@@ -32,6 +32,7 @@ describe EntityDateHelper do
end
it 'converts 86560 seconds' do
+ Rails.logger.debug date_helper_class.inspect
expect(date_helper_class.distance_of_time_as_hash(86560)).to eq(days: 1, mins: 2, seconds: 40)
end
@@ -42,4 +43,58 @@ describe EntityDateHelper do
it 'converts 986760 seconds' do
expect(date_helper_class.distance_of_time_as_hash(986760)).to eq(days: 11, hours: 10, mins: 6)
end
+
+ describe '#remaining_days_in_words' do
+ around do |example|
+ Timecop.freeze(Time.utc(2017, 3, 17)) { example.run }
+ end
+
+ context 'when less than 31 days remaining' do
+ let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) }
+
+ it 'returns days remaining' do
+ expect(milestone_remaining).to eq("<strong>12</strong> days remaining")
+ end
+ end
+
+ context 'when less than 1 year and more than 30 days remaining' do
+ let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) }
+
+ it 'returns months remaining' do
+ expect(milestone_remaining).to eq("<strong>2</strong> months remaining")
+ end
+ end
+
+ context 'when more than 1 year remaining' do
+ let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) }
+
+ it 'returns years remaining' do
+ expect(milestone_remaining).to eq("<strong>1</strong> year remaining")
+ end
+ end
+
+ context 'when milestone is expired' do
+ let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 2.days.ago.utc)) }
+
+ it 'returns "Past due"' do
+ expect(milestone_remaining).to eq("<strong>Past due</strong>")
+ end
+ end
+
+ context 'when milestone has start_date in the future' do
+ let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) }
+
+ it 'returns "Upcoming"' do
+ expect(milestone_remaining).to eq("<strong>Upcoming</strong>")
+ end
+ end
+
+ context 'when milestone has start_date in the past' do
+ let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, start_date: 2.days.ago.utc)) }
+
+ it 'returns days elapsed' do
+ expect(milestone_remaining).to eq("<strong>2</strong> days elapsed")
+ end
+ end
+ end
end
diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb
index 24a6f1a2a8a..c90396ebb28 100644
--- a/spec/serializers/job_entity_spec.rb
+++ b/spec/serializers/job_entity_spec.rb
@@ -133,22 +133,65 @@ describe JobEntity do
context 'when job failed' do
let(:job) { create(:ci_build, :script_failure) }
- describe 'status' do
- it 'should contain the failure reason inside label' do
- expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip
- expect(subject[:status][:label]).to eq('failed')
- expect(subject[:status][:tooltip]).to eq('failed <br> (script failure)')
- end
+ it 'contains details' do
+ expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip
+ end
+
+ it 'states that it failed' do
+ expect(subject[:status][:label]).to eq('failed')
+ end
+
+ it 'should indicate the failure reason on tooltip' do
+ expect(subject[:status][:tooltip]).to eq('failed <br> (script failure)')
+ end
+
+ it 'should include a callout message with a verbose output' do
+ expect(subject[:callout_message]).to eq('There has been a script failure. Check the job log for more information')
+ end
+
+ it 'should state that it is not recoverable' do
+ expect(subject[:recoverable]).to be_falsy
+ end
+ end
+
+ context 'when job is allowed to fail' do
+ let(:job) { create(:ci_build, :allowed_to_fail, :script_failure) }
+
+ it 'contains details' do
+ expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip
+ end
+
+ it 'states that it failed' do
+ expect(subject[:status][:label]).to eq('failed (allowed to fail)')
+ end
+
+ it 'should indicate the failure reason on tooltip' do
+ expect(subject[:status][:tooltip]).to eq('failed <br> (script failure) (allowed to fail)')
+ end
+
+ it 'should include a callout message with a verbose output' do
+ expect(subject[:callout_message]).to eq('There has been a script failure. Check the job log for more information')
+ end
+
+ it 'should state that it is not recoverable' do
+ expect(subject[:recoverable]).to be_falsy
+ end
+ end
+
+ context 'when job failed and is recoverable' do
+ let(:job) { create(:ci_build, :api_failure) }
+
+ it 'should state it is recoverable' do
+ expect(subject[:recoverable]).to be_truthy
end
end
context 'when job passed' do
let(:job) { create(:ci_build, :success) }
- describe 'status' do
- it 'should not contain the failure reason inside label' do
- expect(subject[:status][:label]).to eq('passed')
- end
+ it 'should not include callout message or recoverable keys' do
+ expect(subject).not_to include('callout_message')
+ expect(subject).not_to include('recoverable')
end
end
end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 97a563c1ce1..8a537e83d5f 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -370,10 +370,111 @@ module Ci
it_behaves_like 'validation is not active'
end
end
+ end
+
+ describe '#register_success' do
+ let!(:current_time) { Time.new(2018, 4, 5, 14, 0, 0) }
+ let!(:attempt_counter) { double('Gitlab::Metrics::NullMetric') }
+ let!(:job_queue_duration_seconds) { double('Gitlab::Metrics::NullMetric') }
+
+ before do
+ allow(Time).to receive(:now).and_return(current_time)
+
+ # Stub defaults for any metrics other than the ones we're testing
+ allow(Gitlab::Metrics).to receive(:counter)
+ .with(any_args)
+ .and_return(Gitlab::Metrics::NullMetric.instance)
+ allow(Gitlab::Metrics).to receive(:histogram)
+ .with(any_args)
+ .and_return(Gitlab::Metrics::NullMetric.instance)
+
+ # Stub tested metrics
+ allow(Gitlab::Metrics).to receive(:counter)
+ .with(:job_register_attempts_total, anything)
+ .and_return(attempt_counter)
+ allow(Gitlab::Metrics).to receive(:histogram)
+ .with(:job_queue_duration_seconds, anything, anything, anything)
+ .and_return(job_queue_duration_seconds)
+
+ project.update(shared_runners_enabled: true)
+ pending_job.update(created_at: current_time - 3600, queued_at: current_time - 1800)
+ end
+
+ shared_examples 'attempt counter collector' do
+ it 'increments attempt counter' do
+ allow(job_queue_duration_seconds).to receive(:observe)
+ expect(attempt_counter).to receive(:increment)
+
+ execute(runner)
+ end
+ end
+
+ shared_examples 'jobs queueing time histogram collector' do
+ it 'counts job queuing time histogram with expected labels' do
+ allow(attempt_counter).to receive(:increment)
+ expect(job_queue_duration_seconds).to receive(:observe)
+ .with({ shared_runner: expected_shared_runner,
+ jobs_running_for_project: expected_jobs_running_for_project_first_job }, 1800)
+
+ execute(runner)
+ end
+
+ context 'when project already has running jobs' do
+ let!(:build2) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) }
+ let!(:build3) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) }
+
+ it 'counts job queuing time histogram with expected labels' do
+ allow(attempt_counter).to receive(:increment)
+ expect(job_queue_duration_seconds).to receive(:observe)
+ .with({ shared_runner: expected_shared_runner,
+ jobs_running_for_project: expected_jobs_running_for_project_third_job }, 1800)
+
+ execute(runner)
+ end
+ end
+ end
- def execute(runner)
- described_class.new(runner).execute.build
+ shared_examples 'metrics collector' do
+ it_behaves_like 'attempt counter collector'
+ it_behaves_like 'jobs queueing time histogram collector'
end
+
+ context 'when shared runner is used' do
+ let(:runner) { shared_runner }
+ let(:expected_shared_runner) { true }
+ let(:expected_jobs_running_for_project_first_job) { 0 }
+ let(:expected_jobs_running_for_project_third_job) { 2 }
+
+ it_behaves_like 'metrics collector'
+
+ context 'when pending job with queued_at=nil is used' do
+ before do
+ pending_job.update(queued_at: nil)
+ end
+
+ it_behaves_like 'attempt counter collector'
+
+ it "doesn't count job queuing time histogram" do
+ allow(attempt_counter).to receive(:increment)
+ expect(job_queue_duration_seconds).not_to receive(:observe)
+
+ execute(runner)
+ end
+ end
+ end
+
+ context 'when specific runner is used' do
+ let(:runner) { specific_runner }
+ let(:expected_shared_runner) { false }
+ let(:expected_jobs_running_for_project_first_job) { '+Inf' }
+ let(:expected_jobs_running_for_project_third_job) { '+Inf' }
+
+ it_behaves_like 'metrics collector'
+ end
+ end
+
+ def execute(runner)
+ described_class.new(runner).execute.build
end
end
end
diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb
index ae819c011de..80bac590a11 100644
--- a/spec/services/labels/transfer_service_spec.rb
+++ b/spec/services/labels/transfer_service_spec.rb
@@ -8,6 +8,7 @@ describe Labels::TransferService do
let(:group_3) { create(:group) }
let(:project_1) { create(:project, namespace: group_2) }
let(:project_2) { create(:project, namespace: group_3) }
+ let(:project_3) { create(:project, namespace: group_1) }
let(:group_label_1) { create(:group_label, group: group_1, name: 'Group Label 1') }
let(:group_label_2) { create(:group_label, group: group_1, name: 'Group Label 2') }
@@ -23,6 +24,7 @@ describe Labels::TransferService do
create(:labeled_issue, project: project_1, labels: [group_label_4])
create(:labeled_issue, project: project_1, labels: [project_label_1])
create(:labeled_issue, project: project_2, labels: [group_label_5])
+ create(:labeled_issue, project: project_3, labels: [group_label_1])
create(:labeled_merge_request, source_project: project_1, labels: [group_label_1, group_label_2])
create(:labeled_merge_request, source_project: project_2, labels: [group_label_5])
end
@@ -52,5 +54,13 @@ describe Labels::TransferService do
expect(project_1.labels.where(title: group_label_4.title)).to be_empty
end
+
+ it 'updates only label links in the given project' do
+ service.execute
+
+ targets = LabelLink.where(label_id: group_label_1.id).map(&:target)
+
+ expect(targets).to eq(project_3.issues)
+ end
end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index f8fa2540804..55bbe954491 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -933,6 +933,46 @@ describe NotificationService, :mailer do
let(:notification_trigger) { notification.issue_moved(issue, new_issue, @u_disabled) }
end
end
+
+ describe '#issue_due' do
+ before do
+ issue.update!(due_date: Date.today)
+
+ update_custom_notification(:issue_due, @u_guest_custom, resource: project)
+ update_custom_notification(:issue_due, @u_custom_global)
+ end
+
+ it 'sends email to issue notification recipients, excluding watchers' do
+ notification.issue_due(issue)
+
+ should_email(issue.assignees.first)
+ should_email(issue.author)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
+ should_not_email(@u_watcher)
+ should_not_email(@u_guest_watcher)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ it 'sends the email from the author' do
+ notification.issue_due(issue)
+ email = find_email_for(@subscriber)
+
+ expect(email.header[:from].display_names).to eq([issue.author.name])
+ end
+
+ it_behaves_like 'participating notifications' do
+ let(:participant) { create(:user, username: 'user-participant') }
+ let(:issuable) { issue }
+ let(:notification_trigger) { notification.issue_due(issue) }
+ end
+ end
end
describe 'Merge Requests' do
diff --git a/spec/services/projects/create_from_template_service_spec.rb b/spec/services/projects/create_from_template_service_spec.rb
index 609d678caea..d40e6f1449d 100644
--- a/spec/services/projects/create_from_template_service_spec.rb
+++ b/spec/services/projects/create_from_template_service_spec.rb
@@ -7,7 +7,7 @@ describe Projects::CreateFromTemplateService do
path: user.to_param,
template_name: 'rails',
description: 'project description',
- visibility_level: Gitlab::VisibilityLevel::PRIVATE
+ visibility_level: Gitlab::VisibilityLevel::PUBLIC
}
end
@@ -24,7 +24,23 @@ describe Projects::CreateFromTemplateService do
expect(project).to be_saved
expect(project.scheduled?).to be(true)
- expect(project.description).to match('project description')
- expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ context 'the result project' do
+ before do
+ Sidekiq::Testing.inline! do
+ @project = subject.execute
+ end
+
+ @project.reload
+ end
+
+ it 'overrides template description' do
+ expect(@project.description).to match('project description')
+ end
+
+ it 'overrides template visibility_level' do
+ expect(@project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index e28b0ea5cf2..893804f1470 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -909,7 +909,13 @@ describe SystemNoteService do
it 'sets the note text' do
noteable.update_attribute(:time_estimate, 277200)
- expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
+ expect(subject.note).to eq "changed time estimate to 1w 4d 5h,"
+ end
+
+ it 'appends a comma to separate the note from the update_at time' do
+ noteable.update_attribute(:time_estimate, 277200)
+
+ expect(subject.note).to end_with(',')
end
end
diff --git a/spec/support/http_io/http_io_helpers.rb b/spec/support/http_io/http_io_helpers.rb
index 31e07e720cd..2c68c2cd9a6 100644
--- a/spec/support/http_io/http_io_helpers.rb
+++ b/spec/support/http_io/http_io_helpers.rb
@@ -44,10 +44,11 @@ module HttpIOHelpers
def remote_trace_body
@remote_trace_body ||= File.read(expand_fixture_path('trace/sample_trace'))
+ .force_encoding(Encoding::BINARY)
end
def remote_trace_size
- remote_trace_body.length
+ remote_trace_body.bytesize
end
def set_smaller_buffer_size_than(file_size)
diff --git a/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb b/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb
index 934d53e7bba..93c21a99e59 100644
--- a/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb
+++ b/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb
@@ -4,7 +4,7 @@ shared_examples "matches the method pattern" do |method|
let(:pattern) { patterns[method] }
it do
- return skip "No pattern provided, skipping." unless pattern
+ skip "No pattern provided, skipping." unless pattern
expect(target.method(method).call(*args)).to match(pattern)
end
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index 16455e2517b..e7277b337f6 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -75,36 +75,8 @@ describe ObjectStorage do
expect(object).to receive(:file_store).and_return(nil)
end
- context 'when object storage is enabled' do
- context 'when direct uploads are enabled' do
- before do
- stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: true)
- end
-
- it "uses Store::REMOTE" do
- is_expected.to eq(described_class::Store::REMOTE)
- end
- end
-
- context 'when direct uploads are disabled' do
- before do
- stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: false)
- end
-
- it "uses Store::LOCAL" do
- is_expected.to eq(described_class::Store::LOCAL)
- end
- end
- end
-
- context 'when object storage is disabled' do
- before do
- stub_uploads_object_storage(uploader_class, enabled: false)
- end
-
- it "uses Store::LOCAL" do
- is_expected.to eq(described_class::Store::LOCAL)
- end
+ it "uses Store::LOCAL" do
+ is_expected.to eq(described_class::Store::LOCAL)
end
end
@@ -537,6 +509,72 @@ describe ObjectStorage do
end
end
+ context 'when local file is used' do
+ let(:temp_file) { Tempfile.new("test") }
+
+ before do
+ FileUtils.touch(temp_file)
+ end
+
+ after do
+ FileUtils.rm_f(temp_file)
+ end
+
+ context 'when valid file is used' do
+ context 'when valid file is specified' do
+ let(:uploaded_file) { temp_file }
+
+ context 'when object storage and direct upload is specified' do
+ before do
+ stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: true)
+ end
+
+ context 'when file is stored' do
+ subject do
+ uploader.store!(uploaded_file)
+ end
+
+ it 'file to be remotely stored in permament location' do
+ subject
+
+ expect(uploader).to be_exists
+ expect(uploader).not_to be_cached
+ expect(uploader).not_to be_file_storage
+ expect(uploader.path).not_to be_nil
+ expect(uploader.path).not_to include('tmp/upload')
+ expect(uploader.path).not_to include('tmp/cache')
+ expect(uploader.object_store).to eq(described_class::Store::REMOTE)
+ end
+ end
+ end
+
+ context 'when object storage and direct upload is not used' do
+ before do
+ stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: false)
+ end
+
+ context 'when file is stored' do
+ subject do
+ uploader.store!(uploaded_file)
+ end
+
+ it 'file to be remotely stored in permament location' do
+ subject
+
+ expect(uploader).to be_exists
+ expect(uploader).not_to be_cached
+ expect(uploader).to be_file_storage
+ expect(uploader.path).not_to be_nil
+ expect(uploader.path).not_to include('tmp/upload')
+ expect(uploader.path).not_to include('tmp/cache')
+ expect(uploader.object_store).to eq(described_class::Store::LOCAL)
+ end
+ end
+ end
+ end
+ end
+ end
+
context 'when remote file is used' do
let(:temp_file) { Tempfile.new("test") }
@@ -590,9 +628,9 @@ describe ObjectStorage do
expect(uploader).to be_exists
expect(uploader).to be_cached
+ expect(uploader).not_to be_file_storage
expect(uploader.path).not_to be_nil
expect(uploader.path).not_to include('tmp/cache')
- expect(uploader.url).not_to be_nil
expect(uploader.path).not_to include('tmp/cache')
expect(uploader.object_store).to eq(described_class::Store::REMOTE)
end
@@ -607,6 +645,7 @@ describe ObjectStorage do
expect(uploader).to be_exists
expect(uploader).not_to be_cached
+ expect(uploader).not_to be_file_storage
expect(uploader.path).not_to be_nil
expect(uploader.path).not_to include('tmp/upload')
expect(uploader.path).not_to include('tmp/cache')
diff --git a/spec/workers/issue_due_scheduler_worker_spec.rb b/spec/workers/issue_due_scheduler_worker_spec.rb
new file mode 100644
index 00000000000..7b60835fd26
--- /dev/null
+++ b/spec/workers/issue_due_scheduler_worker_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe IssueDueSchedulerWorker do
+ describe '#perform' do
+ it 'schedules one MailScheduler::IssueDueWorker per project with open issues due tomorrow' do
+ project1 = create(:project)
+ project2 = create(:project)
+ project_closed_issue = create(:project)
+ project_issue_due_another_day = create(:project)
+
+ create(:issue, :opened, project: project1, due_date: Date.tomorrow)
+ create(:issue, :opened, project: project1, due_date: Date.tomorrow)
+ create(:issue, :opened, project: project2, due_date: Date.tomorrow)
+ create(:issue, :closed, project: project_closed_issue, due_date: Date.tomorrow)
+ create(:issue, :opened, project: project_issue_due_another_day, due_date: Date.today)
+
+ expect(MailScheduler::IssueDueWorker).to receive(:bulk_perform_async).with([[project1.id], [project2.id]])
+
+ described_class.new.perform
+ end
+ end
+end
diff --git a/spec/workers/mail_scheduler/issue_due_worker_spec.rb b/spec/workers/mail_scheduler/issue_due_worker_spec.rb
new file mode 100644
index 00000000000..48ac1b8a1a4
--- /dev/null
+++ b/spec/workers/mail_scheduler/issue_due_worker_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe MailScheduler::IssueDueWorker do
+ describe '#perform' do
+ let(:worker) { described_class.new }
+ let(:project) { create(:project) }
+
+ it 'sends emails for open issues due tomorrow in the project specified' do
+ issue1 = create(:issue, :opened, project: project, due_date: Date.tomorrow)
+ issue2 = create(:issue, :opened, project: project, due_date: Date.tomorrow)
+ create(:issue, :closed, project: project, due_date: Date.tomorrow) # closed
+ create(:issue, :opened, project: project, due_date: 2.days.from_now) # due on another day
+ create(:issue, :opened, due_date: Date.tomorrow) # different project
+
+ expect_any_instance_of(NotificationService).to receive(:issue_due).with(issue1)
+ expect_any_instance_of(NotificationService).to receive(:issue_due).with(issue2)
+
+ worker.perform(project.id)
+ end
+ end
+end
diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
index 4f4ed80d101..3b77055b644 100644
--- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
@@ -315,7 +315,9 @@ production:
mv clair-scanner_linux_amd64 clair-scanner
chmod +x clair-scanner
touch clair-whitelist.yml
- while( ! wget -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; done
+ retries=0
+ echo "Waiting for clair daemon to start"
+ while( ! wget -T 10 -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done
./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-sast-container-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true
}