summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2018-04-03 10:59:29 +0100
committerPhil Hughes <me@iamphill.com>2018-04-03 10:59:29 +0100
commitc0dddb511c3bedc9b07df97739a27e07354b2242 (patch)
tree3b1f6042c7fd274579aa82c81a3d5894b6e53d90
parent6bec91bfc9ec15556e833f4d8f441328d135638e (diff)
parent8dca091ff7f04bb92a7835ebeff783b7f0ef76cd (diff)
downloadgitlab-ce-c0dddb511c3bedc9b07df97739a27e07354b2242.tar.gz
Merge branch 'master' into ide-pending-tab
-rw-r--r--.flayignore1
-rw-r--r--.gitlab-ci.yml11
-rw-r--r--CHANGELOG.md8
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile14
-rw-r--r--Gemfile.lock33
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_canceled.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_created.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_failed.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_manual.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_not_found.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_pending.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_running.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_skipped.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_success.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_warning.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/favicon-yellow.icobin0 -> 5430 bytes
-rw-r--r--app/assets/javascripts/api.js146
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_math.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js19
-rw-r--r--app/assets/javascripts/ide/components/changed_file_icon.vue34
-rw-r--r--app/assets/javascripts/ide/components/editor_mode_dropdown.vue87
-rw-r--r--app/assets/javascripts/ide/components/ide.vue3
-rw-r--r--app/assets/javascripts/ide/components/mr_file_icon.vue23
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue30
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue21
-rw-r--r--app/assets/javascripts/ide/components/repo_file_status_icon.vue40
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue6
-rw-r--r--app/assets/javascripts/ide/ide_router.js56
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js13
-rw-r--r--app/assets/javascripts/ide/lib/editor.js20
-rw-r--r--app/assets/javascripts/ide/services/index.js25
-rw-r--r--app/assets/javascripts/ide/stores/actions.js1
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js52
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js84
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js98
-rw-r--r--app/assets/javascripts/ide/stores/getters.js15
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js8
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js11
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js12
-rw-r--r--app/assets/javascripts/ide/stores/mutations/merge_request.js33
-rw-r--r--app/assets/javascripts/ide/stores/mutations/project.js1
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/utils.js22
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_detail_row.vue24
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_details_block.vue27
-rw-r--r--app/assets/javascripts/jobs/job_details_bundle.js1
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js10
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue6
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue8
-rw-r--r--app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js2
-rw-r--r--app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue17
-rw-r--r--app/assets/javascripts/pages/projects/labels/index/index.js2
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue101
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form.vue53
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue21
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form.vue66
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue23
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue114
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue97
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss4
-rw-r--r--app/assets/stylesheets/framework/modal.scss6
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss1
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/assets/stylesheets/pages/repo.scss41
-rw-r--r--app/assets/stylesheets/pages/repo.scss.orig786
-rw-r--r--app/controllers/admin/appearances_controller.rb18
-rw-r--r--app/controllers/projects/branches_controller.rb16
-rw-r--r--app/controllers/projects/labels_controller.rb2
-rw-r--r--app/controllers/projects/milestones_controller.rb4
-rw-r--r--app/controllers/projects/services_controller.rb2
-rw-r--r--app/helpers/appearances_helper.rb16
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/emails_helper.rb4
-rw-r--r--app/helpers/namespaces_helper.rb2
-rw-r--r--app/helpers/page_layout_helper.rb5
-rw-r--r--app/models/ci/artifact_blob.rb7
-rw-r--r--app/models/ci/build.rb141
-rw-r--r--app/models/ci/build_metadata.rb35
-rw-r--r--app/models/ci/runner.rb9
-rw-r--r--app/models/clusters/cluster.rb4
-rw-r--r--app/models/clusters/concerns/application_status.rb2
-rw-r--r--app/models/concerns/chronic_duration_attribute.rb39
-rw-r--r--app/models/deploy_key.rb4
-rw-r--r--app/models/issue.rb6
-rw-r--r--app/models/project.rb35
-rw-r--r--app/models/repository.rb4
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/user.rb7
-rw-r--r--app/presenters/ci/build_metadata_presenter.rb18
-rw-r--r--app/serializers/build_details_entity.rb2
-rw-r--r--app/serializers/build_metadata_entity.rb9
-rw-r--r--app/serializers/status_entity.rb10
-rw-r--r--app/services/boards/list_service.rb6
-rw-r--r--app/services/issues/close_service.rb1
-rw-r--r--app/services/projects/create_service.rb23
-rw-r--r--app/services/projects/import_export/export_service.rb33
-rw-r--r--app/services/projects/import_service.rb6
-rw-r--r--app/services/projects/update_pages_service.rb3
-rw-r--r--app/uploaders/object_storage.rb45
-rw-r--r--app/validators/certificate_fingerprint_validator.rb9
-rw-r--r--app/validators/importable_url_validator.rb6
-rw-r--r--app/validators/top_level_group_validator.rb7
-rw-r--r--app/views/admin/application_settings/_background_jobs.html.haml30
-rw-r--r--app/views/admin/application_settings/_form.html.haml105
-rw-r--r--app/views/admin/application_settings/_performance_bar.html.haml16
-rw-r--r--app/views/admin/application_settings/_spam.html.haml65
-rw-r--r--app/views/admin/application_settings/show.html.haml52
-rw-r--r--app/views/ci/variables/_variable_row.html.haml2
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/import/github/new.html.haml3
-rw-r--r--app/views/layouts/devise.html.haml4
-rw-r--r--app/views/layouts/devise_empty.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml16
-rw-r--r--app/views/projects/_export.html.haml2
-rw-r--r--app/views/projects/clusters/show.html.haml4
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml2
-rw-r--r--app/views/projects/edit.html.haml8
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/jobs/show.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml1
-rw-r--r--app/views/projects/new.html.haml8
-rw-r--r--app/views/projects/no_repo.html.haml6
-rw-r--r--app/views/projects/protected_branches/shared/_index.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_index.html.haml2
-rw-r--r--app/views/projects/runners/_form.html.haml6
-rw-r--r--app/views/projects/runners/show.html.haml3
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml8
-rw-r--r--app/views/projects/show.html.haml2
-rw-r--r--app/views/shared/_import_form.html.haml3
-rw-r--r--app/views/shared/_label.html.haml1
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/milestones/_milestone.html.haml1
-rw-r--r--app/views/shared/milestones/_top.html.haml5
-rw-r--r--app/workers/object_storage/migrate_uploads_worker.rb30
-rw-r--r--app/workers/project_export_worker.rb14
-rw-r--r--changelogs/unreleased/17516-nested-restore-changelog.yml5
-rw-r--r--changelogs/unreleased/41967_issue_api_closed_by_info.yml5
-rw-r--r--changelogs/unreleased/44291-usage-ping-for-kubernetes-integration.yml5
-rw-r--r--changelogs/unreleased/44392-resolve-projects-creation-silently-failing-on-after-create-error.yml5
-rw-r--r--changelogs/unreleased/44508-fix-fork-namespace-images.yml5
-rw-r--r--changelogs/unreleased/44587-autolinking-includes-trailing-exclamation-marks.yml5
-rw-r--r--changelogs/unreleased/44608-Cloning-a-repository-over-HTTPS-with-LDAP-credentials-causes-a-HTTP-401-Access-denied.yml5
-rw-r--r--changelogs/unreleased/44649-reference-parsing-conflicting-with-auto-linking.yml5
-rw-r--r--changelogs/unreleased/44657-reuse-root_ref_hash-on-branches.yml5
-rw-r--r--changelogs/unreleased/44717-no-resolve-issue.yml5
-rw-r--r--changelogs/unreleased/44774-migrate-upload-task-fails-for-upload-with-store-nil.yml5
-rw-r--r--changelogs/unreleased/44776-fix-upload-migrate-fails-for-group.yml5
-rw-r--r--changelogs/unreleased/44878-update-brakeman-3-6-1-to-4-2-1.yml5
-rw-r--r--changelogs/unreleased/Link_to_project_labels_page.yml5
-rw-r--r--changelogs/unreleased/ac-fix-use_file-race.yml5
-rw-r--r--changelogs/unreleased/ac-pages-port.yml5
-rw-r--r--changelogs/unreleased/add-canary-favicon.yml5
-rw-r--r--changelogs/unreleased/add-milestone-path-to-dashboard-milestones-breadcrumb-link.yml5
-rw-r--r--changelogs/unreleased/add-per-runner-job-timeout.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-bump-html-pipeline-gem.yml5
-rw-r--r--changelogs/unreleased/dm-deploy-keys-default-user.yml5
-rw-r--r--changelogs/unreleased/escape-autocomplete-values-for-markdown.yml5
-rw-r--r--changelogs/unreleased/expose-commits-mr-api.yml5
-rw-r--r--changelogs/unreleased/feature-gb-variables-expressions-in-only-except.yml5
-rw-r--r--changelogs/unreleased/fix-gb-fix-background-pipeline-stages-migration.yml5
-rw-r--r--changelogs/unreleased/fix-projects-no-repository-placeholder.yml5
-rw-r--r--changelogs/unreleased/fj-42685-extend-project-export-endpoint.yml5
-rw-r--r--changelogs/unreleased/ide-file-row-hover-style.yml5
-rw-r--r--changelogs/unreleased/jivl-change-copy-text-promote-milestones-labels.yml5
-rw-r--r--changelogs/unreleased/move-registry-after-cicd-project-nav-sidebar.yml5
-rw-r--r--changelogs/unreleased/sh-cleanup-pages-worker.yml5
-rw-r--r--changelogs/unreleased/workhorse-gitaly-mandatory.yml5
-rw-r--r--changelogs/unreleased/zj-remote-repo-exists.yml5
-rw-r--r--config/webpack.config.js2
-rw-r--r--db/migrate/20180209165249_add_closed_by_to_issues.rb20
-rw-r--r--db/migrate/20180219153455_add_maximum_timeout_to_ci_runners.rb9
-rw-r--r--db/migrate/20180301010859_create_ci_builds_metadata_table.rb20
-rw-r--r--db/schema.rb15
-rw-r--r--doc/README.md6
-rw-r--r--doc/administration/index.md4
-rw-r--r--doc/administration/issue_closing_pattern.md4
-rw-r--r--doc/api/commits.md73
-rw-r--r--doc/api/events.md4
-rw-r--r--doc/api/issues.md48
-rw-r--r--doc/api/jobs.md14
-rw-r--r--doc/api/project_import_export.md19
-rw-r--r--doc/api/projects.md4
-rw-r--r--doc/api/runners.md7
-rw-r--r--doc/ci/examples/browser_performance.md93
-rw-r--r--doc/ci/examples/code_climate.md7
-rw-r--r--doc/ci/examples/deployment/README.md2
-rw-r--r--doc/ci/pipelines.md8
-rw-r--r--doc/ci/quick_start/README.md5
-rw-r--r--doc/ci/runners/README.md64
-rw-r--r--doc/ci/variables/README.md66
-rw-r--r--doc/ci/yaml/README.md30
-rw-r--r--doc/development/ee_features.md272
-rw-r--r--doc/development/new_fe_guide/style/javascript.md194
-rw-r--r--doc/install/README.md2
-rw-r--r--doc/integration/google.md2
-rw-r--r--doc/policy/maintenance.md2
-rw-r--r--doc/update/10.5-to-10.6.md8
-rw-r--r--doc/user/gitlab_com/index.md10
-rw-r--r--doc/user/project/integrations/img/jira_workflow_screenshot.pngbin66685 -> 0 bytes
-rw-r--r--doc/user/project/integrations/jira.md15
-rw-r--r--doc/user/project/issue_board.md2
-rw-r--r--doc/user/project/pipelines/settings.md9
-rw-r--r--doc/user/project/repository/index.md5
-rw-r--r--doc/workflow/lfs/lfs_administration.md109
-rw-r--r--doc/workflow/todos.md3
-rw-r--r--features/groups.feature73
-rw-r--r--features/steps/groups.rb147
-rw-r--r--features/steps/project/project.rb2
-rw-r--r--lib/api/commits.rb14
-rw-r--r--lib/api/deploy_keys.rb23
-rw-r--r--lib/api/entities.rb4
-rw-r--r--lib/api/helpers/internal_helpers.rb12
-rw-r--r--lib/api/internal.rb3
-rw-r--r--lib/api/project_export.rb19
-rw-r--r--lib/api/runner.rb3
-rw-r--r--lib/api/runners.rb1
-rw-r--r--lib/backup/artifacts.rb4
-rw-r--r--lib/backup/builds.rb4
-rw-r--r--lib/backup/files.rb18
-rw-r--r--lib/backup/helper.rb17
-rw-r--r--lib/backup/lfs.rb4
-rw-r--r--lib/backup/pages.rb4
-rw-r--r--lib/backup/registry.rb4
-rw-r--r--lib/backup/repository.rb24
-rw-r--r--lib/backup/uploads.rb4
-rw-r--r--lib/banzai/filter/autolink_filter.rb8
-rw-r--r--lib/banzai/filter/emoji_filter.rb2
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb2
-rw-r--r--lib/banzai/filter/inline_diff_filter.rb2
-rw-r--r--lib/gitlab/background_migration/migrate_build_stage.rb1
-rw-r--r--lib/gitlab/ci/build/policy/kubernetes.rb2
-rw-r--r--lib/gitlab/ci/build/policy/refs.rb2
-rw-r--r--lib/gitlab/ci/build/policy/specification.rb2
-rw-r--r--lib/gitlab/ci/build/policy/variables.rb24
-rw-r--r--lib/gitlab/ci/build/step.rb4
-rw-r--r--lib/gitlab/ci/config/entry/policy.rb20
-rw-r--r--lib/gitlab/ci/pipeline/chain/populate.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/string.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/variable.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/statement.rb17
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb18
-rw-r--r--lib/gitlab/ci/pipeline/seed/stage.rb4
-rw-r--r--lib/gitlab/ci/variables/collection.rb8
-rw-r--r--lib/gitlab/ci/variables/collection/item.rb4
-rw-r--r--lib/gitlab/ee_compat_check.rb148
-rw-r--r--lib/gitlab/git/gitmodules_parser.rb4
-rw-r--r--lib/gitlab/git/hook_env.rb (renamed from lib/gitlab/git/env.rb)26
-rw-r--r--lib/gitlab/git/repository.rb14
-rw-r--r--lib/gitlab/git_access.rb8
-rw-r--r--lib/gitlab/gitaly_client.rb4
-rw-r--r--lib/gitlab/gitaly_client/remote_service.rb11
-rw-r--r--lib/gitlab/gitaly_client/util.rb8
-rw-r--r--lib/gitlab/github_import/importer/repository_importer.rb8
-rw-r--r--lib/gitlab/http.rb2
-rw-r--r--lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb83
-rw-r--r--lib/gitlab/import_export/after_export_strategies/download_notification_strategy.rb17
-rw-r--r--lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb61
-rw-r--r--lib/gitlab/import_export/after_export_strategy_builder.rb24
-rw-r--r--lib/gitlab/import_export/relation_factory.rb2
-rw-r--r--lib/gitlab/import_export/shared.rb14
-rw-r--r--lib/gitlab/proxy_http_connection_adapter.rb12
-rw-r--r--lib/gitlab/url_blocker.rb86
-rw-r--r--lib/gitlab/usage_data.rb7
-rw-r--r--lib/gitlab/workhorse.rb27
-rw-r--r--lib/tasks/gitlab/uploads/migrate.rake5
-rw-r--r--locale/gitlab.pot129
-rw-r--r--package.json3
-rw-r--r--qa/qa/scenario/bootable.rb2
-rw-r--r--qa/qa/scenario/test/instance.rb8
-rw-r--r--qa/qa/scenario/test/integration/mattermost.rb4
-rw-r--r--qa/qa/specs/runner.rb6
-rw-r--r--qa/spec/scenario/test/instance_spec.rb4
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb2
-rw-r--r--spec/factories/ci/build_metadata.rb9
-rw-r--r--spec/features/admin/admin_settings_spec.rb70
-rw-r--r--spec/features/groups/activity_spec.rb25
-rw-r--r--spec/features/groups/group_settings_spec.rb21
-rw-r--r--spec/features/groups/issues_spec.rb20
-rw-r--r--spec/features/groups/merge_requests_spec.rb16
-rw-r--r--spec/features/groups/show_spec.rb36
-rw-r--r--spec/features/groups/user_browse_projects_group_page_spec.rb29
-rw-r--r--spec/features/issuables/discussion_lock_spec.rb2
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb44
-rw-r--r--spec/features/profiles/account_spec.rb4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/project/export_status.json11
-rw-r--r--spec/helpers/page_layout_helper_spec.rb5
-rw-r--r--spec/javascripts/api_spec.js183
-rw-r--r--spec/javascripts/boards/mock_data.js49
-rw-r--r--spec/javascripts/droplab/constants_spec.js16
-rw-r--r--spec/javascripts/fixtures/projects.rb2
-rw-r--r--spec/javascripts/gfm_auto_complete_spec.js10
-rw-r--r--spec/javascripts/helpers/vuex_action_helper.js72
-rw-r--r--spec/javascripts/ide/components/changed_file_icon_spec.js3
-rw-r--r--spec/javascripts/ide/components/repo_editor_spec.js58
-rw-r--r--spec/javascripts/ide/components/repo_tabs_spec.js2
-rw-r--r--spec/javascripts/ide/lib/common/model_spec.js14
-rw-r--r--spec/javascripts/ide/lib/editor_spec.js25
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js43
-rw-r--r--spec/javascripts/ide/stores/actions/merge_request_spec.js110
-rw-r--r--spec/javascripts/ide/stores/actions/tree_spec.js12
-rw-r--r--spec/javascripts/ide/stores/getters_spec.js22
-rw-r--r--spec/javascripts/ide/stores/mutations/file_spec.js26
-rw-r--r--spec/javascripts/ide/stores/mutations/merge_request_spec.js65
-rw-r--r--spec/javascripts/jobs/mock_data.js4
-rw-r--r--spec/javascripts/jobs/sidebar_detail_row_spec.js21
-rw-r--r--spec/javascripts/jobs/sidebar_details_block_spec.js6
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js18
-rw-r--r--spec/javascripts/notes/mock_data.js863
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js199
-rw-r--r--spec/javascripts/pages/labels/components/promote_label_modal_spec.js3
-rw-r--r--spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js3
-rw-r--r--spec/javascripts/pipelines/graph/mock_data.js467
-rw-r--r--spec/javascripts/registry/stores/actions_spec.js87
-rw-r--r--spec/javascripts/sidebar/confidential_issue_sidebar_spec.js18
-rw-r--r--spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js18
-rw-r--r--spec/javascripts/sidebar/mock_data.js59
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js244
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js387
-rw-r--r--spec/javascripts/vue_shared/components/mock_data.js2
-rw-r--r--spec/lib/backup/files_spec.rb66
-rw-r--r--spec/lib/backup/repository_spec.rb13
-rw-r--r--spec/lib/banzai/filter/autolink_filter_spec.rb9
-rw-r--r--spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb16
-rw-r--r--spec/lib/gitlab/ci/build/policy/variables_spec.rb72
-rw-r--r--spec/lib/gitlab/ci/build/step_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/config/entry/policy_spec.rb33
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb16
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb91
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/variables/collection/item_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/variables/collection_spec.rb17
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb8
-rw-r--r--spec/lib/gitlab/git/gitmodules_parser_spec.rb3
-rw-r--r--spec/lib/gitlab/git/hook_env_spec.rb (renamed from spec/lib/gitlab/git/env_spec.rb)71
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb20
-rw-r--r--spec/lib/gitlab/git/rev_list_spec.rb13
-rw-r--r--spec/lib/gitlab/gitaly_client/remote_service_spec.rb10
-rw-r--r--spec/lib/gitlab/gitaly_client/util_spec.rb11
-rw-r--r--spec/lib/gitlab/github_import/importer/repository_importer_spec.rb6
-rw-r--r--spec/lib/gitlab/http_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb104
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb36
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategy_builder_spec.rb29
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb12
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb23
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb11
-rw-r--r--spec/mailers/previews/notify_preview.rb2
-rw-r--r--spec/models/ci/artifact_blob_spec.rb13
-rw-r--r--spec/models/ci/build_metadata_spec.rb61
-rw-r--r--spec/models/ci/build_spec.rb185
-rw-r--r--spec/models/ci/pipeline_spec.rb14
-rw-r--r--spec/models/clusters/applications/helm_spec.rb12
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb12
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb12
-rw-r--r--spec/models/clusters/applications/runner_spec.rb12
-rw-r--r--spec/models/clusters/cluster_spec.rb36
-rw-r--r--spec/models/concerns/chronic_duration_attribute_spec.rb115
-rw-r--r--spec/models/deploy_key_spec.rb21
-rw-r--r--spec/models/project_spec.rb51
-rw-r--r--spec/models/service_spec.rb15
-rw-r--r--spec/models/user_spec.rb2
-rw-r--r--spec/requests/api/commits_spec.rb29
-rw-r--r--spec/requests/api/deploy_keys_spec.rb4
-rw-r--r--spec/requests/api/internal_spec.rb49
-rw-r--r--spec/requests/api/project_export_spec.rb79
-rw-r--r--spec/requests/api/runner_spec.rb59
-rw-r--r--spec/requests/api/runners_spec.rb5
-rw-r--r--spec/serializers/status_entity_spec.rb5
-rw-r--r--spec/services/ci/retry_build_service_spec.rb3
-rw-r--r--spec/services/issues/close_service_spec.rb4
-rw-r--r--spec/services/projects/create_service_spec.rb17
-rw-r--r--spec/services/projects/import_export/export_service_spec.rb85
-rw-r--r--spec/services/projects/import_service_spec.rb4
-rw-r--r--spec/support/cookie_helper.rb13
-rw-r--r--spec/support/gitaly.rb6
-rw-r--r--spec/support/login_helpers.rb4
-rw-r--r--spec/support/migrations_helpers.rb5
-rw-r--r--spec/support/shared_examples/uploaders/object_storage_shared_examples.rb16
-rw-r--r--spec/tasks/gitlab/uploads/migrate_rake_spec.rb129
-rw-r--r--spec/uploaders/object_storage_spec.rb24
-rw-r--r--spec/uploaders/workers/object_storage/background_move_worker_spec.rb146
-rw-r--r--spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb119
-rw-r--r--spec/workers/project_export_worker_spec.rb28
-rw-r--r--yarn.lock2
389 files changed, 8500 insertions, 2911 deletions
diff --git a/.flayignore b/.flayignore
index 87cb3507b05..3d69bb2c985 100644
--- a/.flayignore
+++ b/.flayignore
@@ -8,3 +8,4 @@ lib/gitlab/redis/*.rb
lib/gitlab/gitaly_client/operation_service.rb
lib/gitlab/background_migration/*
app/models/project_services/kubernetes_service.rb
+lib/gitlab/workhorse.rb
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 70f41e4dc98..4890738aa3d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -264,8 +264,17 @@ package-and-qa:
stage: build
cache: {}
when: manual
+ variables:
+ GIT_STRATEGY: none
+ before_script:
+ # We need to download the script rather than clone the repo since the
+ # package-and-qa job will not be able to run when the branch gets
+ # deleted (when merging the MR).
+ - apk add --update openssl
+ - wget https://gitlab.com/gitlab-org/gitlab-ce/raw/$CI_COMMIT_SHA/scripts/trigger-build-omnibus
+ - chmod 755 trigger-build-omnibus
script:
- - scripts/trigger-build-omnibus
+ - ./trigger-build-omnibus
only:
- //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee
diff --git a/CHANGELOG.md b/CHANGELOG.md
index adb0ec9f5b1..8a90a7fcdc2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 10.6.2 (2018-03-29)
+
+### Fixed (2 changes, 1 of them is from the community)
+
+- Don't capture trailing punctuation when autolinking. !17965
+- Cloning a repository over HTTPS with LDAP credentials causes a HTTP 401 Access denied. (Horatiu Eugen Vlad)
+
+
## 10.6.1 (2018-03-27)
### Security (1 change)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 8f63f4f9a10..36545ad338e 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.91.0
+0.92.0
diff --git a/Gemfile b/Gemfile
index 035c86efff3..fd174a60c66 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,7 +6,6 @@ end
gem_versions = {}
gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2'
gem_versions['default_value_for'] = rails5? ? '~> 3.0.5' : '~> 3.0.0'
-gem_versions['html-pipeline'] = rails5? ? '~> 2.6.0' : '~> 1.11.0'
gem_versions['rails'] = rails5? ? '5.0.6' : '4.2.10'
gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9'
# --- The end of special code for migrating to Rails 5.0 ---
@@ -28,7 +27,7 @@ gem 'default_value_for', gem_versions['default_value_for']
gem 'mysql2', '~> 0.4.10', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres
-gem 'rugged', '~> 0.26.0'
+gem 'rugged', '~> 0.27'
gem 'grape-route-helpers', '~> 2.1.0'
gem 'faraday', '~> 0.12'
@@ -44,7 +43,7 @@ gem 'omniauth-cas3', '~> 1.1.4'
gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.2'
-gem 'omniauth-google-oauth2', '~> 0.5.2'
+gem 'omniauth-google-oauth2', '~> 0.5.3'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2'
gem 'omniauth-saml', '~> 1.10'
@@ -136,7 +135,7 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.7'
# Markdown and HTML processing
-gem 'html-pipeline', gem_versions['html-pipeline']
+gem 'html-pipeline', '~> 2.7.1'
gem 'deckar01-task_list', '2.0.0'
gem 'gitlab-markup', '~> 1.6.2'
gem 'redcarpet', '~> 3.4'
@@ -310,7 +309,7 @@ end
group :development do
gem 'foreman', '~> 0.84.0'
- gem 'brakeman', '~> 3.6.0', require: false
+ gem 'brakeman', '~> 4.2', require: false
gem 'letter_opener_web', '~> 1.3.0'
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
@@ -376,6 +375,8 @@ group :development, :test do
gem 'stackprof', '~> 0.2.10', require: false
gem 'simple_po_parser', '~> 1.1.2', require: false
+
+ gem 'timecop', '~> 0.8.0'
end
group :test do
@@ -385,7 +386,6 @@ group :test do
gem 'webmock', '~> 2.3.2'
gem 'test_after_commit', '~> 1.1'
gem 'sham_rack', '~> 1.3.6'
- gem 'timecop', '~> 0.8.0'
gem 'concurrent-ruby', '~> 1.0.5'
gem 'test-prof', '~> 0.2.5'
end
@@ -421,7 +421,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.88.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.91.0', require: 'gitaly'
gem 'grpc', '~> 1.10.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
diff --git a/Gemfile.lock b/Gemfile.lock
index 7d8b22359b2..55e7bd9492a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -95,7 +95,7 @@ GEM
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
bootstrap_form (2.7.0)
- brakeman (3.6.1)
+ brakeman (4.2.1)
browser (2.2.0)
builder (3.2.3)
bullet (5.5.1)
@@ -120,7 +120,7 @@ GEM
activesupport (>= 4.0.0)
mime-types (>= 1.16)
cause (0.1)
- charlock_holmes (0.7.5)
+ charlock_holmes (0.7.6)
childprocess (0.7.0)
ffi (~> 1.0, >= 1.0.11)
chronic (0.10.2)
@@ -290,7 +290,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.88.0)
+ gitaly-proto (0.91.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (5.3.3)
@@ -399,9 +399,9 @@ GEM
hipchat (1.5.2)
httparty
mimemagic
- html-pipeline (1.11.0)
+ html-pipeline (2.7.1)
activesupport (>= 2)
- nokogiri (~> 1.4)
+ nokogiri (>= 1.4)
html2text (0.2.0)
nokogiri (~> 1.6)
htmlentities (4.3.4)
@@ -550,11 +550,10 @@ GEM
omniauth-gitlab (1.0.2)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0)
- omniauth-google-oauth2 (0.5.2)
- jwt (~> 1.5)
- multi_json (~> 1.3)
+ omniauth-google-oauth2 (0.5.3)
+ jwt (>= 1.5)
omniauth (>= 1.1.1)
- omniauth-oauth2 (>= 1.3.1)
+ omniauth-oauth2 (>= 1.5)
omniauth-jwt (0.0.2)
jwt
omniauth (~> 1.1)
@@ -566,8 +565,8 @@ GEM
omniauth-oauth (1.1.0)
oauth
omniauth (~> 1.0)
- omniauth-oauth2 (1.4.0)
- oauth2 (~> 1.0)
+ omniauth-oauth2 (1.5.0)
+ oauth2 (~> 1.1)
omniauth (~> 1.2)
omniauth-oauth2-generic (0.2.2)
omniauth-oauth2 (~> 1.0)
@@ -814,7 +813,7 @@ GEM
rubyzip (1.2.1)
rufus-scheduler (3.4.0)
et-orbi (~> 1.0)
- rugged (0.26.0)
+ rugged (0.27.0)
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
@@ -1013,7 +1012,7 @@ DEPENDENCIES
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0)
bootstrap_form (~> 2.7.0)
- brakeman (~> 3.6.0)
+ brakeman (~> 4.2)
browser (~> 2.2)
bullet (~> 5.5.0)
bundler-audit (~> 0.5.0)
@@ -1062,7 +1061,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.88.0)
+ gitaly-proto (~> 0.91.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
@@ -1084,7 +1083,7 @@ DEPENDENCIES
hashie-forbidden_attributes
health_check (~> 2.6.0)
hipchat (~> 1.5.0)
- html-pipeline (~> 1.11.0)
+ html-pipeline (~> 2.7.1)
html2text
httparty (~> 0.13.3)
influxdb (~> 0.2)
@@ -1118,7 +1117,7 @@ DEPENDENCIES
omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.1)
omniauth-gitlab (~> 1.0.2)
- omniauth-google-oauth2 (~> 0.5.2)
+ omniauth-google-oauth2 (~> 0.5.3)
omniauth-jwt (~> 0.0.2)
omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2)
@@ -1174,7 +1173,7 @@ DEPENDENCIES
ruby-prof (~> 0.17.0)
ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
- rugged (~> 0.26.0)
+ rugged (~> 0.27)
sanitize (~> 2.0)
sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0)
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_canceled.ico b/app/assets/images/ci_favicons/canary/favicon_status_canceled.ico
new file mode 100644
index 00000000000..48b1095370d
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_canceled.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_created.ico b/app/assets/images/ci_favicons/canary/favicon_status_created.ico
new file mode 100644
index 00000000000..623c728faf6
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_created.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_failed.ico b/app/assets/images/ci_favicons/canary/favicon_status_failed.ico
new file mode 100644
index 00000000000..3073fe5a761
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_failed.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_manual.ico b/app/assets/images/ci_favicons/canary/favicon_status_manual.ico
new file mode 100644
index 00000000000..6c713d7b675
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_manual.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_not_found.ico b/app/assets/images/ci_favicons/canary/favicon_status_not_found.ico
new file mode 100644
index 00000000000..dbf855fdafd
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_not_found.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_pending.ico b/app/assets/images/ci_favicons/canary/favicon_status_pending.ico
new file mode 100644
index 00000000000..ccd00606aeb
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_pending.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_running.ico b/app/assets/images/ci_favicons/canary/favicon_status_running.ico
new file mode 100644
index 00000000000..968e7c4c2d4
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_running.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_skipped.ico b/app/assets/images/ci_favicons/canary/favicon_status_skipped.ico
new file mode 100644
index 00000000000..7e3be35cc3a
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_skipped.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_success.ico b/app/assets/images/ci_favicons/canary/favicon_status_success.ico
new file mode 100644
index 00000000000..a1fb6e91d65
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_success.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_warning.ico b/app/assets/images/ci_favicons/canary/favicon_status_warning.ico
new file mode 100644
index 00000000000..5d931619fb2
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_warning.ico
Binary files differ
diff --git a/app/assets/images/favicon-yellow.ico b/app/assets/images/favicon-yellow.ico
new file mode 100644
index 00000000000..b650f277fb6
--- /dev/null
+++ b/app/assets/images/favicon-yellow.ico
Binary files differ
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index cbcefb2c18f..8ad3d18b302 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -10,6 +10,9 @@ const Api = {
projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels',
+ mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
+ mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
+ mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
groupLabelsPath: '/groups/:namespace_path/-/labels',
licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key',
@@ -22,25 +25,27 @@ const Api = {
createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) {
- const url = Api.buildUrl(Api.groupPath)
- .replace(':id', groupId);
- return axios.get(url)
- .then(({ data }) => {
- callback(data);
+ const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
+ return axios.get(url).then(({ data }) => {
+ callback(data);
- return data;
- });
+ return data;
+ });
},
// Return groups list. Filtered by query
groups(query, options, callback = $.noop) {
const url = Api.buildUrl(Api.groupsPath);
- return axios.get(url, {
- params: Object.assign({
- search: query,
- per_page: 20,
- }, options),
- })
+ return axios
+ .get(url, {
+ params: Object.assign(
+ {
+ search: query,
+ per_page: 20,
+ },
+ options,
+ ),
+ })
.then(({ data }) => {
callback(data);
@@ -51,12 +56,13 @@ const Api = {
// Return namespaces list. Filtered by query
namespaces(query, callback) {
const url = Api.buildUrl(Api.namespacesPath);
- return axios.get(url, {
- params: {
- search: query,
- per_page: 20,
- },
- })
+ return axios
+ .get(url, {
+ params: {
+ search: query,
+ per_page: 20,
+ },
+ })
.then(({ data }) => callback(data));
},
@@ -73,9 +79,10 @@ const Api = {
defaults.membership = true;
}
- return axios.get(url, {
- params: Object.assign(defaults, options),
- })
+ return axios
+ .get(url, {
+ params: Object.assign(defaults, options),
+ })
.then(({ data }) => {
callback(data);
@@ -85,8 +92,32 @@ const Api = {
// Return single project
project(projectPath) {
- const url = Api.buildUrl(Api.projectPath)
- .replace(':id', encodeURIComponent(projectPath));
+ const url = Api.buildUrl(Api.projectPath).replace(':id', encodeURIComponent(projectPath));
+
+ return axios.get(url);
+ },
+
+ // Return Merge Request for project
+ mergeRequest(projectPath, mergeRequestId) {
+ const url = Api.buildUrl(Api.mergeRequestPath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':mrid', mergeRequestId);
+
+ return axios.get(url);
+ },
+
+ mergeRequestChanges(projectPath, mergeRequestId) {
+ const url = Api.buildUrl(Api.mergeRequestChangesPath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':mrid', mergeRequestId);
+
+ return axios.get(url);
+ },
+
+ mergeRequestVersions(projectPath, mergeRequestId) {
+ const url = Api.buildUrl(Api.mergeRequestVersionsPath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':mrid', mergeRequestId);
return axios.get(url);
},
@@ -102,30 +133,30 @@ const Api = {
url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
}
- return axios.post(url, {
- label: data,
- })
+ return axios
+ .post(url, {
+ label: data,
+ })
.then(res => callback(res.data))
.catch(e => callback(e.response.data));
},
// Return group projects list. Filtered by query
groupProjects(groupId, query, callback) {
- const url = Api.buildUrl(Api.groupProjectsPath)
- .replace(':id', groupId);
- return axios.get(url, {
- params: {
- search: query,
- per_page: 20,
- },
- })
+ const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
+ return axios
+ .get(url, {
+ params: {
+ search: query,
+ per_page: 20,
+ },
+ })
.then(({ data }) => callback(data));
},
commitMultiple(id, data) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
- const url = Api.buildUrl(Api.commitPath)
- .replace(':id', encodeURIComponent(id));
+ const url = Api.buildUrl(Api.commitPath).replace(':id', encodeURIComponent(id));
return axios.post(url, JSON.stringify(data), {
headers: {
'Content-Type': 'application/json; charset=utf-8',
@@ -136,39 +167,34 @@ const Api = {
branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', encodeURIComponent(id))
- .replace(':branch', branch);
+ .replace(':branch', encodeURIComponent(branch));
return axios.get(url);
},
// Return text for a specific license
licenseText(key, data, callback) {
- const url = Api.buildUrl(Api.licensePath)
- .replace(':key', key);
- return axios.get(url, {
- params: data,
- })
+ const url = Api.buildUrl(Api.licensePath).replace(':key', key);
+ return axios
+ .get(url, {
+ params: data,
+ })
.then(res => callback(res.data));
},
gitignoreText(key, callback) {
- const url = Api.buildUrl(Api.gitignorePath)
- .replace(':key', key);
- return axios.get(url)
- .then(({ data }) => callback(data));
+ const url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
+ return axios.get(url).then(({ data }) => callback(data));
},
gitlabCiYml(key, callback) {
- const url = Api.buildUrl(Api.gitlabCiYmlPath)
- .replace(':key', key);
- return axios.get(url)
- .then(({ data }) => callback(data));
+ const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
+ return axios.get(url).then(({ data }) => callback(data));
},
dockerfileYml(key, callback) {
const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
- return axios.get(url)
- .then(({ data }) => callback(data));
+ return axios.get(url).then(({ data }) => callback(data));
},
issueTemplate(namespacePath, projectPath, key, type, callback) {
@@ -177,7 +203,8 @@ const Api = {
.replace(':type', type)
.replace(':project_path', projectPath)
.replace(':namespace_path', namespacePath);
- return axios.get(url)
+ return axios
+ .get(url)
.then(({ data }) => callback(null, data))
.catch(callback);
},
@@ -185,10 +212,13 @@ const Api = {
users(query, options) {
const url = Api.buildUrl(this.usersPath);
return axios.get(url, {
- params: Object.assign({
- search: query,
- per_page: 20,
- }, options),
+ params: Object.assign(
+ {
+ search: query,
+ per_page: 20,
+ },
+ options,
+ ),
});
},
diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js
index 7dcf1aeed17..eb4e59d12b1 100644
--- a/app/assets/javascripts/behaviors/markdown/render_math.js
+++ b/app/assets/javascripts/behaviors/markdown/render_math.js
@@ -31,7 +31,7 @@ export default function renderMath($els) {
if (!$els.length) return;
Promise.all([
import(/* webpackChunkName: 'katex' */ 'katex'),
- import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.css'),
+ import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'),
]).then(([katex]) => {
renderWithKaTeX($els, katex);
}).catch(() => flash(__('An error occurred while rendering KaTeX')));
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 8259133c95b..7e9770a9ea2 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -54,6 +54,7 @@ class GfmAutoComplete {
alias: 'commands',
searchKey: 'search',
skipSpecialCharacterTest: true,
+ skipMarkdownCharacterTest: true,
data: GfmAutoComplete.defaultLoadingData,
displayTpl(value) {
if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template;
@@ -376,15 +377,23 @@ class GfmAutoComplete {
return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
},
beforeInsert(value) {
- let resultantValue = value;
+ let withoutAt = value.substring(1);
+ const at = value.charAt();
+
if (value && !this.setting.skipSpecialCharacterTest) {
- const withoutAt = value.substring(1);
- const regex = value.charAt() === '~' ? /\W|^\d+$/ : /\W/;
+ const regex = at === '~' ? /\W|^\d+$/ : /\W/;
if (withoutAt && regex.test(withoutAt)) {
- resultantValue = `${value.charAt()}"${withoutAt}"`;
+ withoutAt = `"${withoutAt}"`;
}
}
- return resultantValue;
+
+ // We can ignore this for quick actions because they are processed
+ // before Markdown.
+ if (!this.setting.skipMarkdownCharacterTest) {
+ withoutAt = withoutAt.replace(/([~\-_*`])/g, '\\$&');
+ }
+
+ return `${at}${withoutAt}`;
},
matcher(flag, subtext) {
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue
index 0c54c992e51..037e3efb4ce 100644
--- a/app/assets/javascripts/ide/components/changed_file_icon.vue
+++ b/app/assets/javascripts/ide/components/changed_file_icon.vue
@@ -1,25 +1,25 @@
<script>
- import icon from '~/vue_shared/components/icon.vue';
+import icon from '~/vue_shared/components/icon.vue';
- export default {
- components: {
- icon,
+export default {
+ components: {
+ icon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
},
- props: {
- file: {
- type: Object,
- required: true,
- },
+ },
+ computed: {
+ changedIcon() {
+ return this.file.tempFile ? 'file-addition' : 'file-modified';
},
- computed: {
- changedIcon() {
- return this.file.tempFile ? 'file-addition' : 'file-modified';
- },
- changedIconClass() {
- return `multi-${this.changedIcon}`;
- },
+ changedIconClass() {
+ return `multi-${this.changedIcon}`;
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
index 170347881e0..0c44a755f56 100644
--- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -1,31 +1,44 @@
<script>
- import Icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import { __, sprintf } from '~/locale';
- export default {
- components: {
- Icon,
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ hasChanges: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- props: {
- hasChanges: {
- type: Boolean,
- required: false,
- default: false,
- },
- viewer: {
- type: String,
- required: true,
- },
- showShadow: {
- type: Boolean,
- required: true,
- },
+ mergeRequestId: {
+ type: String,
+ required: false,
+ default: '',
},
- methods: {
- changeMode(mode) {
- this.$emit('click', mode);
- },
+ viewer: {
+ type: String,
+ required: true,
},
- };
+ showShadow: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ mergeReviewLine() {
+ return sprintf(__('Reviewing (merge request !%{mergeRequestId})'), {
+ mergeRequestId: this.mergeRequestId,
+ });
+ },
+ },
+ methods: {
+ changeMode(mode) {
+ this.$emit('click', mode);
+ },
+ },
+};
</script>
<template>
@@ -43,7 +56,10 @@
}"
data-toggle="dropdown"
>
- <template v-if="viewer === 'editor'">
+ <template v-if="viewer === 'mrdiff' && mergeRequestId">
+ {{ mergeReviewLine }}
+ </template>
+ <template v-else-if="viewer === 'editor'">
{{ __('Editing') }}
</template>
<template v-else>
@@ -57,6 +73,29 @@
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul>
+ <template v-if="mergeRequestId">
+ <li>
+ <a
+ href="#"
+ @click.prevent="changeMode('mrdiff')"
+ :class="{
+ 'is-active': viewer === 'mrdiff',
+ }"
+ >
+ <strong class="dropdown-menu-inner-title">
+ {{ mergeReviewLine }}
+ </strong>
+ <span class="dropdown-menu-inner-content">
+ {{ __('Compare changes with the merge request target branch') }}
+ </span>
+ </a>
+ </li>
+ <li
+ role="separator"
+ class="divider"
+ >
+ </li>
+ </template>
<li>
<a
href="#"
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 1581e015c39..d22869466c9 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -31,7 +31,7 @@ export default {
},
},
computed: {
- ...mapState(['changedFiles', 'openFiles', 'viewer']),
+ ...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
@@ -64,6 +64,7 @@ export default {
:files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
+ :merge-request-id="currentMergeRequestId"
/>
<repo-editor
class="multi-file-edit-pane-content"
diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue
new file mode 100644
index 00000000000..8a440902dfc
--- /dev/null
+++ b/app/assets/javascripts/ide/components/mr_file_icon.vue
@@ -0,0 +1,23 @@
+<script>
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+};
+</script>
+
+<template>
+ <icon
+ name="git-merge"
+ v-tooltip
+ title="__('Part of merge request changes')"
+ css-classes="ide-file-changed-icon"
+ :size="12"
+ />
+</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 5cf1d9f09c6..0a61b49c950 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -1,6 +1,6 @@
<script>
/* global monaco */
-import { mapState, mapActions } from 'vuex';
+import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
@@ -13,12 +13,8 @@ export default {
},
},
computed: {
- ...mapState([
- 'leftPanelCollapsed',
- 'rightPanelCollapsed',
- 'viewer',
- 'delayViewerUpdated',
- ]),
+ ...mapState(['leftPanelCollapsed', 'rightPanelCollapsed', 'viewer', 'delayViewerUpdated']),
+ ...mapGetters(['currentMergeRequest']),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.raw;
},
@@ -68,7 +64,10 @@ export default {
this.editor.clearEditor();
- this.getRawFileData(this.file)
+ this.getRawFileData({
+ path: this.file.path,
+ baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
+ })
.then(() => {
const viewerPromise = this.delayViewerUpdated
? this.updateViewer(this.file.pending ? 'diff' : 'editor')
@@ -81,14 +80,7 @@ export default {
this.createEditorInstance();
})
.catch(err => {
- flash(
- 'Error setting up monaco. Please try again.',
- 'alert',
- document,
- null,
- false,
- true,
- );
+ flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err;
});
},
@@ -110,7 +102,11 @@ export default {
this.model = this.editor.createModel(this.file);
- this.editor.attachModel(this.model);
+ if (this.viewer === 'mrdiff') {
+ this.editor.attachMergeRequestModel(this.model);
+ } else {
+ this.editor.attachModel(this.model);
+ }
this.model.onChange(model => {
const { file } = model;
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index fedb13339b8..3b5068d4910 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -6,6 +6,7 @@ import router from '../ide_router';
import newDropdown from './new_dropdown/index.vue';
import fileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue';
+import mrFileIcon from './mr_file_icon.vue';
export default {
name: 'RepoFile',
@@ -15,6 +16,7 @@ export default {
fileStatusIcon,
fileIcon,
changedFileIcon,
+ mrFileIcon,
},
props: {
file: {
@@ -56,10 +58,7 @@ export default {
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
clickFile() {
// Manual Action if a tree is selected/opened
- if (
- this.isTree &&
- this.$router.currentRoute.path === `/project${this.file.url}`
- ) {
+ if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.toggleTreeOpen(this.file.path);
}
@@ -98,11 +97,15 @@ export default {
:file="file"
/>
</span>
- <changed-file-icon
- :file="file"
- v-if="file.changed || file.tempFile"
- class="prepend-top-5 pull-right"
- />
+ <span class="pull-right">
+ <mr-file-icon
+ v-if="file.mrChange"
+ />
+ <changed-file-icon
+ :file="file"
+ v-if="file.changed || file.tempFile"
+ />
+ </span>
<new-dropdown
v-if="isTree"
:project-id="file.projectId"
diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
index 25d311142d5..97589e116c5 100644
--- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue
+++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
@@ -1,27 +1,27 @@
<script>
- import icon from '~/vue_shared/components/icon.vue';
- import tooltip from '~/vue_shared/directives/tooltip';
- import '~/lib/utils/datetime_utility';
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import '~/lib/utils/datetime_utility';
- export default {
- components: {
- icon,
+export default {
+ components: {
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ },
+ computed: {
+ lockTooltip() {
+ return `Locked by ${this.file.file_lock.user.name}`;
},
- props: {
- file: {
- type: Object,
- required: true,
- },
- },
- computed: {
- lockTooltip() {
- return `Locked by ${this.file.file_lock.user.name}`;
- },
- },
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index acbb43e867d..7bd646ba9b0 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -26,6 +26,11 @@ export default {
type: Boolean,
required: true,
},
+ mergeRequestId: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -70,6 +75,7 @@ export default {
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
+ :merge-request-id="mergeRequestId"
@click="openFileViewer"
/>
</div>
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index a6013784677..20983666b4a 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -44,7 +44,7 @@ const router = new VueRouter({
component: EmptyRouterComponent,
},
{
- path: 'mr/:mrid',
+ path: 'merge_requests/:mrid',
component: EmptyRouterComponent,
},
],
@@ -98,6 +98,60 @@ router.beforeEach((to, from, next) => {
);
throw e;
});
+ } else if (to.params.mrid) {
+ store.dispatch('updateViewer', 'mrdiff');
+
+ store
+ .dispatch('getMergeRequestData', {
+ projectId: fullProjectId,
+ mergeRequestId: to.params.mrid,
+ })
+ .then(mr => {
+ store.dispatch('getBranchData', {
+ projectId: fullProjectId,
+ branchId: mr.source_branch,
+ });
+
+ return store.dispatch('getFiles', {
+ projectId: fullProjectId,
+ branchId: mr.source_branch,
+ });
+ })
+ .then(() =>
+ store.dispatch('getMergeRequestVersions', {
+ projectId: fullProjectId,
+ mergeRequestId: to.params.mrid,
+ }),
+ )
+ .then(() =>
+ store.dispatch('getMergeRequestChanges', {
+ projectId: fullProjectId,
+ mergeRequestId: to.params.mrid,
+ }),
+ )
+ .then(mrChanges => {
+ mrChanges.changes.forEach((change, ind) => {
+ const changeTreeEntry = store.state.entries[change.new_path];
+
+ if (changeTreeEntry) {
+ store.dispatch('setFileMrChange', {
+ file: changeTreeEntry,
+ mrChange: change,
+ });
+
+ if (ind < 10) {
+ store.dispatch('getFileData', {
+ path: change.new_path,
+ makeFileActive: ind === 0,
+ });
+ }
+ }
+ });
+ })
+ .catch(e => {
+ flash('Error while loading the merge request. Please try again.');
+ throw e;
+ });
}
})
.catch(e => {
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index e659a6868ba..e47adae99ed 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -21,6 +21,15 @@ export default class Model {
new this.monaco.Uri(null, null, this.file.key),
)),
);
+ if (this.file.mrChange) {
+ this.disposable.add(
+ (this.baseModel = this.monaco.editor.createModel(
+ this.file.baseRaw,
+ undefined,
+ new this.monaco.Uri(null, null, `target/${this.file.path}`),
+ )),
+ );
+ }
this.events = new Map();
@@ -55,6 +64,10 @@ export default class Model {
return this.originalModel;
}
+ getBaseModel() {
+ return this.baseModel;
+ }
+
setValue(value) {
this.getModel().setValue(value);
}
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 887dd7e39b1..6b4ba30e086 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -109,11 +109,19 @@ export default class Editor {
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
}
+ attachMergeRequestModel(model) {
+ this.instance.setModel({
+ original: model.getBaseModel(),
+ modified: model.getModel(),
+ });
+
+ this.monaco.editor.createDiffNavigator(this.instance, {
+ alwaysRevealFirst: true,
+ });
+ }
+
setupMonacoTheme() {
- this.monaco.editor.defineTheme(
- gitlabTheme.themeName,
- gitlabTheme.monacoTheme,
- );
+ this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
this.monaco.editor.setTheme('gitlab');
}
@@ -161,8 +169,6 @@ export default class Editor {
onPositionChange(cb) {
if (!this.instance.onDidChangeCursorPosition) return;
- this.disposable.add(
- this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
- );
+ this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)));
}
}
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 5f1fb6cf843..a12e637616a 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -20,12 +20,35 @@ export default {
return Promise.resolve(file.raw);
}
- return Vue.http.get(file.rawPath, { params: { format: 'json' } })
+ return Vue.http.get(file.rawPath, { params: { format: 'json' } }).then(res => res.text());
+ },
+ getBaseRawFileData(file, sha) {
+ if (file.tempFile) {
+ return Promise.resolve(file.baseRaw);
+ }
+
+ if (file.baseRaw) {
+ return Promise.resolve(file.baseRaw);
+ }
+
+ return Vue.http
+ .get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), {
+ params: { format: 'json' },
+ })
.then(res => res.text());
},
getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`);
},
+ getProjectMergeRequestData(projectId, mergeRequestId) {
+ return Api.mergeRequest(projectId, mergeRequestId);
+ },
+ getProjectMergeRequestChanges(projectId, mergeRequestId) {
+ return Api.mergeRequestChanges(projectId, mergeRequestId);
+ },
+ getProjectMergeRequestVersions(projectId, mergeRequestId) {
+ return Api.mergeRequestVersions(projectId, mergeRequestId);
+ },
getBranchData(projectId, currentBranchId) {
return Api.branchSingle(projectId, currentBranchId);
},
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 373ea3f5c08..c6ba679d99c 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -115,3 +115,4 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
+export * from './actions/merge_request';
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 7aacec89116..6b034ea1e82 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -56,22 +56,21 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
commit(types.SET_CURRENT_BRANCH, file.branchId);
};
-export const getFileData = ({ state, commit, dispatch }, file) => {
+export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => {
+ const file = state.entries[path];
commit(types.TOGGLE_LOADING, { entry: file });
-
return service
.getFileData(file.url)
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
-
setPageTitle(pageTitle);
return res.json();
})
.then(data => {
commit(types.SET_FILE_DATA, { data, file });
- commit(types.TOGGLE_FILE_OPEN, file.path);
- dispatch('setFileActive', file.path);
+ commit(types.TOGGLE_FILE_OPEN, path);
+ if (makeFileActive) dispatch('setFileActive', path);
commit(types.TOGGLE_LOADING, { entry: file });
})
.catch(() => {
@@ -80,15 +79,40 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
});
};
-export const getRawFileData = ({ commit, dispatch }, file) =>
- service
- .getRawFileData(file)
- .then(raw => {
- commit(types.SET_FILE_RAW_DATA, { file, raw });
- })
- .catch(() =>
- flash('Error loading file content. Please try again.', 'alert', document, null, false, true),
- );
+export const setFileMrChange = ({ state, commit }, { file, mrChange }) => {
+ commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
+};
+
+export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
+ const file = state.entries[path];
+ return new Promise((resolve, reject) => {
+ service
+ .getRawFileData(file)
+ .then(raw => {
+ commit(types.SET_FILE_RAW_DATA, { file, raw });
+ if (file.mrChange && file.mrChange.new_file === false) {
+ service
+ .getBaseRawFileData(file, baseSha)
+ .then(baseRaw => {
+ commit(types.SET_FILE_BASE_RAW_DATA, {
+ file,
+ baseRaw,
+ });
+ resolve(raw);
+ })
+ .catch(e => {
+ reject(e);
+ });
+ } else {
+ resolve(raw);
+ }
+ })
+ .catch(() => {
+ flash('Error loading file content. Please try again.');
+ reject();
+ });
+ });
+};
export const changeFileContent = ({ state, commit }, { path, content }) => {
const file = state.entries[path];
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
new file mode 100644
index 00000000000..da73034fd7d
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -0,0 +1,84 @@
+import flash from '~/flash';
+import service from '../../services';
+import * as types from '../mutation_types';
+
+export const getMergeRequestData = (
+ { commit, state, dispatch },
+ { projectId, mergeRequestId, force = false } = {},
+) =>
+ new Promise((resolve, reject) => {
+ if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
+ service
+ .getProjectMergeRequestData(projectId, mergeRequestId)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.SET_MERGE_REQUEST, {
+ projectPath: projectId,
+ mergeRequestId,
+ mergeRequest: data,
+ });
+ if (!state.currentMergeRequestId) {
+ commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId);
+ }
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading merge request data. Please try again.');
+ reject(new Error(`Merge Request not loaded ${projectId}`));
+ });
+ } else {
+ resolve(state.projects[projectId].mergeRequests[mergeRequestId]);
+ }
+ });
+
+export const getMergeRequestChanges = (
+ { commit, state, dispatch },
+ { projectId, mergeRequestId, force = false } = {},
+) =>
+ new Promise((resolve, reject) => {
+ if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) {
+ service
+ .getProjectMergeRequestChanges(projectId, mergeRequestId)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.SET_MERGE_REQUEST_CHANGES, {
+ projectPath: projectId,
+ mergeRequestId,
+ changes: data,
+ });
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading merge request changes. Please try again.');
+ reject(new Error(`Merge Request Changes not loaded ${projectId}`));
+ });
+ } else {
+ resolve(state.projects[projectId].mergeRequests[mergeRequestId].changes);
+ }
+ });
+
+export const getMergeRequestVersions = (
+ { commit, state, dispatch },
+ { projectId, mergeRequestId, force = false } = {},
+) =>
+ new Promise((resolve, reject) => {
+ if (!state.projects[projectId].mergeRequests[mergeRequestId].versions.length || force) {
+ service
+ .getProjectMergeRequestVersions(projectId, mergeRequestId)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.SET_MERGE_REQUEST_VERSIONS, {
+ projectPath: projectId,
+ mergeRequestId,
+ versions: data,
+ });
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading merge request versions. Please try again.');
+ reject(new Error(`Merge Request Versions not loaded ${projectId}`));
+ });
+ } else {
+ resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions);
+ }
+ });
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 70a969a0325..6536be04f0a 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -2,9 +2,7 @@ import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
-import {
- findEntry,
-} from '../utils';
+import { findEntry } from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const toggleTreeOpen = ({ commit, dispatch }, path) => {
@@ -21,23 +19,24 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
dispatch('setFileActive', row.path);
} else {
- dispatch('getFileData', row);
+ dispatch('getFileData', { path: row.path });
}
};
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
- service.getTreeLastCommit(tree.lastCommitPath)
- .then((res) => {
+ service
+ .getTreeLastCommit(tree.lastCommitPath)
+ .then(res => {
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
return res.json();
})
- .then((data) => {
- data.forEach((lastCommit) => {
+ .then(data => {
+ data.forEach(lastCommit => {
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
if (entry) {
@@ -50,44 +49,47 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
};
-export const getFiles = (
- { state, commit, dispatch },
- { projectId, branchId } = {},
-) => new Promise((resolve, reject) => {
- if (!state.trees[`${projectId}/${branchId}`]) {
- const selectedProject = state.projects[projectId];
- commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
-
- service
- .getFiles(selectedProject.web_url, branchId)
- .then(res => res.json())
- .then((data) => {
- const worker = new FilesDecoratorWorker();
- worker.addEventListener('message', (e) => {
- const { entries, treeList } = e.data;
- const selectedTree = state.trees[`${projectId}/${branchId}`];
-
- commit(types.SET_ENTRIES, entries);
- commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList });
- commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
-
- worker.terminate();
-
- resolve();
- });
-
- worker.postMessage({
- data,
- projectId,
- branchId,
+export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
+ new Promise((resolve, reject) => {
+ if (!state.trees[`${projectId}/${branchId}`]) {
+ const selectedProject = state.projects[projectId];
+ commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
+
+ service
+ .getFiles(selectedProject.web_url, branchId)
+ .then(res => res.json())
+ .then(data => {
+ const worker = new FilesDecoratorWorker();
+ worker.addEventListener('message', e => {
+ const { entries, treeList } = e.data;
+ const selectedTree = state.trees[`${projectId}/${branchId}`];
+
+ commit(types.SET_ENTRIES, entries);
+ commit(types.SET_DIRECTORY_DATA, {
+ treePath: `${projectId}/${branchId}`,
+ data: treeList,
+ });
+ commit(types.TOGGLE_LOADING, {
+ entry: selectedTree,
+ forceValue: false,
+ });
+
+ worker.terminate();
+
+ resolve();
+ });
+
+ worker.postMessage({
+ data,
+ projectId,
+ branchId,
+ });
+ })
+ .catch(e => {
+ flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
+ reject(e);
});
- })
- .catch((e) => {
- flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
- reject(e);
- });
- } else {
- resolve();
- }
-});
-
+ } else {
+ resolve();
+ }
+ });
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index eba325a31df..a77cdbc13c8 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -1,10 +1,8 @@
-export const activeFile = state =>
- state.openFiles.find(file => file.active) || null;
+export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
-export const modifiedFiles = state =>
- state.changedFiles.filter(f => !f.tempFile);
+export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
export const projectsWithTrees = state =>
Object.keys(state.projects).map(projectId => {
@@ -23,8 +21,17 @@ export const projectsWithTrees = state =>
};
});
+export const currentMergeRequest = state => {
+ if (state.projects[state.currentProjectId]) {
+ return state.projects[state.currentProjectId].mergeRequests[state.currentMergeRequestId];
+ }
+ return null;
+};
+
// eslint-disable-next-line no-confusing-arrow
export const currentIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state => !!state.changedFiles.length;
+
+export const hasMergeRequest = state => !!state.currentMergeRequestId;
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index fa2fbaf8683..ee759bff516 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -11,6 +11,12 @@ export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
+// Merge Request Mutation Types
+export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST';
+export const SET_CURRENT_MERGE_REQUEST = 'SET_CURRENT_MERGE_REQUEST';
+export const SET_MERGE_REQUEST_CHANGES = 'SET_MERGE_REQUEST_CHANGES';
+export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
+
// Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
@@ -28,6 +34,7 @@ export const SET_FILE_DATA = 'SET_FILE_DATA';
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
+export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
@@ -39,6 +46,7 @@ export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const SET_ENTRIES = 'SET_ENTRIES';
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
+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';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index da41fc9285c..5e5eb831662 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,5 +1,6 @@
import * as types from './mutation_types';
import projectMutations from './mutations/project';
+import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
@@ -11,10 +12,7 @@ export default {
[types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
if (entry.path) {
Object.assign(state.entries[entry.path], {
- loading:
- forceValue !== undefined
- ? forceValue
- : !state.entries[entry.path].loading,
+ loading: forceValue !== undefined ? forceValue : !state.entries[entry.path].loading,
});
} else {
Object.assign(entry, {
@@ -83,9 +81,7 @@ export default {
if (!foundEntry) {
Object.assign(state.trees[`${projectId}/${branchId}`], {
- tree: state.trees[`${projectId}/${branchId}`].tree.concat(
- data.treeList,
- ),
+ tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
});
}
},
@@ -100,6 +96,7 @@ export default {
});
},
...projectMutations,
+ ...mergeRequestMutation,
...fileMutations,
...treeMutations,
...branchMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 4fafcfd0ea1..926b6f66d78 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -40,6 +40,8 @@ export default {
rawPath: data.raw_path,
binary: data.binary,
renderError: data.render_error,
+ raw: null,
+ baseRaw: null,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
@@ -47,6 +49,11 @@ export default {
raw,
});
},
+ [types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
+ Object.assign(state.entries[file.path], {
+ baseRaw,
+ });
+ },
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
const changed = content !== state.entries[path].raw;
@@ -71,6 +78,11 @@ export default {
editorColumn,
});
},
+ [types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
+ Object.assign(state.entries[file.path], {
+ mrChange,
+ });
+ },
[types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(state.entries[path], {
content: state.entries[path].raw,
diff --git a/app/assets/javascripts/ide/stores/mutations/merge_request.js b/app/assets/javascripts/ide/stores/mutations/merge_request.js
new file mode 100644
index 00000000000..334819fe702
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/merge_request.js
@@ -0,0 +1,33 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.SET_CURRENT_MERGE_REQUEST](state, currentMergeRequestId) {
+ Object.assign(state, {
+ currentMergeRequestId,
+ });
+ },
+ [types.SET_MERGE_REQUEST](state, { projectPath, mergeRequestId, mergeRequest }) {
+ Object.assign(state.projects[projectPath], {
+ mergeRequests: {
+ [mergeRequestId]: {
+ ...mergeRequest,
+ active: true,
+ changes: [],
+ versions: [],
+ baseCommitSha: null,
+ },
+ },
+ });
+ },
+ [types.SET_MERGE_REQUEST_CHANGES](state, { projectPath, mergeRequestId, changes }) {
+ Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
+ changes,
+ });
+ },
+ [types.SET_MERGE_REQUEST_VERSIONS](state, { projectPath, mergeRequestId, versions }) {
+ Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
+ versions,
+ baseCommitSha: versions.length ? versions[0].base_commit_sha : null,
+ });
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
index 2816562a919..284b39a2c72 100644
--- a/app/assets/javascripts/ide/stores/mutations/project.js
+++ b/app/assets/javascripts/ide/stores/mutations/project.js
@@ -11,6 +11,7 @@ export default {
Object.assign(project, {
tree: [],
branches: {},
+ mergeRequests: {},
active: true,
});
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 6110f54951c..e5cc8814000 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -1,6 +1,7 @@
export default () => ({
currentProjectId: '',
currentBranchId: '',
+ currentMergeRequestId: '',
changedFiles: [],
endpoints: {},
lastCommitMsg: '',
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 487ea1ead8e..3389eeeaa2e 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -38,7 +38,7 @@ export const dataStructure = () => ({
eol: '',
});
-export const decorateData = (entity) => {
+export const decorateData = entity => {
const {
id,
projectId,
@@ -57,7 +57,6 @@ export const decorateData = (entity) => {
base64 = false,
file_lock,
-
} = entity;
return {
@@ -80,17 +79,15 @@ export const decorateData = (entity) => {
base64,
file_lock,
-
};
};
-export const findEntry = (tree, type, name, prop = 'name') => tree.find(
- f => f.type === type && f[prop] === name,
-);
+export const findEntry = (tree, type, name, prop = 'name') =>
+ tree.find(f => f.type === type && f[prop] === name);
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
-export const setPageTitle = (title) => {
+export const setPageTitle = title => {
document.title = title;
};
@@ -120,6 +117,11 @@ const sortTreesByTypeAndName = (a, b) => {
return 0;
};
-export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, {
- tree: entity.tree.length ? sortTree(entity.tree) : [],
-})).sort(sortTreesByTypeAndName);
+export const sortTree = sortedTree =>
+ sortedTree
+ .map(entity =>
+ Object.assign(entity, {
+ tree: entity.tree.length ? sortTree(entity.tree) : [],
+ }),
+ )
+ .sort(sortTreesByTypeAndName);
diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
index a6819aaeb12..dfe87d89a39 100644
--- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
@@ -11,11 +11,19 @@
type: String,
required: true,
},
+ helpUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
hasTitle() {
return this.title.length > 0;
},
+ hasHelpURL() {
+ return this.helpUrl.length > 0;
+ },
},
};
</script>
@@ -28,5 +36,21 @@
{{ title }}:
</span>
{{ value }}
+
+ <span
+ v-if="hasHelpURL"
+ class="help-button pull-right"
+ >
+ <a
+ :href="helpUrl"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ >
+ <i
+ class="fa fa-question-circle"
+ aria-hidden="true"
+ ></i>
+ </a>
+ </span>
</p>
</template>
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
index 56814a52525..172de6b3679 100644
--- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
@@ -22,6 +22,11 @@
type: Boolean,
required: true,
},
+ runnerHelpUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
shouldRenderContent() {
@@ -39,6 +44,21 @@
runnerId() {
return `#${this.job.runner.id}`;
},
+ hasTimeout() {
+ return this.job.metadata != null && this.job.metadata.timeout_human_readable !== '';
+ },
+ 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})`;
+ }
+
+ return t;
+ },
renderBlock() {
return this.job.merge_request ||
this.job.duration ||
@@ -115,6 +135,13 @@
:value="queued"
/>
<detail-row
+ class="js-job-timeout"
+ v-if="hasTimeout"
+ title="Timeout"
+ :help-url="runnerHelpUrl"
+ :value="timeout"
+ />
+ <detail-row
class="js-job-runner"
v-if="job.runner"
title="Runner"
diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js
index 85a88ae409b..656676ead91 100644
--- a/app/assets/javascripts/jobs/job_details_bundle.js
+++ b/app/assets/javascripts/jobs/job_details_bundle.js
@@ -51,6 +51,7 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
job: this.mediator.store.state.job,
+ runnerHelpUrl: dataset.runnerHelpUrl,
},
});
},
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index a266bb6771f..dd17544b656 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -51,7 +51,7 @@ export function removeParams(params) {
const url = document.createElement('a');
url.href = window.location.href;
- params.forEach((param) => {
+ params.forEach(param => {
url.search = removeParamQueryString(url.search, param);
});
@@ -83,3 +83,11 @@ export function refreshCurrentPage() {
export function redirectTo(url) {
return window.location.assign(url);
}
+
+export function webIDEUrl(route = undefined) {
+ let returnUrl = `${gon.relative_url_root}/-/ide/`;
+ if (route) {
+ returnUrl += `project${route}`;
+ }
+ return returnUrl;
+}
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index cf579c5d4dc..e0f883a8e08 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -292,10 +292,12 @@ Please check your network connection and try again.`;
</button>
</div>
<div
+ v-if="note.resolvable"
class="btn-group discussion-actions"
- role="group">
+ role="group"
+ >
<div
- v-if="note.resolvable && !discussionResolved"
+ v-if="!discussionResolved"
class="btn-group"
role="group">
<a
diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
index 22248418c41..2bda2aeb3a1 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
@@ -19,15 +19,19 @@
type: String,
required: true,
},
+ groupName: {
+ type: String,
+ required: true,
+ },
},
computed: {
title() {
return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle });
},
text() {
- return s__(`Milestones|Promoting this milestone will make it available for all projects inside the group.
+ return sprintf(s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}.
Existing project milestones with the same title will be merged.
- This action cannot be reversed.`);
+ This action cannot be reversed.`), { milestoneTitle: this.milestoneTitle, groupName: this.groupName });
},
},
methods: {
diff --git a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
index d00f81c9094..8e79341e96a 100644
--- a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
+++ b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
@@ -25,6 +25,7 @@ export default () => {
const modalProps = {
milestoneTitle: button.dataset.milestoneTitle,
url: button.dataset.url,
+ groupName: button.dataset.groupName,
};
eventHub.$once('promoteMilestoneModal.requestStarted', onRequestStarted);
eventHub.$emit('promoteMilestoneModal.props', modalProps);
@@ -54,6 +55,7 @@ export default () => {
return {
modalProps: {
milestoneTitle: '',
+ groupName: '',
url: '',
},
};
diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
index 54695dfeb99..ad6df51bb7a 100644
--- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
@@ -1,4 +1,5 @@
<script>
+ import _ from 'underscore';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import GlModal from '~/vue_shared/components/gl_modal.vue';
@@ -27,19 +28,26 @@
type: String,
required: true,
},
+ groupName: {
+ type: String,
+ required: true,
+ },
},
computed: {
text() {
- return s__(`Milestones|Promoting this label will make it available for all projects inside the group.
- Existing project labels with the same title will be merged. This action cannot be reversed.`);
+ return sprintf(s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}.
+ Existing project labels with the same title will be merged. This action cannot be reversed.`), {
+ labelTitle: this.labelTitle,
+ groupName: this.groupName,
+ });
},
title() {
const label = `<span
class="label color-label"
style="background-color: ${this.labelColor}; color: ${this.labelTextColor};"
- >${this.labelTitle}</span>`;
+ >${_.escape(this.labelTitle)}</span>`;
- return sprintf(s__('Labels|Promote label %{labelTitle} to Group Label?'), {
+ return sprintf(s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'), {
labelTitle: label,
}, false);
},
@@ -69,6 +77,7 @@
>
<div
slot="title"
+ class="modal-title-with-label"
v-html="title"
>
{{ title }}
diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js
index 2abcbfab1ed..03cfef61311 100644
--- a/app/assets/javascripts/pages/projects/labels/index/index.js
+++ b/app/assets/javascripts/pages/projects/labels/index/index.js
@@ -30,6 +30,7 @@ const initLabelIndex = () => {
labelColor: button.dataset.labelColor,
labelTextColor: button.dataset.labelTextColor,
url: button.dataset.url,
+ groupName: button.dataset.groupName,
};
eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted);
eventHub.$emit('promoteLabelModal.props', modalProps);
@@ -62,6 +63,7 @@ const initLabelIndex = () => {
labelColor: '',
labelTextColor: '',
url: '',
+ groupName: '',
},
};
},
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 8a86c409b62..ceb02309959 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,59 +1,73 @@
<script>
- import Flash from '../../../flash';
- import editForm from './edit_form.vue';
- import Icon from '../../../vue_shared/components/icon.vue';
- import { __ } from '../../../locale';
+import Flash from '../../../flash';
+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,
+export default {
+ components: {
+ editForm,
+ Icon,
+ },
+ props: {
+ isConfidential: {
+ required: true,
+ type: Boolean,
},
- props: {
- isConfidential: {
- required: true,
- type: Boolean,
- },
- isEditable: {
- required: true,
- type: Boolean,
- },
- service: {
- required: true,
- type: Object,
- },
+ isEditable: {
+ required: true,
+ type: Boolean,
},
- data() {
- return {
- edit: false,
- };
+ service: {
+ required: true,
+ type: Object,
},
- computed: {
- confidentialityIcon() {
- return this.isConfidential ? 'eye-slash' : 'eye';
- },
+ },
+ data() {
+ return {
+ edit: false,
+ };
+ },
+ computed: {
+ confidentialityIcon() {
+ return this.isConfidential ? 'eye-slash' : 'eye';
},
- methods: {
- toggleForm() {
- this.edit = !this.edit;
- },
- updateConfidentialAttribute(confidential) {
- this.service.update('issue', { confidential })
- .then(() => location.reload())
- .catch(() => {
- Flash(__('Something went wrong trying to change the confidentiality of this issue'));
- });
- },
+ },
+ created() {
+ eventHub.$on('closeConfidentialityForm', this.toggleForm);
+ },
+ beforeDestroy() {
+ eventHub.$off('closeConfidentialityForm', this.toggleForm);
+ },
+ methods: {
+ toggleForm() {
+ this.edit = !this.edit;
},
- };
+ updateConfidentialAttribute(confidential) {
+ this.service
+ .update('issue', { confidential })
+ .then(() => location.reload())
+ .catch(() => {
+ Flash(
+ __(
+ 'Something went wrong trying to change the confidentiality of this issue',
+ ),
+ );
+ });
+ },
+ },
+};
</script>
<template>
<div class="block issuable-sidebar-item confidentiality">
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ @click="toggleForm"
+ >
<icon
:name="confidentialityIcon"
- :size="16"
aria-hidden="true"
/>
</div>
@@ -71,7 +85,6 @@
<div class="value sidebar-item-value hide-collapsed">
<editForm
v-if="edit"
- :toggle-form="toggleForm"
:is-confidential="isConfidential"
:update-confidential-attribute="updateConfidentialAttribute"
/>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
index c569843b05f..3783f71a848 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
@@ -1,34 +1,34 @@
<script>
- import editFormButtons from './edit_form_buttons.vue';
- import { s__ } from '../../../locale';
+import editFormButtons from './edit_form_buttons.vue';
+import { s__ } from '../../../locale';
- export default {
- components: {
- editFormButtons,
+export default {
+ components: {
+ editFormButtons,
+ },
+ props: {
+ isConfidential: {
+ required: true,
+ type: Boolean,
},
- props: {
- isConfidential: {
- required: true,
- type: Boolean,
- },
- toggleForm: {
- required: true,
- type: Function,
- },
- updateConfidentialAttribute: {
- required: true,
- type: Function,
- },
+ updateConfidentialAttribute: {
+ required: true,
+ type: Function,
},
- computed: {
- confidentialityOnWarning() {
- return s__('confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.');
- },
- confidentialityOffWarning() {
- return s__('confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.');
- },
+ },
+ computed: {
+ confidentialityOnWarning() {
+ return s__(
+ 'confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.',
+ );
},
- };
+ confidentialityOffWarning() {
+ return s__(
+ 'confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.',
+ );
+ },
+ },
+};
</script>
<template>
@@ -45,7 +45,6 @@
</p>
<edit-form-buttons
:is-confidential="isConfidential"
- :toggle-form="toggleForm"
:update-confidential-attribute="updateConfidentialAttribute"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
index 49d5dfeea1a..38b1ddbfd5b 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
@@ -1,14 +1,13 @@
<script>
+import $ from 'jquery';
+import eventHub from '../../event_hub';
+
export default {
props: {
isConfidential: {
required: true,
type: Boolean,
},
- toggleForm: {
- required: true,
- type: Function,
- },
updateConfidentialAttribute: {
required: true,
type: Function,
@@ -22,6 +21,16 @@ export default {
return !this.isConfidential;
},
},
+ methods: {
+ closeForm() {
+ eventHub.$emit('closeConfidentialityForm');
+ $(this.$el).trigger('hidden.gl.dropdown');
+ },
+ submitForm() {
+ this.closeForm();
+ this.updateConfidentialAttribute(this.updateConfidentialBool);
+ },
+ },
};
</script>
@@ -30,14 +39,14 @@ export default {
<button
type="button"
class="btn btn-default append-right-10"
- @click="toggleForm"
+ @click="closeForm"
>
{{ __('Cancel') }}
</button>
<button
type="button"
class="btn btn-close"
- @click.prevent="updateConfidentialAttribute(updateConfidentialBool)"
+ @click.prevent="submitForm"
>
{{ toggleButtonText }}
</button>
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
index bc32e974bc3..e1e4715826a 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
@@ -1,40 +1,43 @@
<script>
- import editFormButtons from './edit_form_buttons.vue';
- import issuableMixin from '../../../vue_shared/mixins/issuable';
- import { __, sprintf } from '../../../locale';
+import editFormButtons from './edit_form_buttons.vue';
+import issuableMixin from '../../../vue_shared/mixins/issuable';
+import { __, sprintf } from '../../../locale';
- export default {
- components: {
- editFormButtons,
+export default {
+ components: {
+ editFormButtons,
+ },
+ mixins: [issuableMixin],
+ props: {
+ isLocked: {
+ required: true,
+ type: Boolean,
},
- mixins: [
- issuableMixin,
- ],
- props: {
- isLocked: {
- required: true,
- type: Boolean,
- },
- toggleForm: {
- required: true,
- type: Function,
- },
-
- updateLockedAttribute: {
- required: true,
- type: Function,
- },
+ updateLockedAttribute: {
+ required: true,
+ type: Function,
+ },
+ },
+ computed: {
+ lockWarning() {
+ return sprintf(
+ __(
+ 'Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.',
+ ),
+ { issuableDisplayName: this.issuableDisplayName },
+ );
},
- computed: {
- lockWarning() {
- return sprintf(__('Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName });
- },
- unlockWarning() {
- return sprintf(__('Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName });
- },
+ unlockWarning() {
+ return sprintf(
+ __(
+ 'Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.',
+ ),
+ { issuableDisplayName: this.issuableDisplayName },
+ );
},
- };
+ },
+};
</script>
<template>
@@ -54,7 +57,6 @@
<edit-form-buttons
:is-locked="isLocked"
- :toggle-form="toggleForm"
:update-locked-attribute="updateLockedAttribute"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
index c3a553a7605..5e7b8f9698f 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -1,4 +1,7 @@
<script>
+import $ from 'jquery';
+import eventHub from '../../event_hub';
+
export default {
props: {
isLocked: {
@@ -6,11 +9,6 @@ export default {
type: Boolean,
},
- toggleForm: {
- required: true,
- type: Function,
- },
-
updateLockedAttribute: {
required: true,
type: Function,
@@ -26,6 +24,17 @@ export default {
return !this.isLocked;
},
},
+
+ methods: {
+ closeForm() {
+ eventHub.$emit('closeLockForm');
+ $(this.$el).trigger('hidden.gl.dropdown');
+ },
+ submitForm() {
+ this.closeForm();
+ this.updateLockedAttribute(this.toggleLock);
+ },
+ },
};
</script>
@@ -34,7 +43,7 @@ export default {
<button
type="button"
class="btn btn-default append-right-10"
- @click="toggleForm"
+ @click="closeForm"
>
{{ __('Cancel') }}
</button>
@@ -42,7 +51,7 @@ export default {
<button
type="button"
class="btn btn-close"
- @click.prevent="updateLockedAttribute(toggleLock)"
+ @click.prevent="submitForm"
>
{{ buttonText }}
</button>
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 0686910fc7e..e4893451af3 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -1,70 +1,93 @@
<script>
- import Flash from '~/flash';
- import editForm from './edit_form.vue';
- import issuableMixin from '../../../vue_shared/mixins/issuable';
- import Icon from '../../../vue_shared/components/icon.vue';
+import Flash from '~/flash';
+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,
- },
- mixins: [
- issuableMixin,
- ],
+export default {
+ components: {
+ editForm,
+ Icon,
+ },
+ mixins: [issuableMixin],
- props: {
- isLocked: {
- required: true,
- type: Boolean,
- },
+ props: {
+ isLocked: {
+ required: true,
+ type: Boolean,
+ },
- isEditable: {
- required: true,
- type: Boolean,
- },
+ isEditable: {
+ required: true,
+ type: Boolean,
+ },
- mediator: {
- required: true,
- type: Object,
- validator(mediatorObject) {
- return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
- },
+ mediator: {
+ required: true,
+ type: Object,
+ validator(mediatorObject) {
+ return (
+ mediatorObject.service &&
+ mediatorObject.service.update &&
+ mediatorObject.store
+ );
},
},
+ },
- computed: {
- lockIcon() {
- return this.isLocked ? 'lock' : 'lock-open';
- },
+ computed: {
+ lockIcon() {
+ return this.isLocked ? 'lock' : 'lock-open';
+ },
- isLockDialogOpen() {
- return this.mediator.store.isLockDialogOpen;
- },
+ isLockDialogOpen() {
+ return this.mediator.store.isLockDialogOpen;
},
+ },
- methods: {
- toggleForm() {
- this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
- },
+ created() {
+ eventHub.$on('closeLockForm', this.toggleForm);
+ },
+
+ beforeDestroy() {
+ eventHub.$off('closeLockForm', this.toggleForm);
+ },
- updateLockedAttribute(locked) {
- this.mediator.service.update(this.issuableType, {
+ methods: {
+ toggleForm() {
+ this.mediator.store.isLockDialogOpen = !this.mediator.store
+ .isLockDialogOpen;
+ },
+
+ updateLockedAttribute(locked) {
+ this.mediator.service
+ .update(this.issuableType, {
discussion_locked: locked,
})
.then(() => location.reload())
- .catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName}`)));
- },
+ .catch(() =>
+ Flash(
+ this.__(
+ `Something went wrong trying to change the locked state of this ${
+ this.issuableDisplayName
+ }`,
+ ),
+ ),
+ );
},
- };
+ },
+};
</script>
<template>
<div class="block issuable-sidebar-item lock">
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ @click="toggleForm"
+ >
<icon
:name="lockIcon"
- :size="16"
aria-hidden="true"
class="sidebar-item-icon is-active"
/>
@@ -85,7 +108,6 @@
<div class="value sidebar-item-value hide-collapsed">
<edit-form
v-if="isLockDialogOpen"
- :toggle-form="toggleForm"
:is-locked="isLocked"
:update-locked-attribute="updateLockedAttribute"
:issuable-type="issuableType"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 3d886e7d628..18ee4c62bf1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -1,53 +1,57 @@
<script>
- import tooltip from '~/vue_shared/directives/tooltip';
- import { n__ } from '~/locale';
- import icon from '~/vue_shared/components/icon.vue';
- import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { n__ } from '~/locale';
+import { webIDEUrl } from '~/lib/utils/url_utility';
+import icon from '~/vue_shared/components/icon.vue';
+import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
- export default {
- name: 'MRWidgetHeader',
- directives: {
- tooltip,
+export default {
+ name: 'MRWidgetHeader',
+ directives: {
+ tooltip,
+ },
+ components: {
+ icon,
+ clipboardButton,
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
},
- components: {
- icon,
- clipboardButton,
+ },
+ computed: {
+ shouldShowCommitsBehindText() {
+ return this.mr.divergedCommitsCount > 0;
},
- props: {
- mr: {
- type: Object,
- required: true,
- },
+ commitsText() {
+ return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
},
- computed: {
- shouldShowCommitsBehindText() {
- return this.mr.divergedCommitsCount > 0;
- },
- commitsText() {
- return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
- },
- branchNameClipboardData() {
- // This supports code in app/assets/javascripts/copy_to_clipboard.js that
- // works around ClipboardJS limitations to allow the context-specific
- // copy/pasting of plain text or GFM.
- return JSON.stringify({
- text: this.mr.sourceBranch,
- gfm: `\`${this.mr.sourceBranch}\``,
- });
- },
- isSourceBranchLong() {
- return this.isBranchTitleLong(this.mr.sourceBranch);
- },
- isTargetBranchLong() {
- return this.isBranchTitleLong(this.mr.targetBranch);
- },
+ branchNameClipboardData() {
+ // This supports code in app/assets/javascripts/copy_to_clipboard.js that
+ // works around ClipboardJS limitations to allow the context-specific
+ // copy/pasting of plain text or GFM.
+ return JSON.stringify({
+ text: this.mr.sourceBranch,
+ gfm: `\`${this.mr.sourceBranch}\``,
+ });
},
- methods: {
- isBranchTitleLong(branchTitle) {
- return branchTitle.length > 32;
- },
+ isSourceBranchLong() {
+ return this.isBranchTitleLong(this.mr.sourceBranch);
},
- };
+ isTargetBranchLong() {
+ return this.isBranchTitleLong(this.mr.targetBranch);
+ },
+ webIdePath() {
+ return webIDEUrl(this.mr.statusPath.replace('.json', ''));
+ },
+ },
+ methods: {
+ isBranchTitleLong(branchTitle) {
+ return branchTitle.length > 32;
+ },
+ },
+};
</script>
<template>
<div class="mr-source-target">
@@ -96,6 +100,13 @@
</div>
<div v-if="mr.isOpen">
+ <a
+ v-if="!mr.sourceBranchRemoved"
+ :href="webIdePath"
+ class="btn btn-sm btn-default inline js-web-ide"
+ >
+ {{ s__("mrWidget|Web IDE") }}
+ </a>
<button
data-target="#modal_merge_info"
data-toggle="modal"
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index 7f3f7e67d76..05cb0196ced 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -199,6 +199,10 @@
.branch-header-title {
color: $color-700;
}
+
+ .ide-file-list .file.file-active {
+ color: $color-700;
+ }
}
body {
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 48b981dd31f..eb789cc64b0 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -4,9 +4,15 @@
.page-title,
.modal-title {
+ .modal-title-with-label span {
+ vertical-align: middle;
+ display: inline-block;
+ }
+
.color-label {
font-size: $gl-font-size;
padding: $gl-vert-padding $label-padding-modal;
+ vertical-align: middle;
}
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 3dd4a613789..798f248dad4 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -88,7 +88,6 @@
.right-sidebar {
border-left: 1px solid $border-color;
- height: calc(100% - #{$header-height});
}
.with-performance-bar .right-sidebar.affix {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index e21a9f0afc9..2c0ed976301 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -522,10 +522,6 @@
.with-performance-bar .right-sidebar {
top: $header-height + $performance-bar-height;
-
- .issuable-sidebar {
- height: calc(100% - #{$performance-bar-height});
- }
}
.sidebar-move-issue-confirmation-button {
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 65046f6665e..1f6f7138e1f 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -19,8 +19,7 @@
.ide-view {
display: flex;
height: calc(100vh - #{$header-height});
- margin-top: 40px;
- color: $almost-black;
+ margin-top: 0;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
@@ -43,13 +42,18 @@
cursor: pointer;
&.file-open {
- background: $white-normal;
+ background: $link-active-background;
+ }
+
+ &.file-active {
+ font-weight: $gl-font-weight-bold;
}
.ide-file-name {
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
+ max-width: inherit;
svg {
vertical-align: middle;
@@ -72,7 +76,10 @@
margin-right: -8px;
}
- &:hover {
+ &:hover,
+ &:focus {
+ background: $link-active-background;
+
.ide-new-btn {
display: block;
}
@@ -450,6 +457,8 @@
display: flex;
flex-direction: column;
flex: 1;
+ max-height: 100%;
+ overflow: auto;
}
.multi-file-commit-empty-state-container {
@@ -460,7 +469,7 @@
.multi-file-commit-panel-header {
display: flex;
align-items: center;
- margin-bottom: 12px;
+ margin-bottom: 0;
border-bottom: 1px solid $white-dark;
padding: $gl-btn-padding 0;
@@ -667,8 +676,14 @@
overflow: hidden;
&.nav-only {
+ padding-top: $header-height;
+
+ .with-performance-bar & {
+ padding-top: $header-height + $performance-bar-height;
+ }
+
.flash-container {
- margin-top: $header-height;
+ margin-top: 0;
margin-bottom: 0;
}
@@ -678,7 +693,7 @@
}
.content-wrapper {
- margin-top: $header-height;
+ margin-top: 0;
padding-bottom: 0;
}
@@ -702,11 +717,11 @@
.with-performance-bar .ide.nav-only {
.flash-container {
- margin-top: #{$header-height + $performance-bar-height};
+ margin-top: 0;
}
.content-wrapper {
- margin-top: #{$header-height + $performance-bar-height};
+ margin-top: 0;
padding-bottom: 0;
}
@@ -715,14 +730,8 @@
}
&.flash-shown {
- .content-wrapper {
- margin-top: 0;
- }
-
.ide-view {
- height: calc(
- 100vh - #{$header-height + $performance-bar-height + $flash-height}
- );
+ height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height});
}
}
}
diff --git a/app/assets/stylesheets/pages/repo.scss.orig b/app/assets/stylesheets/pages/repo.scss.orig
new file mode 100644
index 00000000000..57b995adb64
--- /dev/null
+++ b/app/assets/stylesheets/pages/repo.scss.orig
@@ -0,0 +1,786 @@
+.project-refs-form,
+.project-refs-target-form {
+ display: inline-block;
+}
+
+.fade-enter,
+.fade-leave-to {
+ opacity: 0;
+}
+
+.commit-message {
+ @include str-truncated(250px);
+}
+
+.editable-mode {
+ display: inline-block;
+}
+
+.ide-view {
+ display: flex;
+ height: calc(100vh - #{$header-height});
+ margin-top: 40px;
+ color: $almost-black;
+ border-top: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+
+ &.is-collapsed {
+ .ide-file-list {
+ max-width: 250px;
+ }
+ }
+
+ .file-status-icon {
+ width: 10px;
+ height: 10px;
+ }
+}
+
+.ide-file-list {
+ flex: 1;
+
+ .file {
+ cursor: pointer;
+
+ &.file-open {
+ background: $white-normal;
+ }
+
+ .ide-file-name {
+ flex: 1;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ svg {
+ vertical-align: middle;
+ margin-right: 2px;
+ }
+
+ .loading-container {
+ margin-right: 4px;
+ display: inline-block;
+ }
+ }
+
+ .ide-file-changed-icon {
+ margin-left: auto;
+ }
+
+ .ide-new-btn {
+ display: none;
+ margin-bottom: -4px;
+ margin-right: -8px;
+ }
+
+ &:hover {
+ .ide-new-btn {
+ display: block;
+ }
+ }
+
+ &.folder {
+ svg {
+ fill: $gl-text-color-secondary;
+ }
+ }
+ }
+
+ a {
+ color: $gl-text-color;
+ }
+
+ th {
+ position: sticky;
+ top: 0;
+ }
+}
+
+.file-name,
+.file-col-commit-message {
+ display: flex;
+ overflow: visible;
+ padding: 6px 12px;
+}
+
+.multi-file-loading-container {
+ margin-top: 10px;
+ padding: 10px;
+
+ .animation-container {
+ background: $gray-light;
+
+ div {
+ background: $gray-light;
+ }
+ }
+}
+
+.multi-file-table-col-commit-message {
+ white-space: nowrap;
+ width: 50%;
+}
+
+.multi-file-edit-pane {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ border-left: 1px solid $white-dark;
+ overflow: hidden;
+}
+
+.multi-file-tabs {
+ display: flex;
+ background-color: $white-normal;
+ box-shadow: inset 0 -1px $white-dark;
+
+ > ul {
+ display: flex;
+ overflow-x: auto;
+ }
+
+ li {
+ position: relative;
+ }
+
+ .dropdown {
+ display: flex;
+ margin-left: auto;
+ margin-bottom: 1px;
+ padding: 0 $grid-size;
+ border-left: 1px solid $white-dark;
+ background-color: $white-light;
+
+ &.shadow {
+ box-shadow: 0 0 10px $dropdown-shadow-color;
+ }
+
+ .btn {
+ margin-top: auto;
+ margin-bottom: auto;
+ }
+ }
+}
+
+.multi-file-tab {
+ @include str-truncated(150px);
+ padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
+ background-color: $gray-normal;
+ border-right: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+ cursor: pointer;
+
+ svg {
+ vertical-align: middle;
+ }
+
+ &.active {
+ background-color: $white-light;
+ border-bottom-color: $white-light;
+ }
+}
+
+.multi-file-tab-close {
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ width: 16px;
+ height: 16px;
+ padding: 0;
+ background: none;
+ border: 0;
+ border-radius: $border-radius-default;
+ color: $theme-gray-900;
+ transform: translateY(-50%);
+
+ svg {
+ position: relative;
+ top: -1px;
+ }
+
+ &:hover {
+ background-color: $theme-gray-200;
+ }
+
+ &:focus {
+ background-color: $blue-500;
+ color: $white-light;
+ outline: 0;
+
+ svg {
+ fill: currentColor;
+ }
+ }
+}
+
+.multi-file-edit-pane-content {
+ flex: 1;
+ height: 0;
+}
+
+.blob-editor-container {
+ flex: 1;
+ height: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ .vertical-center {
+ min-height: auto;
+ }
+
+ .monaco-editor .lines-content .cigr {
+ display: none;
+ }
+
+ .monaco-diff-editor.vs {
+ .editor.modified {
+ box-shadow: none;
+ }
+
+ .diagonal-fill {
+ display: none !important;
+ }
+
+ .diffOverview {
+ background-color: $white-light;
+ border-left: 1px solid $white-dark;
+ cursor: ns-resize;
+ }
+
+ .diffViewport {
+ display: none;
+ }
+
+ .char-insert {
+ background-color: $line-added-dark;
+ }
+
+ .char-delete {
+ background-color: $line-removed-dark;
+ }
+
+ .line-numbers {
+ color: $black-transparent;
+ }
+
+ .view-overlays {
+ .line-insert {
+ background-color: $line-added;
+ }
+
+ .line-delete {
+ background-color: $line-removed;
+ }
+ }
+
+ .margin {
+ background-color: $gray-light;
+ border-right: 1px solid $white-normal;
+
+ .line-insert {
+ border-right: 1px solid $line-added-dark;
+ }
+
+ .line-delete {
+ border-right: 1px solid $line-removed-dark;
+ }
+ }
+
+ .margin-view-overlays .insert-sign,
+ .margin-view-overlays .delete-sign {
+ opacity: 0.4;
+ }
+
+ .cursors-layer {
+ display: none;
+ }
+ }
+}
+
+.multi-file-editor-holder {
+ height: 100%;
+}
+
+.multi-file-editor-btn-group {
+ padding: $gl-bar-padding $gl-padding;
+ border-top: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+ background: $white-light;
+}
+
+.ide-status-bar {
+ padding: $gl-bar-padding $gl-padding;
+ background: $white-light;
+ display: flex;
+ justify-content: space-between;
+
+ svg {
+ vertical-align: middle;
+ }
+}
+
+// Not great, but this is to deal with our current output
+.multi-file-preview-holder {
+ height: 100%;
+ overflow: scroll;
+
+ .file-content.code {
+ display: flex;
+
+ i {
+ margin-left: -10px;
+ }
+ }
+
+ .line-numbers {
+ min-width: 50px;
+ }
+
+ .file-content,
+ .line-numbers,
+ .blob-content,
+ .code {
+ min-height: 100%;
+ }
+}
+
+.file-content.blob-no-preview {
+ a {
+ margin-left: auto;
+ margin-right: auto;
+ }
+}
+
+.multi-file-commit-panel {
+ display: flex;
+ position: relative;
+ flex-direction: column;
+ width: 340px;
+ padding: 0;
+ background-color: $gray-light;
+ padding-right: 3px;
+
+ .projects-sidebar {
+ display: flex;
+ flex-direction: column;
+
+ .context-header {
+ width: auto;
+ margin-right: 0;
+ }
+ }
+
+ .multi-file-commit-panel-inner {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ }
+
+ .multi-file-commit-panel-inner-scroll {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ overflow: auto;
+ }
+
+ &.is-collapsed {
+ width: 60px;
+
+ .multi-file-commit-list {
+ padding-top: $gl-padding;
+ overflow: hidden;
+ }
+
+ .multi-file-context-bar-icon {
+ align-items: center;
+
+ svg {
+ float: none;
+ margin: 0;
+ }
+ }
+ }
+
+ .branch-container {
+ border-left: 4px solid $indigo-700;
+ margin-bottom: $gl-bar-padding;
+ }
+
+ .branch-header {
+ background: $white-dark;
+ display: flex;
+ }
+
+ .branch-header-title {
+ flex: 1;
+ padding: $grid-size $gl-padding;
+ color: $indigo-700;
+ font-weight: $gl-font-weight-bold;
+
+ svg {
+ vertical-align: middle;
+ }
+ }
+
+ .branch-header-btns {
+ padding: $gl-vert-padding $gl-padding;
+ }
+
+ .left-collapse-btn {
+ display: none;
+ background: $gray-light;
+ text-align: left;
+ border-top: 1px solid $white-dark;
+
+ svg {
+ vertical-align: middle;
+ }
+ }
+}
+
+.multi-file-context-bar-icon {
+ padding: 10px;
+
+ svg {
+ margin-right: 10px;
+ float: left;
+ }
+}
+
+.multi-file-commit-panel-section {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+}
+
+.multi-file-commit-empty-state-container {
+ align-items: center;
+ justify-content: center;
+}
+
+.multi-file-commit-panel-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 12px;
+ 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;
+
+ svg {
+ margin-right: $gl-btn-padding;
+ }
+}
+
+.multi-file-commit-panel-collapse-btn {
+ border-left: 1px solid $white-dark;
+}
+
+.multi-file-commit-list {
+ flex: 1;
+ overflow: auto;
+ padding: $gl-padding 0;
+ min-height: 60px;
+}
+
+.multi-file-commit-list-item {
+ display: flex;
+ padding: 0;
+ align-items: center;
+
+ .multi-file-discard-btn {
+ display: none;
+ margin-left: auto;
+ color: $gl-link-color;
+ padding: 0 2px;
+
+ &:focus,
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ &:hover {
+ background: $white-normal;
+
+ .multi-file-discard-btn {
+ display: block;
+ }
+ }
+}
+
+.multi-file-addition {
+ fill: $green-500;
+}
+
+.multi-file-modified {
+ fill: $orange-500;
+}
+
+.multi-file-commit-list-collapsed {
+ display: flex;
+ flex-direction: column;
+
+ > svg {
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ .file-status-icon {
+ width: 10px;
+ height: 10px;
+ margin-left: 3px;
+ }
+}
+
+.multi-file-commit-list-path {
+ padding: $grid-size / 2;
+ padding-left: $gl-padding;
+ background: none;
+ border: 0;
+ text-align: left;
+ width: 100%;
+ min-width: 0;
+
+ svg {
+ min-width: 16px;
+ vertical-align: middle;
+ display: inline-block;
+ }
+
+ &:hover,
+ &:focus {
+ outline: 0;
+ }
+}
+
+.multi-file-commit-list-file-path {
+ @include str-truncated(100%);
+
+ &:hover {
+ text-decoration: underline;
+ }
+
+ &:active {
+ text-decoration: none;
+ }
+}
+
+.multi-file-commit-form {
+ padding: $gl-padding;
+ border-top: 1px solid $white-dark;
+
+ .btn {
+ font-size: $gl-font-size;
+ }
+}
+
+.multi-file-commit-message.form-control {
+ height: 160px;
+ resize: none;
+}
+
+.dirty-diff {
+ // !important need to override monaco inline style
+ width: 4px !important;
+ left: 0 !important;
+
+ &-modified {
+ background-color: $blue-500;
+ }
+
+ &-added {
+ background-color: $green-600;
+ }
+
+ &-removed {
+ height: 0 !important;
+ width: 0 !important;
+ bottom: -2px;
+ border-style: solid;
+ border-width: 5px;
+ border-color: transparent transparent transparent $red-500;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100px;
+ height: 1px;
+ background-color: rgba($red-500, 0.5);
+ }
+ }
+}
+
+.ide-loading {
+ display: flex;
+ height: 100vh;
+ align-items: center;
+ justify-content: center;
+}
+
+.ide-empty-state {
+ display: flex;
+ height: 100vh;
+ align-items: center;
+ justify-content: center;
+}
+
+.ide-new-btn {
+ .dropdown-toggle svg {
+ margin-top: -2px;
+ margin-bottom: 2px;
+ }
+
+ .dropdown-menu {
+ left: auto;
+ right: 0;
+
+ label {
+ font-weight: $gl-font-weight-normal;
+ padding: 5px 8px;
+ margin-bottom: 0;
+ }
+ }
+}
+
+.ide {
+ overflow: hidden;
+
+ &.nav-only {
+ .flash-container {
+ margin-top: $header-height;
+ margin-bottom: 0;
+ }
+
+ .alert-wrapper .flash-container .flash-alert:last-child,
+ .alert-wrapper .flash-container .flash-notice:last-child {
+ margin-bottom: 0;
+ }
+
+ .content-wrapper {
+ margin-top: $header-height;
+ padding-bottom: 0;
+ }
+
+ &.flash-shown {
+ .content-wrapper {
+ margin-top: 0;
+ }
+
+ .ide-view {
+ height: calc(100vh - #{$header-height + $flash-height});
+ }
+ }
+
+ .projects-sidebar {
+ .multi-file-commit-panel-inner-scroll {
+ flex: 1;
+ }
+ }
+ }
+}
+
+.with-performance-bar .ide.nav-only {
+ .flash-container {
+ margin-top: #{$header-height + $performance-bar-height};
+ }
+
+ .content-wrapper {
+ margin-top: #{$header-height + $performance-bar-height};
+ padding-bottom: 0;
+ }
+
+ .ide-view {
+ height: calc(100vh - #{$header-height + $performance-bar-height});
+ }
+
+ &.flash-shown {
+ .content-wrapper {
+ margin-top: 0;
+ }
+
+ .ide-view {
+ height: calc(
+ 100vh - #{$header-height + $performance-bar-height + $flash-height}
+ );
+ }
+ }
+}
+
+.dragHandle {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 3px;
+ background-color: $white-dark;
+
+ &.dragright {
+ right: 0;
+ }
+
+ &.dragleft {
+ left: 0;
+ }
+}
+
+.ide-commit-radios {
+ label {
+ font-weight: normal;
+ }
+
+ .help-block {
+ margin-top: 0;
+ line-height: 0;
+ }
+}
+
+.ide-commit-new-branch {
+ margin-left: 25px;
+}
+
+.ide-external-links {
+ p {
+ margin: 0;
+ }
+}
+
+.ide-sidebar-link {
+ padding: $gl-padding-8 $gl-padding;
+ background: $indigo-700;
+ color: $white-light;
+ text-decoration: none;
+ display: flex;
+ align-items: center;
+
+ &:focus,
+ &:hover {
+ color: $white-light;
+ text-decoration: underline;
+ background: $indigo-500;
+ }
+
+ &:active {
+ background: $indigo-800;
+ }
+}
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
index dd0b38970bd..ea302f17d16 100644
--- a/app/controllers/admin/appearances_controller.rb
+++ b/app/controllers/admin/appearances_controller.rb
@@ -50,9 +50,19 @@ class Admin::AppearancesController < Admin::ApplicationController
# Only allow a trusted parameter "white list" through.
def appearance_params
- params.require(:appearance).permit(
- :title, :description, :logo, :logo_cache, :header_logo, :header_logo_cache,
- :new_project_guidelines, :updated_by
- )
+ params.require(:appearance).permit(allowed_appearance_params)
+ end
+
+ def allowed_appearance_params
+ %i[
+ title
+ description
+ logo
+ logo_cache
+ header_logo
+ header_logo_cache
+ new_project_guidelines
+ updated_by
+ ]
end
end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 965cece600e..176679f0849 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -21,17 +21,13 @@ class Projects::BranchesController < Projects::ApplicationController
fetch_branches_by_mode
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
- @merged_branch_names =
- repository.merged_branch_names(@branches.map(&:name))
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- @max_commits = @branches.reduce(0) do |memo, branch|
- diverging_commit_counts = repository.diverging_commit_counts(branch)
- [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
- end
-
- render
+ @merged_branch_names = repository.merged_branch_names(@branches.map(&:name))
+ @max_commits = @branches.reduce(0) do |memo, branch|
+ diverging_commit_counts = repository.diverging_commit_counts(branch)
+ [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
end
+
+ render
end
format.json do
branches = BranchesFinder.new(@repository, params).execute
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 99790b8e7e8..516198b1b8a 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -112,7 +112,7 @@ class Projects::LabelsController < Projects::ApplicationController
begin
return render_404 unless promote_service.execute(@label)
- flash[:notice] = "#{@label.title} promoted to group label."
+ flash[:notice] = "#{@label.title} promoted to <a href=\"#{group_labels_path(@project.group)}\">group label</a>.".html_safe
respond_to do |format|
format.html do
redirect_to(project_labels_path(@project), status: 303)
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index cf84629fadc..e898136d203 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -74,9 +74,9 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def promote
- Milestones::PromoteService.new(project, current_user).execute(milestone)
+ promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone)
- flash[:notice] = "#{milestone.title} promoted to group milestone"
+ flash[:notice] = "#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, promoted_milestone.iid)}\">group milestone</a>.".html_safe
respond_to do |format|
format.html do
redirect_to project_milestones_path(project)
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index f14cb5f6a9f..a5ea9ff7ed7 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -46,6 +46,8 @@ class Projects::ServicesController < Projects::ApplicationController
else
{ error: true, message: 'Validations failed.', service_response: @service.errors.full_messages.join(',') }
end
+ rescue Gitlab::HTTP::BlockedUrlError => e
+ { error: true, message: 'Test failed.', service_response: e.message }
end
def success_message
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index c037de33c22..f48db024e3f 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -1,27 +1,27 @@
module AppearancesHelper
def brand_title
- brand_item&.title.presence || 'GitLab Community Edition'
+ current_appearance&.title.presence || 'GitLab Community Edition'
end
def brand_image
- image_tag(brand_item.logo) if brand_item&.logo?
+ image_tag(current_appearance.logo) if current_appearance&.logo?
end
def brand_text
- markdown_field(brand_item, :description)
+ markdown_field(current_appearance, :description)
end
def brand_new_project_guidelines
- markdown_field(brand_item, :new_project_guidelines)
+ markdown_field(current_appearance, :new_project_guidelines)
end
- def brand_item
+ def current_appearance
@appearance ||= Appearance.current
end
def brand_header_logo
- if brand_item&.header_logo?
- image_tag brand_item.header_logo
+ if current_appearance&.header_logo?
+ image_tag current_appearance.header_logo
else
render 'shared/logo.svg'
end
@@ -29,7 +29,7 @@ module AppearancesHelper
# Skip the 'GitLab' type logo when custom brand logo is set
def brand_header_logo_type
- unless brand_item&.header_logo?
+ unless current_appearance&.header_logo?
render 'shared/logo_type.svg'
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 701be97ee96..86ec500ceb3 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -285,6 +285,10 @@ module ApplicationHelper
class_names
end
+ # EE feature: System header and footer, unavailable in CE
+ def system_message_class
+ end
+
# Returns active css class when condition returns true
# otherwise returns nil.
#
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 4ddc1dbed49..c86a26ac30f 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -54,9 +54,9 @@ module EmailsHelper
end
def header_logo
- if brand_item && brand_item.header_logo?
+ if current_appearance&.header_logo?
image_tag(
- brand_item.header_logo,
+ current_appearance.header_logo,
style: 'height: 50px'
)
else
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 40ca666f1bf..9be93fa69ae 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -31,7 +31,7 @@ module NamespacesHelper
def namespace_icon(namespace, size = 40)
if namespace.is_a?(Group)
- group_icon(namespace)
+ group_icon_url(namespace)
else
avatar_icon_for_user(namespace.owner, size)
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 18b9bf214a3..a8397b03d63 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -39,7 +39,10 @@ module PageLayoutHelper
end
def favicon
- Rails.env.development? ? 'favicon-blue.ico' : 'favicon.ico'
+ return 'favicon-yellow.ico' if Gitlab::Utils.to_boolean(ENV['CANARY'])
+ return 'favicon-blue.ico' if Rails.env.development?
+
+ 'favicon.ico'
end
def page_image
diff --git a/app/models/ci/artifact_blob.rb b/app/models/ci/artifact_blob.rb
index ec56cc53aea..760f01f225b 100644
--- a/app/models/ci/artifact_blob.rb
+++ b/app/models/ci/artifact_blob.rb
@@ -36,16 +36,15 @@ module Ci
def external_url(project, job)
return unless external_link?(job)
- full_path_parts = project.full_path_components
- top_level_group = full_path_parts.shift
+ url_project_path = project.full_path.partition('/').last
artifact_path = [
- '-', *full_path_parts, '-',
+ '-', url_project_path, '-',
'jobs', job.id,
'artifacts', path
].join('/')
- "#{pages_config.protocol}://#{top_level_group}.#{pages_config.host}/#{artifact_path}"
+ "#{project.pages_group_url}/#{artifact_path}"
end
def external_link?(job)
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 08bb5915d10..18e96389199 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -6,6 +6,7 @@ module Ci
include ObjectStorage::BackgroundMove
include Presentable
include Importable
+ include Gitlab::Utils::StrongMemoize
MissingDependenciesError = Class.new(StandardError)
@@ -24,12 +25,18 @@ module Ci
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
- # The "environment" field for builds is a String, and is the unexpanded name
+ has_one :metadata, class_name: 'Ci::BuildMetadata'
+ delegate :timeout, to: :metadata, prefix: true, allow_nil: true
+
+ ##
+ # The "environment" field for builds is a String, and is the unexpanded name!
+ #
def persisted_environment
- @persisted_environment ||= Environment.find_by(
- name: expanded_environment_name,
- project: project
- )
+ return unless has_environment?
+
+ strong_memoize(:persisted_environment) do
+ Environment.find_by(name: expanded_environment_name, project: project)
+ end
end
serialize :options # rubocop:disable Cop/ActiveRecordSerialize
@@ -153,6 +160,14 @@ module Ci
before_transition any => [:running] do |build|
build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies')
end
+
+ before_transition pending: :running do |build|
+ build.ensure_metadata.update_timeout_state
+ end
+ end
+
+ def ensure_metadata
+ metadata || build_metadata(project: project)
end
def detailed_status(current_user)
@@ -200,7 +215,11 @@ module Ci
end
def expanded_environment_name
- ExpandVariables.expand(environment, simple_variables) if environment
+ return unless has_environment?
+
+ strong_memoize(:expanded_environment_name) do
+ ExpandVariables.expand(environment, simple_variables)
+ end
end
def has_environment?
@@ -231,10 +250,6 @@ module Ci
latest_builds.where('stage_idx < ?', stage_idx)
end
- def timeout
- project.build_timeout
- end
-
def triggered_by?(current_user)
user == current_user
end
@@ -250,31 +265,52 @@ module Ci
Gitlab::Utils.slugify(ref.to_s)
end
- # Variables whose value does not depend on environment
- def simple_variables
- variables(environment: nil)
- end
-
- # All variables, including those dependent on environment, which could
- # contain unexpanded variables.
- def variables(environment: persisted_environment)
- collection = Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ ##
+ # Variables in the environment name scope.
+ #
+ def scoped_variables(environment: expanded_environment_name)
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.concat(predefined_variables)
variables.concat(project.predefined_variables)
variables.concat(pipeline.predefined_variables)
variables.concat(runner.predefined_variables) if runner
- variables.concat(project.deployment_variables(environment: environment)) if has_environment?
+ variables.concat(project.deployment_variables(environment: environment)) if environment
variables.concat(yaml_variables)
variables.concat(user_variables)
- variables.concat(project.group.secret_variables_for(ref, project)) if project.group
- variables.concat(secret_variables(environment: environment))
+ variables.concat(secret_group_variables)
+ variables.concat(secret_project_variables(environment: environment))
variables.concat(trigger_request.user_variables) if trigger_request
variables.concat(pipeline.variables)
variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
- variables.concat(persisted_environment_variables) if environment
end
+ end
+
+ ##
+ # Variables that do not depend on the environment name.
+ #
+ def simple_variables
+ strong_memoize(:simple_variables) do
+ scoped_variables(environment: nil).to_runner_variables
+ end
+ end
- collection.to_runner_variables
+ ##
+ # All variables, including persisted environment variables.
+ #
+ def variables
+ Gitlab::Ci::Variables::Collection.new
+ .concat(persisted_variables)
+ .concat(scoped_variables)
+ .concat(persisted_environment_variables)
+ .to_runner_variables
+ end
+
+ ##
+ # Regular Ruby hash of scoped variables, without duplicates that are
+ # possible to be present in an array of hashes returned from `variables`.
+ #
+ def scoped_variables_hash
+ scoped_variables.to_hash
end
def features
@@ -451,9 +487,14 @@ module Ci
end
end
- def secret_variables(environment: persisted_environment)
+ def secret_group_variables
+ return [] unless project.group
+
+ project.group.secret_variables_for(ref, project)
+ end
+
+ def secret_project_variables(environment: persisted_environment)
project.secret_variables_for(ref: ref, environment: environment)
- .map(&:to_runner_variable)
end
def steps
@@ -550,6 +591,21 @@ module Ci
CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
+ def persisted_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ return variables unless persisted?
+
+ variables
+ .append(key: 'CI_JOB_ID', value: id.to_s)
+ .append(key: 'CI_JOB_TOKEN', value: token, public: false)
+ .append(key: 'CI_BUILD_ID', value: id.to_s)
+ .append(key: 'CI_BUILD_TOKEN', value: token, public: false)
+ .append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
+ .append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
+ .append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
+ end
+ end
+
def predefined_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI', value: 'true')
@@ -558,16 +614,11 @@ module Ci
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)
- variables.append(key: 'CI_JOB_ID', value: id.to_s)
variables.append(key: 'CI_JOB_NAME', value: name)
variables.append(key: 'CI_JOB_STAGE', value: stage)
- variables.append(key: 'CI_JOB_TOKEN', value: token, public: false)
variables.append(key: 'CI_COMMIT_SHA', value: sha)
variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug)
- variables.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
- variables.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
- variables.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
@@ -575,23 +626,8 @@ module Ci
end
end
- def persisted_environment_variables
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- return variables unless persisted_environment
-
- variables.concat(persisted_environment.predefined_variables)
-
- # Here we're passing unexpanded environment_url for runner to expand,
- # and we need to make sure that CI_ENVIRONMENT_NAME and
- # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
- variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
- end
- end
-
def legacy_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables.append(key: 'CI_BUILD_ID', value: id.to_s)
- variables.append(key: 'CI_BUILD_TOKEN', value: token, public: false)
variables.append(key: 'CI_BUILD_REF', value: sha)
variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
variables.append(key: 'CI_BUILD_REF_NAME', value: ref)
@@ -604,6 +640,19 @@ module Ci
end
end
+ def persisted_environment_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ return variables unless persisted? && persisted_environment.present?
+
+ variables.concat(persisted_environment.predefined_variables)
+
+ # Here we're passing unexpanded environment_url for runner to expand,
+ # and we need to make sure that CI_ENVIRONMENT_NAME and
+ # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
+ variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
+ end
+ end
+
def environment_url
options&.dig(:environment, :url) || persisted_environment&.external_url
end
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
new file mode 100644
index 00000000000..96762f8845c
--- /dev/null
+++ b/app/models/ci/build_metadata.rb
@@ -0,0 +1,35 @@
+module Ci
+ # The purpose of this class is to store Build related data that can be disposed.
+ # Data that should be persisted forever, should be stored with Ci::Build model.
+ class BuildMetadata < ActiveRecord::Base
+ extend Gitlab::Ci::Model
+ include Presentable
+ include ChronicDurationAttribute
+
+ self.table_name = 'ci_builds_metadata'
+
+ belongs_to :build, class_name: 'Ci::Build'
+ belongs_to :project
+
+ validates :build, presence: true
+ validates :project, presence: true
+
+ chronic_duration_attr_reader :timeout_human_readable, :timeout
+
+ enum timeout_source: {
+ unknown_timeout_source: 1,
+ project_timeout_source: 2,
+ runner_timeout_source: 3
+ }
+
+ def update_timeout_state
+ return unless build.runner.present?
+
+ project_timeout = project&.build_timeout
+ timeout = [project_timeout, build.runner.maximum_timeout].compact.min
+ timeout_source = timeout < project_timeout ? :runner_timeout_source : :project_timeout_source
+
+ update(timeout: timeout, timeout_source: timeout_source)
+ end
+ end
+end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 7173f88f1c7..5a4c56ec0dc 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -3,12 +3,13 @@ module Ci
extend Gitlab::Ci::Model
include Gitlab::SQL::Pattern
include RedisCacheable
+ include ChronicDurationAttribute
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
ONLINE_CONTACT_TIMEOUT = 1.hour
UPDATE_DB_RUNNER_INFO_EVERY = 40.minutes
AVAILABLE_SCOPES = %w[specific shared active paused online].freeze
- FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level].freeze
+ FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
has_many :builds
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -51,6 +52,12 @@ module Ci
cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address
+ chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout
+
+ validates :maximum_timeout, allow_nil: true,
+ numericality: { greater_than_or_equal_to: 600,
+ message: 'needs to be at least 10 minutes' }
+
# Searches for runners matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index bfdfc5ae6fe..77947d515c1 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -51,6 +51,10 @@ module Clusters
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
+ scope :user_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:user]) }
+ scope :gcp_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:gcp]) }
+ scope :gcp_installed, -> { gcp_provided.includes(:provider_gcp).where(cluster_providers_gcp: { status: ::Clusters::Providers::Gcp.state_machines[:status].states[:created].value }) }
+
scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
def status_name
diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb
index 7b7c8eac773..8f3eb75bfa9 100644
--- a/app/models/clusters/concerns/application_status.rb
+++ b/app/models/clusters/concerns/application_status.rb
@@ -4,6 +4,8 @@ module Clusters
extend ActiveSupport::Concern
included do
+ scope :installed, -> { where(status: self.state_machines[:status].states[:installed].value) }
+
state_machine :status, initial: :not_installable do
state :not_installable, value: -2
state :errored, value: -1
diff --git a/app/models/concerns/chronic_duration_attribute.rb b/app/models/concerns/chronic_duration_attribute.rb
new file mode 100644
index 00000000000..fa1eafb1d7a
--- /dev/null
+++ b/app/models/concerns/chronic_duration_attribute.rb
@@ -0,0 +1,39 @@
+module ChronicDurationAttribute
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def chronic_duration_attr_reader(virtual_attribute, source_attribute)
+ define_method(virtual_attribute) do
+ chronic_duration_attributes[virtual_attribute] || output_chronic_duration_attribute(source_attribute)
+ end
+ end
+
+ def chronic_duration_attr_writer(virtual_attribute, source_attribute)
+ chronic_duration_attr_reader(virtual_attribute, source_attribute)
+
+ define_method("#{virtual_attribute}=") do |value|
+ chronic_duration_attributes[virtual_attribute] = value.presence || ''
+
+ begin
+ new_value = ChronicDuration.parse(value).to_i if value.present?
+ assign_attributes(source_attribute => new_value)
+ rescue ChronicDuration::DurationParseError
+ # ignore error as it will be caught by validation
+ end
+ end
+
+ validates virtual_attribute, allow_nil: true, duration: true
+ end
+
+ alias_method :chronic_duration_attr, :chronic_duration_attr_writer
+ end
+
+ def chronic_duration_attributes
+ @chronic_duration_attributes ||= {}
+ end
+
+ def output_chronic_duration_attribute(source_attribute)
+ value = attributes[source_attribute.to_s]
+ ChronicDuration.output(value, format: :short) if value
+ end
+end
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index c2e0a5fa126..89a74b7dcb1 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -27,6 +27,10 @@ class DeployKey < Key
self.private?
end
+ def user
+ super || User.ghost
+ end
+
def has_access_to?(project)
deploy_keys_project_for(project).present?
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 7bfc45c1f43..6a94d60c828 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -23,6 +23,7 @@ class Issue < ActiveRecord::Base
belongs_to :project
belongs_to :moved_to, class_name: 'Issue'
+ belongs_to :closed_by, class_name: 'User'
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
@@ -78,6 +79,11 @@ class Issue < ActiveRecord::Base
before_transition any => :closed do |issue|
issue.closed_at = Time.zone.now
end
+
+ before_transition closed: :opened do |issue|
+ issue.closed_at = nil
+ issue.closed_by = nil
+ end
end
class << self
diff --git a/app/models/project.rb b/app/models/project.rb
index 6a420663644..b343786d2c9 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1346,20 +1346,19 @@ class Project < ActiveRecord::Base
Dir.exist?(public_pages_path)
end
- def pages_url
- subdomain, _, url_path = full_path.partition('/')
-
- # The hostname always needs to be in downcased
- # All web servers convert hostname to lowercase
- host = "#{subdomain}.#{Settings.pages.host}".downcase
-
+ def pages_group_url
# The host in URL always needs to be downcased
- url = Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
- "#{prefix}#{subdomain}."
+ Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
+ "#{prefix}#{pages_subdomain}."
end.downcase
+ end
+
+ def pages_url
+ url = pages_group_url
+ url_path = full_path.partition('/').last
# If the project path is the same as host, we serve it as group page
- return url if host == url_path
+ return url if url == "#{Settings.pages.protocol}://#{url_path}"
"#{url}/#{url_path}"
end
@@ -1545,8 +1544,8 @@ class Project < ActiveRecord::Base
@errors = original_errors
end
- def add_export_job(current_user:, params: {})
- job_id = ProjectExportWorker.perform_async(current_user.id, self.id, params)
+ def add_export_job(current_user:, after_export_strategy: nil, params: {})
+ job_id = ProjectExportWorker.perform_async(current_user.id, self.id, after_export_strategy, params)
if job_id
Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}"
@@ -1572,6 +1571,8 @@ class Project < ActiveRecord::Base
def export_status
if export_in_progress?
:started
+ elsif after_export_in_progress?
+ :after_export_action
elsif export_project_path
:finished
else
@@ -1583,12 +1584,22 @@ class Project < ActiveRecord::Base
import_export_shared.active_export_count > 0
end
+ def after_export_in_progress?
+ import_export_shared.after_export_in_progress?
+ end
+
def remove_exports
return nil unless export_path.present?
FileUtils.rm_rf(export_path)
end
+ def remove_exported_project_file
+ return unless export_project_path.present?
+
+ FileUtils.rm_f(export_project_path)
+ end
+
def full_path_slug
Gitlab::Utils.slugify(full_path.to_s)
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 2ba1c6cb8c9..fd1afafe4df 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -249,13 +249,13 @@ class Repository
end
def diverging_commit_counts(branch)
- root_ref_hash = raw_repository.commit(root_ref).id
+ @root_ref_hash ||= raw_repository.commit(root_ref).id
cache.fetch(:"diverging_commit_counts_#{branch.name}") do
# Rugged seems to throw a `ReferenceError` when given branch_names rather
# than SHA-1 hashes
number_commits_behind, number_commits_ahead =
raw_repository.count_commits_between(
- root_ref_hash,
+ @root_ref_hash,
branch.dereferenced_target.sha,
left_right: true,
max_count: MAX_DIVERGING_COUNT)
diff --git a/app/models/service.rb b/app/models/service.rb
index 1dcb79157a2..7424cef0fc0 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -273,6 +273,7 @@ class Service < ActiveRecord::Base
def self.build_from_template(project_id, template)
service = template.dup
+ service.active = false unless service.valid?
service.template = false
service.project_id = project_id
service
diff --git a/app/models/user.rb b/app/models/user.rb
index 187878f4fb5..f934b654225 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -82,11 +82,8 @@ class User < ActiveRecord::Base
has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent
# Profile
- has_many :keys, -> do
- type = Key.arel_table[:type]
- where(type.not_eq('DeployKey').or(type.eq(nil)))
- end, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :keys, -> { where(type: ['Key', nil]) }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
has_many :gpg_keys
has_many :emails, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
diff --git a/app/presenters/ci/build_metadata_presenter.rb b/app/presenters/ci/build_metadata_presenter.rb
new file mode 100644
index 00000000000..5048f967ea8
--- /dev/null
+++ b/app/presenters/ci/build_metadata_presenter.rb
@@ -0,0 +1,18 @@
+module Ci
+ class BuildMetadataPresenter < Gitlab::View::Presenter::Delegated
+ TIMEOUT_SOURCES = {
+ unknown_timeout_source: nil,
+ project_timeout_source: 'project',
+ runner_timeout_source: 'runner'
+ }.freeze
+
+ presents :metadata
+
+ def timeout_source
+ return unless metadata.timeout_source?
+
+ TIMEOUT_SOURCES[metadata.timeout_source.to_sym] ||
+ metadata.timeout_source
+ end
+ end
+end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 69d46f5ec14..ca4480fe2b1 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -5,6 +5,8 @@ class BuildDetailsEntity < JobEntity
expose :runner, using: RunnerEntity
expose :pipeline, using: PipelineEntity
+ expose :metadata, using: BuildMetadataEntity
+
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
erase_project_job_path(project, build)
diff --git a/app/serializers/build_metadata_entity.rb b/app/serializers/build_metadata_entity.rb
new file mode 100644
index 00000000000..39f429aa6c3
--- /dev/null
+++ b/app/serializers/build_metadata_entity.rb
@@ -0,0 +1,9 @@
+class BuildMetadataEntity < Grape::Entity
+ expose :timeout_human_readable do |metadata|
+ metadata.timeout_human_readable unless metadata.timeout.nil?
+ end
+
+ expose :timeout_source do |metadata|
+ metadata.present.timeout_source
+ end
+end
diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb
index 3e40ecf1c1c..a7c2e21e92b 100644
--- a/app/serializers/status_entity.rb
+++ b/app/serializers/status_entity.rb
@@ -7,8 +7,14 @@ class StatusEntity < Grape::Entity
expose :details_path
expose :favicon do |status|
- dir = 'ci_favicons'
- dir = File.join(dir, 'dev') if Rails.env.development?
+ dir =
+ if Gitlab::Utils.to_boolean(ENV['CANARY'])
+ File.join('ci_favicons', 'canary')
+ elsif Rails.env.development?
+ File.join('ci_favicons', 'dev')
+ else
+ 'ci_favicons'
+ end
ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
end
diff --git a/app/services/boards/list_service.rb b/app/services/boards/list_service.rb
index 6d0dd0a9f99..9269b8d2620 100644
--- a/app/services/boards/list_service.rb
+++ b/app/services/boards/list_service.rb
@@ -2,11 +2,15 @@ module Boards
class ListService < Boards::BaseService
def execute
create_board! if parent.boards.empty?
- parent.boards
+ boards
end
private
+ def boards
+ parent.boards
+ end
+
def create_board!
Boards::CreateService.new(parent, current_user).execute
end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 0c5cf2c62ad..fee5bc38f7b 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -23,6 +23,7 @@ module Issues
end
if project.issues_enabled? && issue.close
+ issue.update(closed_by: current_user)
event_service.close_issue(issue, current_user)
create_note(issue, commit) if system_note
notification_service.close_issue(issue, current_user) if notifications
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 7fa1387084c..633e2c8236c 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -90,9 +90,6 @@ module Projects
unless @project.gitlab_project_import?
@project.write_repository_config
@project.create_wiki unless skip_wiki?
- create_services_from_active_templates(@project)
-
- @project.create_labels
end
event_service.create_project(@project, current_user)
@@ -121,21 +118,29 @@ module Projects
Project.transaction do
@project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data
- if @project.save && !@project.import?
- raise 'Failed to create repository' unless @project.create_repository
+ if @project.save
+ unless @project.gitlab_project_import?
+ create_services_from_active_templates(@project)
+ @project.create_labels
+ end
+
+ unless @project.import?
+ raise 'Failed to create repository' unless @project.create_repository
+ end
end
end
end
def fail(error:)
message = "Unable to save project. Error: #{error}"
- message << "Project ID: #{@project.id}" if @project && @project.id
+ log_message = message.dup
- Rails.logger.error(message)
+ log_message << " Project ID: #{@project.id}" if @project&.id
+ Rails.logger.error(log_message)
- if @project && @project.import?
+ if @project
@project.errors.add(:base, message)
- @project.mark_import_as_failed(message)
+ @project.mark_import_as_failed(message) if @project.import?
end
@project
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index d16aa3de639..402cddd3ec1 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -1,22 +1,36 @@
module Projects
module ImportExport
class ExportService < BaseService
- def execute(_options = {})
+ def execute(after_export_strategy = nil, options = {})
@shared = project.import_export_shared
- save_all
+
+ save_all!
+ execute_after_export_action(after_export_strategy)
end
private
- def save_all
- if [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
+ def execute_after_export_action(after_export_strategy)
+ return unless after_export_strategy
+
+ unless after_export_strategy.execute(current_user, project)
+ cleanup_and_notify_error
+ end
+ end
+
+ def save_all!
+ if save_services
Gitlab::ImportExport::Saver.save(project: project, shared: @shared)
notify_success
else
- cleanup_and_notify
+ cleanup_and_notify_error!
end
end
+ def save_services
+ [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
+ end
+
def version_saver
Gitlab::ImportExport::VersionSaver.new(shared: @shared)
end
@@ -41,19 +55,22 @@ module Projects
Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
end
- def cleanup_and_notify
+ def cleanup_and_notify_error
Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}")
FileUtils.rm_rf(@shared.export_path)
notify_error
+ end
+
+ def cleanup_and_notify_error!
+ cleanup_and_notify_error
+
raise Gitlab::ImportExport::Error.new(@shared.errors.join(', '))
end
def notify_success
Rails.logger.info("Import/Export - Project #{project.name} with ID: #{project.id} successfully exported")
-
- notification_service.project_exported(@project, @current_user)
end
def notify_error
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index a34024f4f80..a3828acc50b 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -28,7 +28,11 @@ module Projects
def add_repository_to_project
if project.external_import? && !unknown_url?
- raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url, valid_ports: Project::VALID_IMPORT_PORTS)
+ begin
+ Gitlab::UrlBlocker.validate!(project.import_url, valid_ports: Project::VALID_IMPORT_PORTS)
+ rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ raise Error, "Blocked import URL: #{e.message}"
+ end
end
# We should skip the repository for a GitHub import or GitLab project import,
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 5bf8208e035..9c8877be14e 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -178,6 +178,9 @@ module Projects
def latest_sha
project.commit(build.ref).try(:sha).to_s
+ ensure
+ # Close any file descriptors that were opened and free libgit2 buffers
+ project.cleanup
end
def sha
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index 30cc4425ae4..4028b052768 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -228,16 +228,9 @@ module ObjectStorage
raise 'Failed to update object store' unless updated
end
- def use_file
- if file_storage?
- return yield path
- end
-
- begin
- cache_stored_file!
- yield cache_path
- ensure
- cache_storage.delete_dir!(cache_path(nil))
+ def use_file(&blk)
+ with_exclusive_lease do
+ unsafe_use_file(&blk)
end
end
@@ -247,12 +240,9 @@ module ObjectStorage
# new_store: Enum (Store::LOCAL, Store::REMOTE)
#
def migrate!(new_store)
- uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain
- raise 'Already running' unless uuid
-
- unsafe_migrate!(new_store)
- ensure
- Gitlab::ExclusiveLease.cancel(exclusive_lease_key, uuid)
+ with_exclusive_lease do
+ unsafe_migrate!(new_store)
+ end
end
def schedule_background_upload(*args)
@@ -384,6 +374,15 @@ module ObjectStorage
"object_storage_migrate:#{model.class}:#{model.id}"
end
+ def with_exclusive_lease
+ uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain
+ raise 'exclusive lease already taken' unless uuid
+
+ yield uuid
+ ensure
+ Gitlab::ExclusiveLease.cancel(exclusive_lease_key, uuid)
+ end
+
#
# Move the file to another store
#
@@ -418,4 +417,18 @@ module ObjectStorage
raise e
end
end
+
+ def unsafe_use_file
+ if file_storage?
+ return yield path
+ end
+
+ begin
+ cache_stored_file!
+ yield cache_path
+ ensure
+ FileUtils.rm_f(cache_path)
+ cache_storage.delete_dir!(cache_path(nil))
+ end
+ end
end
diff --git a/app/validators/certificate_fingerprint_validator.rb b/app/validators/certificate_fingerprint_validator.rb
new file mode 100644
index 00000000000..17df756183a
--- /dev/null
+++ b/app/validators/certificate_fingerprint_validator.rb
@@ -0,0 +1,9 @@
+class CertificateFingerprintValidator < ActiveModel::EachValidator
+ FINGERPRINT_PATTERN = /\A([a-zA-Z0-9]{2}[\s\-:]?){16,}\z/.freeze
+
+ def validate_each(record, attribute, value)
+ unless value.try(:match, FINGERPRINT_PATTERN)
+ record.errors.add(attribute, "must be a hash containing only letters, numbers, spaces, : and -")
+ end
+ end
+end
diff --git a/app/validators/importable_url_validator.rb b/app/validators/importable_url_validator.rb
index 3ec1594e202..612d3c71913 100644
--- a/app/validators/importable_url_validator.rb
+++ b/app/validators/importable_url_validator.rb
@@ -4,8 +4,8 @@
# protect against Server-side Request Forgery (SSRF).
class ImportableUrlValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
- if Gitlab::UrlBlocker.blocked_url?(value, valid_ports: Project::VALID_IMPORT_PORTS)
- record.errors.add(attribute, "imports are not allowed from that URL")
- end
+ Gitlab::UrlBlocker.validate!(value, valid_ports: Project::VALID_IMPORT_PORTS)
+ rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ record.errors.add(attribute, "is blocked: #{e.message}")
end
end
diff --git a/app/validators/top_level_group_validator.rb b/app/validators/top_level_group_validator.rb
new file mode 100644
index 00000000000..7e2e735e0cf
--- /dev/null
+++ b/app/validators/top_level_group_validator.rb
@@ -0,0 +1,7 @@
+class TopLevelGroupValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ if value&.subgroup?
+ record.errors.add(attribute, "must be a top level Group")
+ end
+ end
+end
diff --git a/app/views/admin/application_settings/_background_jobs.html.haml b/app/views/admin/application_settings/_background_jobs.html.haml
new file mode 100644
index 00000000000..8198a822a10
--- /dev/null
+++ b/app/views/admin/application_settings/_background_jobs.html.haml
@@ -0,0 +1,30 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ %p
+ These settings require a
+ = link_to 'restart', help_page_path('administration/restart_gitlab')
+ to take effect.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :sidekiq_throttling_enabled do
+ = f.check_box :sidekiq_throttling_enabled
+ Enable Sidekiq Job Throttling
+ .help-block
+ Limit the amount of resources slow running jobs are assigned.
+ .form-group
+ = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
+ .help-block
+ Choose which queues you wish to throttle.
+ .form-group
+ = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
+ .help-block
+ The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 636535fba84..309c7ed5dfa 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -10,111 +10,6 @@
= f.number_field :container_registry_token_expire_delay, class: 'form-control'
%fieldset
- %legend Profiling - Performance Bar
- %p
- Enable the Performance Bar for a given group.
- = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :performance_bar_enabled do
- = f.check_box :performance_bar_enabled
- Enable the Performance Bar
- .form-group
- = f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
-
- %fieldset
- %legend Background Jobs
- %p
- These settings require a
- = link_to 'restart', help_page_path('administration/restart_gitlab')
- to take effect.
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :sidekiq_throttling_enabled do
- = f.check_box :sidekiq_throttling_enabled
- Enable Sidekiq Job Throttling
- .help-block
- Limit the amount of resources slow running jobs are assigned.
- .form-group
- = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2'
- .col-sm-10
- = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
- .help-block
- Choose which queues you wish to throttle.
- .form-group
- = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
- .help-block
- The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
-
- %fieldset
- %legend Spam and Anti-bot Protection
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :recaptcha_enabled do
- = f.check_box :recaptcha_enabled
- Enable reCAPTCHA
- %span.help-block#recaptcha_help_block Helps prevent bots from creating accounts
-
- .form-group
- = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :recaptcha_site_key, class: 'form-control'
- .help-block
- Generate site and private keys at
- %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
-
- .form-group
- = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :recaptcha_private_key, class: 'form-control'
-
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :akismet_enabled do
- = f.check_box :akismet_enabled
- Enable Akismet
- %span.help-block#akismet_help_block Helps prevent bots from creating issues
-
- .form-group
- = f.label :akismet_api_key, 'Akismet API Key', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :akismet_api_key, class: 'form-control'
- .help-block
- Generate API key at
- %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
-
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :unique_ips_limit_enabled do
- = f.check_box :unique_ips_limit_enabled
- Limit sign in from multiple ips
- %span.help-block#unique_ip_help_block
- Helps prevent malicious users hide their activity
-
- .form-group
- = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :unique_ips_limit_per_user, class: 'form-control'
- .help-block
- Maximum number of unique IPs per user
-
- .form-group
- = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :unique_ips_limit_time_window, class: 'form-control'
- .help-block
- How many seconds an IP will be counted towards the limit
-
- %fieldset
%legend Abuse reports
.form-group
= f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2'
diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml
new file mode 100644
index 00000000000..5344f030c97
--- /dev/null
+++ b/app/views/admin/application_settings/_performance_bar.html.haml
@@ -0,0 +1,16 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :performance_bar_enabled do
+ = f.check_box :performance_bar_enabled
+ Enable the Performance Bar
+ .form-group
+ = f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml
new file mode 100644
index 00000000000..25e89097dfe
--- /dev/null
+++ b/app/views/admin/application_settings/_spam.html.haml
@@ -0,0 +1,65 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :recaptcha_enabled do
+ = f.check_box :recaptcha_enabled
+ Enable reCAPTCHA
+ %span.help-block#recaptcha_help_block Helps prevent bots from creating accounts
+
+ .form-group
+ = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :recaptcha_site_key, class: 'form-control'
+ .help-block
+ Generate site and private keys at
+ %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
+
+ .form-group
+ = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :recaptcha_private_key, class: 'form-control'
+
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :akismet_enabled do
+ = f.check_box :akismet_enabled
+ Enable Akismet
+ %span.help-block#akismet_help_block Helps prevent bots from creating issues
+
+ .form-group
+ = f.label :akismet_api_key, 'Akismet API Key', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :akismet_api_key, class: 'form-control'
+ .help-block
+ Generate API key at
+ %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
+
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :unique_ips_limit_enabled do
+ = f.check_box :unique_ips_limit_enabled
+ Limit sign in from multiple ips
+ %span.help-block#unique_ip_help_block
+ Helps prevent malicious users hide their activity
+
+ .form-group
+ = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :unique_ips_limit_per_user, class: 'form-control'
+ .help-block
+ Maximum number of unique IPs per user
+
+ .form-group
+ = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :unique_ips_limit_time_window, class: 'form-control'
+ .help-block
+ How many seconds an IP will be counted towards the limit
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index 17f2f37d24e..d0e612e62e5 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -7,7 +7,7 @@
.settings-header
%h4
= _('Visibility and access controls')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Set default and restrict visibility levels. Configure import sources and git access protocol.')
@@ -18,7 +18,7 @@
.settings-header
%h4
= _('Account and limit settings')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Session expiration, projects limit and attachment size.')
@@ -29,7 +29,7 @@
.settings-header
%h4
= _('Sign-up restrictions')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Configure the way a user creates a new account.')
@@ -40,7 +40,7 @@
.settings-header
%h4
= _('Sign-in restrictions')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Set requirements for a user to sign-in. Enable mandatory two-factor authentication.')
@@ -51,7 +51,7 @@
.settings-header
%h4
= _('Help page')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Help page text and support page url.')
@@ -62,7 +62,7 @@
.settings-header
%h4
= _('Pages')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Size and domain settings for static websites')
@@ -73,7 +73,7 @@
.settings-header
%h4
= _('Continuous Integration and Deployment')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Auto DevOps, runners amd job artifacts')
@@ -84,7 +84,7 @@
.settings-header
%h4
= _('Metrics - Influx')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Enable and configure InfluxDB metrics.')
@@ -95,12 +95,46 @@
.settings-header
%h4
= _('Metrics - Prometheus')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Enable and configure Prometheus metrics.')
.settings-content
= render 'prometheus'
+%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Profiling - Performance bar')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Enable the Performance Bar for a given group.')
+ = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
+ .settings-content
+ = render 'performance_bar'
+
+%section.settings.as-background.no-animate#js-background-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Background jobs')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Configure Sidekiq job throttling.')
+ .settings-content
+ = render 'background_jobs'
+
+%section.settings.as-spam.no-animate#js-spam-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Spam and Anti-bot Protection')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Enable reCAPTCHA or Akismet and set IP limits.')
+ .settings-content
+ = render 'spam'
+
.prepend-top-20
= render 'form'
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index 5d4229c80af..440623b34f5 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -43,7 +43,5 @@
%span.toggle-icon
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
- -# EE-specific start
- -# EE-specific end
%button.js-row-remove-button.ci-variable-row-remove-button{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') }
= icon('minus-circle')
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 7f9486d08d9..8e1dea4afc1 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- breadcrumb_title "Details"
+- breadcrumb_title _("Details")
- can_create_subgroups = can?(current_user, :create_subgroup, @group)
= content_for :meta_tags do
diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml
index 54ef51b30e3..c63cf2b31cb 100644
--- a/app/views/import/github/new.html.haml
+++ b/app/views/import/github/new.html.haml
@@ -22,9 +22,6 @@
= text_field_tag :personal_access_token, '', class: 'form-control', placeholder: _('Personal Access Token'), size: 40
= submit_tag _('List your GitHub repositories'), class: 'btn btn-success'
- -# EE-specific start
- -# EE-specific end
-
- unless github_import_configured?
%hr
%p
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 257f7326409..6513b719199 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -1,5 +1,5 @@
!!! 5
-%html.devise-layout-html
+%html.devise-layout-html{ class: system_message_class }
= render "layouts/head"
%body.ui_indigo.login-page.application.navless{ data: { page: body_data_page } }
.page-wrap
@@ -16,7 +16,7 @@
%h1
= brand_title
= brand_image
- - if brand_item&.description?
+ - if current_appearance&.description?
= brand_text
- else
%h3 Open source software to collaborate on code
diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml
index 8718bb3db1a..adf90cb7667 100644
--- a/app/views/layouts/devise_empty.html.haml
+++ b/app/views/layouts/devise_empty.html.haml
@@ -1,5 +1,5 @@
!!! 5
-%html{ lang: "en" }
+%html{ lang: "en", class: system_message_class }
= render "layouts/head"
%body.ui_indigo.login-page.application.navless
= render "layouts/header/empty"
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 059571f795f..5c90d13420f 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -80,14 +80,6 @@
= link_to charts_project_graph_path(@project, current_ref) do
#{ _('Charts') }
- - if project_nav_tab? :container_registry
- = nav_link(controller: %w[projects/registry/repositories]) do
- = link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
- .nav-icon-container
- = sprite_icon('disk')
- %span.nav-item-name
- Registry
-
- if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
= link_to project_issues_path(@project), class: 'shortcuts-issues' do
@@ -231,6 +223,14 @@
%span
Charts
+ - if project_nav_tab? :container_registry
+ = nav_link(controller: %w[projects/registry/repositories]) do
+ = link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
+ .nav-icon-container
+ = sprite_icon('disk')
+ %span.nav-item-name
+ Registry
+
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
= link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index 5dfe973f33c..825bfd0707f 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -7,7 +7,7 @@
.settings-header
%h4
Export project
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index 2ee0eafcf1a..4c510293204 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -31,7 +31,7 @@
%section.settings#js-cluster-details{ class: ('expanded' if expanded) }
.settings-header
%h4= s_('ClusterIntegration|Kubernetes cluster details')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster')
.settings-content
@@ -43,7 +43,7 @@
%section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
%h4= _('Advanced settings')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p= s_("ClusterIntegration|Advanced options on this Kubernetes cluster's integration")
.settings-content
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
index 75dd4c9ae15..7dd8dc28e5b 100644
--- a/app/views/projects/deploy_keys/_index.html.haml
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -3,7 +3,7 @@
.settings-header
%h4
Deploy Keys
- %button.btn.js-settings-toggle.qa-expand-deploy-keys
+ %button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index a96485ab155..99eeb9551e3 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -8,7 +8,7 @@
.settings-header
%h4
General project settings
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Update your project name, description, avatar, and other general settings.
@@ -64,7 +64,7 @@
.settings-header
%h4
Permissions
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Enable or disable certain project features and choose access levels.
@@ -79,7 +79,7 @@
.settings-header
%h4
Merge request settings
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Customize your merge request restrictions.
@@ -94,7 +94,7 @@
.settings-header
%h4
Advanced settings
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project.
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 8a36fada389..b15fe514a08 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- breadcrumb_title "Details"
+- breadcrumb_title _("Details")
= render partial: 'flash_messages', locals: { project: @project }
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index 849c273db8c..fa27ded7cc2 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -111,4 +111,4 @@
.js-build-options{ data: javascript_build_options }
-#js-job-details-vue{ data: { endpoint: project_job_path(@project, @build, format: :json) } }
+#js-job-details-vue{ data: { endpoint: project_job_path(@project, @build, format: :json), runner_help_url: help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner') } }
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index b423888c875..5ec219fdf00 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -30,6 +30,7 @@
%button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal',
target: '#promote-milestone-modal',
milestone_title: @milestone.title,
+ group_name: @project.group.name,
url: promote_project_milestone_path(@milestone.project, @milestone),
container: 'body' },
disabled: true,
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 8cdb0a6aff4..b66e0559603 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -18,8 +18,6 @@
= _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link }
%p
= _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.')
- -# EE-specific start
- -# EE-specific end
.md
= brand_new_project_guidelines
%p
@@ -43,8 +41,6 @@
%a{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' }
%span.hidden-xs Import project
%span.visible-xs Import
- -# EE-specific start
- -# EE-specific end
.tab-content.gitlab-tab-content
.tab-pane{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' }
@@ -110,10 +106,6 @@
= render "shared/import_form", f: f
= render 'new_project_fields', f: f, project_name_id: "import-url-name"
-
- -# EE-specific start
- -# EE-specific end
-
.save-project-loader.hide
.center
%h2
diff --git a/app/views/projects/no_repo.html.haml b/app/views/projects/no_repo.html.haml
index ba5845877e5..14d880028c7 100644
--- a/app/views/projects/no_repo.html.haml
+++ b/app/views/projects/no_repo.html.haml
@@ -1,3 +1,5 @@
+- breadcrumb_title _("Details")
+
%h2
%i.fa.fa-warning
#{ _('No repository') }
@@ -10,7 +12,7 @@
.no-repo-actions
= link_to project_repository_path(@project), method: :post, class: 'btn btn-primary' do
- #{ _('Create empty bare repository') }
+ #{ _('Create empty repository') }
%strong.prepend-left-10.append-right-10 or
@@ -19,4 +21,4 @@
- if can? current_user, :remove_project, @project
.prepend-top-20
- = link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
+ = link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove pull-right"
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml
index e662b877fbb..55d87c35a80 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
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Keep stable branches secure and force developers to use merge requests.
diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml
index 24baf1cfc89..c33723d8072 100644
--- a/app/views/projects/protected_tags/shared/_index.html.haml
+++ b/app/views/projects/protected_tags/shared/_index.html.haml
@@ -4,7 +4,7 @@
.settings-header
%h4
Protected Tags
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Limit access to creating and updating tags.
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
index 49c90869146..6a681736b6f 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/projects/runners/_form.html.haml
@@ -40,6 +40,12 @@
.col-sm-10
= f.text_field :description, class: 'form-control'
.form-group
+ = label_tag :maximum_timeout_human_readable, class: 'control-label' do
+ Maximum job timeout
+ .col-sm-10
+ = f.text_field :maximum_timeout_human_readable, class: 'form-control'
+ .help-block This timeout will take precedence when lower than Project-defined timeout
+ .form-group
= label_tag :tag_list, class: 'control-label' do
Tags
.col-sm-10
diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml
index 4e57f5f844d..f33e7e25b68 100644
--- a/app/views/projects/runners/show.html.haml
+++ b/app/views/projects/runners/show.html.haml
@@ -56,6 +56,9 @@
%td Description
%td= @runner.description
%tr
+ %td Maximum job timeout
+ %td= @runner.maximum_timeout_human_readable
+ %tr
%td Last contact
%td
- if @runner.contacted_at
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 756f31f91d9..d65341dbd40 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -8,7 +8,7 @@
.settings-header
%h4
General pipelines settings
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Update your CI/CD configuration, like job timeout or Auto DevOps.
@@ -19,7 +19,7 @@
.settings-header
%h4
Runners settings
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Register and see your runners for this project.
@@ -31,7 +31,7 @@
%h4
= _('Secret variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p.append-bottom-0
= render "ci/variables/content"
@@ -42,7 +42,7 @@
.settings-header
%h4
Pipeline triggers
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index fa281327eb7..94331a16abd 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- breadcrumb_title "Details"
+- breadcrumb_title _("Details")
- @content_class = "limit-container-width" unless fluid_layout
- show_auto_devops_callout = show_auto_devops_callout?(@project)
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 5eaaa1448d5..3806ead6c87 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -17,6 +17,3 @@
= import_will_timeout_message(ci_cd_only)
%li
= import_svn_message(ci_cd_only)
-
--# EE-specific start
--# EE-specific end
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 5afbc78df53..56403907844 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -55,6 +55,7 @@
label_title: label.title,
label_color: label.color,
label_text_color: label.text_color,
+ group_name: label.project.group.name,
target: '#promote-label-modal',
container: 'body',
toggle: 'modal' } }
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index adaddda13eb..6afcd447f28 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -84,7 +84,7 @@
= dropdown_content do
.js-due-date-calendar
- - if @labels && @labels.any?
+ - if @labels
- selected_labels = issuable.labels
.block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 5926867e2d7..ac494814f55 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -56,6 +56,7 @@
type: 'button',
data: { url: promote_project_milestone_path(milestone.project, milestone),
milestone_title: milestone.title,
+ group_name: @project.group.name,
target: '#promote-milestone-modal',
container: 'body',
toggle: 'modal' } }
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 6006ab8b43f..f302299eb24 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -1,4 +1,5 @@
-- page_title milestone.title, "Milestones"
+- page_title @milestone.title
+- @breadcrumb_link = dashboard_milestone_path(milestone.safe_title, title: milestone.title)
- group = local_assigns[:group]
@@ -17,7 +18,7 @@
Milestone #{milestone.title}
- if milestone.due_date || milestone.start_date
%span.creator
- &middot;
+ &nbsp;&middot;
= milestone_date_range(milestone)
- if group
.pull-right
diff --git a/app/workers/object_storage/migrate_uploads_worker.rb b/app/workers/object_storage/migrate_uploads_worker.rb
index 01ed123e6c8..a6b2c251254 100644
--- a/app/workers/object_storage/migrate_uploads_worker.rb
+++ b/app/workers/object_storage/migrate_uploads_worker.rb
@@ -138,21 +138,18 @@ module ObjectStorage
include Report
- def self.enqueue!(uploads, mounted_as, to_store)
- sanity_check!(uploads, mounted_as)
+ def self.enqueue!(uploads, model_class, mounted_as, to_store)
+ sanity_check!(uploads, model_class, mounted_as)
- perform_async(uploads.ids, mounted_as, to_store)
+ perform_async(uploads.ids, model_class.to_s, mounted_as, to_store)
end
# We need to be sure all the uploads are for the same uploader and model type
# and that the mount point exists if provided.
#
- def self.sanity_check!(uploads, mounted_as)
+ def self.sanity_check!(uploads, model_class, mounted_as)
upload = uploads.first
-
uploader_class = upload.uploader.constantize
- model_class = uploads.first.model_type.constantize
-
uploader_types = uploads.map(&:uploader).uniq
model_types = uploads.map(&:model_type).uniq
model_has_mount = mounted_as.nil? || model_class.uploaders[mounted_as] == uploader_class
@@ -162,7 +159,12 @@ module ObjectStorage
raise(SanityCheckError, "Mount point #{mounted_as} not found in #{model_class}.") unless model_has_mount
end
- def perform(ids, mounted_as, to_store)
+ def perform(*args)
+ args_check!(args)
+
+ (ids, model_type, mounted_as, to_store) = args
+
+ @model_class = model_type.constantize
@mounted_as = mounted_as&.to_sym
@to_store = to_store
@@ -178,7 +180,17 @@ module ObjectStorage
end
def sanity_check!(uploads)
- self.class.sanity_check!(uploads, @mounted_as)
+ self.class.sanity_check!(uploads, @model_class, @mounted_as)
+ end
+
+ def args_check!(args)
+ return if args.count == 4
+
+ case args.count
+ when 3 then raise SanityCheckError, "Job is missing the `model_type` argument."
+ else
+ raise SanityCheckError, "Job has wrong arguments format."
+ end
end
def build_uploaders(uploads)
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
index 0b502143e5d..c3d84bb0b93 100644
--- a/app/workers/project_export_worker.rb
+++ b/app/workers/project_export_worker.rb
@@ -4,11 +4,19 @@ class ProjectExportWorker
sidekiq_options retry: 3
- def perform(current_user_id, project_id, params = {})
- params = params.with_indifferent_access
+ def perform(current_user_id, project_id, after_export_strategy = {}, params = {})
current_user = User.find(current_user_id)
project = Project.find(project_id)
+ after_export = build!(after_export_strategy)
- ::Projects::ImportExport::ExportService.new(project, current_user, params).execute
+ ::Projects::ImportExport::ExportService.new(project, current_user, params).execute(after_export)
+ end
+
+ private
+
+ def build!(after_export_strategy)
+ strategy_klass = after_export_strategy&.delete('klass')
+
+ Gitlab::ImportExport::AfterExportStrategyBuilder.build!(strategy_klass, after_export_strategy)
end
end
diff --git a/changelogs/unreleased/17516-nested-restore-changelog.yml b/changelogs/unreleased/17516-nested-restore-changelog.yml
new file mode 100644
index 00000000000..89753f45457
--- /dev/null
+++ b/changelogs/unreleased/17516-nested-restore-changelog.yml
@@ -0,0 +1,5 @@
+---
+title: Enable restore rake task to handle nested storage directories
+merge_request: 17516
+author: Balasankar C
+type: fixed
diff --git a/changelogs/unreleased/41967_issue_api_closed_by_info.yml b/changelogs/unreleased/41967_issue_api_closed_by_info.yml
new file mode 100644
index 00000000000..436574c3638
--- /dev/null
+++ b/changelogs/unreleased/41967_issue_api_closed_by_info.yml
@@ -0,0 +1,5 @@
+---
+title: adds closed by informations in issue api
+merge_request: 17042
+author: haseebeqx
+type: added
diff --git a/changelogs/unreleased/44291-usage-ping-for-kubernetes-integration.yml b/changelogs/unreleased/44291-usage-ping-for-kubernetes-integration.yml
new file mode 100644
index 00000000000..b5c12d8f40e
--- /dev/null
+++ b/changelogs/unreleased/44291-usage-ping-for-kubernetes-integration.yml
@@ -0,0 +1,5 @@
+---
+title: Add additional cluster usage metrics to usage ping.
+merge_request: 17922
+author:
+type: changed
diff --git a/changelogs/unreleased/44392-resolve-projects-creation-silently-failing-on-after-create-error.yml b/changelogs/unreleased/44392-resolve-projects-creation-silently-failing-on-after-create-error.yml
new file mode 100644
index 00000000000..3bbd5a05b98
--- /dev/null
+++ b/changelogs/unreleased/44392-resolve-projects-creation-silently-failing-on-after-create-error.yml
@@ -0,0 +1,5 @@
+---
+title: Project creation will now raise an error if a service template is invalid
+merge_request: 18013
+author:
+type: fixed
diff --git a/changelogs/unreleased/44508-fix-fork-namespace-images.yml b/changelogs/unreleased/44508-fix-fork-namespace-images.yml
new file mode 100644
index 00000000000..63b4b9a5e56
--- /dev/null
+++ b/changelogs/unreleased/44508-fix-fork-namespace-images.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug rendering group icons when forking
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/44587-autolinking-includes-trailing-exclamation-marks.yml b/changelogs/unreleased/44587-autolinking-includes-trailing-exclamation-marks.yml
deleted file mode 100644
index 636fde601ee..00000000000
--- a/changelogs/unreleased/44587-autolinking-includes-trailing-exclamation-marks.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Don't capture trailing punctuation when autolinking
-merge_request: 17965
-author:
-type: fixed
diff --git a/changelogs/unreleased/44608-Cloning-a-repository-over-HTTPS-with-LDAP-credentials-causes-a-HTTP-401-Access-denied.yml b/changelogs/unreleased/44608-Cloning-a-repository-over-HTTPS-with-LDAP-credentials-causes-a-HTTP-401-Access-denied.yml
deleted file mode 100644
index 5afb1e3d908..00000000000
--- a/changelogs/unreleased/44608-Cloning-a-repository-over-HTTPS-with-LDAP-credentials-causes-a-HTTP-401-Access-denied.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Cloning a repository over HTTPS with LDAP credentials causes a HTTP 401 Access denied'
-merge_request: !17988
-author: Horatiu Eugen Vlad
-type: fixed
diff --git a/changelogs/unreleased/44649-reference-parsing-conflicting-with-auto-linking.yml b/changelogs/unreleased/44649-reference-parsing-conflicting-with-auto-linking.yml
new file mode 100644
index 00000000000..a64b0efa1ed
--- /dev/null
+++ b/changelogs/unreleased/44649-reference-parsing-conflicting-with-auto-linking.yml
@@ -0,0 +1,5 @@
+---
+title: Fix autolinking URLs containing ampersands
+merge_request: 18045
+author:
+type: fixed
diff --git a/changelogs/unreleased/44657-reuse-root_ref_hash-on-branches.yml b/changelogs/unreleased/44657-reuse-root_ref_hash-on-branches.yml
new file mode 100644
index 00000000000..4f21aadd86b
--- /dev/null
+++ b/changelogs/unreleased/44657-reuse-root_ref_hash-on-branches.yml
@@ -0,0 +1,5 @@
+---
+title: Reuse root_ref_hash for performance on Branches
+merge_request: 17998
+author: Takuya Noguchi
+type: performance
diff --git a/changelogs/unreleased/44717-no-resolve-issue.yml b/changelogs/unreleased/44717-no-resolve-issue.yml
new file mode 100644
index 00000000000..ce23f4e6e9f
--- /dev/null
+++ b/changelogs/unreleased/44717-no-resolve-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Don't show Jump to Discussion button on Issues
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/44774-migrate-upload-task-fails-for-upload-with-store-nil.yml b/changelogs/unreleased/44774-migrate-upload-task-fails-for-upload-with-store-nil.yml
new file mode 100644
index 00000000000..372f4293964
--- /dev/null
+++ b/changelogs/unreleased/44774-migrate-upload-task-fails-for-upload-with-store-nil.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed gitlab:uploads:migrate task ignoring some uploads.
+merge_request: 18082
+author:
+type: fixed
diff --git a/changelogs/unreleased/44776-fix-upload-migrate-fails-for-group.yml b/changelogs/unreleased/44776-fix-upload-migrate-fails-for-group.yml
new file mode 100644
index 00000000000..6094fcd0b3e
--- /dev/null
+++ b/changelogs/unreleased/44776-fix-upload-migrate-fails-for-group.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed gitlab:uploads:migrate task failing for Groups' avatar.
+merge_request: 18088
+author:
+type: fixed
diff --git a/changelogs/unreleased/44878-update-brakeman-3-6-1-to-4-2-1.yml b/changelogs/unreleased/44878-update-brakeman-3-6-1-to-4-2-1.yml
new file mode 100644
index 00000000000..f5710cf4f7f
--- /dev/null
+++ b/changelogs/unreleased/44878-update-brakeman-3-6-1-to-4-2-1.yml
@@ -0,0 +1,5 @@
+---
+title: Update brakeman 3.6.1 to 4.2.1
+merge_request: 18122
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/Link_to_project_labels_page.yml b/changelogs/unreleased/Link_to_project_labels_page.yml
new file mode 100644
index 00000000000..7bdeec423fc
--- /dev/null
+++ b/changelogs/unreleased/Link_to_project_labels_page.yml
@@ -0,0 +1,5 @@
+---
+title: Always display Labels section in issuable sidebar, even when the project has no labels
+merge_request: 18081
+author: Branka Martinovic
+type: fixed
diff --git a/changelogs/unreleased/ac-fix-use_file-race.yml b/changelogs/unreleased/ac-fix-use_file-race.yml
new file mode 100644
index 00000000000..f1315d5d50e
--- /dev/null
+++ b/changelogs/unreleased/ac-fix-use_file-race.yml
@@ -0,0 +1,5 @@
+---
+title: Fix data race between ObjectStorage background_upload and Pages publishing
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/ac-pages-port.yml b/changelogs/unreleased/ac-pages-port.yml
new file mode 100644
index 00000000000..4f7257b4798
--- /dev/null
+++ b/changelogs/unreleased/ac-pages-port.yml
@@ -0,0 +1,5 @@
+---
+title: Add missing port to artifact links
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-canary-favicon.yml b/changelogs/unreleased/add-canary-favicon.yml
new file mode 100644
index 00000000000..1af6572588d
--- /dev/null
+++ b/changelogs/unreleased/add-canary-favicon.yml
@@ -0,0 +1,5 @@
+---
+title: Add yellow favicon when `CANARY=true` to differientate canary environment
+merge_request: 12477
+author:
+type: changed
diff --git a/changelogs/unreleased/add-milestone-path-to-dashboard-milestones-breadcrumb-link.yml b/changelogs/unreleased/add-milestone-path-to-dashboard-milestones-breadcrumb-link.yml
new file mode 100644
index 00000000000..015bee99170
--- /dev/null
+++ b/changelogs/unreleased/add-milestone-path-to-dashboard-milestones-breadcrumb-link.yml
@@ -0,0 +1,5 @@
+---
+title: Update dashboard milestones breadcrumb link
+merge_request: 17933
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/add-per-runner-job-timeout.yml b/changelogs/unreleased/add-per-runner-job-timeout.yml
new file mode 100644
index 00000000000..336b4d15ddf
--- /dev/null
+++ b/changelogs/unreleased/add-per-runner-job-timeout.yml
@@ -0,0 +1,5 @@
+---
+title: Add per-runner configured job timeout
+merge_request: 17221
+author:
+type: added
diff --git a/changelogs/unreleased/blackst0ne-bump-html-pipeline-gem.yml b/changelogs/unreleased/blackst0ne-bump-html-pipeline-gem.yml
new file mode 100644
index 00000000000..9885c8853cc
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-bump-html-pipeline-gem.yml
@@ -0,0 +1,5 @@
+---
+title: Bump html-pipeline to 2.7.1
+merge_request: 18132
+author: "@blackst0ne"
+type: other
diff --git a/changelogs/unreleased/dm-deploy-keys-default-user.yml b/changelogs/unreleased/dm-deploy-keys-default-user.yml
new file mode 100644
index 00000000000..b82d67d028c
--- /dev/null
+++ b/changelogs/unreleased/dm-deploy-keys-default-user.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure hooks run when a deploy key without a user pushes
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/escape-autocomplete-values-for-markdown.yml b/changelogs/unreleased/escape-autocomplete-values-for-markdown.yml
new file mode 100644
index 00000000000..eea9da4c579
--- /dev/null
+++ b/changelogs/unreleased/escape-autocomplete-values-for-markdown.yml
@@ -0,0 +1,5 @@
+---
+title: Escape Markdown characters properly when using autocomplete
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/expose-commits-mr-api.yml b/changelogs/unreleased/expose-commits-mr-api.yml
new file mode 100644
index 00000000000..77ea2f27431
--- /dev/null
+++ b/changelogs/unreleased/expose-commits-mr-api.yml
@@ -0,0 +1,5 @@
+---
+title: Allow merge requests related to a commit to be found via API
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/feature-gb-variables-expressions-in-only-except.yml b/changelogs/unreleased/feature-gb-variables-expressions-in-only-except.yml
new file mode 100644
index 00000000000..84977ce11c8
--- /dev/null
+++ b/changelogs/unreleased/feature-gb-variables-expressions-in-only-except.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for pipeline variables expressions in only/except
+merge_request: 17316
+author:
+type: added
diff --git a/changelogs/unreleased/fix-gb-fix-background-pipeline-stages-migration.yml b/changelogs/unreleased/fix-gb-fix-background-pipeline-stages-migration.yml
new file mode 100644
index 00000000000..63948f0c196
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-fix-background-pipeline-stages-migration.yml
@@ -0,0 +1,5 @@
+---
+title: Fix exceptions raised when migrating pipeline stages in the background
+merge_request: 18076
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-projects-no-repository-placeholder.yml b/changelogs/unreleased/fix-projects-no-repository-placeholder.yml
new file mode 100644
index 00000000000..3d11c897020
--- /dev/null
+++ b/changelogs/unreleased/fix-projects-no-repository-placeholder.yml
@@ -0,0 +1,5 @@
+---
+title: Update no repository placeholder
+merge_request: 17964
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/fj-42685-extend-project-export-endpoint.yml b/changelogs/unreleased/fj-42685-extend-project-export-endpoint.yml
new file mode 100644
index 00000000000..a06499d821a
--- /dev/null
+++ b/changelogs/unreleased/fj-42685-extend-project-export-endpoint.yml
@@ -0,0 +1,5 @@
+---
+title: Extend API for exporting a project with direct upload URL
+merge_request: 17686
+author:
+type: added
diff --git a/changelogs/unreleased/ide-file-row-hover-style.yml b/changelogs/unreleased/ide-file-row-hover-style.yml
new file mode 100644
index 00000000000..158379a5aef
--- /dev/null
+++ b/changelogs/unreleased/ide-file-row-hover-style.yml
@@ -0,0 +1,5 @@
+---
+title: Added hover background color to IDE file list rows
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/jivl-change-copy-text-promote-milestones-labels.yml b/changelogs/unreleased/jivl-change-copy-text-promote-milestones-labels.yml
new file mode 100644
index 00000000000..fb3095552d3
--- /dev/null
+++ b/changelogs/unreleased/jivl-change-copy-text-promote-milestones-labels.yml
@@ -0,0 +1,5 @@
+---
+title: Correct copy text for the promote milestone and label modals
+merge_request: 17726
+author:
+type: fixed
diff --git a/changelogs/unreleased/move-registry-after-cicd-project-nav-sidebar.yml b/changelogs/unreleased/move-registry-after-cicd-project-nav-sidebar.yml
new file mode 100644
index 00000000000..03a6fd42228
--- /dev/null
+++ b/changelogs/unreleased/move-registry-after-cicd-project-nav-sidebar.yml
@@ -0,0 +1,5 @@
+---
+ title: Move 'Registry' after 'CI/CD' in project navigation sidebar
+ merge_request: 18018
+ author: Elias Werberich
+ type: changed
diff --git a/changelogs/unreleased/sh-cleanup-pages-worker.yml b/changelogs/unreleased/sh-cleanup-pages-worker.yml
new file mode 100644
index 00000000000..c26e1342dd2
--- /dev/null
+++ b/changelogs/unreleased/sh-cleanup-pages-worker.yml
@@ -0,0 +1,5 @@
+---
+title: Free open file descriptors and libgit2 buffers in UpdatePagesService
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/workhorse-gitaly-mandatory.yml b/changelogs/unreleased/workhorse-gitaly-mandatory.yml
new file mode 100644
index 00000000000..77b62302e86
--- /dev/null
+++ b/changelogs/unreleased/workhorse-gitaly-mandatory.yml
@@ -0,0 +1,5 @@
+---
+title: Make all workhorse gitaly calls opt-out, take 2
+merge_request: 18043
+author:
+type: other
diff --git a/changelogs/unreleased/zj-remote-repo-exists.yml b/changelogs/unreleased/zj-remote-repo-exists.yml
new file mode 100644
index 00000000000..f024b83159b
--- /dev/null
+++ b/changelogs/unreleased/zj-remote-repo-exists.yml
@@ -0,0 +1,5 @@
+---
+title: Test if remote repository exists when importing wikis
+merge_request:
+author:
+type: fixed
diff --git a/config/webpack.config.js b/config/webpack.config.js
index b74d9dde494..39e9fbbd530 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -104,7 +104,7 @@ const config = {
},
},
{
- test: /katex.css$/,
+ test: /katex.min.css$/,
include: /node_modules\/katex\/dist/,
use: [
{ loader: 'style-loader' },
diff --git a/db/migrate/20180209165249_add_closed_by_to_issues.rb b/db/migrate/20180209165249_add_closed_by_to_issues.rb
new file mode 100644
index 00000000000..e251afd7b49
--- /dev/null
+++ b/db/migrate/20180209165249_add_closed_by_to_issues.rb
@@ -0,0 +1,20 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddClosedByToIssues < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ add_column :issues, :closed_by_id, :integer
+ add_concurrent_foreign_key :issues, :users, column: :closed_by_id, on_delete: :nullify
+ end
+
+ def down
+ remove_foreign_key :issues, column: :closed_by_id
+ remove_column :issues, :closed_by_id
+ end
+end
diff --git a/db/migrate/20180219153455_add_maximum_timeout_to_ci_runners.rb b/db/migrate/20180219153455_add_maximum_timeout_to_ci_runners.rb
new file mode 100644
index 00000000000..072e696a43e
--- /dev/null
+++ b/db/migrate/20180219153455_add_maximum_timeout_to_ci_runners.rb
@@ -0,0 +1,9 @@
+class AddMaximumTimeoutToCiRunners < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_runners, :maximum_timeout, :integer
+ end
+end
diff --git a/db/migrate/20180301010859_create_ci_builds_metadata_table.rb b/db/migrate/20180301010859_create_ci_builds_metadata_table.rb
new file mode 100644
index 00000000000..ce737444092
--- /dev/null
+++ b/db/migrate/20180301010859_create_ci_builds_metadata_table.rb
@@ -0,0 +1,20 @@
+class CreateCiBuildsMetadataTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :ci_builds_metadata do |t|
+ t.integer :build_id, null: false
+ t.integer :project_id, null: false
+ t.integer :timeout
+ t.integer :timeout_source, null: false, default: 1
+
+ t.foreign_key :ci_builds, column: :build_id, on_delete: :cascade
+ t.foreign_key :projects, column: :project_id, on_delete: :cascade
+
+ t.index :build_id, unique: true
+ t.index :project_id
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 77b3d836287..06fc1a9d7e9 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -329,6 +329,16 @@ ActiveRecord::Schema.define(version: 20180327101207) do
add_index "ci_builds", ["updated_at"], name: "index_ci_builds_on_updated_at", using: :btree
add_index "ci_builds", ["user_id"], name: "index_ci_builds_on_user_id", using: :btree
+ create_table "ci_builds_metadata", force: :cascade do |t|
+ t.integer "build_id", null: false
+ t.integer "project_id", null: false
+ t.integer "timeout"
+ t.integer "timeout_source", default: 1, null: false
+ end
+
+ add_index "ci_builds_metadata", ["build_id"], name: "index_ci_builds_metadata_on_build_id", unique: true, using: :btree
+ add_index "ci_builds_metadata", ["project_id"], name: "index_ci_builds_metadata_on_project_id", using: :btree
+
create_table "ci_group_variables", force: :cascade do |t|
t.string "key", null: false
t.text "value"
@@ -459,6 +469,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do
t.boolean "locked", default: false, null: false
t.integer "access_level", default: 0, null: false
t.string "ip_address"
+ t.integer "maximum_timeout"
end
add_index "ci_runners", ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree
@@ -921,6 +932,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do
t.integer "last_edited_by_id"
t.boolean "discussion_locked"
t.datetime_with_timezone "closed_at"
+ t.integer "closed_by_id"
end
add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
@@ -2027,6 +2039,8 @@ ActiveRecord::Schema.define(version: 20180327101207) do
add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify
add_foreign_key "ci_builds", "ci_stages", column: "stage_id", name: "fk_3a9eaa254d", on_delete: :cascade
add_foreign_key "ci_builds", "projects", name: "fk_befce0568a", on_delete: :cascade
+ add_foreign_key "ci_builds_metadata", "ci_builds", column: "build_id", on_delete: :cascade
+ add_foreign_key "ci_builds_metadata", "projects", on_delete: :cascade
add_foreign_key "ci_group_variables", "namespaces", column: "group_id", name: "fk_33ae4d58d8", on_delete: :cascade
add_foreign_key "ci_job_artifacts", "ci_builds", column: "job_id", on_delete: :cascade
add_foreign_key "ci_job_artifacts", "projects", on_delete: :cascade
@@ -2082,6 +2096,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do
add_foreign_key "issues", "milestones", name: "fk_96b1dd429c", on_delete: :nullify
add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade
add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :nullify
+ add_foreign_key "issues", "users", column: "closed_by_id", name: "fk_c63cbf6c25", on_delete: :nullify
add_foreign_key "issues", "users", column: "updated_by_id", name: "fk_ffed080f01", on_delete: :nullify
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index be805a2ccc4..604f7244a34 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -211,9 +211,9 @@ straight away.
### GitLab self-hosted
-With GitLab self-hosted, you deploy your own GitLab instance on-premises or on a private cloud of your choice. GitLab self-hosted is available for [free and with paid subscriptions](https://about.gitlab.com/products/): Libre, Starter, Premium, and Ultimate.
+With GitLab self-hosted, you deploy your own GitLab instance on-premises or on a private cloud of your choice. GitLab self-hosted is available for [free and with paid subscriptions](https://about.gitlab.com/products/): Core, Starter, Premium, and Ultimate.
-Every feature available in Libre is also available in Starter, Premium, and Ultimate.
+Every feature available in Core is also available in Starter, Premium, and Ultimate.
Starter features are also available in Premium and Ultimate, and Premium features are also
available in Ultimate.
@@ -227,7 +227,7 @@ GitLab.com subscriptions grants access
to the same features available in GitLab self-hosted, **expect
[administration](administration/index.md) tools and settings**:
-- GitLab.com Free includes the same features available in GitLab Libre
+- GitLab.com Free includes the same features available in Core
- GitLab.com Bronze includes the same features available in GitLab Starter
- GitLab.com Silver includes the same features available in GitLab Premium
- GitLab.com Gold includes the same features available in GitLab Ultimate
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 4366590578a..60a45426636 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -11,8 +11,8 @@ available through [different subscriptions](https://about.gitlab.com/products/).
You can [install GitLab CE or GitLab EE](https://about.gitlab.com/installation/ce-or-ee/),
but the features you'll have access to depend on the subscription you choose
-(Libre, Starter, Premium, or Ultimate). GitLab Community Edition installations
-only have access to Libre features.
+(Core, Starter, Premium, or Ultimate). GitLab Community Edition installations
+only have access to Core features.
GitLab.com is administered by GitLab, Inc., therefore, only GitLab team members have
access to its admin configurations. If you're a GitLab.com user, please check the
diff --git a/doc/administration/issue_closing_pattern.md b/doc/administration/issue_closing_pattern.md
index 28e1fd4e12e..466bb1f851e 100644
--- a/doc/administration/issue_closing_pattern.md
+++ b/doc/administration/issue_closing_pattern.md
@@ -24,11 +24,11 @@ Because Rubular doesn't understand `%{issue_ref}`, you can replace this by
**For Omnibus installations**
1. Open `/etc/gitlab/gitlab.rb` with your editor.
-1. Change the value of `gitlab_rails['issue_closing_pattern']` to a regular
+1. Change the value of `gitlab_rails['gitlab_issue_closing_pattern']` to a regular
expression of your liking:
```ruby
- gitlab_rails['issue_closing_pattern'] = "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)"
+ gitlab_rails['gitlab_issue_closing_pattern'] = "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)"
```
1. [Reconfigure] GitLab for the changes to take effect.
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 55c673fd06a..db0a80d04d9 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -412,9 +412,10 @@ Example response:
Since GitLab 8.1, this is the new commit status API.
-### Get the status of a commit
+### List the statuses of a commit
-Get the statuses of a commit in a project.
+List the statuses of a commit in a project.
+The pagination parameters `page` and `per_page` can be used to restrict the list of references.
```
GET /projects/:id/repository/commits/:sha/statuses
@@ -536,6 +537,74 @@ Example response:
}
```
+## List Merge Requests associated with a commit
+
+Get a list of Merge Requests related to the specified commit.
+
+```
+GET /projects/:id/repository/commits/:sha/merge_requests
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
+| `sha` | string | yes | The commit SHA
+
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits/af5b13261899fb2c0db30abdd0af8b07cb44fdc5/merge_requests"
+```
+
+Example response:
+
+```json
+[
+ {
+ "id":45,
+ "iid":1,
+ "project_id":35,
+ "title":"Add new file",
+ "description":"",
+ "state":"opened",
+ "created_at":"2018-03-26T17:26:30.916Z",
+ "updated_at":"2018-03-26T17:26:30.916Z",
+ "target_branch":"master",
+ "source_branch":"test-branch",
+ "upvotes":0,
+ "downvotes":0,
+ "author" : {
+ "web_url" : "https://gitlab.example.com/thedude",
+ "name" : "Jeff Lebowski",
+ "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
+ "username" : "thedude",
+ "state" : "active",
+ "id" : 28
+ },
+ "assignee":null,
+ "source_project_id":35,
+ "target_project_id":35,
+ "labels":[ ],
+ "work_in_progress":false,
+ "milestone":null,
+ "merge_when_pipeline_succeeds":false,
+ "merge_status":"can_be_merged",
+ "sha":"af5b13261899fb2c0db30abdd0af8b07cb44fdc5",
+ "merge_commit_sha":null,
+ "user_notes_count":0,
+ "discussion_locked":null,
+ "should_remove_source_branch":null,
+ "force_remove_source_branch":false,
+ "web_url":"http://https://gitlab.example.com/root/test-project/merge_requests/1",
+ "time_stats":{
+ "time_estimate":0,
+ "total_time_spent":0,
+ "human_time_estimate":null,
+ "human_total_time_spent":null
+ }
+ }
+]
+```
+
[ce-6096]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6096 "Multi-file commit"
[ce-8047]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8047
[ce-15026]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15026
diff --git a/doc/api/events.md b/doc/api/events.md
index 129af0afa35..f4d26c4de1c 100644
--- a/doc/api/events.md
+++ b/doc/api/events.md
@@ -42,6 +42,10 @@ Dates for the `before` and `after` parameters should be supplied in the followin
YYYY-MM-DD
```
+### Event Time Period Limit
+
+GitLab removes events older than 1 year from the events table for performance reasons. The range of 1 year was chosen because user contribution calendars only show contributions of the past year.
+
## List currently authenticated user's events
>**Note:** This endpoint was introduced in GitLab 9.3.
diff --git a/doc/api/issues.md b/doc/api/issues.md
index a4a51101297..7479c1d2f93 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -100,6 +100,7 @@ Example response:
},
"updated_at" : "2016-01-04T15:31:51.081Z",
"closed_at" : null,
+ "closed_by" : null,
"id" : 76,
"title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.",
"created_at" : "2016-01-04T15:31:51.081Z",
@@ -122,6 +123,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
+**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
+
## List group issues
Get a list of a group's issues.
@@ -216,6 +219,7 @@ Example response:
"updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z",
"closed_at" : null,
+ "closed_by" : null,
"user_notes_count": 1,
"due_date": null,
"web_url": "http://example.com/example/example/issues/1",
@@ -233,6 +237,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
+**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
+
## List project issues
Get a list of a project's issues.
@@ -326,6 +332,14 @@ Example response:
"updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z",
"closed_at" : "2016-01-05T15:31:46.176Z",
+ "closed_by" : {
+ "state" : "active",
+ "web_url" : "https://gitlab.example.com/root",
+ "avatar_url" : null,
+ "username" : "root",
+ "id" : 1,
+ "name" : "Administrator"
+ },
"user_notes_count": 1,
"due_date": "2016-07-22",
"web_url": "http://example.com/example/example/issues/1",
@@ -343,6 +357,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
+**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
+
## Single issue
Get a single project issue.
@@ -409,6 +425,8 @@ Example response:
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z",
+ "closed_at" : null,
+ "closed_by" : null,
"subscribed": false,
"user_notes_count": 1,
"due_date": null,
@@ -432,6 +450,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
+**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
+
## New issue
Creates a new project issue.
@@ -484,6 +504,7 @@ Example response:
"description" : null,
"updated_at" : "2016-01-07T12:44:33.959Z",
"closed_at" : null,
+ "closed_by" : null,
"milestone" : null,
"subscribed" : true,
"user_notes_count": 0,
@@ -508,6 +529,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
+**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
+
## Edit issue
Updates an existing project issue. This call is also used to mark an issue as
@@ -556,6 +579,14 @@ Example response:
"description" : null,
"updated_at" : "2016-01-07T12:55:16.213Z",
"closed_at" : "2016-01-08T12:55:16.213Z",
+ "closed_by" : {
+ "state" : "active",
+ "web_url" : "https://gitlab.example.com/root",
+ "avatar_url" : null,
+ "username" : "root",
+ "id" : 1,
+ "name" : "Administrator"
+ },
"iid" : 15,
"labels" : [
"bug"
@@ -587,6 +618,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
+**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
+
## Delete an issue
Only for admins and project owners. Soft deletes the issue in question.
@@ -640,6 +673,7 @@ Example response:
"created_at": "2016-04-05T21:41:45.652Z",
"updated_at": "2016-04-07T12:20:17.596Z",
"closed_at": null,
+ "closed_by": null,
"labels": [],
"milestone": null,
"assignees": [{
@@ -687,6 +721,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
+**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
+
## Subscribe to an issue
Subscribes the authenticated user to an issue to receive notifications.
@@ -719,6 +755,7 @@ Example response:
"created_at": "2016-04-05T21:41:45.652Z",
"updated_at": "2016-04-07T12:20:17.596Z",
"closed_at": null,
+ "closed_by": null,
"labels": [],
"milestone": null,
"assignees": [{
@@ -766,6 +803,9 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
+
+**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
+
## Unsubscribe from an issue
Unsubscribes the authenticated user from the issue to not receive notifications
@@ -807,6 +847,8 @@ Example response:
"avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon",
"web_url": "https://gitlab.example.com/keyon"
},
+ "closed_at": null,
+ "closed_by": null,
"author": {
"name": "Vivian Hermann",
"username": "orville",
@@ -927,6 +969,9 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
+
+**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
+
## Set a time estimate for an issue
Sets an estimated time of work for this issue.
@@ -1112,6 +1157,8 @@ Example response:
"assignee": null,
"source_project_id": 1,
"target_project_id": 1,
+ "closed_at": null,
+ "closed_by": null,
"labels": [],
"work_in_progress": false,
"milestone": null,
@@ -1206,3 +1253,4 @@ Example response:
[ce-13004]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13004
[ce-14016]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14016
+[ce-17042]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17042
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index e7060e154f4..db4fe2f6880 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -294,9 +294,10 @@ Example of response
## Get job artifacts
-> [Introduced][ce-2893] in GitLab 8.5
+> **Notes**:
+- [Introduced][ce-2893] in GitLab 8.5.
-Get job artifacts of a project
+Get job artifacts of a project.
```
GET /projects/:id/jobs/:job_id/artifacts
@@ -307,8 +308,10 @@ GET /projects/:id/jobs/:job_id/artifacts
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `job_id` | integer | yes | The ID of a job |
+Example requests:
+
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/8/artifacts"
+curl --location --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/8/artifacts"
```
Response:
@@ -322,7 +325,8 @@ Response:
## Download the artifacts archive
-> [Introduced][ce-5347] in GitLab 8.10.
+> **Notes**:
+- [Introduced][ce-5347] in GitLab 8.10.
Download the artifacts archive from the given reference name and job provided the
job finished successfully.
@@ -339,7 +343,7 @@ Parameters
| `ref_name` | string | yes | The ref from a repository (can only be branch or tag name, not HEAD or SHA) |
| `job` | string | yes | The name of the job |
-Example request:
+Example requests:
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/download?job=test"
diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md
index de5207fc5e4..5467187788a 100644
--- a/doc/api/project_import_export.md
+++ b/doc/api/project_import_export.md
@@ -8,6 +8,14 @@
Start a new export.
+The endpoint also accepts an `upload` param. This param is a hash that contains
+all the necessary information to upload the exported project to a web server or
+to any S3-compatible platform. At the moment we only support binary
+data file uploads to the final server.
+
+If the `upload` params is present, `upload[url]` param is required.
+ (**Note:** This feature was introduced in GitLab 10.7)
+
```http
POST /projects/:id/export
```
@@ -16,9 +24,12 @@ POST /projects/:id/export
| --------- | -------------- | -------- | ---------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `description` | string | no | Overrides the project description |
+| `upload` | hash | no | Hash that contains the information to upload the exported project to a web server |
+| `upload[url]` | string | yes | The URL to upload the project |
+| `upload[http_method]` | string | no | The HTTP method to upload the exported project. Only `PUT` and `POST` methods allowed. Default is `PUT` |
```console
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "description=Foo Bar" https://gitlab.example.com/api/v4/projects/1/export
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/export --data "description=FooBar&upload[http_method]=PUT&upload[url]=https://example-bucket.s3.eu-west-3.amazonaws.com/backup?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIMBJHN2O62W8IELQ%2F20180312%2Feu-west-3%2Fs3%2Faws4_request&X-Amz-Date=20180312T110328Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=8413facb20ff33a49a147a0b4abcff4c8487cc33ee1f7e450c46e8f695569dbd"
```
```json
@@ -43,7 +54,11 @@ GET /projects/:id/export
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/export
```
-Status can be one of `none`, `started`, or `finished`.
+Status can be one of `none`, `started`, `after_export_action` or `finished`. The
+`after_export_action` state represents that the export process has been completed successfully and
+the platform is performing some actions on the resulted file. For example, sending
+an email notifying the user to download the file, uploading the exported file
+to a web server, etc.
`_links` are only present when export has finished.
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 271ee91dc72..f388fae42a9 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -1344,3 +1344,7 @@ Read more in the [Project members](members.md) documentation.
## Project badges
Read more in the [Project Badges](project_badges.md) documentation.
+
+## Issue and merge request description templates
+
+The non-default [issue and merge request description templates](../user/project/description_templates.md) are managed inside the project's repository. So you can manage them via the API through the [Repositories API](repositories.md) and the [Repository Files API](repository_files.md). \ No newline at end of file
diff --git a/doc/api/runners.md b/doc/api/runners.md
index 7495c6cdedb..f384ac57bfe 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -153,7 +153,8 @@ Example response:
"mysql"
],
"version": null,
- "access_level": "ref_protected"
+ "access_level": "ref_protected",
+ "maximum_timeout": 3600
}
```
@@ -174,6 +175,7 @@ PUT /runners/:id
| `run_untagged` | boolean | no | Flag indicating the runner can execute untagged jobs |
| `locked` | boolean | no | Flag indicating the runner is locked |
| `access_level` | string | no | The access_level of the runner; `not_protected` or `ref_protected` |
+| `maximum_timeout` | integer | no | Maximum timeout set when this Runner will handle the job |
```
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
@@ -211,7 +213,8 @@ Example response:
"tag2"
],
"version": null,
- "access_level": "ref_protected"
+ "access_level": "ref_protected",
+ "maximum_timeout": null
}
```
diff --git a/doc/ci/examples/browser_performance.md b/doc/ci/examples/browser_performance.md
index 42dc6ef36ba..691370d7195 100644
--- a/doc/ci/examples/browser_performance.md
+++ b/doc/ci/examples/browser_performance.md
@@ -1,22 +1,28 @@
# Browser Performance Testing with the Sitespeed.io container
-This example shows how to run the [Sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/) on your code by using
-GitLab CI/CD and [Sitespeed.io](https://www.sitespeed.io) using Docker-in-Docker.
+This example shows how to run the
+[Sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/) on
+your code by using GitLab CI/CD and [Sitespeed.io](https://www.sitespeed.io)
+using Docker-in-Docker.
-First, you need a GitLab Runner with the [docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-executor).
-
-Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `performance`:
+First, you need a GitLab Runner with the
+[docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-executor).
+Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called
+`performance`:
```yaml
+performance:
stage: performance
image: docker:git
+ variables:
+ URL: https://example.com
services:
- docker:dind
script:
- mkdir gitlab-exporter
- - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/10-5/index.js
+ - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
- mkdir sitespeed-results
- - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results https://my.website.com
+ - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL
- mv sitespeed-results/data/performance.json performance.json
artifacts:
paths:
@@ -24,37 +30,84 @@ Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `performan
- sitespeed-results/
```
-This will create a `performance` job in your CI/CD pipeline and will run Sitespeed.io against the webpage you define. The GitLab plugin for Sitespeed.io is downloaded in order to export key metrics to JSON. The full HTML Sitespeed.io report will also be saved as an artifact, and if you have Pages enabled it can be viewed directly in your browser. For further customization options of Sitespeed.io, including the ability to provide a list of URLs to test, please consult their [documentation](https://www.sitespeed.io/documentation/sitespeed.io/configuration/).
+The above example will:
+
+1. Create a `performance` job in your CI/CD pipeline and will run
+ Sitespeed.io against the webpage you defined in `URL`.
+1. The [GitLab plugin](https://gitlab.com/gitlab-org/gl-performance) for
+ Sitespeed.io is downloaded in order to export key metrics to JSON. The full
+ HTML Sitespeed.io report will also be saved as an artifact, and if you have
+ [GitLab Pages](../../user/project/pages/index.md) enabled, it can be viewed
+ directly in your browser.
+
+For further customization options of Sitespeed.io, including the ability to
+provide a list of URLs to test, please consult
+[their documentation](https://www.sitespeed.io/documentation/sitespeed.io/configuration/).
-For [GitLab Premium](https://about.gitlab.com/products/) users, key metrics are automatically
-extracted and shown right in the merge request widget. Learn more about [Browser Performance Testing](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html).
+TIP: **Tip:**
+For [GitLab Premium](https://about.gitlab.com/pricing/) users, key metrics are automatically
+extracted and shown right in the merge request widget. Learn more about
+[Browser Performance Testing](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html).
## Performance testing on Review Apps
-The above CI YML is great for testing against static environments, and it can be extended for dynamic environments. There are a few extra steps to take to set this up:
-1. The `performance` job should run after the environment has started.
-1. In the `deploy` job, persist the hostname so it is available to the `performance` job. The same can be done for static environments like staging and production to unify the code path. Saving it as an artifact is as simple as `echo $CI_ENVIRONMENT_URL > environment_url.txt`.
-1. In the `performance` job read the artifact into an environment variable, like `$CI_ENVIRONMENT_URL`, and use it to parameterize the test URL's.
-1. Now you can run the Sitespeed.io container against the desired hostname and paths.
+The above CI YML is great for testing against static environments, and it can
+be extended for dynamic environments. There are a few extra steps to take to
+set this up:
-A simple `performance` job would look like:
+1. The `performance` job should run after the dynamic environment has started.
+1. In the `review` job, persist the hostname and upload it as an artifact so
+ it's available to the `performance` job (the same can be done for static
+ environments like staging and production to unify the code path). Saving it
+ as an artifact is as simple as `echo $CI_ENVIRONMENT_URL > environment_url.txt`
+ in your job's `script`.
+1. In the `performance` job, read the previous artifact into an environment
+ variable, like `$CI_ENVIRONMENT_URL`, and use it to parameterize the test
+ URLs.
+1. You can now run the Sitespeed.io container against the desired hostname and
+ paths.
+
+Your `.gitlab-ci.yml` file would look like:
```yaml
+stages:
+ - deploy
+ - performance
+
+review:
+ stage: deploy
+ environment:
+ name: review/$CI_COMMIT_REF_SLUG
+ url: http://$CI_COMMIT_REF_SLUG.$APPS_DOMAIN
+ script:
+ - run_deploy_script
+ - echo $CI_ENVIRONMENT_URL > environment_url.txt
+ artifacts:
+ paths:
+ - environment_url.txt
+ only:
+ - branches
+ except:
+ - master
+
+performance:
stage: performance
image: docker:git
services:
- docker:dind
+ dependencies:
+ - review
script:
- export CI_ENVIRONMENT_URL=$(cat environment_url.txt)
- mkdir gitlab-exporter
- - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/10-5/index.js
+ - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
- mkdir sitespeed-results
- docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL"
- mv sitespeed-results/data/performance.json performance.json
artifacts:
paths:
- - performance.json
- - sitespeed-results/
+ - performance.json
+ - sitespeed-results/
```
-A complete example can be found in our [Auto DevOps CI YML](https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml). \ No newline at end of file
+A complete example can be found in our [Auto DevOps CI YML](https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml).
diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md
index ec5e5afb8c6..64a759a9a99 100644
--- a/doc/ci/examples/code_climate.md
+++ b/doc/ci/examples/code_climate.md
@@ -15,13 +15,8 @@ codequality:
services:
- docker:dind
script:
- - docker pull codeclimate/codeclimate
- 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/deployment/README.md b/doc/ci/examples/deployment/README.md
index e80e246c5dd..2dcdc2d41ec 100644
--- a/doc/ci/examples/deployment/README.md
+++ b/doc/ci/examples/deployment/README.md
@@ -111,7 +111,7 @@ We also use two secure variables:
## Storing API keys
Secure Variables can added by going to your project's
-**Settings ➔ Pipelines ➔ Secret variables**. The variables that are defined
+**Settings ➔ CI / CD ➔ Secret variables**. The variables that are defined
in the project settings are sent along with the build script to the Runner.
The secure variables are stored out of the repository. Never store secrets in
your project's `.gitlab-ci.yml`. It is also important that the secret's value
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 856d7f264e4..301cccc80a3 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -2,6 +2,11 @@
> Introduced in GitLab 8.8.
+NOTE: **Note:**
+If you have a [mirrored repository where GitLab pulls from](https://docs.gitlab.com/ee/workflow/repository_mirroring.html#pulling-from-a-remote-repository),
+you may need to enable pipeline triggering in your project's
+**Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**.
+
## Pipelines
A pipeline is a group of [jobs][] that get executed in [stages][](batches).
@@ -121,9 +126,8 @@ The basic requirements is that there are two numbers separated with one of
the following (you can even use them interchangeably):
- a space
-- a forward slash (`/`)
+- a slash (`/`)
- a colon (`:`)
-- a dot (`.`)
>**Note:**
More specifically, [it uses][regexp] this regular expression: `\d+[\s:\/\\]+\d+\s*`.
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index f64e868d390..fec0ff87326 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -126,6 +126,11 @@ git push origin master
Now if you go to the **Pipelines** page you will see that the pipeline is
pending.
+NOTE: **Note:**
+If you have a [mirrored repository where GitLab pulls from](https://docs.gitlab.com/ee/workflow/repository_mirroring.html#pulling-from-a-remote-repository),
+you may need to enable pipeline triggering in your project's
+**Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**.
+
You can also go to the **Commits** page and notice the little pause icon next
to the commit SHA.
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index 7a7b50b294d..60dc2ef9ac5 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -35,7 +35,7 @@ are:
A Runner that is specific only runs for the specified project(s). A shared Runner
can run jobs for every project that has enabled the option **Allow shared Runners**
-under **Settings ➔ CI/CD**.
+under **Settings > CI/CD**.
Projects with high demand of CI activity can also benefit from using specific
Runners. By having dedicated Runners you are guaranteed that the Runner is not
@@ -76,7 +76,7 @@ Registering a specific can be done in two ways:
To create a specific Runner without having admin rights to the GitLab instance,
visit the project you want to make the Runner work for in GitLab:
-1. Go to **Settings ➔ CI/CD** to obtain the token
+1. Go to **Settings > CI/CD** to obtain the token
1. [Register the Runner][register]
### Making an existing shared Runner specific
@@ -85,7 +85,7 @@ If you are an admin on your GitLab instance, you can turn any shared Runner into
a specific one, but not the other way around. Keep in mind that this is a one
way transition.
-1. Go to the Runners in the admin area **Overview ➔ Runners** (`/admin/runners`)
+1. Go to the Runners in the admin area **Overview > Runners** (`/admin/runners`)
and find your Runner
1. Enable any projects under **Restrict projects for this Runner** to be used
with the Runner
@@ -101,7 +101,7 @@ can be changed afterwards under each Runner's settings.
To lock/unlock a Runner:
-1. Visit your project's **Settings ➔ CI/CD**
+1. Visit your project's **Settings > CI/CD**
1. Find the Runner you wish to lock/unlock and make sure it's enabled
1. Click the pencil button
1. Check the **Lock to current projects** option
@@ -115,7 +115,7 @@ you can enable the Runner also on any other project where you have Master permis
To enable/disable a Runner in your project:
-1. Visit your project's **Settings ➔ CI/CD**
+1. Visit your project's **Settings > CI/CD**
1. Find the Runner you wish to enable/disable
1. Click **Enable for this project** or **Disable for this project**
@@ -124,6 +124,13 @@ Consider that if you don't lock your specific Runner to a specific project, any
user with Master role in you project can assign your runner to another arbitrary
project without requiring your authorization, so use it with caution.
+An admin can enable/disable a specific Runner for projects:
+
+1. Navigate to **Admin > Runners**
+2. Find the Runner you wish to enable/disable
+3. Click edit on the Runner
+4. Click **Enable** or **Disable** on the project
+
## Protected Runners
>
@@ -136,7 +143,7 @@ Whenever a Runner is protected, the Runner picks only jobs created on
To protect/unprotect Runners:
-1. Visit your project's **Settings ➔ CI/CD**
+1. Visit your project's **Settings > CI/CD**
1. Find a Runner you want to protect/unprotect and make sure it's enabled
1. Click the pencil button besides the Runner name
1. Check the **Protected** option
@@ -231,6 +238,38 @@ To make a Runner pick tagged/untagged jobs:
1. Check the **Run untagged jobs** option
1. Click **Save changes** for the changes to take effect
+### Setting maximum job timeout for a Runner
+
+For each Runner you can specify a _maximum job timeout_. Such timeout,
+if smaller than [project defined timeout], will take the precedence. This
+feature can be used to prevent Shared Runner from being appropriated
+by a project by setting a ridiculous big timeout (e.g. one week).
+
+When not configured, Runner will not override project timeout.
+
+How this feature will work:
+
+**Example 1 - Runner timeout bigger than project timeout**
+
+1. You set the _maximum job timeout_ for a Runner to 24 hours
+1. You set the _CI/CD Timeout_ for a project to **2 hours**
+1. You start a job
+1. The job, if running longer, will be timeouted after **2 hours**
+
+**Example 2 - Runner timeout not configured**
+
+1. You remove the _maximum job timeout_ configuration from a Runner
+1. You set the _CI/CD Timeout_ for a project to **2 hours**
+1. You start a job
+1. The job, if running longer, will be timeouted after **2 hours**
+
+**Example 3 - Runner timeout smaller than project timeout**
+
+1. You set the _maximum job timeout_ for a Runner to **30 minutes**
+1. You set the _CI/CD Timeout_ for a project to 2 hours
+1. You start a job
+1. The job, if running longer, will be timeouted after **30 minutes**
+
### Be careful with sensitive information
With some [Runner Executors](https://docs.gitlab.com/runner/executors/README.html),
@@ -259,12 +298,6 @@ Mentioned briefly earlier, but the following things of Runners can be exploited.
We're always looking for contributions that can mitigate these
[Security Considerations](https://docs.gitlab.com/runner/security/).
-[install]: http://docs.gitlab.com/runner/install/
-[fifo]: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)
-[register]: http://docs.gitlab.com/runner/register/
-[protected branches]: ../../user/project/protected_branches.md
-[protected tags]: ../../user/project/protected_tags.md
-
## Determining the IP address of a Runner
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17286) in GitLab 10.6.
@@ -297,3 +330,10 @@ You can find the IP address of a Runner for a specific project by:
1. On the details page you should see a row for "IP Address"
![specific Runner IP address](img/specific_runner_ip_address.png)
+
+[install]: http://docs.gitlab.com/runner/install/
+[fifo]: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)
+[register]: http://docs.gitlab.com/runner/register/
+[protected branches]: ../../user/project/protected_branches.md
+[protected tags]: ../../user/project/protected_tags.md
+[project defined timeout]: ../../user/project/pipelines/settings.html#timeout
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index bd4aeb006bd..9f268f47e6f 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -449,6 +449,72 @@ export CI_REGISTRY_USER="gitlab-ci-token"
export CI_REGISTRY_PASSWORD="longalfanumstring"
```
+## Variables expressions
+
+> Variables expressions were added in GitLab 10.7.
+
+It is possible to use variables expressions with only / except policies in
+`.gitlab-ci.yml`. By using this approach you can limit what builds are going to
+be created within a pipeline after pushing code to GitLab.
+
+This is particularly useful in combination with secret variables and triggered
+pipeline variables.
+
+```yaml
+deploy:
+ script: cap staging deploy
+ environment: staging
+ only:
+ variables:
+ - $RELEASE == "staging"
+ - $STAGING
+```
+
+Each provided variables expression is going to be evaluated before creating
+a pipeline.
+
+If any of the conditions in `variables` evaluates to truth when using `only`,
+a new job is going to be created. If any of the expressions evaluates to truth
+when `except` is being used, a job is not going to be created.
+
+This follows usual rules for `only` / `except` policies.
+
+### Supported syntax
+
+Below you can find currently supported syntax reference:
+
+1. Equality matching using a string
+
+ Example: `$VARIABLE == "some value"`
+
+ You can use equality operator `==` to compare a variable content to a
+ string. We support both, double quotes and single quotes to define a string
+ value, so both `$VARIABLE == "some value"` and `$VARIABLE == 'some value'`
+ are supported. `"some value" == $VARIABLE` is correct too.
+
+1. Checking for an undefined value
+
+ It sometimes happens that you want to check whether variable is defined or
+ not. To do that, you can compare variable to `null` value, like
+ `$VARIABLE == null`. This expression is going to evaluate to truth if
+ variable is not set.
+
+1. Checking for an empty variable
+
+ If you want to check whether a variable is defined, but is empty, you can
+ simply compare it against an empty string, like `$VAR == ''`.
+
+1. Comparing two variables
+
+ It is possible to compare two variables. `$VARIABLE_1 == $VARIABLE_2`.
+
+1. Variable presence check
+
+ If you only want to create a job when there is some variable present,
+ which means that it is defined and non-empty, you can simply use
+ variable name as an expression, like `$STAGING`. If `$STAGING` variable
+ is defined, and is non empty, expression will evaluate to truth.
+
[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI secret variables"
[eep]: https://about.gitlab.com/products/ "Available only in GitLab Premium"
[envs]: ../environments.md
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index c2b06e53c2f..be114e7008e 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -10,6 +10,11 @@ of your repository and contains definitions of how your project should be built.
If you want a quick introduction to GitLab CI, follow our
[quick start guide](../quick_start/README.md).
+NOTE: **Note:**
+If you have a [mirrored repository where GitLab pulls from](https://docs.gitlab.com/ee/workflow/repository_mirroring.html#pulling-from-a-remote-repository),
+you may need to enable pipeline triggering in your project's
+**Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**.
+
## Jobs
The YAML file defines a set of jobs with constraints stating when they should
@@ -315,9 +320,14 @@ policy configuration.
GitLab now supports both, simple and complex strategies, so it is possible to
use an array and a hash configuration scheme.
-Two keys are now available: `refs` and `kubernetes`. Refs strategy equals to
-simplified only/except configuration, whereas kubernetes strategy accepts only
-`active` keyword.
+Three keys are now available: `refs`, `kubernetes` and `variables`.
+Refs strategy equals to simplified only/except configuration, whereas
+kubernetes strategy accepts only `active` keyword.
+
+`variables` keyword is used to define variables expressions. In other words
+you can use predefined variables / secret variables / project / group or
+environment-scoped variables to define an expression GitLab is going to
+evaluate in order to decide whether a job should be created or not.
See the example below. Job is going to be created only when pipeline has been
scheduled or runs for a `master` branch, and only if kubernetes service is
@@ -332,6 +342,20 @@ job:
kubernetes: active
```
+Example of using variables expressions:
+
+```yaml
+deploy:
+ only:
+ refs:
+ - branches
+ variables:
+ - $RELEASE == "staging"
+ - $STAGING
+```
+
+Learn more about variables expressions on a separate page.
+
## `tags`
`tags` is used to select specific Runners from the list of all Runners that are
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index fea92e740cb..3ba03d2d591 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -33,6 +33,26 @@ rest of the code should be as close to the CE files as possible.
[single code base]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2952#note_41016454
+### EE-specific comments
+
+When complete separation can't be achieved with the `ee/` directory, you can wrap
+code in EE specific comments to designate the difference from CE/EE and add
+some context for someone resolving a conflict.
+
+```rb
+# EE-specific start
+stub_licensed_features(variable_environment_scope: true)
+# EE specific end
+```
+
+```haml
+-# EE-specific start
+= render 'ci/variables/environment_scope', form_field: form_field, variable: variable
+-# EE-specific end
+```
+
+EE-specific comments should not be backported to CE.
+
### Detection of EE-only files
For each commit (except on `master`), the `ee-files-location-check` CI job tries
@@ -350,6 +370,255 @@ class beneath the `EE` module just as you would normally.
For example, if CE has LDAP classes in `lib/gitlab/ldap/` then you would place
EE-specific LDAP classes in `ee/lib/ee/gitlab/ldap`.
+### Code in `lib/api/`
+
+It can be very tricky to extend EE features by a single line of `prepend`,
+and for each different [Grape](https://github.com/ruby-grape/grape) feature,
+we might need different strategies to extend it. To apply different strategies
+easily, we would use `extend ActiveSupport::Concern` in the EE module.
+
+Put the EE module files following
+[EE features based on CE features](#ee-features-based-on-ce-features).
+
+#### EE API routes
+
+For EE API routes, we put them in a `prepended` block:
+
+``` ruby
+module EE
+ module API
+ module MergeRequests
+ extend ActiveSupport::Concern
+
+ prepended do
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: ::API::API::PROJECT_ENDPOINT_REQUIREMENTS do
+ # ...
+ end
+ end
+ end
+ end
+end
+```
+
+Note that due to namespace differences, we need to use the full qualifier for some
+constants.
+
+#### EE params
+
+We can define `params` and utilize `use` in another `params` definition to
+include params defined in EE. However, we need to define the "interface" first
+in CE in order for EE to override it. We don't have to do this in other places
+due to `prepend`, but Grape is complex internally and we couldn't easily do
+that, so we'll follow regular object-oriented practices that we define the
+interface first here.
+
+For example, suppose we have a few more optional params for EE, given this CE
+API code:
+
+``` ruby
+module API
+ class MergeRequests < Grape::API
+ # EE::API::MergeRequests would override the following helpers
+ helpers do
+ params :optional_params_ee do
+ end
+ end
+
+ prepend EE::API::MergeRequests
+
+ params :optional_params do
+ # CE specific params go here...
+
+ use :optional_params_ee
+ end
+ end
+end
+```
+
+And then we could override it in EE module:
+
+``` ruby
+module EE
+ module API
+ module MergeRequests
+ extend ActiveSupport::Concern
+
+ prepended do
+ helpers do
+ params :optional_params_ee do
+ # EE specific params go here...
+ end
+ end
+ end
+ end
+ end
+end
+```
+
+This way, the only difference between CE and EE for that API file would be
+`prepend EE::API::MergeRequests`.
+
+#### EE helpers
+
+To make it easy for an EE module to override the CE helpers, we need to define
+those helpers we want to extend first. Try to do that immediately after the
+class definition to make it easy and clear:
+
+``` ruby
+module API
+ class JobArtifacts < Grape::API
+ # EE::API::JobArtifacts would override the following helpers
+ helpers do
+ def authorize_download_artifacts!
+ authorize_read_builds!
+ end
+ end
+
+ prepend EE::API::JobArtifacts
+ end
+end
+```
+
+And then we can follow regular object-oriented practices to override it:
+
+``` ruby
+module EE
+ module API
+ module JobArtifacts
+ extend ActiveSupport::Concern
+
+ prepended do
+ helpers do
+ def authorize_download_artifacts!
+ super
+ check_cross_project_pipelines_feature!
+ end
+ end
+ end
+ end
+ end
+end
+```
+
+#### EE-specific behaviour
+
+Sometimes we need EE-specific behaviour in some of the APIs. Normally we could
+use EE methods to override CE methods, however API routes are not methods and
+therefore can't be simply overridden. We need to extract them into a standalone
+method, or introduce some "hooks" where we could inject behavior in the CE
+route. Something like this:
+
+``` ruby
+module API
+ class MergeRequests < Grape::API
+ helpers do
+ # EE::API::MergeRequests would override the following helpers
+ def update_merge_request_ee(merge_request)
+ end
+ end
+
+ prepend EE::API::MergeRequests
+
+ put ':id/merge_requests/:merge_request_iid/merge' do
+ merge_request = find_project_merge_request(params[:merge_request_iid])
+
+ # ...
+
+ update_merge_request_ee(merge_request)
+
+ # ...
+ end
+ end
+end
+```
+
+Note that `update_merge_request_ee` doesn't do anything in CE, but
+then we could override it in EE:
+
+``` ruby
+module EE
+ module API
+ module MergeRequests
+ extend ActiveSupport::Concern
+
+ prepended do
+ helpers do
+ def update_merge_request_ee(merge_request)
+ # ...
+ end
+ end
+ end
+ end
+ end
+end
+```
+
+#### EE `route_setting`
+
+It's very hard to extend this in an EE module, and this is simply storing
+some meta-data for a particular route. Given that, we could simply leave the
+EE `route_setting` in CE as it won't hurt and we are just not going to use
+those meta-data in CE.
+
+We could revisit this policy when we're using `route_setting` more and whether
+or not we really need to extend it from EE. For now we're not using it much.
+
+#### Utilizing class methods for setting up EE-specific data
+
+Sometimes we need to use different arguments for a particular API route, and we
+can't easily extend it with an EE module because Grape has different context in
+different blocks. In order to overcome this, we could use class methods from the
+API class.
+
+For example, in one place we need to pass an extra argument to
+`at_least_one_of` so that the API could consider an EE-only argument as the
+least argument. This is not quite beautiful but it's working:
+
+``` ruby
+module API
+ class MergeRequests < Grape::API
+ def self.update_params_at_least_one_of
+ %i[
+ assignee_id
+ description
+ ]
+ end
+
+ prepend EE::API::MergeRequests
+
+ params do
+ at_least_one_of(*::API::MergeRequests.update_params_at_least_one_of)
+ end
+ end
+end
+```
+
+And then we could easily extend that argument in the EE class method:
+
+``` ruby
+module EE
+ module API
+ module MergeRequests
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def update_params_at_least_one_of
+ super.push(*%i[
+ squash
+ ])
+ end
+ end
+ end
+ end
+end
+```
+
+It could be annoying if we need this for a lot of routes, but it might be the
+simplest solution right now.
+
### Code in `spec/`
When you're testing EE-only features, avoid adding examples to the
@@ -405,12 +674,13 @@ to avoid conflicts during CE to EE merge.
}
}
-/* EE-specific styles */
+// EE-specific start
.section-body.ee-section-body {
.section-title {
background: $gl-header-color-cyan;
}
}
+// EE-specific end
```
## gitlab-svgs
diff --git a/doc/development/new_fe_guide/style/javascript.md b/doc/development/new_fe_guide/style/javascript.md
index 480d50a211f..57efd9353bc 100644
--- a/doc/development/new_fe_guide/style/javascript.md
+++ b/doc/development/new_fe_guide/style/javascript.md
@@ -1,3 +1,195 @@
# JavaScript style guide
-> TODO: Add content
+We use [Airbnb's JavaScript Style Guide][airbnb-style-guide] and it's accompanying linter to manage most of our JavaScript style guidelines.
+
+In addition to the style guidelines set by Airbnb, we also have a few specific rules listed below.
+
+> **Tip:**
+You can run eslint locally by running `yarn eslint`
+
+## Arrays
+
+<a name="avoid-foreach"></a><a name="1.1"></a>
+- [1.1](#avoid-foreach) **Avoid ForEach when mutating data** Use `map`, `reduce` or `filter` instead of `forEach` when mutating data. This will minimize mutations in functions ([which is aligned with Airbnb's style guide][airbnb-minimize-mutations])
+
+```
+// bad
+users.forEach((user, index) => {
+ user.id = index;
+});
+
+// good
+const usersWithId = users.map((user, index) => {
+ return Object.assign({}, user, { id: index });
+});
+```
+
+## Functions
+
+<a name="limit-params"></a><a name="2.1"></a>
+- [2.1](#limit-params) **Limit number of parameters** If your function or method has more than 3 parameters, use an object as a parameter instead.
+
+```
+// bad
+function a(p1, p2, p3) {
+ // ...
+};
+
+// good
+function a(p) {
+ // ...
+};
+```
+
+## Classes & constructors
+
+<a name="avoid-constructor-side-effects"></a><a name="3.1"></a>
+- [3.1](#avoid-constructor-side-effects) **Avoid side effects in constructors** Avoid making some operations in the `constructor`, such as asynchronous calls, API requests and DOM manipulations. Prefer moving them into separate functions. This will make tests easier to write and code easier to maintain.
+
+ ```javascript
+ // bad
+ class myClass {
+ constructor(config) {
+ this.config = config;
+ axios.get(this.config.endpoint)
+ }
+ }
+
+ // good
+ class myClass {
+ constructor(config) {
+ this.config = config;
+ }
+
+ makeRequest() {
+ axios.get(this.config.endpoint)
+ }
+ }
+ const instance = new myClass();
+ instance.makeRequest();
+
+ ```
+
+<a name="avoid-classes-to-handle-dom-events"></a><a name="3.2"></a>
+- [3.2](#avoid-classes-to-handle-dom-events) **Avoid classes to handle DOM events** If the only purpose of the class is to bind a DOM event and handle the callback, prefer using a function.
+
+```
+// bad
+class myClass {
+ constructor(config) {
+ this.config = config;
+ }
+
+ init() {
+ document.addEventListener('click', () => {});
+ }
+}
+
+// good
+
+const myFunction = () => {
+ document.addEventListener('click', () => {
+ // handle callback here
+ });
+}
+```
+
+<a name="element-container"></a><a name="3.3"></a>
+- [3.3](#element-container) **Pass element container to constructor** When your class manipulates the DOM, receive the element container as a parameter.
+This is more maintainable and performant.
+
+```
+// bad
+class a {
+ constructor() {
+ document.querySelector('.b');
+ }
+}
+
+// good
+class a {
+ constructor(options) {
+ options.container.querySelector('.b');
+ }
+}
+```
+
+## Type Casting & Coercion
+
+<a name="use-parseint"></a><a name="4.1"></a>
+- [4.1](#use-parseint) **Use ParseInt** Use `ParseInt` when converting a numeric string into a number.
+
+```
+// bad
+Number('10')
+
+
+// good
+parseInt('10', 10);
+```
+
+## CSS Selectors
+
+<a name="use-js-prefix"></a><a name="5.1"></a>
+- [5.1](#use-js-prefix) **Use js prefix** If a CSS class is only being used in JavaScript as a reference to the element, prefix the class name with `js-`
+
+```
+// bad
+<button class="add-user"></button>
+
+// good
+<button class="js-add-user"></button>
+```
+
+## Modules
+
+<a name="use-absolute-paths"></a><a name="6.1"></a>
+- [6.1](#use-absolute-paths) **Use absolute paths for nearby modules** Use absolute paths if the module you are importing is less than two levels up.
+
+```
+// bad
+import GitLabStyleGuide from '~/guides/GitLabStyleGuide';
+
+// good
+import GitLabStyleGuide from '../GitLabStyleGuide';
+```
+
+<a name="use-relative-paths"></a><a name="6.2"></a>
+- [6.2](#use-relative-paths) **Use relative paths for distant modules** If the module you are importing is two or more levels up, use a relative path instead of an absolute path.
+
+```
+// bad
+import GitLabStyleGuide from '../../../guides/GitLabStyleGuide';
+
+// good
+import GitLabStyleGuide from '~/GitLabStyleGuide';
+```
+
+<a name="global-namespace"></a><a name="6.3"></a>
+- [6.3](#global-namespace) **Do not add to global namespace**
+
+<a name="domcontentloaded"></a><a name="6.4"></a>
+- [6.4](#domcontentloaded) **Do not use DOMContentLoaded in non-page modules** Imported modules should act the same each time they are loaded. `DOMContentLoaded` events are only allowed on modules loaded in the `/pages/*` directory because those are loaded dynamically with webpack.
+
+## Security
+
+<a name="avoid-xss"></a><a name="7.1"></a>
+- [7.1](#avoid-xss) **Avoid XSS** Do not use `innerHTML`, `append()` or `html()` to set content. It opens up too many vulnerabilities.
+
+## ESLint
+
+<a name="disable-eslint-file"></a><a name="8.1"></a>
+- [8.1](#disable-eslint-file) **Disabling ESLint in new files** Do not disable ESLint when creating new files. Existing files may have existing rules disabled due to legacy compatibility reasons but they are in the process of being refactored.
+
+<a name="disable-eslint-rule"></a><a name="8.2"></a>
+- [8.2](#disable-eslint-rule) **Disabling ESLint rule** Do not disable specific ESLint rules. Due to technical debt, you may disable the following rules only if you are invoking/instantiating existing code modules
+
+ - [no-new][no-new]
+ - [class-method-use-this][class-method-use-this]
+
+> Note: Disable these rules on a per line basis. This makes it easier to refactor in the future. E.g. use `eslint-disable-next-line` or `eslint-disable-line`
+
+[airbnb-style-guide]: https://github.com/airbnb/javascript
+[airbnb-minimize-mutations]: https://github.com/airbnb/javascript#testing--for-real
+[no-new]: http://eslint.org/docs/rules/no-new
+[class-method-use-this]: http://eslint.org/docs/rules/class-methods-use-this
diff --git a/doc/install/README.md b/doc/install/README.md
index 87f6969b415..9724b56910d 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -36,7 +36,7 @@ the full process of installing GitLab on Google Container Engine (GKE), pushing
- [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.
-- [Getting started with GitLab and DigitalOcean](ttps://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/): requirements, installation process, updates.
+- [Getting started with GitLab and DigitalOcean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/): requirements, installation process, updates.
- [Demo: Cloud Native Development with GitLab](https://about.gitlab.com/2017/04/18/cloud-native-demo/): video demonstration on how to install GitLab on Kubernetes, build a project, create Review Apps, store Docker images in Container Registry, deploy to production on Kubernetes, and monitor with Prometheus.
## Database
diff --git a/doc/integration/google.md b/doc/integration/google.md
index 07a700f7b64..ae1d848f439 100644
--- a/doc/integration/google.md
+++ b/doc/integration/google.md
@@ -35,7 +35,7 @@ In Google's side:
1. You should now be able to see a Client ID and Client secret. Note them down
or keep this page open as you will need them later.
-1. From the **Dashboard** select **ENABLE APIS AND SERVICES > Compute > Google Container Engine API > Enable**
+1. From the **Dashboard** select **ENABLE APIS AND SERVICES > Compute > Google Kubernetes Engine API > Enable**
On your GitLab server:
diff --git a/doc/policy/maintenance.md b/doc/policy/maintenance.md
index 8d0afa9e692..7f028565412 100644
--- a/doc/policy/maintenance.md
+++ b/doc/policy/maintenance.md
@@ -44,7 +44,7 @@ This decision is made on a case-by-case basis.
## Upgrade recommendations
-We encourage everyone to run the latest stable release to ensure that you can
+We encourage everyone to run the [latest stable release](https://about.gitlab.com/blog/categories/release/) to ensure that you can
easily upgrade to the most secure and feature-rich GitLab experience. In order
to make sure you can easily run the most recent stable release, we are working
hard to keep the update process simple and reliable.
diff --git a/doc/update/10.5-to-10.6.md b/doc/update/10.5-to-10.6.md
index f5c5c305726..2f90fb62c4a 100644
--- a/doc/update/10.5-to-10.6.md
+++ b/doc/update/10.5-to-10.6.md
@@ -103,7 +103,7 @@ rm go1.8.3.linux-amd64.tar.gz
```bash
cd /home/git/gitlab
-sudo -u git -H git fetch --all
+sudo -u git -H git fetch --all --prune
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
sudo -u git -H git checkout -- locale
```
@@ -131,7 +131,7 @@ sudo -u git -H git checkout 10-6-stable-ee
```bash
cd /home/git/gitlab-shell
-sudo -u git -H git fetch --all --tags
+sudo -u git -H git fetch --all --tags --prune
sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
sudo -u git -H bin/compile
```
@@ -146,7 +146,7 @@ If you are not using Linux you may have to run `gmake` instead of
```bash
cd /home/git/gitlab-workhorse
-sudo -u git -H git fetch --all --tags
+sudo -u git -H git fetch --all --tags --prune
sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
sudo -u git -H make
```
@@ -182,7 +182,7 @@ sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gital
```shell
cd /home/git/gitaly
-sudo -u git -H git fetch --all --tags
+sudo -u git -H git fetch --all --tags --prune
sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
sudo -u git -H make
```
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 83eb7a225b2..d5f77191938 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -51,7 +51,7 @@ Below are the settings for [GitLab Pages].
| TLS certificates support| yes | no |
The maximum size of your Pages site is regulated by the artifacts maximum size
-which is part of [GitLab CI](#gitlab-ci).
+which is part of [GitLab CI/CD](#gitlab-ci-cd).
## GitLab CI/CD
@@ -61,6 +61,14 @@ Below are the current settings regarding [GitLab CI/CD](../../ci/README.md).
| ----------- | ----------------- | ------------- |
| Artifacts maximum size | 1G | 100M |
+## Repository size limit
+
+The maximum size your Git repository is allowed to be including LFS.
+
+| Setting | GitLab.com | Default |
+| ----------- | ----------------- | ------------- |
+| Repository size including LFS | 10G | Unlimited |
+
## Shared Runners
Shared Runners on GitLab.com run in [autoscale mode] and powered by
diff --git a/doc/user/project/integrations/img/jira_workflow_screenshot.png b/doc/user/project/integrations/img/jira_workflow_screenshot.png
deleted file mode 100644
index e62fb202613..00000000000
--- a/doc/user/project/integrations/img/jira_workflow_screenshot.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md
index fc527663db0..5933bcedc8b 100644
--- a/doc/user/project/integrations/jira.md
+++ b/doc/user/project/integrations/jira.md
@@ -113,7 +113,20 @@ in the table below.
| `JIRA API URL` | The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. |
| `Username` | The user name created in [configuring JIRA step](#configuring-jira). |
| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). |
-| `Transition ID` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** |
+| `Transition ID` | This is the ID of a transition that moves issues to the desired state. **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** |
+
+### Getting a transition ID
+
+In the most recent JIRA UI, you can no longer see transition IDs in the workflow
+administration UI. You can get the ID you need in either of the following ways:
+
+1. By using the API, with a request like `https://yourcompany.atlassian.net/rest/api/2/issue/ISSUE-123/transitions`
+ using an issue that is in the appropriate "open" state
+1. By mousing over the link for the transition you want and looking for the
+ "action" parameter in the URL
+
+Note that the transition ID may vary between workflows (e.g., bug vs. story),
+even if the status you are changing to is the same.
After saving the configuration, your GitLab project will be able to interact
with all JIRA projects in your JIRA instance and you'll see the JIRA link on the GitLab project pages that takes you to the appropriate JIRA project.
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index d403d5698a9..b4a842f33d6 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -251,7 +251,7 @@ Different issue board features are available in different [GitLab tiers](https:/
| Tier | Number of project issue boards | Board with configuration in project issue boards | Number of group issue boards | Board with configuration in group issue boards |
| --- | --- | --- | --- | --- |
-| Libre | 1 | No | 1 | No |
+| Core | 1 | No | 1 | No |
| Starter | Multiple | Yes | 1 | No |
| Premium | Multiple | Yes | Multiple | Yes |
| Ultimate | Multiple | Yes | Multiple | Yes |
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index 43451844f2d..6cead7b9961 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -27,6 +27,13 @@ The default value is 60 minutes. Decrease the time limit if you want to impose
a hard limit on your jobs' running time or increase it otherwise. In any case,
if the job surpasses the threshold, it is marked as failed.
+### Timeout overriding on Runner level
+
+> - [Introduced][ce-17221] in GitLab 10.7.
+
+Project defined timeout (either specific timeout set by user or the default
+60 minutes timeout) may be [overridden on Runner level][timeout overriding].
+
## Custom CI config path
> - [Introduced][ce-12509] in GitLab 9.4.
@@ -152,5 +159,7 @@ into your `README.md`:
[var]: ../../../ci/yaml/README.md#git-strategy
[coverage report]: #test-coverage-parsing
+[timeout overriding]: ../../../ci/runners/README.html#setting-maximum-job-timeout-for-a-runner
[ce-9362]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9362
[ce-12509]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12509
+[ce-17221]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17221
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index ae131d51305..376f4e3cbe4 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -132,8 +132,9 @@ Use GPG to [sign your commits](gpg_signed_commits/index.md).
## Repository size
-In GitLab.com, your repository size limit it 10GB. For other instances,
-the repository size is limited by your system administrators.
+On GitLab.com, your [repository size limit is 10GB](../../gitlab_com/index.md#repository-size-limit)
+(including LFS). For other instances, the repository size is limited by your
+system administrators.
You can also [reduce a repository size using Git](reducing_the_repo_size_using_git.md).
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index cac3cb599dd..f824756c10c 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -19,7 +19,7 @@ There are various configuration options to help GitLab server administrators:
* Changing the location of LFS object storage
* Setting up AWS S3 compatible object storage
-### Omnibus packages
+### Configuration for Omnibus installations
In `/etc/gitlab/gitlab.rb`:
@@ -33,7 +33,7 @@ gitlab_rails['lfs_enabled'] = false
gitlab_rails['lfs_storage_path'] = "/mnt/storage/lfs-objects"
```
-### Installations from source
+### Configuration for installations from source
In `config/gitlab.yml`:
@@ -44,20 +44,21 @@ In `config/gitlab.yml`:
storage_path: /mnt/storage/lfs-objects
```
-## Setting up S3 compatible object storage
+## Storing the LFS objects in an S3-compatible object storage
-> **Note:** [Introduced][ee-2760] in [GitLab Premium][eep] 10.0.
-> Available in [GitLab CE][ce] 10.7
+> [Introduced][ee-2760] in [GitLab Premium][eep] 10.0. Brought to GitLab Core
+in 10.7.
-It is possible to store LFS objects on remote object storage instead of on a local disk.
+It is possible to store LFS objects on a remote object storage which allows you
+to offload storage to an external AWS S3 compatible service, freeing up disk
+space locally. You can also host your own S3 compatible storage decoupled from
+GitLab, with with a service such as [Minio](https://www.minio.io/).
-This allows you to offload storage to an external AWS S3 compatible service, freeing up disk space locally. You can also host your own S3 compatible storage decoupled from GitLab, with with a service such as [Minio](https://www.minio.io/).
+Object storage currently transfers files first to GitLab, and then on the
+object storage in a second stage. This can be done either by using a rake task
+to transfer existing objects, or in a background job after each file is received.
-Object storage currently transfers files first to GitLab, and then on the object storage in a second stage. This can be done either by using a rake task to transfer existing objects, or in a background job after each file is received.
-
-### Object Storage Settings
-
-For source installations the following settings are nested under `lfs:` and then `object_store:`. On omnibus installs they are prefixed by `lfs_object_store_`.
+The following general settings are supported.
| Setting | Description | Default |
|---------|-------------|---------|
@@ -68,9 +69,7 @@ For source installations the following settings are nested under `lfs:` and then
| `proxy_download` | Set to true to enable proxying all files served. Option allows to reduce egress traffic as this allows clients to download directly from remote storage instead of proxying all data | `false` |
| `connection` | Various connection options described below | |
-#### S3 compatible connection settings
-
-The connection settings match those provided by [Fog](https://github.com/fog), and are as follows:
+The `connection` settings match those provided by [Fog](https://github.com/fog).
| Setting | Description | Default |
|---------|-------------|---------|
@@ -82,8 +81,43 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
| `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
| `path_style` | Set to true to use `host/bucket_name/object` style paths instead of `bucket_name.host/object`. Leave as false for AWS S3 | false |
+### S3 for Omnibus installations
+
+On Omnibus installations, the settings are prefixed by `lfs_object_store_`:
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following lines by replacing with
+ the values you want:
+
+ ```ruby
+ gitlab_rails['lfs_object_store_enabled'] = true
+ gitlab_rails['lfs_object_store_remote_directory'] = "lfs-objects"
+ gitlab_rails['lfs_object_store_connection'] = {
+ 'provider' => 'AWS',
+ 'region' => 'eu-central-1',
+ 'aws_access_key_id' => '1ABCD2EFGHI34JKLM567N',
+ 'aws_secret_access_key' => 'abcdefhijklmnopQRSTUVwxyz0123456789ABCDE',
+ # The below options configure an S3 compatible host instead of AWS
+ 'host' => 'localhost',
+ 'endpoint' => 'http://127.0.0.1:9000',
+ 'path_style' => true
+ }
+ ```
+
+1. Save the file and [reconfigure GitLab]s for the changes to take effect.
+1. Migrate any existing local LFS objects to the object storage:
+
+ ```bash
+ gitlab-rake gitlab:lfs:migrate
+ ```
+
+ This will migrate existing LFS objects to object storage. New LFS objects
+ will be forwarded to object storage unless
+ `gitlab_rails['lfs_object_store_background_upload']` is set to false.
-### From source
+### S3 for installations from source
+
+For source installations the settings are nested under `lfs:` and then
+`object_store:`:
1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following
lines:
@@ -108,44 +142,13 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
1. Save the file and [restart GitLab][] for the changes to take effect.
1. Migrate any existing local LFS objects to the object storage:
- ```bash
- sudo -u git -H bundle exec rake gitlab:lfs:migrate RAILS_ENV=production
- ```
-
- This will migrate existing LFS objects to object storage. New LFS objects
- will be forwarded to object storage unless
- `gitlab_rails['lfs_object_store_background_upload']` is set to false.
-
-### In Omnibus
-
-1. Edit `/etc/gitlab/gitlab.rb` and add the following lines by replacing with
- the values you want:
-
- ```ruby
- gitlab_rails['lfs_object_store_enabled'] = true
- gitlab_rails['lfs_object_store_remote_directory'] = "lfs-objects"
- gitlab_rails['lfs_object_store_connection'] = {
- 'provider' => 'AWS',
- 'region' => 'eu-central-1',
- 'aws_access_key_id' => '1ABCD2EFGHI34JKLM567N',
- 'aws_secret_access_key' => 'abcdefhijklmnopQRSTUVwxyz0123456789ABCDE',
- # The below options configure an S3 compatible host instead of AWS
- 'host' => 'localhost',
- 'endpoint' => 'http://127.0.0.1:9000',
- 'path_style' => true
- }
- ```
-
-1. Save the file and [reconfigure GitLab]s for the changes to take effect.
-1. Migrate any existing local LFS objects to the object storage:
-
- ```bash
- gitlab-rake gitlab:lfs:migrate
- ```
+ ```bash
+ sudo -u git -H bundle exec rake gitlab:lfs:migrate RAILS_ENV=production
+ ```
- This will migrate existing LFS objects to object storage. New LFS objects
- will be forwarded to object storage unless
- `gitlab_rails['lfs_object_store_background_upload']` is set to false.
+ This will migrate existing LFS objects to object storage. New LFS objects
+ will be forwarded to object storage unless `background_upload` is set to
+ false.
## Storage statistics
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index 3d8d3ce8f13..e612646cfbc 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -28,11 +28,10 @@ A Todo appears in your Todos dashboard when:
- an issue or merge request is assigned to you,
- you are `@mentioned` in an issue or merge request, be it the description of
the issue/merge request or in a comment,
+- you are `@mentioned` in a comment on a commit,
- a job in the CI pipeline running for your merge request failed, but this
job is not allowed to fail.
->**Note:** Commenting on a commit will _not_ trigger a Todo.
-
### Directly addressed Todos
> [Introduced][ce-7926] in GitLab 9.0.
diff --git a/features/groups.feature b/features/groups.feature
deleted file mode 100644
index 4044bd9be79..00000000000
--- a/features/groups.feature
+++ /dev/null
@@ -1,73 +0,0 @@
-Feature: Groups
- Background:
- Given I sign in as "John Doe"
- And "John Doe" is owner of group "Owned"
-
- Scenario: I should not see a group if it does not exist
- When I visit group "NonExistentGroup" page
- Then page status code should be 404
-
- @javascript
- Scenario: I should see group "Owned" dashboard list
- When I visit group "Owned" page
- Then I should see group "Owned" projects list
-
- @javascript
- Scenario: I should see group "Owned" activity feed
- When I visit group "Owned" activity page
- And I should see projects activity feed
-
- Scenario: I should see group "Owned" issues list
- Given project from group "Owned" has issues assigned to me
- When I visit group "Owned" issues page
- Then I should see issues from group "Owned" assigned to me
-
- Scenario: I should not see issues from archived project in "Owned" group issues list
- Given Group "Owned" has archived project
- And the archived project have some issues
- When I visit group "Owned" issues page
- Then I should not see issues from the archived project
-
- Scenario: I should see group "Owned" merge requests list
- Given project from group "Owned" has merge requests assigned to me
- When I visit group "Owned" merge requests page
- Then I should see merge requests from group "Owned" assigned to me
-
- Scenario: I should not see merge requests from archived project in "Owned" group merge requests list
- Given Group "Owned" has archived project
- And the archived project have some merge_requests
- When I visit group "Owned" merge requests page
- Then I should not see merge requests from the archived project
-
- Scenario: I edit group "Owned" avatar
- When I visit group "Owned" settings page
- And I change group "Owned" avatar
- And I visit group "Owned" settings page
- Then I should see new group "Owned" avatar
- And I should see the "Remove avatar" button
-
- Scenario: I remove group "Owned" avatar
- When I visit group "Owned" settings page
- And I have group "Owned" avatar
- And I visit group "Owned" settings page
- And I remove group "Owned" avatar
- Then I should not see group "Owned" avatar
- And I should not see the "Remove avatar" button
-
- # Group projects in settings
- Scenario: I should see all projects in the project list in settings
- Given Group "Owned" has archived project
- When I visit group "Owned" projects page
- Then I should see group "Owned" projects list
- And I should see "archived" label
-
- # Public group
- @javascript
- Scenario: Signed out user should see group
- Given "Mary Jane" is owner of group "Owned"
- And I am a signed out user
- And Group "Owned" has a public project "Public-project"
- When I visit group "Owned" page
- Then I should see group "Owned"
- Then I should see project "Public-project"
-
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
deleted file mode 100644
index 753694a5392..00000000000
--- a/features/steps/groups.rb
+++ /dev/null
@@ -1,147 +0,0 @@
-class Spinach::Features::Groups < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedGroup
- include SharedUser
-
- step 'I should see group "Owned"' do
- expect(page).to have_content 'Owned'
- end
-
- step 'I am a signed out user' do
- logout
- end
-
- step 'Group "Owned" has a public project "Public-project"' do
- group = owned_group
-
- @project = create :project, :public,
- group: group,
- name: "Public-project"
- end
-
- step 'I should see project "Public-project"' do
- expect(page).to have_content 'Public-project'
- end
-
- step 'I should see group "Owned" projects list' do
- owned_group.projects.each do |project|
- expect(page).to have_link project.name
- end
- end
-
- step 'I should see projects activity feed' do
- expect(page).to have_content 'joined project'
- end
-
- step 'I should see issues from group "Owned" assigned to me' do
- assigned_to_me(:issues).each do |issue|
- expect(page).to have_content issue.title
- end
- end
-
- step 'I should not see issues from the archived project' do
- @archived_project.issues.each do |issue|
- expect(page).not_to have_content issue.title
- end
- end
-
- step 'I should not see merge requests from the archived project' do
- @archived_project.merge_requests.each do |mr|
- expect(page).not_to have_content mr.title
- end
- end
-
- step 'I should see merge requests from group "Owned" assigned to me' do
- assigned_to_me(:merge_requests).each do |issue|
- expect(page).to have_content issue.title[0..80]
- end
- end
-
- step 'project from group "Owned" has issues assigned to me' do
- create :issue,
- project: project,
- assignees: [current_user],
- author: current_user
- end
-
- step 'project from group "Owned" has merge requests assigned to me' do
- create :merge_request,
- source_project: project,
- target_project: project,
- assignee: current_user,
- author: current_user
- end
-
- step 'I change group "Owned" avatar' do
- attach_file(:group_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
- click_button "Save group"
- owned_group.reload
- end
-
- step 'I should see new group "Owned" avatar' do
- expect(owned_group.avatar).to be_instance_of AvatarUploader
- expect(owned_group.avatar.url).to eq "/uploads/-/system/group/avatar/#{Group.find_by(name: "Owned").id}/banana_sample.gif"
- end
-
- step 'I should see the "Remove avatar" button' do
- expect(page).to have_link("Remove avatar")
- end
-
- step 'I have group "Owned" avatar' do
- attach_file(:group_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
- click_button "Save group"
- owned_group.reload
- end
-
- step 'I remove group "Owned" avatar' do
- click_link "Remove avatar"
- owned_group.reload
- end
-
- step 'I should not see group "Owned" avatar' do
- expect(owned_group.avatar?).to eq false
- end
-
- step 'I should not see the "Remove avatar" button' do
- expect(page).not_to have_link("Remove avatar")
- end
-
- step 'Group "Owned" has archived project' do
- group = Group.find_by(name: 'Owned')
- @archived_project = create(:project, :archived, namespace: group, path: "archived-project")
- end
-
- step 'I should see "archived" label' do
- expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
- end
-
- step 'I visit group "NonExistentGroup" page' do
- visit group_path("NonExistentGroup")
- end
-
- step 'the archived project have some issues' do
- create :issue,
- project: @archived_project,
- assignees: [current_user],
- author: current_user
- end
-
- step 'the archived project have some merge requests' do
- create :merge_request,
- source_project: @archived_project,
- target_project: @archived_project,
- assignee: current_user,
- author: current_user
- end
-
- private
-
- def assigned_to_me(key)
- project.send(key).assigned_to(current_user)
- end
-
- def project
- owned_group.projects.first
- end
-end
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index 3a762be8f1f..bba30a72325 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -143,7 +143,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end
step 'I create bare repo' do
- click_link 'Create empty bare repository'
+ click_link 'Create empty repository'
end
step 'I should see command line instructions' do
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 982f45425a3..684955a1b24 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -231,6 +231,20 @@ module API
render_api_error!("Failed to save note #{note.errors.messages}", 400)
end
end
+
+ desc 'Get Merge Requests associated with a commit' do
+ success Entities::MergeRequestBasic
+ end
+ params do
+ requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag on which to find Merge Requests'
+ use :pagination
+ end
+ get ':id/repository/commits/:sha/merge_requests', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
+ commit = user_project.commit(params[:sha])
+ not_found! 'Commit' unless commit
+
+ present paginate(commit.merge_requests), with: Entities::MergeRequestBasic
+ end
end
end
end
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index b0b7b50998f..70d43ac1d79 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -54,7 +54,7 @@ module API
present key, with: Entities::DeployKeysProject
end
- desc 'Add new deploy key to currently authenticated user' do
+ desc 'Add new deploy key to a project' do
success Entities::DeployKeysProject
end
params do
@@ -66,33 +66,32 @@ module API
params[:key].strip!
# Check for an existing key joined to this project
- key = user_project.deploy_keys_projects
+ deploy_key_project = user_project.deploy_keys_projects
.joins(:deploy_key)
.find_by(keys: { key: params[:key] })
- if key
- present key, with: Entities::DeployKeysProject
+ if deploy_key_project
+ present deploy_key_project, with: Entities::DeployKeysProject
break
end
# Check for available deploy keys in other projects
key = current_user.accessible_deploy_keys.find_by(key: params[:key])
if key
- added_key = add_deploy_keys_project(user_project, deploy_key: key, can_push: !!params[:can_push])
+ deploy_key_project = add_deploy_keys_project(user_project, deploy_key: key, can_push: !!params[:can_push])
- present added_key, with: Entities::DeployKeysProject
+ present deploy_key_project, with: Entities::DeployKeysProject
break
end
# Create a new deploy key
- key_attributes = { can_push: !!params[:can_push],
- deploy_key_attributes: declared_params.except(:can_push) }
- key = add_deploy_keys_project(user_project, key_attributes)
+ deploy_key_attributes = declared_params.except(:can_push).merge(user: current_user)
+ deploy_key_project = add_deploy_keys_project(user_project, deploy_key_attributes: deploy_key_attributes, can_push: !!params[:can_push])
- if key.valid?
- present key, with: Entities::DeployKeysProject
+ if deploy_key_project.valid?
+ present deploy_key_project, with: Entities::DeployKeysProject
else
- render_validation_error!(key)
+ render_validation_error!(deploy_key_project)
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 16147ee90c9..b7a390696c7 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -405,6 +405,7 @@ module API
class IssueBasic < ProjectEntity
expose :closed_at
+ expose :closed_by, using: Entities::UserBasic
expose :labels do |issue, options|
# Avoids an N+1 query since labels are preloaded
issue.labels.map(&:title).sort
@@ -951,6 +952,7 @@ module API
expose :tag_list
expose :run_untagged
expose :locked
+ expose :maximum_timeout
expose :access_level
expose :version, :revision, :platform, :architecture
expose :contacted_at
@@ -1119,7 +1121,7 @@ module API
end
class RunnerInfo < Grape::Entity
- expose :timeout
+ expose :metadata_timeout, as: :timeout
end
class Step < Grape::Entity
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 14648588dfd..abe3d353984 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -29,18 +29,6 @@ module API
{}
end
- def fix_git_env_repository_paths(env, repository_path)
- if obj_dir_relative = env['GIT_OBJECT_DIRECTORY_RELATIVE'].presence
- env['GIT_OBJECT_DIRECTORY'] = File.join(repository_path, obj_dir_relative)
- end
-
- if alt_obj_dirs_relative = env['GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE'].presence
- env['GIT_ALTERNATE_OBJECT_DIRECTORIES'] = alt_obj_dirs_relative.map { |dir| File.join(repository_path, dir) }
- end
-
- env
- end
-
def log_user_activity(actor)
commands = Gitlab::GitAccess::DOWNLOAD_COMMANDS
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index b3660e4a1d0..fcbc248fc3b 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -21,8 +21,7 @@ module API
# Stores some Git-specific env thread-safely
env = parse_env
- env = fix_git_env_repository_paths(env, repository_path) if project
- Gitlab::Git::Env.set(env)
+ Gitlab::Git::HookEnv.set(gl_repository, env) if project
actor =
if params[:key_id]
diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb
index efc4a33ae1b..5ef4e9d530c 100644
--- a/lib/api/project_export.rb
+++ b/lib/api/project_export.rb
@@ -33,11 +33,28 @@ module API
end
params do
optional :description, type: String, desc: 'Override the project description'
+ optional :upload, type: Hash do
+ optional :url, type: String, desc: 'The URL to upload the project'
+ optional :http_method, type: String, default: 'PUT', desc: 'HTTP method to upload the exported project'
+ end
end
post ':id/export' do
project_export_params = declared_params(include_missing: false)
+ after_export_params = project_export_params.delete(:upload) || {}
- user_project.add_export_job(current_user: current_user, params: project_export_params)
+ export_strategy = if after_export_params[:url].present?
+ params = after_export_params.slice(:url, :http_method).symbolize_keys
+
+ Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy.new(params)
+ end
+
+ if export_strategy&.invalid?
+ render_validation_error!(export_strategy)
+ else
+ user_project.add_export_job(current_user: current_user,
+ after_export_strategy: export_strategy,
+ params: project_export_params)
+ end
accepted!
end
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 8da97a97754..57c0a729535 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -14,9 +14,10 @@ module API
optional :locked, type: Boolean, desc: 'Should Runner be locked for current project'
optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs'
optional :tag_list, type: Array[String], desc: %q(List of Runner's tags)
+ optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job'
end
post '/' do
- attributes = attributes_for_keys([:description, :locked, :run_untagged, :tag_list])
+ attributes = attributes_for_keys([:description, :locked, :run_untagged, :tag_list, :maximum_timeout])
.merge(get_runner_details_from_request)
runner =
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index 996457c5dfe..5f2a9567605 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -57,6 +57,7 @@ module API
optional :locked, type: Boolean, desc: 'Flag indicating the runner is locked'
optional :access_level, type: String, values: Ci::Runner.access_levels.keys,
desc: 'The access_level of the runner'
+ optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job'
at_least_one_of :description, :active, :tag_list, :run_untagged, :locked, :access_level
end
put ':id' do
diff --git a/lib/backup/artifacts.rb b/lib/backup/artifacts.rb
index 4383124d150..6a5a223a614 100644
--- a/lib/backup/artifacts.rb
+++ b/lib/backup/artifacts.rb
@@ -5,9 +5,5 @@ module Backup
def initialize
super('artifacts', JobArtifactUploader.root)
end
-
- def create_files_dir
- Dir.mkdir(app_files_dir, 0700)
- end
end
end
diff --git a/lib/backup/builds.rb b/lib/backup/builds.rb
index 635967f4bd4..f869916e199 100644
--- a/lib/backup/builds.rb
+++ b/lib/backup/builds.rb
@@ -5,9 +5,5 @@ module Backup
def initialize
super('builds', Settings.gitlab_ci.builds_path)
end
-
- def create_files_dir
- Dir.mkdir(app_files_dir, 0700)
- end
end
end
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index 287d591e88d..88cb7e7b5a4 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -1,7 +1,10 @@
require 'open3'
+require_relative 'helper'
module Backup
class Files
+ include Backup::Helper
+
attr_reader :name, :app_files_dir, :backup_tarball, :files_parent_dir
def initialize(name, app_files_dir)
@@ -35,15 +38,22 @@ module Backup
def restore
backup_existing_files_dir
- create_files_dir
- run_pipeline!([%w(gzip -cd), %W(tar -C #{app_files_dir} -xf -)], in: backup_tarball)
+ run_pipeline!([%w(gzip -cd), %W(tar --unlink-first --recursive-unlink -C #{app_files_dir} -xf -)], in: backup_tarball)
end
def backup_existing_files_dir
- timestamped_files_path = File.join(files_parent_dir, "#{name}.#{Time.now.to_i}")
+ timestamped_files_path = File.join(Gitlab.config.backup.path, "tmp", "#{name}.#{Time.now.to_i}")
if File.exist?(app_files_dir)
- FileUtils.mv(app_files_dir, File.expand_path(timestamped_files_path))
+ # Move all files in the existing repos directory except . and .. to
+ # repositories.old.<timestamp> directory
+ FileUtils.mkdir_p(timestamped_files_path, mode: 0700)
+ files = Dir.glob(File.join(app_files_dir, "*"), File::FNM_DOTMATCH) - [File.join(app_files_dir, "."), File.join(app_files_dir, "..")]
+ begin
+ FileUtils.mv(files, timestamped_files_path)
+ rescue Errno::EACCES
+ access_denied_error(app_files_dir)
+ end
end
end
diff --git a/lib/backup/helper.rb b/lib/backup/helper.rb
new file mode 100644
index 00000000000..a1ee0faefe9
--- /dev/null
+++ b/lib/backup/helper.rb
@@ -0,0 +1,17 @@
+module Backup
+ module Helper
+ def access_denied_error(path)
+ message = <<~EOS
+
+ ### NOTICE ###
+ As part of restore, the task tried to move existing content from #{path}.
+ However, it seems that directory contains files/folders that are not owned
+ by the user #{Gitlab.config.gitlab.user}. To proceed, please move the files
+ or folders inside #{path} to a secure location so that #{path} is empty and
+ run restore task again.
+
+ EOS
+ raise message
+ end
+ end
+end
diff --git a/lib/backup/lfs.rb b/lib/backup/lfs.rb
index 4153467fbee..4e234e50a7a 100644
--- a/lib/backup/lfs.rb
+++ b/lib/backup/lfs.rb
@@ -5,9 +5,5 @@ module Backup
def initialize
super('lfs', Settings.lfs.storage_path)
end
-
- def create_files_dir
- Dir.mkdir(app_files_dir, 0700)
- end
end
end
diff --git a/lib/backup/pages.rb b/lib/backup/pages.rb
index 215ded93bfe..5830b209d6e 100644
--- a/lib/backup/pages.rb
+++ b/lib/backup/pages.rb
@@ -5,9 +5,5 @@ module Backup
def initialize
super('pages', Gitlab.config.pages.path)
end
-
- def create_files_dir
- Dir.mkdir(app_files_dir, 0700)
- end
end
end
diff --git a/lib/backup/registry.rb b/lib/backup/registry.rb
index 67fe0231087..91698669402 100644
--- a/lib/backup/registry.rb
+++ b/lib/backup/registry.rb
@@ -5,9 +5,5 @@ module Backup
def initialize
super('registry', Settings.registry.path)
end
-
- def create_files_dir
- Dir.mkdir(app_files_dir, 0700)
- end
end
end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 88a7f2a4235..89e3f1d9076 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -1,8 +1,11 @@
require 'yaml'
+require_relative 'helper'
module Backup
class Repository
+ include Backup::Helper
# rubocop:disable Metrics/AbcSize
+
def dump
prepare
@@ -63,18 +66,27 @@ module Backup
end
end
- def restore
+ def prepare_directories
Gitlab.config.repositories.storages.each do |name, repository_storage|
path = repository_storage.legacy_disk_path
next unless File.exist?(path)
- # Move repos dir to 'repositories.old' dir
- bk_repos_path = File.join(path, '..', 'repositories.old.' + Time.now.to_i.to_s)
- FileUtils.mv(path, bk_repos_path)
- # This is expected from gitlab:check
- FileUtils.mkdir_p(path, mode: 02770)
+ # Move all files in the existing repos directory except . and .. to
+ # repositories.old.<timestamp> directory
+ bk_repos_path = File.join(Gitlab.config.backup.path, "tmp", "#{name}-repositories.old." + Time.now.to_i.to_s)
+ FileUtils.mkdir_p(bk_repos_path, mode: 0700)
+ files = Dir.glob(File.join(path, "*"), File::FNM_DOTMATCH) - [File.join(path, "."), File.join(path, "..")]
+
+ begin
+ FileUtils.mv(files, bk_repos_path)
+ rescue Errno::EACCES
+ access_denied_error(path)
+ end
end
+ end
+ def restore
+ prepare_directories
Project.find_each(batch_size: 1000) do |project|
progress.print " * #{display_repo_path(project)} ... "
path_to_project_repo = path_to_repo(project)
diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb
index 35118375499..d46e2cd869d 100644
--- a/lib/backup/uploads.rb
+++ b/lib/backup/uploads.rb
@@ -5,9 +5,5 @@ module Backup
def initialize
super('uploads', Rails.root.join('public/uploads'))
end
-
- def create_files_dir
- Dir.mkdir(app_files_dir)
- end
end
end
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index ce401c1c31c..4a143baeef6 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -105,8 +105,12 @@ module Banzai
end
end
- options = link_options.merge(href: match)
- content_tag(:a, match.html_safe, options) + dropped
+ # match has come from node.to_html above, so we know it's encoded
+ # correctly.
+ html_safe_match = match.html_safe
+ options = link_options.merge(href: html_safe_match)
+
+ content_tag(:a, html_safe_match, options) + dropped
end
def autolink_filter(text)
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index b82c6ca6393..e1261e7bbbe 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -11,7 +11,7 @@ module Banzai
IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
def call
- search_text_nodes(doc).each do |node|
+ doc.search(".//text()").each do |node|
content = node.to_html
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index c2b42673376..f2e9a5a1116 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -57,7 +57,7 @@ module Banzai
ALLOWED_IMAGE_EXTENSIONS = /.+(jpg|png|gif|svg|bmp)\z/i.freeze
def call
- search_text_nodes(doc).each do |node|
+ doc.search(".//text()").each do |node|
# A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running
# before this one, it will be converted into `[[<em>TOC</em>]]`, so it
# needs special-case handling
diff --git a/lib/banzai/filter/inline_diff_filter.rb b/lib/banzai/filter/inline_diff_filter.rb
index beb21b19ab3..73e82a4d7e3 100644
--- a/lib/banzai/filter/inline_diff_filter.rb
+++ b/lib/banzai/filter/inline_diff_filter.rb
@@ -4,7 +4,7 @@ module Banzai
IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
def call
- search_text_nodes(doc).each do |node|
+ doc.search(".//text()").each do |node|
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
content = node.to_html
diff --git a/lib/gitlab/background_migration/migrate_build_stage.rb b/lib/gitlab/background_migration/migrate_build_stage.rb
index 8fe4f1a2289..242e3143e71 100644
--- a/lib/gitlab/background_migration/migrate_build_stage.rb
+++ b/lib/gitlab/background_migration/migrate_build_stage.rb
@@ -12,6 +12,7 @@ module Gitlab
class Build < ActiveRecord::Base
self.table_name = 'ci_builds'
+ self.inheritance_column = :_type_disabled
def ensure_stage!(attempts: 2)
find_stage || create_stage!
diff --git a/lib/gitlab/ci/build/policy/kubernetes.rb b/lib/gitlab/ci/build/policy/kubernetes.rb
index b20d374288f..782f6c4c0af 100644
--- a/lib/gitlab/ci/build/policy/kubernetes.rb
+++ b/lib/gitlab/ci/build/policy/kubernetes.rb
@@ -9,7 +9,7 @@ module Gitlab
end
end
- def satisfied_by?(pipeline)
+ def satisfied_by?(pipeline, seed = nil)
pipeline.has_kubernetes_active?
end
end
diff --git a/lib/gitlab/ci/build/policy/refs.rb b/lib/gitlab/ci/build/policy/refs.rb
index eadc0948d2f..4aa5dc89f47 100644
--- a/lib/gitlab/ci/build/policy/refs.rb
+++ b/lib/gitlab/ci/build/policy/refs.rb
@@ -7,7 +7,7 @@ module Gitlab
@patterns = Array(refs)
end
- def satisfied_by?(pipeline)
+ def satisfied_by?(pipeline, seed = nil)
@patterns.any? do |pattern|
pattern, path = pattern.split('@', 2)
diff --git a/lib/gitlab/ci/build/policy/specification.rb b/lib/gitlab/ci/build/policy/specification.rb
index c317291f29d..f09ba42c074 100644
--- a/lib/gitlab/ci/build/policy/specification.rb
+++ b/lib/gitlab/ci/build/policy/specification.rb
@@ -15,7 +15,7 @@ module Gitlab
@spec = spec
end
- def satisfied_by?(pipeline)
+ def satisfied_by?(pipeline, seed = nil)
raise NotImplementedError
end
end
diff --git a/lib/gitlab/ci/build/policy/variables.rb b/lib/gitlab/ci/build/policy/variables.rb
new file mode 100644
index 00000000000..9d2a362b7d4
--- /dev/null
+++ b/lib/gitlab/ci/build/policy/variables.rb
@@ -0,0 +1,24 @@
+module Gitlab
+ module Ci
+ module Build
+ module Policy
+ class Variables < Policy::Specification
+ def initialize(expressions)
+ @expressions = Array(expressions)
+ end
+
+ def satisfied_by?(pipeline, seed)
+ variables = seed.to_resource.scoped_variables_hash
+
+ statements = @expressions.map do |statement|
+ ::Gitlab::Ci::Pipeline::Expression::Statement
+ .new(statement, variables)
+ end
+
+ statements.any?(&:truthful?)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/step.rb b/lib/gitlab/ci/build/step.rb
index 411f67f8ce7..0b1ebe4e048 100644
--- a/lib/gitlab/ci/build/step.rb
+++ b/lib/gitlab/ci/build/step.rb
@@ -14,7 +14,7 @@ module Gitlab
self.new(:script).tap do |step|
step.script = job.options[:before_script].to_a + job.options[:script].to_a
step.script = job.commands.split("\n") if step.script.empty?
- step.timeout = job.timeout
+ step.timeout = job.metadata_timeout
step.when = WHEN_ON_SUCCESS
end
end
@@ -25,7 +25,7 @@ module Gitlab
self.new(:after_script).tap do |step|
step.script = after_script
- step.timeout = job.timeout
+ step.timeout = job.metadata_timeout
step.when = WHEN_ALWAYS
step.allow_failure = true
end
diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb
index 0027e9ec8c5..09e8e52b60f 100644
--- a/lib/gitlab/ci/config/entry/policy.rb
+++ b/lib/gitlab/ci/config/entry/policy.rb
@@ -25,15 +25,31 @@ module Gitlab
include Entry::Validatable
include Entry::Attributable
- attributes :refs, :kubernetes
+ attributes :refs, :kubernetes, :variables
validations do
validates :config, presence: true
- validates :config, allowed_keys: %i[refs kubernetes]
+ validates :config, allowed_keys: %i[refs kubernetes variables]
+ validate :variables_expressions_syntax
with_options allow_nil: true do
validates :refs, array_of_strings_or_regexps: true
validates :kubernetes, allowed_values: %w[active]
+ validates :variables, array_of_strings: true
+ end
+
+ def variables_expressions_syntax
+ return unless variables.is_a?(Array)
+
+ statements = variables.map do |statement|
+ ::Gitlab::Ci::Pipeline::Expression::Statement.new(statement)
+ end
+
+ statements.each do |statement|
+ unless statement.valid?
+ errors.add(:variables, "Invalid expression syntax")
+ end
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb
index b2b00c8cb4b..d299a5677de 100644
--- a/lib/gitlab/ci/pipeline/chain/populate.rb
+++ b/lib/gitlab/ci/pipeline/chain/populate.rb
@@ -17,8 +17,6 @@ module Gitlab
# Populate pipeline with all stages and builds from pipeline seeds.
#
pipeline.stage_seeds.each do |stage|
- stage.user = current_user
-
pipeline.stages << stage.to_resource
stage.seeds.each do |build|
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/string.rb b/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
index 48bde213d44..346c92dc51e 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
@@ -4,7 +4,7 @@ module Gitlab
module Expression
module Lexeme
class String < Lexeme::Value
- PATTERN = /("(?<string>.+?)")|('(?<string>.+?)')/.freeze
+ PATTERN = /("(?<string>.*?)")|('(?<string>.*?)')/.freeze
def initialize(value)
@value = value
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb b/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
index b781c15fd67..37643c8ef53 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def evaluate(variables = {})
- HashWithIndifferentAccess.new(variables).fetch(@name, nil)
+ variables.with_indifferent_access.fetch(@name, nil)
end
def self.build(string)
diff --git a/lib/gitlab/ci/pipeline/expression/statement.rb b/lib/gitlab/ci/pipeline/expression/statement.rb
index 4f0e101b730..09a7c98464b 100644
--- a/lib/gitlab/ci/pipeline/expression/statement.rb
+++ b/lib/gitlab/ci/pipeline/expression/statement.rb
@@ -14,12 +14,9 @@ module Gitlab
%w[variable]
].freeze
- def initialize(statement, pipeline)
+ def initialize(statement, variables = {})
@lexer = Expression::Lexer.new(statement)
-
- @variables = pipeline.variables.map do |variable|
- [variable.key, variable.value]
- end
+ @variables = variables.with_indifferent_access
end
def parse_tree
@@ -35,6 +32,16 @@ module Gitlab
def evaluate
parse_tree.evaluate(@variables.to_h)
end
+
+ def truthful?
+ evaluate.present?
+ end
+
+ def valid?
+ parse_tree.is_a?(Lexeme::Base)
+ rescue StatementError
+ false
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index 7cd7c864448..6980b0b7aff 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -11,21 +11,16 @@ module Gitlab
@pipeline = pipeline
@attributes = attributes
- @only = attributes.delete(:only)
- @except = attributes.delete(:except)
- end
-
- def user=(current_user)
- @attributes.merge!(user: current_user)
+ @only = Gitlab::Ci::Build::Policy
+ .fabricate(attributes.delete(:only))
+ @except = Gitlab::Ci::Build::Policy
+ .fabricate(attributes.delete(:except))
end
def included?
strong_memoize(:inclusion) do
- only_specs = Gitlab::Ci::Build::Policy.fabricate(@only)
- except_specs = Gitlab::Ci::Build::Policy.fabricate(@except)
-
- only_specs.all? { |spec| spec.satisfied_by?(@pipeline) } &&
- except_specs.none? { |spec| spec.satisfied_by?(@pipeline) }
+ @only.all? { |spec| spec.satisfied_by?(@pipeline, self) } &&
+ @except.none? { |spec| spec.satisfied_by?(@pipeline, self) }
end
end
@@ -33,6 +28,7 @@ module Gitlab
@attributes.merge(
pipeline: @pipeline,
project: @pipeline.project,
+ user: @pipeline.user,
ref: @pipeline.ref,
tag: @pipeline.tag,
trigger_request: @pipeline.legacy_trigger,
diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb
index 1fcbdc1b15a..c101f30d6e8 100644
--- a/lib/gitlab/ci/pipeline/seed/stage.rb
+++ b/lib/gitlab/ci/pipeline/seed/stage.rb
@@ -17,10 +17,6 @@ module Gitlab
end
end
- def user=(current_user)
- @builds.each { |seed| seed.user = current_user }
- end
-
def attributes
{ name: @attributes.fetch(:name),
pipeline: @pipeline,
diff --git a/lib/gitlab/ci/variables/collection.rb b/lib/gitlab/ci/variables/collection.rb
index 0deca55fe8f..ad30b3f427c 100644
--- a/lib/gitlab/ci/variables/collection.rb
+++ b/lib/gitlab/ci/variables/collection.rb
@@ -30,7 +30,13 @@ module Gitlab
end
def to_runner_variables
- self.map(&:to_hash)
+ self.map(&:to_runner_variable)
+ end
+
+ def to_hash
+ self.to_runner_variables
+ .map { |env| [env.fetch(:key), env.fetch(:value)] }
+ .to_h.with_indifferent_access
end
end
end
diff --git a/lib/gitlab/ci/variables/collection/item.rb b/lib/gitlab/ci/variables/collection/item.rb
index 939912981e6..23ed71db8b0 100644
--- a/lib/gitlab/ci/variables/collection/item.rb
+++ b/lib/gitlab/ci/variables/collection/item.rb
@@ -17,7 +17,7 @@ module Gitlab
end
def ==(other)
- to_hash == self.class.fabricate(other).to_hash
+ to_runner_variable == self.class.fabricate(other).to_runner_variable
end
##
@@ -25,7 +25,7 @@ module Gitlab
# don't expose `file` attribute at all (stems from what the runner
# expects).
#
- def to_hash
+ def to_runner_variable
@variable.reject do |hash_key, hash_value|
hash_key == :file && hash_value == false
end
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index 0fb71976883..5fdd5dcd374 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -2,8 +2,8 @@
module Gitlab
# Checks if a set of migrations requires downtime or not.
class EeCompatCheck
- DEFAULT_CE_PROJECT_URL = 'https://gitlab.com/gitlab-org/gitlab-ce'.freeze
- EE_REPO_URL = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
+ 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
PLEASE_READ_THIS_BANNER = %Q{
@@ -11,57 +11,81 @@ module Gitlab
===================== PLEASE READ THIS =====================
============================================================
}.freeze
+ STAY_STRONG_LINK_TO_DOCS = %Q{
+ Stay 💪! For more information, see
+ https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
+ }.freeze
THANKS_FOR_READING_BANNER = %Q{
============================================================
==================== THANKS FOR READING ====================
============================================================\n
}.freeze
- attr_reader :ee_repo_dir, :patches_dir, :ce_project_url, :ce_repo_url, :ce_branch, :ee_branch_found
+ attr_reader :ee_repo_dir, :patches_dir
+ attr_reader :ce_project_url, :ee_repo_url
+ attr_reader :ce_branch, :ee_remote_with_branch, :ee_branch_found
attr_reader :job_id, :failed_files
- def initialize(branch:, ce_project_url: DEFAULT_CE_PROJECT_URL, job_id: nil)
+ def initialize(branch:, ce_project_url: CANONICAL_CE_PROJECT_URL, job_id: nil)
@ee_repo_dir = CHECK_DIR.join('ee-repo')
@patches_dir = CHECK_DIR.join('patches')
@ce_branch = branch
@ce_project_url = ce_project_url
- @ce_repo_url = "#{ce_project_url}.git"
+ @ee_repo_url = ce_public_repo_url.sub('gitlab-ce', 'gitlab-ee')
@job_id = job_id
end
def check
ensure_patches_dir
- add_remote('canonical-ce', "#{DEFAULT_CE_PROJECT_URL}.git")
- generate_patch(branch: ce_branch, patch_path: ce_patch_full_path, remote: 'canonical-ce')
+ # We're generating the patch against the canonical-ce remote since forks'
+ # master branch are not necessarily up-to-date.
+ add_remote('canonical-ce', "#{CANONICAL_CE_PROJECT_URL}.git")
+ generate_patch(branch: ce_branch, patch_path: ce_patch_full_path, branch_remote: 'origin', master_remote: 'canonical-ce')
ensure_ee_repo
Dir.chdir(ee_repo_dir) do
step("In the #{ee_repo_dir} directory")
- add_remote('canonical-ee', EE_REPO_URL)
+ ee_remotes.each do |key, url|
+ add_remote(key, url)
+ end
+ fetch(branch: 'master', depth: 20, remote: 'canonical-ee')
status = catch(:halt_check) do
ce_branch_compat_check!
delete_ee_branches_locally!
ee_branch_presence_check!
- step("Checking out #{ee_branch_found}", %W[git checkout -b #{ee_branch_found} canonical-ee/#{ee_branch_found}])
- generate_patch(branch: ee_branch_found, patch_path: ee_patch_full_path, remote: 'canonical-ee')
+ step("Checking out #{ee_remote_with_branch}/#{ee_branch_found}", %W[git checkout -b #{ee_branch_found} #{ee_remote_with_branch}/#{ee_branch_found}])
+ generate_patch(branch: ee_branch_found, patch_path: ee_patch_full_path, branch_remote: ee_remote_with_branch, master_remote: 'canonical-ee')
ee_branch_compat_check!
end
delete_ee_branches_locally!
- if status.nil?
- true
- else
- false
- end
+ status.nil?
end
end
private
+ def fork?
+ ce_project_url != CANONICAL_CE_PROJECT_URL
+ end
+
+ def ee_remotes
+ return @ee_remotes if defined?(@ee_remotes)
+
+ remotes =
+ {
+ 'ee' => ee_repo_url,
+ 'canonical-ee' => CANONICAL_EE_REPO_URL
+ }
+ remotes.delete('ee') unless fork?
+
+ @ee_remotes = remotes
+ end
+
def add_remote(name, url)
step(
"Adding the #{name} remote (#{url})",
@@ -70,28 +94,32 @@ module Gitlab
end
def ensure_ee_repo
- if Dir.exist?(ee_repo_dir)
- step("#{ee_repo_dir} already exists")
- else
- step(
- "Cloning #{EE_REPO_URL} into #{ee_repo_dir}",
- %W[git clone --branch master --single-branch --depth=200 #{EE_REPO_URL} #{ee_repo_dir}]
- )
+ unless clone_repo(ee_repo_url, ee_repo_dir)
+ # Fallback to using the canonical EE if there is no forked EE
+ clone_repo(CANONICAL_EE_REPO_URL, ee_repo_dir)
end
end
+ def clone_repo(url, dir)
+ _, status = step(
+ "Cloning #{url} into #{dir}",
+ %W[git clone --branch master --single-branch --depth=200 #{url} #{dir}]
+ )
+ status.zero?
+ end
+
def ensure_patches_dir
FileUtils.mkdir_p(patches_dir)
end
- def generate_patch(branch:, patch_path:, remote:)
+ def generate_patch(branch:, patch_path:, branch_remote:, master_remote:)
FileUtils.rm(patch_path, force: true)
- find_merge_base_with_master(branch: branch, master_remote: remote)
+ find_merge_base_with_master(branch: branch, branch_remote: branch_remote, master_remote: master_remote)
step(
- "Generating the patch against #{remote}/master in #{patch_path}",
- %W[git diff --binary #{remote}/master...origin/#{branch}]
+ "Generating the patch against #{master_remote}/master in #{patch_path}",
+ %W[git diff --binary #{master_remote}/master...#{branch_remote}/#{branch}]
) do |output, status|
throw(:halt_check, :ko) unless status.zero?
@@ -109,23 +137,22 @@ module Gitlab
end
def ee_branch_presence_check!
- _, status = step("Fetching origin/#{ee_branch_prefix}", %W[git fetch canonical-ee #{ee_branch_prefix}])
-
- if status.zero?
- @ee_branch_found = ee_branch_prefix
- return
+ ee_remotes.keys.each do |remote|
+ [ee_branch_prefix, ee_branch_suffix].each do |branch|
+ _, status = step("Fetching #{remote}/#{ee_branch_prefix}", %W[git fetch #{remote} #{branch}])
+
+ if status.zero?
+ @ee_remote_with_branch = remote
+ @ee_branch_found = branch
+ return true
+ end
+ end
end
- _, status = step("Fetching origin/#{ee_branch_suffix}", %W[git fetch canonical-ee #{ee_branch_suffix}])
-
- if status.zero?
- @ee_branch_found = ee_branch_suffix
- else
- puts
- puts ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg
+ puts
+ puts ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg
- throw(:halt_check, :ko)
- end
+ throw(:halt_check, :ko)
end
def ee_branch_compat_check!
@@ -181,10 +208,10 @@ module Gitlab
command(%W[git branch --delete --force #{ee_branch_suffix}])
end
- def merge_base_found?(master_remote:, branch:)
+ def merge_base_found?(branch:, branch_remote:, master_remote:)
step(
"Finding merge base with #{master_remote}/master",
- %W[git merge-base #{master_remote}/master origin/#{branch}]
+ %W[git merge-base #{master_remote}/master #{branch_remote}/#{branch}]
) do |output, status|
if status.zero?
puts "Merge base was found: #{output}"
@@ -193,7 +220,7 @@ module Gitlab
end
end
- def find_merge_base_with_master(branch:, master_remote:)
+ def find_merge_base_with_master(branch:, branch_remote:, master_remote:)
# Start with (Math.exp(3).to_i = 20) until (Math.exp(6).to_i = 403)
# In total we go (20 + 54 + 148 + 403 = 625) commits deeper
depth = 20
@@ -202,10 +229,10 @@ module Gitlab
depth += Math.exp(factor).to_i
# Repository is initially cloned with a depth of 20 so we need to fetch
# deeper in the case the branch has more than 20 commits on top of master
- fetch(branch: branch, depth: depth, remote: 'origin')
+ fetch(branch: branch, depth: depth, remote: branch_remote)
fetch(branch: 'master', depth: depth, remote: master_remote)
- merge_base_found?(master_remote: master_remote, branch: branch)
+ merge_base_found?(branch: branch, branch_remote: branch_remote, master_remote: master_remote)
end
raise "\n#{branch} is too far behind #{master_remote}/master, please rebase it!\n" unless success
@@ -274,6 +301,13 @@ module Gitlab
Gitlab::Popen.popen(cmd)
end
+ # We're "re-creating" the repo URL because ENV['CI_REPOSITORY_URL'] contains
+ # redacted credentials (e.g. "***:****") which are useless in instructions
+ # the job gives.
+ def ce_public_repo_url
+ "#{ce_project_url}.git"
+ end
+
def applies_cleanly_msg(branch)
%Q{
#{PLEASE_READ_THIS_BANNER}
@@ -288,13 +322,15 @@ module Gitlab
end
def ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg
+ ee_repos = ee_remotes.values.uniq
+
%Q{
#{PLEASE_READ_THIS_BANNER}
💥 Oh no! 💥
The `#{ce_branch}` branch does not apply cleanly to the current
EE/master, and no `#{ee_branch_prefix}` or `#{ee_branch_suffix}` branch
- was found in the EE repository.
+ was found in #{ee_repos.join(' nor in ')}.
If you're a community contributor, don't worry, someone from
GitLab Inc. will take care of this, and you don't have to do anything.
@@ -314,17 +350,17 @@ module Gitlab
1. Create a new branch from master and cherry-pick your CE commits
# In the EE repo
- $ git fetch #{EE_REPO_URL} master
+ $ git fetch #{CANONICAL_EE_REPO_URL} master
$ git checkout -b #{ee_branch_prefix} FETCH_HEAD
- $ git fetch #{ce_repo_url} #{ce_branch}
+ $ git fetch #{ce_public_repo_url} #{ce_branch}
$ git cherry-pick SHA # Repeat for all the commits you want to pick
- You can squash the `#{ce_branch}` commits into a single "Port of #{ce_branch} to EE" commit.
+ Note: You can squash the `#{ce_branch}` commits into a single "Port of #{ce_branch} to EE" commit.
2. Apply your branch's patch to EE
# In the EE repo
- $ git fetch #{EE_REPO_URL} master
+ $ git fetch #{CANONICAL_EE_REPO_URL} master
$ git checkout -b #{ee_branch_prefix} FETCH_HEAD
$ wget #{patch_url} && git apply --3way #{ce_patch_name}
@@ -356,10 +392,9 @@ module Gitlab
⚠️ Also, don't forget to create a new merge request on gitlab-ee and
cross-link it with the CE merge request.
- Once this is done, you can retry this failed build, and it should pass.
+ Once this is done, you can retry this failed job, and it should pass.
- Stay 💪 ! For more information, see
- https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
+ #{STAY_STRONG_LINK_TO_DOCS}
#{THANKS_FOR_READING_BANNER}
}
end
@@ -371,16 +406,15 @@ module Gitlab
The `#{ce_branch}` does not apply cleanly to the current EE/master, and
even though a `#{ee_branch_found}` branch
- exists in the EE repository, it does not apply cleanly either to
+ exists in #{ee_repo_url}, it does not apply cleanly either to
EE/master!
#{conflicting_files_msg}
Please update the `#{ee_branch_found}`, push it again to gitlab-ee, and
- retry this build.
+ retry this job.
- Stay 💪 ! For more information, see
- https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
+ #{STAY_STRONG_LINK_TO_DOCS}
#{THANKS_FOR_READING_BANNER}
}
end
diff --git a/lib/gitlab/git/gitmodules_parser.rb b/lib/gitlab/git/gitmodules_parser.rb
index 4a43b9b444d..4b505312f60 100644
--- a/lib/gitlab/git/gitmodules_parser.rb
+++ b/lib/gitlab/git/gitmodules_parser.rb
@@ -46,6 +46,8 @@ module Gitlab
iterator = State.new
@content.split("\n").each_with_object(iterator) do |text, iterator|
+ text.chomp!
+
next if text =~ /^\s*#/
if text =~ /\A\[submodule "(?<name>[^"]+)"\]\z/
@@ -55,7 +57,7 @@ module Gitlab
next unless text =~ /\A\s*(?<key>\w+)\s*=\s*(?<value>.*)\z/
- value = $~[:value].chomp
+ value = $~[:value]
iterator.set_attribute($~[:key], value)
end
end
diff --git a/lib/gitlab/git/env.rb b/lib/gitlab/git/hook_env.rb
index 9d0b47a1a6d..455e8451c10 100644
--- a/lib/gitlab/git/env.rb
+++ b/lib/gitlab/git/hook_env.rb
@@ -3,37 +3,39 @@
module Gitlab
module Git
# Ephemeral (per request) storage for environment variables that some Git
- # commands may need.
+ # commands need during internal API calls made from Git push hooks.
#
# For example, in pre-receive hooks, new objects are put in a temporary
# $GIT_OBJECT_DIRECTORY. Without it set, the new objects cannot be retrieved
# (this would break push rules for instance).
#
# This class is thread-safe via RequestStore.
- class Env
+ class HookEnv
WHITELISTED_VARIABLES = %w[
- GIT_OBJECT_DIRECTORY
GIT_OBJECT_DIRECTORY_RELATIVE
- GIT_ALTERNATE_OBJECT_DIRECTORIES
GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE
].freeze
- def self.set(env)
+ def self.set(gl_repository, env)
return unless RequestStore.active?
- RequestStore.store[:gitlab_git_env] = whitelist_git_env(env)
+ raise "missing gl_repository" if gl_repository.blank?
+
+ RequestStore.store[:gitlab_git_env] ||= {}
+ RequestStore.store[:gitlab_git_env][gl_repository] = whitelist_git_env(env)
end
- def self.all
+ def self.all(gl_repository)
return {} unless RequestStore.active?
- RequestStore.fetch(:gitlab_git_env) { {} }
+ h = RequestStore.fetch(:gitlab_git_env) { {} }
+ h.fetch(gl_repository, {})
end
- def self.to_env_hash
+ def self.to_env_hash(gl_repository)
env = {}
- all.compact.each do |key, value|
+ all(gl_repository).compact.each do |key, value|
value = value.join(File::PATH_SEPARATOR) if value.is_a?(Array)
env[key.to_s] = value
end
@@ -41,10 +43,6 @@ module Gitlab
env
end
- def self.[](key)
- all[key]
- end
-
def self.whitelist_git_env(env)
env.select { |key, _| WHITELISTED_VARIABLES.include?(key.to_s) }.with_indifferent_access
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 2d16a81c888..e692c9ce342 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -1745,21 +1745,11 @@ module Gitlab
end
def alternate_object_directories
- relative_paths = relative_object_directories
-
- if relative_paths.any?
- relative_paths.map { |d| File.join(path, d) }
- else
- absolute_object_directories.flat_map { |d| d.split(File::PATH_SEPARATOR) }
- end
+ relative_object_directories.map { |d| File.join(path, d) }
end
def relative_object_directories
- Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES).flatten.compact
- end
-
- def absolute_object_directories
- Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES).flatten.compact
+ Gitlab::Git::HookEnv.all(gl_repository).values_at(*ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES).flatten.compact
end
# Get the content of a blob for a given commit. If the blob is a commit
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 0a2a23e835b..ed0644f6cf1 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -99,8 +99,6 @@ module Gitlab
end
def check_active_user!
- return if deploy_key?
-
if user && !user_access.allowed?
raise UnauthorizedError, ERROR_MESSAGES[:account_blocked]
end
@@ -215,7 +213,7 @@ module Gitlab
raise UnauthorizedError, ERROR_MESSAGES[:read_only]
end
- if deploy_key
+ if deploy_key?
unless deploy_key.can_push_to?(project)
raise UnauthorizedError, ERROR_MESSAGES[:deploy_key_upload]
end
@@ -305,8 +303,10 @@ module Gitlab
case actor
when User
actor
+ when DeployKey
+ nil
when Key
- actor.user unless actor.is_a?(DeployKey)
+ actor.user
when :ci
nil
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 8ca30ffc232..0abae70c443 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -83,6 +83,10 @@ module Gitlab
end
end
+ def self.random_storage
+ Gitlab.config.repositories.storages.keys.sample
+ end
+
def self.address(storage)
params = Gitlab.config.repositories.storages[storage]
raise "storage not found: #{storage.inspect}" if params.nil?
diff --git a/lib/gitlab/gitaly_client/remote_service.rb b/lib/gitlab/gitaly_client/remote_service.rb
index 58c356edfd1..f2d699d9dfb 100644
--- a/lib/gitlab/gitaly_client/remote_service.rb
+++ b/lib/gitlab/gitaly_client/remote_service.rb
@@ -3,6 +3,17 @@ module Gitlab
class RemoteService
MAX_MSG_SIZE = 128.kilobytes.freeze
+ def self.exists?(remote_url)
+ request = Gitaly::FindRemoteRepositoryRequest.new(remote: remote_url)
+
+ response = GitalyClient.call(GitalyClient.random_storage,
+ :remote_service,
+ :find_remote_repository, request,
+ timeout: GitalyClient.medium_timeout)
+
+ response.exists
+ end
+
def initialize(repository)
@repository = repository
@gitaly_repo = repository.gitaly_repository
diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb
index a8c6d478de8..405567db94a 100644
--- a/lib/gitlab/gitaly_client/util.rb
+++ b/lib/gitlab/gitaly_client/util.rb
@@ -3,11 +3,9 @@ module Gitlab
module Util
class << self
def repository(repository_storage, relative_path, gl_repository)
- git_object_directory = Gitlab::Git::Env['GIT_OBJECT_DIRECTORY_RELATIVE'].presence ||
- Gitlab::Git::Env['GIT_OBJECT_DIRECTORY'].presence
- git_alternate_object_directories =
- Array.wrap(Gitlab::Git::Env['GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE']).presence ||
- Array.wrap(Gitlab::Git::Env['GIT_ALTERNATE_OBJECT_DIRECTORIES']).flat_map { |d| d.split(File::PATH_SEPARATOR) }
+ git_env = Gitlab::Git::HookEnv.all(gl_repository)
+ git_object_directory = git_env['GIT_OBJECT_DIRECTORY_RELATIVE'].presence
+ git_alternate_object_directories = Array.wrap(git_env['GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE'])
Gitaly::Repository.new(
storage_name: repository_storage,
diff --git a/lib/gitlab/github_import/importer/repository_importer.rb b/lib/gitlab/github_import/importer/repository_importer.rb
index ab0b751fe24..b1b283e98b5 100644
--- a/lib/gitlab/github_import/importer/repository_importer.rb
+++ b/lib/gitlab/github_import/importer/repository_importer.rb
@@ -16,7 +16,8 @@ module Gitlab
# Returns true if we should import the wiki for the project.
def import_wiki?
client.repository(project.import_source)&.has_wiki &&
- !project.wiki_repository_exists?
+ !project.wiki_repository_exists? &&
+ Gitlab::GitalyClient::RemoteService.exists?(wiki_url)
end
# Imports the repository data.
@@ -55,7 +56,6 @@ module Gitlab
def import_wiki_repository
wiki_path = "#{project.disk_path}.wiki"
- wiki_url = project.import_url.sub(/\.git\z/, '.wiki.git')
storage_path = project.repository_storage_path
gitlab_shell.import_repository(storage_path, wiki_path, wiki_url)
@@ -70,6 +70,10 @@ module Gitlab
end
end
+ def wiki_url
+ project.import_url.sub(/\.git\z/, '.wiki.git')
+ end
+
def update_clone_time
project.update_column(:last_repository_updated_at, Time.zone.now)
end
diff --git a/lib/gitlab/http.rb b/lib/gitlab/http.rb
index 96558872a37..9aca3b0fb26 100644
--- a/lib/gitlab/http.rb
+++ b/lib/gitlab/http.rb
@@ -4,6 +4,8 @@
# calling internal IP or services.
module Gitlab
class HTTP
+ BlockedUrlError = Class.new(StandardError)
+
include HTTParty # rubocop:disable Gitlab/HTTParty
connection_adapter ProxyHTTPConnectionAdapter
diff --git a/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb b/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
new file mode 100644
index 00000000000..aef371d81eb
--- /dev/null
+++ b/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
@@ -0,0 +1,83 @@
+module Gitlab
+ module ImportExport
+ module AfterExportStrategies
+ class BaseAfterExportStrategy
+ include ActiveModel::Validations
+ extend Forwardable
+
+ StrategyError = Class.new(StandardError)
+
+ AFTER_EXPORT_LOCK_FILE_NAME = '.after_export_action'.freeze
+
+ private
+
+ attr_reader :project, :current_user
+
+ public
+
+ def initialize(attributes = {})
+ @options = OpenStruct.new(attributes)
+
+ self.class.instance_eval do
+ def_delegators :@options, *attributes.keys
+ end
+ end
+
+ def execute(current_user, project)
+ return unless project&.export_project_path
+
+ @project = project
+ @current_user = current_user
+
+ if invalid?
+ log_validation_errors
+
+ return
+ end
+
+ create_or_update_after_export_lock
+ strategy_execute
+
+ true
+ rescue => e
+ project.import_export_shared.error(e)
+ false
+ ensure
+ delete_after_export_lock
+ end
+
+ def to_json(options = {})
+ @options.to_h.merge!(klass: self.class.name).to_json
+ end
+
+ def self.lock_file_path(project)
+ return unless project&.export_path
+
+ File.join(project.export_path, AFTER_EXPORT_LOCK_FILE_NAME)
+ end
+
+ protected
+
+ def strategy_execute
+ raise NotImplementedError
+ end
+
+ private
+
+ def create_or_update_after_export_lock
+ FileUtils.touch(self.class.lock_file_path(project))
+ end
+
+ def delete_after_export_lock
+ lock_file = self.class.lock_file_path(project)
+
+ FileUtils.rm(lock_file) if lock_file.present? && File.exist?(lock_file)
+ end
+
+ def log_validation_errors
+ errors.full_messages.each { |msg| project.import_export_shared.add_error_message(msg) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/after_export_strategies/download_notification_strategy.rb b/lib/gitlab/import_export/after_export_strategies/download_notification_strategy.rb
new file mode 100644
index 00000000000..4371a7eff56
--- /dev/null
+++ b/lib/gitlab/import_export/after_export_strategies/download_notification_strategy.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module ImportExport
+ module AfterExportStrategies
+ class DownloadNotificationStrategy < BaseAfterExportStrategy
+ private
+
+ def strategy_execute
+ notification_service.project_exported(project, current_user)
+ end
+
+ def notification_service
+ @notification_service ||= NotificationService.new
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
new file mode 100644
index 00000000000..938664a95a1
--- /dev/null
+++ b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
@@ -0,0 +1,61 @@
+module Gitlab
+ module ImportExport
+ module AfterExportStrategies
+ class WebUploadStrategy < BaseAfterExportStrategy
+ PUT_METHOD = 'PUT'.freeze
+ POST_METHOD = 'POST'.freeze
+ INVALID_HTTP_METHOD = 'invalid. Only PUT and POST methods allowed.'.freeze
+
+ validates :url, url: true
+
+ validate do
+ unless [PUT_METHOD, POST_METHOD].include?(http_method.upcase)
+ errors.add(:http_method, INVALID_HTTP_METHOD)
+ end
+ end
+
+ def initialize(url:, http_method: PUT_METHOD)
+ super
+ end
+
+ protected
+
+ def strategy_execute
+ handle_response_error(send_file)
+
+ project.remove_exported_project_file
+ end
+
+ def handle_response_error(response)
+ unless response.success?
+ error_code = response.dig('Error', 'Code') || response.code
+ error_message = response.dig('Error', 'Message') || response.message
+
+ raise StrategyError.new("Error uploading the project. Code #{error_code}: #{error_message}")
+ end
+ end
+
+ private
+
+ def send_file
+ export_file = File.open(project.export_project_path)
+
+ Gitlab::HTTP.public_send(http_method.downcase, url, send_file_options(export_file)) # rubocop:disable GitlabSecurity/PublicSend
+ ensure
+ export_file.close if export_file
+ end
+
+ def send_file_options(export_file)
+ {
+ body_stream: export_file,
+ headers: headers
+ }
+ end
+
+ def headers
+ { 'Content-Length' => File.size(project.export_project_path).to_s }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/after_export_strategy_builder.rb b/lib/gitlab/import_export/after_export_strategy_builder.rb
new file mode 100644
index 00000000000..7eabcae2380
--- /dev/null
+++ b/lib/gitlab/import_export/after_export_strategy_builder.rb
@@ -0,0 +1,24 @@
+module Gitlab
+ module ImportExport
+ class AfterExportStrategyBuilder
+ StrategyNotFoundError = Class.new(StandardError)
+
+ def self.build!(strategy_klass, attributes = {})
+ return default_strategy.new unless strategy_klass
+
+ attributes ||= {}
+ klass = strategy_klass.constantize rescue nil
+
+ unless klass && klass < AfterExportStrategies::BaseAfterExportStrategy
+ raise StrategyNotFoundError.new("Strategy #{strategy_klass} not found")
+ end
+
+ klass.new(**attributes.symbolize_keys)
+ end
+
+ def self.default_strategy
+ AfterExportStrategies::DownloadNotificationStrategy
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 791a54e1b69..598832fb2df 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -19,7 +19,7 @@ module Gitlab
custom_attributes: 'ProjectCustomAttribute',
project_badges: 'Badge' }.freeze
- USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id].freeze
+ USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id closed_by_id].freeze
PROJECT_REFERENCES = %w[project_id source_project_id target_project_id].freeze
diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb
index 3d3d998a6a3..6d7c36ce38b 100644
--- a/lib/gitlab/import_export/shared.rb
+++ b/lib/gitlab/import_export/shared.rb
@@ -22,7 +22,7 @@ module Gitlab
def error(error)
error_out(error.message, caller[0].dup)
- @errors << error.message
+ add_error_message(error.message)
# Debug:
if error.backtrace
@@ -32,6 +32,14 @@ module Gitlab
end
end
+ def add_error_message(error_message)
+ @errors << error_message
+ end
+
+ def after_export_in_progress?
+ File.exist?(after_export_lock_file)
+ end
+
private
def relative_path
@@ -45,6 +53,10 @@ module Gitlab
def error_out(message, caller)
Rails.logger.error("Import/Export error raised on #{caller}: #{message}")
end
+
+ def after_export_lock_file
+ AfterExportStrategies::BaseAfterExportStrategy.lock_file_path(project)
+ end
end
end
end
diff --git a/lib/gitlab/proxy_http_connection_adapter.rb b/lib/gitlab/proxy_http_connection_adapter.rb
index c70d6f4cd84..d682289b632 100644
--- a/lib/gitlab/proxy_http_connection_adapter.rb
+++ b/lib/gitlab/proxy_http_connection_adapter.rb
@@ -10,8 +10,12 @@
module Gitlab
class ProxyHTTPConnectionAdapter < HTTParty::ConnectionAdapter
def connection
- if !allow_local_requests? && blocked_url?
- raise URI::InvalidURIError
+ unless allow_local_requests?
+ begin
+ Gitlab::UrlBlocker.validate!(uri, allow_local_network: false)
+ rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ raise Gitlab::HTTP::BlockedUrlError, "URL '#{uri}' is blocked: #{e.message}"
+ end
end
super
@@ -19,10 +23,6 @@ module Gitlab
private
- def blocked_url?
- Gitlab::UrlBlocker.blocked_url?(uri, allow_private_networks: false)
- end
-
def allow_local_requests?
options.fetch(:allow_local_requests, allow_settings_local_requests?)
end
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
index 0f9f939e204..db97f65bd54 100644
--- a/lib/gitlab/url_blocker.rb
+++ b/lib/gitlab/url_blocker.rb
@@ -2,48 +2,84 @@ require 'resolv'
module Gitlab
class UrlBlocker
- class << self
- def blocked_url?(url, allow_private_networks: true, valid_ports: [])
- return false if url.nil?
+ BlockedUrlError = Class.new(StandardError)
- blocked_ips = ["127.0.0.1", "::1", "0.0.0.0"]
- blocked_ips.concat(Socket.ip_address_list.map(&:ip_address))
+ class << self
+ def validate!(url, allow_localhost: false, allow_local_network: true, valid_ports: [])
+ return true if url.nil?
begin
uri = Addressable::URI.parse(url)
- # Allow imports from the GitLab instance itself but only from the configured ports
- return false if internal?(uri)
+ rescue Addressable::URI::InvalidURIError
+ raise BlockedUrlError, "URI is invalid"
+ end
- return true if blocked_port?(uri.port, valid_ports)
- return true if blocked_user_or_hostname?(uri.user)
- return true if blocked_user_or_hostname?(uri.hostname)
+ # Allow imports from the GitLab instance itself but only from the configured ports
+ return true if internal?(uri)
- addrs_info = Addrinfo.getaddrinfo(uri.hostname, 80, nil, :STREAM)
- server_ips = addrs_info.map(&:ip_address)
+ port = uri.port || uri.default_port
+ validate_port!(port, valid_ports) if valid_ports.any?
+ validate_user!(uri.user)
+ validate_hostname!(uri.hostname)
- return true if (blocked_ips & server_ips).any?
- return true if !allow_private_networks && private_network?(addrs_info)
- rescue Addressable::URI::InvalidURIError
- return true
+ begin
+ addrs_info = Addrinfo.getaddrinfo(uri.hostname, port, nil, :STREAM)
rescue SocketError
- return false
+ return true
end
+ validate_localhost!(addrs_info) unless allow_localhost
+ validate_local_network!(addrs_info) unless allow_local_network
+
+ true
+ end
+
+ def blocked_url?(*args)
+ validate!(*args)
+
false
+ rescue BlockedUrlError
+ true
end
private
- def blocked_port?(port, valid_ports)
- return false if port.blank? || valid_ports.blank?
+ def validate_port!(port, valid_ports)
+ return if port.blank?
+ # Only ports under 1024 are restricted
+ return if port >= 1024
+ return if valid_ports.include?(port)
- port < 1024 && !valid_ports.include?(port)
+ raise BlockedUrlError, "Only allowed ports are #{valid_ports.join(', ')}, and any over 1024"
end
- def blocked_user_or_hostname?(value)
- return false if value.blank?
+ def validate_user!(value)
+ return if value.blank?
+ return if value =~ /\A\p{Alnum}/
- value !~ /\A\p{Alnum}/
+ raise BlockedUrlError, "Username needs to start with an alphanumeric character"
+ end
+
+ def validate_hostname!(value)
+ return if value.blank?
+ return if value =~ /\A\p{Alnum}/
+
+ raise BlockedUrlError, "Hostname needs to start with an alphanumeric character"
+ end
+
+ def validate_localhost!(addrs_info)
+ local_ips = ["127.0.0.1", "::1", "0.0.0.0"]
+ local_ips.concat(Socket.ip_address_list.map(&:ip_address))
+
+ return if (local_ips & addrs_info.map(&:ip_address)).empty?
+
+ raise BlockedUrlError, "Requests to localhost are not allowed"
+ end
+
+ def validate_local_network!(addrs_info)
+ return unless addrs_info.any? { |addr| addr.ipv4_private? || addr.ipv6_sitelocal? }
+
+ raise BlockedUrlError, "Requests to the local network are not allowed"
end
def internal?(uri)
@@ -60,10 +96,6 @@ module Gitlab
(uri.port.blank? || uri.port == config.gitlab_shell.ssh_port)
end
- def private_network?(addrs_info)
- addrs_info.any? { |addr| addr.ipv4_private? || addr.ipv6_sitelocal? }
- end
-
def config
Gitlab.config
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 37d3512990e..8c0a4d55ea2 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -30,6 +30,7 @@ module Gitlab
usage_data
end
+ # rubocop:disable Metrics/AbcSize
def system_usage_data
{
counts: {
@@ -50,6 +51,12 @@ module Gitlab
clusters: ::Clusters::Cluster.count,
clusters_enabled: ::Clusters::Cluster.enabled.count,
clusters_disabled: ::Clusters::Cluster.disabled.count,
+ clusters_platforms_gke: ::Clusters::Cluster.gcp_installed.enabled.count,
+ clusters_platforms_user: ::Clusters::Cluster.user_provided.enabled.count,
+ clusters_applications_helm: ::Clusters::Applications::Helm.installed.count,
+ clusters_applications_ingress: ::Clusters::Applications::Ingress.installed.count,
+ clusters_applications_prometheus: ::Clusters::Applications::Prometheus.installed.count,
+ clusters_applications_runner: ::Clusters::Applications::Runner.installed.count,
in_review_folder: ::Environment.in_review_folder.count,
groups: Group.count,
issues: Issue.count,
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 5619130c263..b102812ec12 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -21,20 +21,19 @@ module Gitlab
raise "Unsupported action: #{action}" unless ALLOWED_GIT_HTTP_ACTIONS.include?(action.to_s)
project = repository.project
- params = {
+
+ {
GL_ID: Gitlab::GlId.gl_id(user),
GL_REPOSITORY: Gitlab::GlRepository.gl_repository(project, is_wiki),
GL_USERNAME: user&.username,
- ShowAllRefs: show_all_refs
- }
- server = {
- address: Gitlab::GitalyClient.address(project.repository_storage),
- token: Gitlab::GitalyClient.token(project.repository_storage)
+ ShowAllRefs: show_all_refs,
+ Repository: repository.gitaly_repository.to_h,
+ RepoPath: 'ignored but not allowed to be empty in gitlab-workhorse',
+ GitalyServer: {
+ address: Gitlab::GitalyClient.address(project.repository_storage),
+ token: Gitlab::GitalyClient.token(project.repository_storage)
+ }
}
- params[:Repository] = repository.gitaly_repository.to_h
- params[:GitalyServer] = server
-
- params
end
def artifact_upload_ok
@@ -42,7 +41,7 @@ module Gitlab
end
def send_git_blob(repository, blob)
- params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_raw_show)
+ params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_raw_show, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
{
'GitalyServer' => gitaly_server_hash(repository),
'GetBlobRequest' => {
@@ -70,7 +69,7 @@ module Gitlab
params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format)
raise "Repository or ref not found" if params.empty?
- if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive)
+ if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
params.merge!(
'GitalyServer' => gitaly_server_hash(repository),
'GitalyRepository' => repository.gitaly_repository.to_h
@@ -87,7 +86,7 @@ module Gitlab
end
def send_git_diff(repository, diff_refs)
- params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff)
+ params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
{
'GitalyServer' => gitaly_server_hash(repository),
'RawDiffRequest' => Gitaly::RawDiffRequest.new(
@@ -105,7 +104,7 @@ module Gitlab
end
def send_git_patch(repository, diff_refs)
- params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_patch)
+ params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_patch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
{
'GitalyServer' => gitaly_server_hash(repository),
'RawPatchRequest' => Gitaly::RawPatchRequest.new(
diff --git a/lib/tasks/gitlab/uploads/migrate.rake b/lib/tasks/gitlab/uploads/migrate.rake
index c26c3ccb3be..78e18992a8e 100644
--- a/lib/tasks/gitlab/uploads/migrate.rake
+++ b/lib/tasks/gitlab/uploads/migrate.rake
@@ -13,6 +13,7 @@ namespace :gitlab do
def enqueue_batch(batch, index)
job = ObjectStorage::MigrateUploadsWorker.enqueue!(batch,
+ @model_class,
@mounted_as,
@to_store)
puts "Enqueued job ##{index}: #{job}"
@@ -25,8 +26,8 @@ namespace :gitlab do
Upload.class_eval { include EachBatch } unless Upload < EachBatch
Upload
- .where.not(store: @to_store)
- .where(uploader: @uploader_class.to_s,
+ .where(store: [nil, ObjectStorage::Store::LOCAL],
+ uploader: @uploader_class.to_s,
model_type: @model_class.base_class.sti_name)
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a04f869f2bb..68d0c0c8854 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-03-12 19:50+0100\n"
-"PO-Revision-Date: 2018-03-12 19:50+0100\n"
+"POT-Creation-Date: 2018-03-27 14:40+0300\n"
+"PO-Revision-Date: 2018-03-27 14:40+0300\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -226,6 +226,9 @@ msgstr ""
msgid "All"
msgstr ""
+msgid "All changes are committed"
+msgstr ""
+
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
@@ -340,6 +343,9 @@ msgstr ""
msgid "Assign to"
msgstr ""
+msgid "Assigned to :name"
+msgstr ""
+
msgid "Assignee"
msgstr ""
@@ -417,6 +423,9 @@ msgstr[1] ""
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr ""
+msgid "Branch has changed"
+msgstr ""
+
msgid "Branch is already taken"
msgstr ""
@@ -570,6 +579,9 @@ msgstr ""
msgid "Cancel"
msgstr ""
+msgid "Cannot be merged automatically"
+msgstr ""
+
msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
@@ -1039,6 +1051,9 @@ msgstr ""
msgid "Commit statistics for %{ref} %{start_time} - %{end_time}"
msgstr ""
+msgid "Commit to %{branchName} branch"
+msgstr ""
+
msgid "CommitBoxTitle|Commit"
msgstr ""
@@ -1084,6 +1099,9 @@ msgstr ""
msgid "Compare Revisions"
msgstr ""
+msgid "Compare changes with the last commit"
+msgstr ""
+
msgid "CompareBranches|%{source_branch} and %{target_branch} are the same."
msgstr ""
@@ -1099,6 +1117,9 @@ msgstr ""
msgid "CompareBranches|There isn't anything to compare."
msgstr ""
+msgid "Confidential"
+msgstr ""
+
msgid "Confidentiality"
msgstr ""
@@ -1195,6 +1216,12 @@ msgstr ""
msgid "Create New Directory"
msgstr ""
+msgid "Create a new branch"
+msgstr ""
+
+msgid "Create a new branch and merge request"
+msgstr ""
+
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr ""
@@ -1204,7 +1231,10 @@ msgstr ""
msgid "Create directory"
msgstr ""
-msgid "Create empty bare repository"
+msgid "Create empty repository"
+msgstr ""
+
+msgid "Create file"
msgstr ""
msgid "Create group label"
@@ -1219,6 +1249,15 @@ msgstr ""
msgid "Create merge request and branch"
msgstr ""
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new label"
msgstr ""
@@ -1237,6 +1276,12 @@ msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr ""
+msgid "Creates a new branch from %{branchName}"
+msgstr ""
+
+msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
+msgstr ""
+
msgid "Cron Timezone"
msgstr ""
@@ -1311,6 +1356,9 @@ msgstr ""
msgid "Directory name"
msgstr ""
+msgid "Discard draft"
+msgstr ""
+
msgid "Dismiss Cycle Analytics introduction box"
msgstr ""
@@ -1347,6 +1395,9 @@ msgstr ""
msgid "DownloadSource|Download"
msgstr ""
+msgid "Downvotes"
+msgstr ""
+
msgid "Due date"
msgstr ""
@@ -1356,6 +1407,12 @@ msgstr ""
msgid "Edit Pipeline Schedule %{id}"
msgstr ""
+msgid "Edit files in the editor and commit changes here"
+msgstr ""
+
+msgid "Editing"
+msgstr ""
+
msgid "Emails"
msgstr ""
@@ -1410,6 +1467,12 @@ msgstr ""
msgid "Environments|You don't have any environments right now."
msgstr ""
+msgid "Error checking branch data. Please try again."
+msgstr ""
+
+msgid "Error committing changes. Please try again."
+msgstr ""
+
msgid "Error fetching contributors data."
msgstr ""
@@ -1497,6 +1560,9 @@ msgstr ""
msgid "Fields on this page are now uneditable, you can configure"
msgstr ""
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr ""
@@ -1628,9 +1694,6 @@ msgstr ""
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr ""
-msgid "GroupsTree|Are you sure you want to leave the \"${group.fullName}\" group?"
-msgstr ""
-
msgid "GroupsTree|Create a project in this group."
msgstr ""
@@ -1676,6 +1739,9 @@ msgstr ""
msgid "HealthCheck|Unhealthy"
msgstr ""
+msgid "Help"
+msgstr ""
+
msgid "Hide value"
msgid_plural "Hide values"
msgstr[0] ""
@@ -1875,6 +1941,9 @@ msgstr ""
msgid "List your GitHub repositories"
msgstr ""
+msgid "Loading the GitLab IDE..."
+msgstr ""
+
msgid "Lock"
msgstr ""
@@ -1890,6 +1959,9 @@ msgstr ""
msgid "Login"
msgstr ""
+msgid "Manage all notifications"
+msgstr ""
+
msgid "Manage group labels"
msgstr ""
@@ -2042,6 +2114,9 @@ msgstr ""
msgid "No assignee"
msgstr ""
+msgid "No changes"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -2633,6 +2708,9 @@ msgstr ""
msgid "Related Merged Requests"
msgstr ""
+msgid "Related merge requests"
+msgstr ""
+
msgid "Remind later"
msgstr ""
@@ -2671,6 +2749,12 @@ msgstr ""
msgid "Revert this merge request"
msgstr ""
+msgid "Reviewing"
+msgstr ""
+
+msgid "Runners"
+msgstr ""
+
msgid "Running"
msgstr ""
@@ -2805,21 +2889,12 @@ msgstr ""
msgid "Something went wrong when toggling the button"
msgstr ""
-msgid "Something went wrong while closing the %{issuable}. Please try again later"
-msgstr ""
-
msgid "Something went wrong while fetching the projects."
msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
-msgid "Something went wrong while reopening the %{issuable}. Please try again later"
-msgstr ""
-
-msgid "Something went wrong while resolving this discussion. Please try again."
-msgstr ""
-
msgid "Something went wrong. Please try again."
msgstr ""
@@ -3044,6 +3119,9 @@ msgstr ""
msgid "Target Branch"
msgstr ""
+msgid "Target branch"
+msgstr ""
+
msgid "Team"
msgstr ""
@@ -3435,6 +3513,9 @@ msgstr ""
msgid "UploadLink|click to upload"
msgstr ""
+msgid "Upvotes"
+msgstr ""
+
msgid "Use the following registration token during setup:"
msgstr ""
@@ -3444,6 +3525,9 @@ 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 "View and edit lines"
+msgstr ""
+
msgid "View file @ "
msgstr ""
@@ -3486,6 +3570,9 @@ msgstr ""
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "Web IDE"
+msgstr ""
+
msgid "Wiki"
msgstr ""
@@ -3597,6 +3684,9 @@ msgstr ""
msgid "Withdraw Access Request"
msgstr ""
+msgid "Write a commit message..."
+msgstr ""
+
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr ""
@@ -3669,12 +3759,18 @@ msgstr ""
msgid "You'll need to use different branch names to get a valid comparison."
msgstr ""
+msgid "You're receiving this email because of your account on %{host}. %{manage_notifications_link} &middot; %{help_link}"
+msgstr ""
+
msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure"
msgstr ""
msgid "Your changes can be committed to %{branch_name} because a merge request is open."
msgstr ""
+msgid "Your changes have been committed. Commit %{commitId} %{commitStats}"
+msgstr ""
+
msgid "Your comment will not be visible to the public."
msgstr ""
@@ -3902,3 +3998,6 @@ msgstr ""
msgid "uses Kubernetes clusters to deploy your code!"
msgstr ""
+
+msgid "with %{additions} additions, %{deletions} deletions."
+msgstr ""
diff --git a/package.json b/package.json
index 31edc3a8016..56fd2575e91 100644
--- a/package.json
+++ b/package.json
@@ -121,8 +121,5 @@
"nodemon": "^1.15.1",
"prettier": "1.11.1",
"webpack-dev-server": "^2.11.2"
- },
- "optionalDependencies": {
- "fsevents": "^1.1.3"
}
}
diff --git a/qa/qa/scenario/bootable.rb b/qa/qa/scenario/bootable.rb
index d6de4d404c8..dd12ea6d492 100644
--- a/qa/qa/scenario/bootable.rb
+++ b/qa/qa/scenario/bootable.rb
@@ -23,7 +23,7 @@ module QA
arguments.parse!(argv)
- self.perform(**Runtime::Scenario.attributes)
+ self.perform(Runtime::Scenario.attributes, *arguments.default_argv)
end
private
diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb
index 0af9afd1ea4..567e5fd6cca 100644
--- a/qa/qa/scenario/test/instance.rb
+++ b/qa/qa/scenario/test/instance.rb
@@ -11,7 +11,7 @@ module QA
tags :core
- def perform(address, *files)
+ def perform(address, *rspec_options)
Runtime::Scenario.define(:gitlab_address, address)
##
@@ -22,9 +22,9 @@ module QA
Specs::Runner.perform do |specs|
specs.tty = true
specs.tags = self.class.focus
- specs.files =
- if files.any?
- files
+ specs.options =
+ if rspec_options.any?
+ rspec_options
else
File.expand_path('../../specs/features', __dir__)
end
diff --git a/qa/qa/scenario/test/integration/mattermost.rb b/qa/qa/scenario/test/integration/mattermost.rb
index d939f52ab16..13bfad28b0b 100644
--- a/qa/qa/scenario/test/integration/mattermost.rb
+++ b/qa/qa/scenario/test/integration/mattermost.rb
@@ -9,10 +9,10 @@ module QA
class Mattermost < Test::Instance
tags :core, :mattermost
- def perform(address, mattermost, *files)
+ def perform(address, mattermost, *rspec_options)
Runtime::Scenario.define(:mattermost_address, mattermost)
- super(address, *files)
+ super(address, *rspec_options)
end
end
end
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index 752e3e60b8c..f8f6fe65599 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -3,19 +3,19 @@ require 'rspec/core'
module QA
module Specs
class Runner < Scenario::Template
- attr_accessor :tty, :tags, :files
+ attr_accessor :tty, :tags, :options
def initialize
@tty = false
@tags = []
- @files = [File.expand_path('./features', __dir__)]
+ @options = [File.expand_path('./features', __dir__)]
end
def perform
args = []
args.push('--tty') if tty
tags.to_a.each { |tag| args.push(['-t', tag.to_s]) }
- args.push(files)
+ args.push(options)
Runtime::Browser.configure!
diff --git a/qa/spec/scenario/test/instance_spec.rb b/qa/spec/scenario/test/instance_spec.rb
index bd09c28e924..a74a9538be8 100644
--- a/qa/spec/scenario/test/instance_spec.rb
+++ b/qa/spec/scenario/test/instance_spec.rb
@@ -29,7 +29,7 @@ describe QA::Scenario::Test::Instance do
it 'should call runner with default arguments' do
subject.perform("test")
- expect(runner).to have_received(:files=)
+ expect(runner).to have_received(:options=)
.with(File.expand_path('../../../qa/specs/features', __dir__))
end
end
@@ -38,7 +38,7 @@ describe QA::Scenario::Test::Instance do
it 'should call runner with paths' do
subject.perform('test', 'path1', 'path2')
- expect(runner).to have_received(:files=).with(%w[path1 path2])
+ expect(runner).to have_received(:options=).with(%w[path1 path2])
end
end
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 5b2614163ff..548c5ef36e7 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -107,7 +107,7 @@ describe Projects::MilestonesController do
it 'shows group milestone' do
post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
- expect(flash[:notice]).to eq("#{milestone.title} promoted to group milestone")
+ expect(flash[:notice]).to eq("#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, milestone.iid)}\">group milestone</a>.")
expect(response).to redirect_to(project_milestones_path(project))
end
end
diff --git a/spec/factories/ci/build_metadata.rb b/spec/factories/ci/build_metadata.rb
new file mode 100644
index 00000000000..66bbd977b88
--- /dev/null
+++ b/spec/factories/ci/build_metadata.rb
@@ -0,0 +1,9 @@
+FactoryBot.define do
+ factory :ci_build_metadata, class: Ci::BuildMetadata do
+ build factory: :ci_build
+
+ after(:build) do |build_metadata, _|
+ build_metadata.project ||= build_metadata.build.project
+ end
+ end
+end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index c89bc54cad4..3005d74c3cf 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -120,6 +120,53 @@ feature 'Admin updates settings' do
expect(page).to have_content "Application settings saved successfully"
end
+ scenario 'Change Performance bar settings' do
+ group = create(:group)
+
+ page.within('.as-performance') do
+ check 'Enable the Performance Bar'
+ fill_in 'Allowed group', with: group.path
+ click_on 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(find_field('Enable the Performance Bar')).to be_checked
+ expect(find_field('Allowed group').value).to eq group.path
+
+ page.within('.as-performance') do
+ uncheck 'Enable the Performance Bar'
+ click_on 'Save changes'
+ end
+
+ expect(page).to have_content 'Application settings saved successfully'
+ expect(find_field('Enable the Performance Bar')).not_to be_checked
+ expect(find_field('Allowed group').value).to be_nil
+ end
+
+ scenario 'Change Background jobs settings' do
+ page.within('.as-background') do
+ fill_in 'Throttling Factor', with: 1
+ click_button 'Save changes'
+ end
+
+ expect(Gitlab::CurrentSettings.sidekiq_throttling_factor).to eq(1)
+ expect(page).to have_content "Application settings saved successfully"
+ end
+
+ scenario 'Change Spam settings' do
+ page.within('.as-spam') do
+ check 'Enable reCAPTCHA'
+ fill_in 'reCAPTCHA Site Key', with: 'key'
+ fill_in 'reCAPTCHA Private Key', with: 'key'
+ fill_in 'IPs per user', with: 15
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(Gitlab::CurrentSettings.recaptcha_enabled).to be true
+ expect(Gitlab::CurrentSettings.unique_ips_limit_per_user).to eq(15)
+ end
+
scenario 'Change Slack Notifications Service template settings' do
first(:link, 'Service Templates').click
click_link 'Slack notifications'
@@ -172,29 +219,6 @@ feature 'Admin updates settings' do
expect(find_field('ED25519 SSH keys').value).to eq(forbidden)
end
- scenario 'Change Performance Bar settings' do
- group = create(:group)
-
- check 'Enable the Performance Bar'
- fill_in 'Allowed group', with: group.path
-
- click_on 'Save'
-
- expect(page).to have_content 'Application settings saved successfully'
-
- expect(find_field('Enable the Performance Bar')).to be_checked
- expect(find_field('Allowed group').value).to eq group.path
-
- uncheck 'Enable the Performance Bar'
-
- click_on 'Save'
-
- expect(page).to have_content 'Application settings saved successfully'
-
- expect(find_field('Enable the Performance Bar')).not_to be_checked
- expect(find_field('Allowed group').value).to be_nil
- end
-
def check_all_events
page.check('Active')
page.check('Push')
diff --git a/spec/features/groups/activity_spec.rb b/spec/features/groups/activity_spec.rb
index d3b25ec3d6c..7bc809b3104 100644
--- a/spec/features/groups/activity_spec.rb
+++ b/spec/features/groups/activity_spec.rb
@@ -8,11 +8,30 @@ feature 'Group activity page' do
context 'when signed in' do
before do
sign_in(user)
- visit path
end
- it_behaves_like "it has an RSS button with current_user's RSS token"
- it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+ describe 'RSS' do
+ before do
+ visit path
+ end
+
+ it_behaves_like "it has an RSS button with current_user's RSS token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+ end
+
+ context 'when project is in the group', :js do
+ let(:project) { create(:project, :public, namespace: group) }
+
+ before do
+ project.add_master(user)
+
+ visit path
+ end
+
+ it 'renders user joined to project event' do
+ expect(page).to have_content 'joined project'
+ end
+ end
end
context 'when signed out' do
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index b83bad3befb..1ce30015e81 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -76,6 +76,27 @@ feature 'Edit group settings' do
end
end
end
+
+ describe 'edit group avatar' do
+ before do
+ visit edit_group_path(group)
+
+ attach_file(:group_avatar, Rails.root.join('spec', 'fixtures', 'banana_sample.gif'))
+
+ expect { click_button 'Save group' }.to change { group.reload.avatar? }.to(true)
+ end
+
+ it 'uploads new group avatar' do
+ expect(group.avatar).to be_instance_of AvatarUploader
+ expect(group.avatar.url).to eq "/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif"
+ expect(page).to have_link('Remove avatar')
+ end
+
+ it 'removes group avatar' do
+ expect { click_link 'Remove avatar' }.to change { group.reload.avatar? }.to(false)
+ expect(page).not_to have_link('Remove avatar')
+ end
+ end
end
def update_path(new_group_path)
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 450bc0ff8cf..90bf7ba49f6 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -3,8 +3,11 @@ require 'spec_helper'
feature 'Group issues page' do
include FilteredSearchHelpers
+ let(:group) { create(:group) }
+ let(:project) { create(:project, :public, group: group)}
+ let(:path) { issues_group_path(group) }
+
context 'with shared examples' do
- let(:path) { issues_group_path(group) }
let(:issuable) { create(:issue, project: project, title: "this is my created issuable")}
include_examples 'project features apply to issuables', Issue
@@ -31,7 +34,6 @@ feature 'Group issues page' do
let(:access_level) { ProjectFeature::ENABLED }
let(:user) { user_in_group }
let(:user2) { user_outside_group }
- let(:path) { issues_group_path(group) }
it 'filters by only group users' do
filtered_search.set('assignee:')
@@ -43,9 +45,7 @@ feature 'Group issues page' do
end
context 'issues list', :nested_groups do
- let(:group) { create(:group)}
let(:subgroup) { create(:group, parent: group) }
- let(:project) { create(:project, :public, group: group)}
let(:subgroup_project) { create(:project, :public, group: subgroup)}
let!(:issue) { create(:issue, project: project, title: 'root group issue') }
let!(:subgroup_issue) { create(:issue, project: subgroup_project, title: 'subgroup issue') }
@@ -59,5 +59,17 @@ feature 'Group issues page' do
expect(page).to have_content('subgroup issue')
end
end
+
+ context 'when project is archived' do
+ before do
+ project.archive!
+ end
+
+ it 'does not render issue' do
+ visit path
+
+ expect(page).not_to have_content issue.title[0..80]
+ end
+ end
end
end
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 7ce6a61d50c..672ae785c2d 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -5,14 +5,14 @@ feature 'Group merge requests page' do
let(:path) { merge_requests_group_path(group) }
let(:issuable) { create(:merge_request, source_project: project, target_project: project, title: 'this is my created issuable') }
+ let(:access_level) { ProjectFeature::ENABLED }
+ let(:user) { user_in_group }
include_examples 'project features apply to issuables', MergeRequest
context 'archived issuable' do
let(:project_archived) { create(:project, :archived, :merge_requests_enabled, :repository, group: group) }
let(:issuable_archived) { create(:merge_request, source_project: project_archived, target_project: project_archived, title: 'issuable of an archived project') }
- let(:access_level) { ProjectFeature::ENABLED }
- let(:user) { user_in_group }
before do
issuable_archived
@@ -36,9 +36,17 @@ feature 'Group merge requests page' do
end
end
+ context 'when merge request assignee to user' do
+ before do
+ issuable.update!(assignee: user)
+
+ visit path
+ end
+
+ it { expect(page).to have_content issuable.title[0..80] }
+ end
+
context 'group filtered search', :js do
- let(:access_level) { ProjectFeature::ENABLED }
- let(:user) { user_in_group }
let(:user2) { user_outside_group }
it 'filters by assignee only group users' do
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
index ceccc471405..4ffadbbcd35 100644
--- a/spec/features/groups/show_spec.rb
+++ b/spec/features/groups/show_spec.rb
@@ -15,14 +15,44 @@ feature 'Group show page' do
end
it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+
+ context 'when group does not exist' do
+ let(:path) { group_path('not-exist') }
+
+ it { expect(status_code).to eq(404) }
+ end
end
context 'when signed out' do
- before do
- visit path
+ describe 'RSS' do
+ before do
+ visit path
+ end
+
+ it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+ end
+
+ context 'when group has a public project', :js do
+ let!(:project) { create(:project, :public, namespace: group) }
+
+ it 'renders public project' do
+ visit path
+
+ expect(page).to have_link group.name
+ expect(page).to have_link project.name
+ end
end
- it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+ context 'when group has a private project', :js do
+ let!(:project) { create(:project, :private, namespace: group) }
+
+ it 'does not render private project' do
+ visit path
+
+ expect(page).to have_link group.name
+ expect(page).not_to have_link project.name
+ end
+ end
end
context 'subgroup support' do
diff --git a/spec/features/groups/user_browse_projects_group_page_spec.rb b/spec/features/groups/user_browse_projects_group_page_spec.rb
new file mode 100644
index 00000000000..e81c3180e78
--- /dev/null
+++ b/spec/features/groups/user_browse_projects_group_page_spec.rb
@@ -0,0 +1,29 @@
+require 'rails_helper'
+
+describe 'User browse group projects page' do
+ let(:user) { create :user }
+ let(:group) { create :group }
+
+ context 'when user is owner' do
+ before do
+ group.add_owner(user)
+ end
+
+ context 'when user signed in' do
+ before do
+ sign_in(user)
+ end
+
+ context 'when group has archived project', :js do
+ let!(:project) { create :project, :archived, namespace: group }
+
+ it 'renders projects list' do
+ visit projects_group_path(group)
+
+ expect(page).to have_link project.name
+ expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/issuables/discussion_lock_spec.rb b/spec/features/issuables/discussion_lock_spec.rb
index ecbe51a7bc2..7ea29ff252b 100644
--- a/spec/features/issuables/discussion_lock_spec.rb
+++ b/spec/features/issuables/discussion_lock_spec.rb
@@ -14,7 +14,7 @@ describe 'Discussion Lock', :js do
project.add_developer(user)
end
- context 'when the discussion is unlocked' do
+ context 'when the discussion is unlocked' do
it 'the user can lock the issue' do
visit project_issue_path(project, issue)
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index b835558b142..27551bb70ee 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -161,6 +161,50 @@ feature 'Issue Sidebar' do
end
end
end
+
+ context 'interacting with collapsed sidebar', :js do
+ collapsed_sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed'
+ expanded_sidebar_selector = 'aside.right-sidebar.right-sidebar-expanded'
+ confidentiality_sidebar_block = '.block.confidentiality'
+ lock_sidebar_block = '.block.lock'
+ collapsed_sidebar_block_icon = '.sidebar-collapsed-icon'
+
+ before do
+ resize_screen_sm
+ end
+
+ it 'confidentiality block expands then collapses sidebar' do
+ expect(page).to have_css(collapsed_sidebar_selector)
+
+ page.within(confidentiality_sidebar_block) do
+ find(collapsed_sidebar_block_icon).click
+ end
+
+ expect(page).to have_css(expanded_sidebar_selector)
+
+ page.within(confidentiality_sidebar_block) do
+ page.find('button', text: 'Cancel').click
+ end
+
+ expect(page).to have_css(collapsed_sidebar_selector)
+ end
+
+ it 'lock block expands then collapses sidebar' do
+ expect(page).to have_css(collapsed_sidebar_selector)
+
+ page.within(lock_sidebar_block) do
+ find(collapsed_sidebar_block_icon).click
+ end
+
+ expect(page).to have_css(expanded_sidebar_selector)
+
+ page.within(lock_sidebar_block) do
+ page.find('button', text: 'Cancel').click
+ end
+
+ expect(page).to have_css(collapsed_sidebar_selector)
+ end
+ end
end
context 'as a guest' do
diff --git a/spec/features/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb
index 171e061e60e..e8eb0d17ca4 100644
--- a/spec/features/profiles/account_spec.rb
+++ b/spec/features/profiles/account_spec.rb
@@ -43,14 +43,14 @@ feature 'Profile > Account' do
update_username(new_username)
visit new_project_path
expect(current_path).to eq(new_project_path)
- expect(find('.breadcrumbs-sub-title')).to have_content(project.path)
+ expect(find('.breadcrumbs-sub-title')).to have_content('Details')
end
scenario 'the old project path redirects to the new path' do
update_username(new_username)
visit old_project_path
expect(current_path).to eq(new_project_path)
- expect(find('.breadcrumbs-sub-title')).to have_content(project.path)
+ expect(find('.breadcrumbs-sub-title')).to have_content('Details')
end
end
end
diff --git a/spec/fixtures/api/schemas/public_api/v4/project/export_status.json b/spec/fixtures/api/schemas/public_api/v4/project/export_status.json
index d24a6f93f4b..81c8815caf6 100644
--- a/spec/fixtures/api/schemas/public_api/v4/project/export_status.json
+++ b/spec/fixtures/api/schemas/public_api/v4/project/export_status.json
@@ -1,7 +1,9 @@
{
"type": "object",
"allOf": [
- { "$ref": "identity.json" },
+ {
+ "$ref": "identity.json"
+ },
{
"required": [
"export_status"
@@ -9,7 +11,12 @@
"properties": {
"export_status": {
"type": "string",
- "enum": ["none", "started", "finished"]
+ "enum": [
+ "none",
+ "started",
+ "finished",
+ "after_export_action"
+ ]
}
}
}
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index baf927a9acc..b77114a8152 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -50,6 +50,11 @@ describe PageLayoutHelper do
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development'))
expect(helper.favicon).to eq 'favicon-blue.ico'
end
+
+ it 'has yellow favicon for canary' do
+ stub_env('CANARY', 'true')
+ expect(helper.favicon).to eq 'favicon-yellow.ico'
+ end
end
describe 'page_image' do
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js
index 5477581c1b9..3d7ccf432be 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -35,14 +35,14 @@ describe('Api', () => {
});
describe('group', () => {
- it('fetches a group', (done) => {
+ it('fetches a group', done => {
const groupId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}`;
mock.onGet(expectedUrl).reply(200, {
name: 'test',
});
- Api.group(groupId, (response) => {
+ Api.group(groupId, response => {
expect(response.name).toBe('test');
done();
});
@@ -50,15 +50,17 @@ describe('Api', () => {
});
describe('groups', () => {
- it('fetches groups', (done) => {
+ it('fetches groups', done => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups.json`;
- mock.onGet(expectedUrl).reply(200, [{
- name: 'test',
- }]);
+ mock.onGet(expectedUrl).reply(200, [
+ {
+ name: 'test',
+ },
+ ]);
- Api.groups(query, options, (response) => {
+ Api.groups(query, options, response => {
expect(response.length).toBe(1);
expect(response[0].name).toBe('test');
done();
@@ -67,14 +69,16 @@ describe('Api', () => {
});
describe('namespaces', () => {
- it('fetches namespaces', (done) => {
+ it('fetches namespaces', done => {
const query = 'dummy query';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/namespaces.json`;
- mock.onGet(expectedUrl).reply(200, [{
- name: 'test',
- }]);
+ mock.onGet(expectedUrl).reply(200, [
+ {
+ name: 'test',
+ },
+ ]);
- Api.namespaces(query, (response) => {
+ Api.namespaces(query, response => {
expect(response.length).toBe(1);
expect(response[0].name).toBe('test');
done();
@@ -83,31 +87,35 @@ describe('Api', () => {
});
describe('projects', () => {
- it('fetches projects with membership when logged in', (done) => {
+ it('fetches projects with membership when logged in', done => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
window.gon.current_user_id = 1;
- mock.onGet(expectedUrl).reply(200, [{
- name: 'test',
- }]);
+ mock.onGet(expectedUrl).reply(200, [
+ {
+ name: 'test',
+ },
+ ]);
- Api.projects(query, options, (response) => {
+ Api.projects(query, options, response => {
expect(response.length).toBe(1);
expect(response[0].name).toBe('test');
done();
});
});
- it('fetches projects without membership when not logged in', (done) => {
+ it('fetches projects without membership when not logged in', done => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
- mock.onGet(expectedUrl).reply(200, [{
- name: 'test',
- }]);
+ mock.onGet(expectedUrl).reply(200, [
+ {
+ name: 'test',
+ },
+ ]);
- Api.projects(query, options, (response) => {
+ Api.projects(query, options, response => {
expect(response.length).toBe(1);
expect(response[0].name).toBe('test');
done();
@@ -115,8 +123,65 @@ describe('Api', () => {
});
});
+ describe('mergerequest', () => {
+ it('fetches a merge request', done => {
+ const projectPath = 'abc';
+ const mergeRequestId = '123456';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}`;
+ mock.onGet(expectedUrl).reply(200, {
+ title: 'test',
+ });
+
+ Api.mergeRequest(projectPath, mergeRequestId)
+ .then(({ data }) => {
+ expect(data.title).toBe('test');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('mergerequest changes', () => {
+ it('fetches the changes of a merge request', done => {
+ const projectPath = 'abc';
+ const mergeRequestId = '123456';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/changes`;
+ mock.onGet(expectedUrl).reply(200, {
+ title: 'test',
+ });
+
+ Api.mergeRequestChanges(projectPath, mergeRequestId)
+ .then(({ data }) => {
+ expect(data.title).toBe('test');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('mergerequest versions', () => {
+ it('fetches the versions of a merge request', done => {
+ const projectPath = 'abc';
+ const mergeRequestId = '123456';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/versions`;
+ mock.onGet(expectedUrl).reply(200, [
+ {
+ id: 123,
+ },
+ ]);
+
+ Api.mergeRequestVersions(projectPath, mergeRequestId)
+ .then(({ data }) => {
+ expect(data.length).toBe(1);
+ expect(data[0].id).toBe(123);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
describe('newLabel', () => {
- it('creates a new label', (done) => {
+ it('creates a new label', done => {
const namespace = 'some namespace';
const project = 'some project';
const labelData = { some: 'data' };
@@ -124,36 +189,42 @@ describe('Api', () => {
const expectedData = {
label: labelData,
};
- mock.onPost(expectedUrl).reply((config) => {
+ mock.onPost(expectedUrl).reply(config => {
expect(config.data).toBe(JSON.stringify(expectedData));
- return [200, {
- name: 'test',
- }];
+ return [
+ 200,
+ {
+ name: 'test',
+ },
+ ];
});
- Api.newLabel(namespace, project, labelData, (response) => {
+ Api.newLabel(namespace, project, labelData, response => {
expect(response.name).toBe('test');
done();
});
});
- it('creates a group label', (done) => {
+ it('creates a group label', done => {
const namespace = 'group/subgroup';
const labelData = { some: 'data' };
const expectedUrl = `${dummyUrlRoot}/groups/${namespace}/-/labels`;
const expectedData = {
label: labelData,
};
- mock.onPost(expectedUrl).reply((config) => {
+ mock.onPost(expectedUrl).reply(config => {
expect(config.data).toBe(JSON.stringify(expectedData));
- return [200, {
- name: 'test',
- }];
+ return [
+ 200,
+ {
+ name: 'test',
+ },
+ ];
});
- Api.newLabel(namespace, undefined, labelData, (response) => {
+ Api.newLabel(namespace, undefined, labelData, response => {
expect(response.name).toBe('test');
done();
});
@@ -161,15 +232,17 @@ describe('Api', () => {
});
describe('groupProjects', () => {
- it('fetches group projects', (done) => {
+ it('fetches group projects', done => {
const groupId = '123456';
const query = 'dummy query';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
- mock.onGet(expectedUrl).reply(200, [{
- name: 'test',
- }]);
+ mock.onGet(expectedUrl).reply(200, [
+ {
+ name: 'test',
+ },
+ ]);
- Api.groupProjects(groupId, query, (response) => {
+ Api.groupProjects(groupId, query, response => {
expect(response.length).toBe(1);
expect(response[0].name).toBe('test');
done();
@@ -178,13 +251,13 @@ describe('Api', () => {
});
describe('licenseText', () => {
- it('fetches a license text', (done) => {
+ it('fetches a license text', done => {
const licenseKey = "driver's license";
const data = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/licenses/${licenseKey}`;
mock.onGet(expectedUrl).reply(200, 'test');
- Api.licenseText(licenseKey, data, (response) => {
+ Api.licenseText(licenseKey, data, response => {
expect(response).toBe('test');
done();
});
@@ -192,12 +265,12 @@ describe('Api', () => {
});
describe('gitignoreText', () => {
- it('fetches a gitignore text', (done) => {
+ it('fetches a gitignore text', done => {
const gitignoreKey = 'ignore git';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitignores/${gitignoreKey}`;
mock.onGet(expectedUrl).reply(200, 'test');
- Api.gitignoreText(gitignoreKey, (response) => {
+ Api.gitignoreText(gitignoreKey, response => {
expect(response).toBe('test');
done();
});
@@ -205,12 +278,12 @@ describe('Api', () => {
});
describe('gitlabCiYml', () => {
- it('fetches a .gitlab-ci.yml', (done) => {
+ it('fetches a .gitlab-ci.yml', done => {
const gitlabCiYmlKey = 'Y CI ML';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitlab_ci_ymls/${gitlabCiYmlKey}`;
mock.onGet(expectedUrl).reply(200, 'test');
- Api.gitlabCiYml(gitlabCiYmlKey, (response) => {
+ Api.gitlabCiYml(gitlabCiYmlKey, response => {
expect(response).toBe('test');
done();
});
@@ -218,12 +291,12 @@ describe('Api', () => {
});
describe('dockerfileYml', () => {
- it('fetches a Dockerfile', (done) => {
+ it('fetches a Dockerfile', done => {
const dockerfileYmlKey = 'a giant whale';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/dockerfiles/${dockerfileYmlKey}`;
mock.onGet(expectedUrl).reply(200, 'test');
- Api.dockerfileYml(dockerfileYmlKey, (response) => {
+ Api.dockerfileYml(dockerfileYmlKey, response => {
expect(response).toBe('test');
done();
});
@@ -231,12 +304,14 @@ describe('Api', () => {
});
describe('issueTemplate', () => {
- it('fetches an issue template', (done) => {
+ it('fetches an issue template', done => {
const namespace = 'some namespace';
const project = 'some project';
const templateKey = ' template #%?.key ';
const templateType = 'template type';
- const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent(templateKey)}`;
+ const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent(
+ templateKey,
+ )}`;
mock.onGet(expectedUrl).reply(200, 'test');
Api.issueTemplate(namespace, project, templateKey, templateType, (error, response) => {
@@ -247,13 +322,15 @@ describe('Api', () => {
});
describe('users', () => {
- it('fetches users', (done) => {
+ it('fetches users', done => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users.json`;
- mock.onGet(expectedUrl).reply(200, [{
- name: 'test',
- }]);
+ mock.onGet(expectedUrl).reply(200, [
+ {
+ name: 'test',
+ },
+ ]);
Api.users(query, options)
.then(({ data }) => {
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index 0671facb285..81f1a97112f 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -1,7 +1,4 @@
/* global BoardService */
-/* eslint-disable comma-dangle, no-unused-vars, quote-props */
-import _ from 'underscore';
-
export const listObj = {
id: 300,
position: 0,
@@ -11,8 +8,8 @@ export const listObj = {
id: 5000,
title: 'Testing',
color: 'red',
- description: 'testing;'
- }
+ description: 'testing;',
+ },
};
export const listObjDuplicate = {
@@ -24,35 +21,37 @@ export const listObjDuplicate = {
id: listObj.label.id,
title: 'Testing',
color: 'red',
- description: 'testing;'
- }
+ description: 'testing;',
+ },
};
export const BoardsMockData = {
- 'GET': {
+ GET: {
'/test/-/boards/1/lists/300/issues?id=300&page=1&=': {
- issues: [{
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [],
- assignees: [],
- }],
- }
+ issues: [
+ {
+ title: 'Testing',
+ id: 1,
+ iid: 1,
+ confidential: false,
+ labels: [],
+ assignees: [],
+ },
+ ],
+ },
+ },
+ POST: {
+ '/test/-/boards/1/lists': listObj,
},
- 'POST': {
- '/test/-/boards/1/lists': listObj
+ PUT: {
+ '/test/issue-boards/board/1/lists{/id}': {},
},
- 'PUT': {
- '/test/issue-boards/board/1/lists{/id}': {}
+ DELETE: {
+ '/test/issue-boards/board/1/lists{/id}': {},
},
- 'DELETE': {
- '/test/issue-boards/board/1/lists{/id}': {}
- }
};
-export const boardsMockInterceptor = (config) => {
+export const boardsMockInterceptor = config => {
const body = BoardsMockData[config.method.toUpperCase()][config.url];
return [200, body];
};
diff --git a/spec/javascripts/droplab/constants_spec.js b/spec/javascripts/droplab/constants_spec.js
index b9d28db74cc..23b69defec6 100644
--- a/spec/javascripts/droplab/constants_spec.js
+++ b/spec/javascripts/droplab/constants_spec.js
@@ -1,39 +1,37 @@
-/* eslint-disable */
-
import * as constants from '~/droplab/constants';
-describe('constants', function () {
- describe('DATA_TRIGGER', function () {
+describe('constants', function() {
+ describe('DATA_TRIGGER', function() {
it('should be `data-dropdown-trigger`', function() {
expect(constants.DATA_TRIGGER).toBe('data-dropdown-trigger');
});
});
- describe('DATA_DROPDOWN', function () {
+ describe('DATA_DROPDOWN', function() {
it('should be `data-dropdown`', function() {
expect(constants.DATA_DROPDOWN).toBe('data-dropdown');
});
});
- describe('SELECTED_CLASS', function () {
+ describe('SELECTED_CLASS', function() {
it('should be `droplab-item-selected`', function() {
expect(constants.SELECTED_CLASS).toBe('droplab-item-selected');
});
});
- describe('ACTIVE_CLASS', function () {
+ describe('ACTIVE_CLASS', function() {
it('should be `droplab-item-active`', function() {
expect(constants.ACTIVE_CLASS).toBe('droplab-item-active');
});
});
- describe('TEMPLATE_REGEX', function () {
+ describe('TEMPLATE_REGEX', function() {
it('should be a handlebars templating syntax regex', function() {
expect(constants.TEMPLATE_REGEX).toEqual(/\{\{(.+?)\}\}/g);
});
});
- describe('IGNORE_CLASS', function () {
+ describe('IGNORE_CLASS', function() {
it('should be `droplab-item-ignore`', function() {
expect(constants.IGNORE_CLASS).toBe('droplab-item-ignore');
});
diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb
index b344b389241..e8865b04874 100644
--- a/spec/javascripts/fixtures/projects.rb
+++ b/spec/javascripts/fixtures/projects.rb
@@ -17,8 +17,6 @@ describe 'Projects (JavaScript fixtures)', type: :controller do
end
before do
- # EE-specific start
- # EE specific end
project.add_master(admin)
sign_in(admin)
end
diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js
index dc0a5bc275c..1cb20a1e7ff 100644
--- a/spec/javascripts/gfm_auto_complete_spec.js
+++ b/spec/javascripts/gfm_auto_complete_spec.js
@@ -81,13 +81,21 @@ describe('GfmAutoComplete', function () {
});
it('should quote if value contains any non-alphanumeric characters', () => {
- expect(beforeInsert(atwhoInstance, '~label-20')).toBe('~"label-20"');
+ expect(beforeInsert(atwhoInstance, '~label-20')).toBe('~"label\\-20"');
expect(beforeInsert(atwhoInstance, '~label 20')).toBe('~"label 20"');
});
it('should quote integer labels', () => {
expect(beforeInsert(atwhoInstance, '~1234')).toBe('~"1234"');
});
+
+ it('should escape Markdown emphasis characters, except in the first character', () => {
+ expect(beforeInsert(atwhoInstance, '@_group')).toEqual('@\\_group');
+ expect(beforeInsert(atwhoInstance, '~_bug')).toEqual('~\\_bug');
+ expect(beforeInsert(atwhoInstance, '~a `bug`')).toEqual('~"a \\`bug\\`"');
+ expect(beforeInsert(atwhoInstance, '~a ~bug')).toEqual('~"a \\~bug"');
+ expect(beforeInsert(atwhoInstance, '~a **bug')).toEqual('~"a \\*\\*bug"');
+ });
});
describe('DefaultOptions.matcher', function () {
diff --git a/spec/javascripts/helpers/vuex_action_helper.js b/spec/javascripts/helpers/vuex_action_helper.js
index 2d386fe1da5..83f29d1b0c2 100644
--- a/spec/javascripts/helpers/vuex_action_helper.js
+++ b/spec/javascripts/helpers/vuex_action_helper.js
@@ -1,37 +1,71 @@
-/* eslint-disable */
-
/**
- * helper for testing action with expected mutations
+ * helper for testing action with expected mutations inspired in
* https://vuex.vuejs.org/en/testing.html
+ *
+ * @example
+ * testAction(
+ * actions.actionName, // action
+ * { }, // mocked response
+ * state, // state
+ * [
+ * { type: types.MUTATION}
+ * { type: types.MUTATION_1, payload: {}}
+ * ], // mutations
+ * [
+ * { type: 'actionName', payload: {}},
+ * { type: 'actionName1', payload: {}}
+ * ] //actions
+ * done,
+ * );
*/
-export default (action, payload, state, expectedMutations, done) => {
- let count = 0;
+export default (action, payload, state, expectedMutations, expectedActions, done) => {
+ let mutationsCount = 0;
+ let actionsCount = 0;
// mock commit
- const commit = (type, payload) => {
- const mutation = expectedMutations[count];
-
- try {
- expect(mutation.type).to.equal(type);
- if (payload) {
- expect(mutation.payload).to.deep.equal(payload);
- }
- } catch (error) {
- done(error);
+ const commit = (type, mutationPayload) => {
+ const mutation = expectedMutations[mutationsCount];
+
+ expect(mutation.type).toEqual(type);
+
+ if (mutation.payload) {
+ expect(mutation.payload).toEqual(mutationPayload);
}
- count++;
- if (count >= expectedMutations.length) {
+ mutationsCount += 1;
+ if (mutationsCount >= expectedMutations.length) {
+ done();
+ }
+ };
+
+ // mock dispatch
+ const dispatch = (type, actionPayload) => {
+ const actionExpected = expectedActions[actionsCount];
+
+ expect(actionExpected.type).toEqual(type);
+
+ if (actionExpected.payload) {
+ expect(actionExpected.payload).toEqual(actionPayload);
+ }
+
+ actionsCount += 1;
+ if (actionsCount >= expectedActions.length) {
done();
}
};
// call the action with mocked store and arguments
- action({ commit, state }, payload);
+ action({ commit, state, dispatch }, payload);
// check if no mutations should have been dispatched
if (expectedMutations.length === 0) {
- expect(count).to.equal(0);
+ expect(mutationsCount).toEqual(0);
+ done();
+ }
+
+ // check if no mutations should have been dispatched
+ if (expectedActions.length === 0) {
+ expect(actionsCount).toEqual(0);
done();
}
};
diff --git a/spec/javascripts/ide/components/changed_file_icon_spec.js b/spec/javascripts/ide/components/changed_file_icon_spec.js
index 987aea7befc..541864e912e 100644
--- a/spec/javascripts/ide/components/changed_file_icon_spec.js
+++ b/spec/javascripts/ide/components/changed_file_icon_spec.js
@@ -11,6 +11,7 @@ describe('IDE changed file icon', () => {
vm = createComponent(component, {
file: {
tempFile: false,
+ changed: true,
},
});
});
@@ -20,7 +21,7 @@ describe('IDE changed file icon', () => {
});
describe('changedIcon', () => {
- it('equals file-modified when not a temp file', () => {
+ it('equals file-modified when not a temp file and has changes', () => {
expect(vm.changedIcon).toBe('file-modified');
});
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
index ae657e8c881..9d3fa1280f4 100644
--- a/spec/javascripts/ide/components/repo_editor_spec.js
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -89,6 +89,20 @@ describe('RepoEditor', () => {
done();
});
});
+
+ it('calls createDiffInstance when viewer is a merge request diff', done => {
+ vm.$store.state.viewer = 'mrdiff';
+
+ spyOn(vm.editor, 'createDiffInstance');
+
+ vm.createEditorInstance();
+
+ vm.$nextTick(() => {
+ expect(vm.editor.createDiffInstance).toHaveBeenCalled();
+
+ done();
+ });
+ });
});
describe('setupEditor', () => {
@@ -134,4 +148,48 @@ describe('RepoEditor', () => {
});
});
});
+
+ describe('setup editor for merge request viewing', () => {
+ beforeEach(done => {
+ // Resetting as the main test setup has already done it
+ vm.$destroy();
+ resetStore(vm.$store);
+ Editor.editorInstance.modelManager.dispose();
+
+ const f = {
+ ...file(),
+ active: true,
+ tempFile: true,
+ html: 'testing',
+ mrChange: { diff: 'ABC' },
+ baseRaw: 'testing',
+ content: 'test',
+ };
+ const RepoEditor = Vue.extend(repoEditor);
+ vm = createComponentWithStore(RepoEditor, store, {
+ file: f,
+ });
+
+ vm.$store.state.openFiles.push(f);
+ vm.$store.state.entries[f.path] = f;
+
+ vm.$store.state.viewer = 'mrdiff';
+
+ vm.monaco = true;
+
+ vm.$mount();
+
+ monacoLoader(['vs/editor/editor.main'], () => {
+ setTimeout(done, 0);
+ });
+ });
+
+ it('attaches merge request model to editor when merge request diff', () => {
+ spyOn(vm.editor, 'attachMergeRequestModel').and.callThrough();
+
+ vm.setupEditor();
+
+ expect(vm.editor.attachMergeRequestModel).toHaveBeenCalledWith(vm.model);
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/repo_tabs_spec.js b/spec/javascripts/ide/components/repo_tabs_spec.js
index 1cd2e362f50..cb785ba2cd3 100644
--- a/spec/javascripts/ide/components/repo_tabs_spec.js
+++ b/spec/javascripts/ide/components/repo_tabs_spec.js
@@ -18,6 +18,7 @@ describe('RepoTabs', () => {
viewer: 'editor',
hasChanges: false,
activeFile: file('activeFile'),
+ hasMergeRequest: false,
});
openedFiles[0].active = true;
@@ -58,6 +59,7 @@ describe('RepoTabs', () => {
viewer: 'editor',
hasChanges: false,
activeFile: file('activeFile'),
+ hasMergeRequest: false,
},
'#test-app',
);
diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js
index f6979e32cb3..8fc2fccb64c 100644
--- a/spec/javascripts/ide/lib/common/model_spec.js
+++ b/spec/javascripts/ide/lib/common/model_spec.js
@@ -11,7 +11,10 @@ describe('Multi-file editor library model', () => {
spyOn(eventHub, '$on').and.callThrough();
monacoLoader(['vs/editor/editor.main'], () => {
- model = new Model(monaco, file('path'));
+ const f = file('path');
+ f.mrChange = { diff: 'ABC' };
+ f.baseRaw = 'test';
+ model = new Model(monaco, f);
done();
});
@@ -21,9 +24,10 @@ describe('Multi-file editor library model', () => {
model.dispose();
});
- it('creates original model & new model', () => {
+ it('creates original model & base model & new model', () => {
expect(model.originalModel).not.toBeNull();
expect(model.model).not.toBeNull();
+ expect(model.baseModel).not.toBeNull();
});
it('adds eventHub listener', () => {
@@ -51,6 +55,12 @@ describe('Multi-file editor library model', () => {
});
});
+ describe('getBaseModel', () => {
+ it('returns base model', () => {
+ expect(model.getBaseModel()).toBe(model.baseModel);
+ });
+ });
+
describe('setValue', () => {
it('updates models value', () => {
model.setValue('testing 123');
diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js
index 2ccd87de1a7..ec56ebc0341 100644
--- a/spec/javascripts/ide/lib/editor_spec.js
+++ b/spec/javascripts/ide/lib/editor_spec.js
@@ -143,6 +143,31 @@ describe('Multi-file editor library', () => {
});
});
+ describe('attachMergeRequestModel', () => {
+ let model;
+
+ beforeEach(() => {
+ instance.createDiffInstance(document.createElement('div'));
+
+ const f = file();
+ f.mrChanges = { diff: 'ABC' };
+ f.baseRaw = 'testing';
+
+ model = instance.createModel(f);
+ });
+
+ it('sets original & modified', () => {
+ spyOn(instance.instance, 'setModel');
+
+ instance.attachMergeRequestModel(model);
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith({
+ original: model.getBaseModel(),
+ modified: model.getModel(),
+ });
+ });
+ });
+
describe('clearEditor', () => {
it('resets the editor model', () => {
instance.createInstance(document.createElement('div'));
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index eb8933b2b3f..479ed7ce49e 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -5,7 +5,7 @@ import router from '~/ide/ide_router';
import eventHub from '~/ide/eventhub';
import { file, resetStore } from '../../helpers';
-describe('Multi-file store file actions', () => {
+describe('IDE store file actions', () => {
beforeEach(() => {
spyOn(router, 'push');
});
@@ -205,7 +205,7 @@ describe('Multi-file store file actions', () => {
it('calls the service', done => {
store
- .dispatch('getFileData', localFile)
+ .dispatch('getFileData', { path: localFile.path })
.then(() => {
expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL');
@@ -216,7 +216,7 @@ describe('Multi-file store file actions', () => {
it('sets the file data', done => {
store
- .dispatch('getFileData', localFile)
+ .dispatch('getFileData', { path: localFile.path })
.then(() => {
expect(localFile.blamePath).toBe('blame_path');
@@ -227,7 +227,7 @@ describe('Multi-file store file actions', () => {
it('sets document title', done => {
store
- .dispatch('getFileData', localFile)
+ .dispatch('getFileData', { path: localFile.path })
.then(() => {
expect(document.title).toBe('testing getFileData');
@@ -238,7 +238,7 @@ describe('Multi-file store file actions', () => {
it('sets the file as active', done => {
store
- .dispatch('getFileData', localFile)
+ .dispatch('getFileData', { path: localFile.path })
.then(() => {
expect(localFile.active).toBeTruthy();
@@ -247,9 +247,20 @@ describe('Multi-file store file actions', () => {
.catch(done.fail);
});
+ it('sets the file not as active if we pass makeFileActive false', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path, makeFileActive: false })
+ .then(() => {
+ expect(localFile.active).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
it('adds the file to open files', done => {
store
- .dispatch('getFileData', localFile)
+ .dispatch('getFileData', { path: localFile.path })
.then(() => {
expect(store.state.openFiles.length).toBe(1);
expect(store.state.openFiles[0].name).toBe(localFile.name);
@@ -272,7 +283,7 @@ describe('Multi-file store file actions', () => {
it('calls getRawFileData service method', done => {
store
- .dispatch('getRawFileData', tmpFile)
+ .dispatch('getRawFileData', { path: tmpFile.path })
.then(() => {
expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
@@ -283,7 +294,7 @@ describe('Multi-file store file actions', () => {
it('updates file raw data', done => {
store
- .dispatch('getRawFileData', tmpFile)
+ .dispatch('getRawFileData', { path: tmpFile.path })
.then(() => {
expect(tmpFile.raw).toBe('raw');
@@ -291,6 +302,22 @@ describe('Multi-file store file actions', () => {
})
.catch(done.fail);
});
+
+ it('calls also getBaseRawFileData service method', done => {
+ spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw'));
+
+ tmpFile.mrChange = { new_file: false };
+
+ store
+ .dispatch('getRawFileData', { path: tmpFile.path, baseSha: 'SHA' })
+ .then(() => {
+ expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA');
+ expect(tmpFile.baseRaw).toBe('baseraw');
+
+ done();
+ })
+ .catch(done.fail);
+ });
});
describe('changeFileContent', () => {
diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js
new file mode 100644
index 00000000000..b4ec4a0b173
--- /dev/null
+++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js
@@ -0,0 +1,110 @@
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import { resetStore } from '../../helpers';
+
+describe('IDE store merge request actions', () => {
+ beforeEach(() => {
+ store.state.projects.abcproject = {
+ mergeRequests: {},
+ };
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('getMergeRequestData', () => {
+ beforeEach(() => {
+ spyOn(service, 'getProjectMergeRequestData').and.returnValue(
+ Promise.resolve({ data: { title: 'mergerequest' } }),
+ );
+ });
+
+ it('calls getProjectMergeRequestData service method', done => {
+ store
+ .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the Merge Request Object', done => {
+ store
+ .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(store.state.projects.abcproject.mergeRequests['1'].title).toBe('mergerequest');
+ expect(store.state.currentMergeRequestId).toBe(1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('getMergeRequestChanges', () => {
+ beforeEach(() => {
+ spyOn(service, 'getProjectMergeRequestChanges').and.returnValue(
+ Promise.resolve({ data: { title: 'mergerequest' } }),
+ );
+
+ store.state.projects.abcproject.mergeRequests['1'] = { changes: [] };
+ });
+
+ it('calls getProjectMergeRequestChanges service method', done => {
+ store
+ .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith('abcproject', 1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the Merge Request Changes Object', done => {
+ store
+ .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(store.state.projects.abcproject.mergeRequests['1'].changes.title).toBe(
+ 'mergerequest',
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('getMergeRequestVersions', () => {
+ beforeEach(() => {
+ spyOn(service, 'getProjectMergeRequestVersions').and.returnValue(
+ Promise.resolve({ data: [{ id: 789 }] }),
+ );
+
+ store.state.projects.abcproject.mergeRequests['1'] = { versions: [] };
+ });
+
+ it('calls getProjectMergeRequestVersions service method', done => {
+ store
+ .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith('abcproject', 1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the Merge Request Versions Object', done => {
+ store
+ .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(store.state.projects.abcproject.mergeRequests['1'].versions.length).toBe(1);
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js
index 381f038067b..e0ef57a3966 100644
--- a/spec/javascripts/ide/stores/actions/tree_spec.js
+++ b/spec/javascripts/ide/stores/actions/tree_spec.js
@@ -68,9 +68,7 @@ describe('Multi-file store tree actions', () => {
expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js');
expect(projectTree.tree[1].type).toBe('blob');
expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob');
- expect(projectTree.tree[0].tree[0].tree[0].name).toBe(
- 'fileinsubfolder.js',
- );
+ expect(projectTree.tree[0].tree[0].tree[0].name).toBe('fileinsubfolder.js');
done();
})
@@ -132,9 +130,7 @@ describe('Multi-file store tree actions', () => {
store
.dispatch('getLastCommitData', projectTree)
.then(() => {
- expect(service.getTreeLastCommit).toHaveBeenCalledWith(
- 'lastcommitpath',
- );
+ expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath');
done();
})
@@ -160,9 +156,7 @@ describe('Multi-file store tree actions', () => {
.dispatch('getLastCommitData', projectTree)
.then(Vue.nextTick)
.then(() => {
- expect(projectTree.tree[0].lastCommit.message).not.toBe(
- 'commit message',
- );
+ expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message');
done();
})
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
index a613f3a21cc..33733b97dff 100644
--- a/spec/javascripts/ide/stores/getters_spec.js
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -2,7 +2,7 @@ import * as getters from '~/ide/stores/getters';
import state from '~/ide/stores/state';
import { file } from '../helpers';
-describe('Multi-file store getters', () => {
+describe('IDE store getters', () => {
let localState;
beforeEach(() => {
@@ -52,4 +52,24 @@ describe('Multi-file store getters', () => {
expect(modifiedFiles[0].name).toBe('added');
});
});
+
+ describe('currentMergeRequest', () => {
+ it('returns Current Merge Request', () => {
+ localState.currentProjectId = 'abcproject';
+ localState.currentMergeRequestId = 1;
+ localState.projects.abcproject = {
+ mergeRequests: {
+ 1: { mergeId: 1 },
+ },
+ };
+
+ expect(getters.currentMergeRequest(localState).mergeId).toBe(1);
+ });
+
+ it('returns null if no active Merge Request was found', () => {
+ localState.currentProjectId = 'otherproject';
+
+ expect(getters.currentMergeRequest(localState)).toBeNull();
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
index 4f9e00b8543..88285ee409f 100644
--- a/spec/javascripts/ide/stores/mutations/file_spec.js
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -2,7 +2,7 @@ import mutations from '~/ide/stores/mutations/file';
import state from '~/ide/stores/state';
import { file } from '../../helpers';
-describe('Multi-file store file mutations', () => {
+describe('IDE store file mutations', () => {
let localState;
let localFile;
@@ -77,6 +77,8 @@ describe('Multi-file store file mutations', () => {
expect(localFile.rawPath).toBe('raw');
expect(localFile.binary).toBeTruthy();
expect(localFile.renderError).toBe('render_error');
+ expect(localFile.raw).toBeNull();
+ expect(localFile.baseRaw).toBeNull();
});
});
@@ -91,6 +93,17 @@ describe('Multi-file store file mutations', () => {
});
});
+ describe('SET_FILE_BASE_RAW_DATA', () => {
+ it('sets raw data from base branch', () => {
+ mutations.SET_FILE_BASE_RAW_DATA(localState, {
+ file: localFile,
+ baseRaw: 'testing',
+ });
+
+ expect(localFile.baseRaw).toBe('testing');
+ });
+ });
+
describe('UPDATE_FILE_CONTENT', () => {
beforeEach(() => {
localFile.raw = 'test';
@@ -127,6 +140,17 @@ describe('Multi-file store file mutations', () => {
});
});
+ describe('SET_FILE_MERGE_REQUEST_CHANGE', () => {
+ it('sets file mr change', () => {
+ mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, {
+ file: localFile,
+ mrChange: { diff: 'ABC' },
+ });
+
+ expect(localFile.mrChange.diff).toBe('ABC');
+ });
+ });
+
describe('DISCARD_FILE_CHANGES', () => {
beforeEach(() => {
localFile.content = 'test';
diff --git a/spec/javascripts/ide/stores/mutations/merge_request_spec.js b/spec/javascripts/ide/stores/mutations/merge_request_spec.js
new file mode 100644
index 00000000000..f724bf464f5
--- /dev/null
+++ b/spec/javascripts/ide/stores/mutations/merge_request_spec.js
@@ -0,0 +1,65 @@
+import mutations from '~/ide/stores/mutations/merge_request';
+import state from '~/ide/stores/state';
+
+describe('IDE store merge request mutations', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ localState.projects = { abcproject: { mergeRequests: {} } };
+
+ mutations.SET_MERGE_REQUEST(localState, {
+ projectPath: 'abcproject',
+ mergeRequestId: 1,
+ mergeRequest: {
+ title: 'mr',
+ },
+ });
+ });
+
+ describe('SET_CURRENT_MERGE_REQUEST', () => {
+ it('sets current merge request', () => {
+ mutations.SET_CURRENT_MERGE_REQUEST(localState, 2);
+
+ expect(localState.currentMergeRequestId).toBe(2);
+ });
+ });
+
+ describe('SET_MERGE_REQUEST', () => {
+ it('setsmerge request data', () => {
+ const newMr = localState.projects.abcproject.mergeRequests[1];
+
+ expect(newMr.title).toBe('mr');
+ expect(newMr.active).toBeTruthy();
+ });
+ });
+
+ describe('SET_MERGE_REQUEST_CHANGES', () => {
+ it('sets merge request changes', () => {
+ mutations.SET_MERGE_REQUEST_CHANGES(localState, {
+ projectPath: 'abcproject',
+ mergeRequestId: 1,
+ changes: {
+ diff: 'abc',
+ },
+ });
+
+ const newMr = localState.projects.abcproject.mergeRequests[1];
+ expect(newMr.changes.diff).toBe('abc');
+ });
+ });
+
+ describe('SET_MERGE_REQUEST_VERSIONS', () => {
+ it('sets merge request versions', () => {
+ mutations.SET_MERGE_REQUEST_VERSIONS(localState, {
+ projectPath: 'abcproject',
+ mergeRequestId: 1,
+ versions: [{ id: 123 }],
+ });
+
+ const newMr = localState.projects.abcproject.mergeRequests[1];
+ expect(newMr.versions.length).toBe(1);
+ expect(newMr.versions[0].id).toBe(123);
+ });
+ });
+});
diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js
index 43589d54be4..25ca8eb6c0b 100644
--- a/spec/javascripts/jobs/mock_data.js
+++ b/spec/javascripts/jobs/mock_data.js
@@ -115,6 +115,10 @@ export default {
commit_path: '/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6',
},
},
+ metadata: {
+ timeout_human_readable: '1m 40s',
+ timeout_source: 'runner',
+ },
merge_request: {
iid: 2,
path: '/root/ci-mock/merge_requests/2',
diff --git a/spec/javascripts/jobs/sidebar_detail_row_spec.js b/spec/javascripts/jobs/sidebar_detail_row_spec.js
index 3ac65709c4a..e6bfb0c4adc 100644
--- a/spec/javascripts/jobs/sidebar_detail_row_spec.js
+++ b/spec/javascripts/jobs/sidebar_detail_row_spec.js
@@ -37,4 +37,25 @@ describe('Sidebar detail row', () => {
vm.$el.textContent.replace(/\s+/g, ' ').trim(),
).toEqual('this is the title: this is the value');
});
+
+ describe('when helpUrl not provided', () => {
+ it('should not render help', () => {
+ expect(vm.$el.querySelector('.help-button')).toBeNull();
+ });
+ });
+
+ describe('when helpUrl provided', () => {
+ beforeEach(() => {
+ vm = new SidebarDetailRow({
+ propsData: {
+ helpUrl: 'help url',
+ value: 'foo',
+ },
+ }).$mount();
+ });
+
+ it('should render help', () => {
+ expect(vm.$el.querySelector('.help-button a').getAttribute('href')).toEqual('help url');
+ });
+ });
});
diff --git a/spec/javascripts/jobs/sidebar_details_block_spec.js b/spec/javascripts/jobs/sidebar_details_block_spec.js
index 95532ef5382..602dae514b1 100644
--- a/spec/javascripts/jobs/sidebar_details_block_spec.js
+++ b/spec/javascripts/jobs/sidebar_details_block_spec.js
@@ -96,6 +96,12 @@ describe('Sidebar details block', () => {
).toEqual('Runner: #1');
});
+ it('should render timeout information', () => {
+ expect(
+ trimWhitespace(vm.$el.querySelector('.js-job-timeout')),
+ ).toEqual('Timeout: 1m 40s (from runner)');
+ });
+
it('should render coverage', () => {
expect(
trimWhitespace(vm.$el.querySelector('.js-job-coverage')),
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index 19504e4f7c8..cda550760fe 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -25,26 +25,34 @@ describe('issue_discussion component', () => {
});
it('should render user avatar', () => {
- expect(vm.$el.querySelector('.user-avatar-link')).toBeDefined();
+ expect(vm.$el.querySelector('.user-avatar-link')).not.toBeNull();
});
it('should render discussion header', () => {
- expect(vm.$el.querySelector('.discussion-header')).toBeDefined();
+ expect(vm.$el.querySelector('.discussion-header')).not.toBeNull();
expect(vm.$el.querySelector('.notes').children.length).toEqual(discussionMock.notes.length);
});
describe('actions', () => {
it('should render reply button', () => {
- expect(vm.$el.querySelector('.js-vue-discussion-reply').textContent.trim()).toEqual('Reply...');
+ expect(vm.$el.querySelector('.js-vue-discussion-reply').textContent.trim()).toEqual(
+ 'Reply...',
+ );
});
- it('should toggle reply form', (done) => {
+ it('should toggle reply form', done => {
vm.$el.querySelector('.js-vue-discussion-reply').click();
Vue.nextTick(() => {
- expect(vm.$refs.noteForm).toBeDefined();
+ expect(vm.$refs.noteForm).not.toBeNull();
expect(vm.isReplying).toEqual(true);
done();
});
});
+
+ it('does not render jump to discussion button', () => {
+ expect(
+ vm.$el.querySelector('*[data-original-title="Jump to next unresolved discussion"]'),
+ ).toBeNull();
+ });
});
});
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index 5be13ed0dfe..2d88cee61f1 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -1,4 +1,3 @@
-/* eslint-disable */
export const notesDataMock = {
discussionsPath: '/gitlab-org/gitlab-ce/issues/26/discussions.json',
lastFetchedAt: 1501862675,
@@ -43,7 +42,8 @@ export const noteableDataMock = {
milestone: null,
milestone_id: null,
moved_to_id: null,
- preview_note_path: '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue',
+ preview_note_path:
+ '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue',
project_id: 2,
state: 'opened',
time_estimate: 0,
@@ -60,465 +60,504 @@ export const individualNote = {
expanded: true,
id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
individual_note: true,
- notes: [{
- id: 1390,
- attachment: {
- url: null,
- filename: null,
- image: false,
- },
- author: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: 'test',
- path: '/root',
+ notes: [
+ {
+ id: 1390,
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'test',
+ path: '/root',
+ },
+ created_at: '2017-08-01T17: 09: 33.762Z',
+ updated_at: '2017-08-01T17: 09: 33.762Z',
+ system: false,
+ noteable_id: 98,
+ noteable_type: 'Issue',
+ type: null,
+ human_access: 'Owner',
+ note: 'sdfdsaf',
+ note_html: "<p dir='auto'>sdfdsaf</p>",
+ current_user: { can_edit: true },
+ discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+ emoji_awardable: true,
+ award_emoji: [
+ { name: 'baseball', user: { id: 1, name: 'Root', username: 'root' } },
+ { name: 'art', user: { id: 1, name: 'Root', username: 'root' } },
+ ],
+ toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390&user_id=1',
+ path: '/gitlab-org/gitlab-ce/notes/1390',
},
- created_at: '2017-08-01T17: 09: 33.762Z',
- updated_at: '2017-08-01T17: 09: 33.762Z',
- system: false,
- noteable_id: 98,
- noteable_type: 'Issue',
- type: null,
- human_access: 'Owner',
- note: 'sdfdsaf',
- note_html: '<p dir=\'auto\'>sdfdsaf</p>',
- current_user: { can_edit: true },
- discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
- emoji_awardable: true,
- award_emoji: [
- { name: 'baseball', user: { id: 1, name: 'Root', username: 'root' } },
- { name: 'art', user: { id: 1, name: 'Root', username: 'root' } },
- ],
- toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji',
- report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390&user_id=1',
- path: '/gitlab-org/gitlab-ce/notes/1390',
- }],
+ ],
reply_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
};
export const note = {
- "id": 546,
- "attachment": {
- "url": null,
- "filename": null,
- "image": false
+ id: 546,
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
},
- "author": {
- "id": 1,
- "name": "Administrator",
- "username": "root",
- "state": "active",
- "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "path": "/root"
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
},
- "created_at": "2017-08-10T15:24:03.087Z",
- "updated_at": "2017-08-10T15:24:03.087Z",
- "system": false,
- "noteable_id": 67,
- "noteable_type": "Issue",
- "noteable_iid": 7,
- "type": null,
- "human_access": "Owner",
- "note": "Vel id placeat reprehenderit sit numquam.",
- "note_html": "<p dir=\"auto\">Vel id placeat reprehenderit sit numquam.</p>",
- "current_user": {
- "can_edit": true
+ created_at: '2017-08-10T15:24:03.087Z',
+ updated_at: '2017-08-10T15:24:03.087Z',
+ system: false,
+ noteable_id: 67,
+ noteable_type: 'Issue',
+ noteable_iid: 7,
+ type: null,
+ human_access: 'Owner',
+ note: 'Vel id placeat reprehenderit sit numquam.',
+ note_html: '<p dir="auto">Vel id placeat reprehenderit sit numquam.</p>',
+ current_user: {
+ can_edit: true,
},
- "discussion_id": "d3842a451b7f3d9a5dfce329515127b2d29a4cd0",
- "emoji_awardable": true,
- "award_emoji": [{
- "name": "baseball",
- "user": {
- "id": 1,
- "name": "Administrator",
- "username": "root"
- }
- }, {
- "name": "bath_tone3",
- "user": {
- "id": 1,
- "name": "Administrator",
- "username": "root"
- }
- }],
- "toggle_award_path": "/gitlab-org/gitlab-ce/notes/546/toggle_award_emoji",
- "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1",
- "path": "/gitlab-org/gitlab-ce/notes/546"
- }
+ discussion_id: 'd3842a451b7f3d9a5dfce329515127b2d29a4cd0',
+ emoji_awardable: true,
+ award_emoji: [
+ {
+ name: 'baseball',
+ user: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ },
+ },
+ {
+ name: 'bath_tone3',
+ user: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ },
+ },
+ ],
+ toggle_award_path: '/gitlab-org/gitlab-ce/notes/546/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1',
+ path: '/gitlab-org/gitlab-ce/notes/546',
+};
export const discussionMock = {
id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
reply_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
expanded: true,
- notes: [{
- id: 1395,
- attachment: {
- url: null,
- filename: null,
- image: false,
- },
- author: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: null,
- path: '/root',
- },
- created_at: '2017-08-02T10:51:58.559Z',
- updated_at: '2017-08-02T10:51:58.559Z',
- system: false,
- noteable_id: 98,
- noteable_type: 'Issue',
- type: 'DiscussionNote',
- human_access: 'Owner',
- note: 'THIS IS A DICUSSSION!',
- note_html: '<p dir=\'auto\'>THIS IS A DICUSSSION!</p>',
- current_user: {
- can_edit: true,
- },
- discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
- emoji_awardable: true,
- award_emoji: [],
- toggle_award_path: '/gitlab-org/gitlab-ce/notes/1395/toggle_award_emoji',
- report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1395&user_id=1',
- path: '/gitlab-org/gitlab-ce/notes/1395',
- }, {
- id: 1396,
- attachment: {
- url: null,
- filename: null,
- image: false,
- },
- author: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: null,
- path: '/root',
- },
- created_at: '2017-08-02T10:56:50.980Z',
- updated_at: '2017-08-03T14:19:35.691Z',
- system: false,
- noteable_id: 98,
- noteable_type: 'Issue',
- type: 'DiscussionNote',
- human_access: 'Owner',
- note: 'sadfasdsdgdsf',
- note_html: '<p dir=\'auto\'>sadfasdsdgdsf</p>',
- last_edited_at: '2017-08-03T14:19:35.691Z',
- last_edited_by: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: null,
- path: '/root',
- },
- current_user: {
- can_edit: true,
+ notes: [
+ {
+ id: 1395,
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ created_at: '2017-08-02T10:51:58.559Z',
+ updated_at: '2017-08-02T10:51:58.559Z',
+ system: false,
+ noteable_id: 98,
+ noteable_type: 'Issue',
+ type: 'DiscussionNote',
+ human_access: 'Owner',
+ note: 'THIS IS A DICUSSSION!',
+ note_html: "<p dir='auto'>THIS IS A DICUSSSION!</p>",
+ current_user: {
+ can_edit: true,
+ },
+ discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-ce/notes/1395/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1395&user_id=1',
+ path: '/gitlab-org/gitlab-ce/notes/1395',
},
- discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
- emoji_awardable: true,
- award_emoji: [],
- toggle_award_path: '/gitlab-org/gitlab-ce/notes/1396/toggle_award_emoji',
- report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1396&user_id=1',
- path: '/gitlab-org/gitlab-ce/notes/1396',
- }, {
- id: 1437,
- attachment: {
- url: null,
- filename: null,
- image: false,
+ {
+ id: 1396,
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ created_at: '2017-08-02T10:56:50.980Z',
+ updated_at: '2017-08-03T14:19:35.691Z',
+ system: false,
+ noteable_id: 98,
+ noteable_type: 'Issue',
+ type: 'DiscussionNote',
+ human_access: 'Owner',
+ note: 'sadfasdsdgdsf',
+ note_html: "<p dir='auto'>sadfasdsdgdsf</p>",
+ last_edited_at: '2017-08-03T14:19:35.691Z',
+ last_edited_by: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ current_user: {
+ can_edit: true,
+ },
+ discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-ce/notes/1396/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1396&user_id=1',
+ path: '/gitlab-org/gitlab-ce/notes/1396',
},
- author: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: null,
- path: '/root',
+ {
+ id: 1437,
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ created_at: '2017-08-03T18:11:18.780Z',
+ updated_at: '2017-08-04T09:52:31.062Z',
+ system: false,
+ noteable_id: 98,
+ noteable_type: 'Issue',
+ type: 'DiscussionNote',
+ human_access: 'Owner',
+ note: 'adsfasf Should disappear',
+ note_html: "<p dir='auto'>adsfasf Should disappear</p>",
+ last_edited_at: '2017-08-04T09:52:31.062Z',
+ last_edited_by: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ current_user: {
+ can_edit: true,
+ },
+ discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-ce/notes/1437/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1437&user_id=1',
+ path: '/gitlab-org/gitlab-ce/notes/1437',
},
- created_at: '2017-08-03T18:11:18.780Z',
- updated_at: '2017-08-04T09:52:31.062Z',
- system: false,
- noteable_id: 98,
- noteable_type: 'Issue',
- type: 'DiscussionNote',
- human_access: 'Owner',
- note: 'adsfasf Should disappear',
- note_html: '<p dir=\'auto\'>adsfasf Should disappear</p>',
- last_edited_at: '2017-08-04T09:52:31.062Z',
- last_edited_by: {
+ ],
+ individual_note: false,
+};
+
+export const loggedOutnoteableData = {
+ id: 98,
+ iid: 26,
+ author_id: 1,
+ description: '',
+ lock_version: 1,
+ milestone_id: null,
+ state: 'opened',
+ title: 'asdsa',
+ updated_by_id: 1,
+ created_at: '2017-02-07T10:11:18.395Z',
+ updated_at: '2017-08-08T10:22:51.564Z',
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ milestone: null,
+ labels: [],
+ branch_name: null,
+ confidential: false,
+ assignees: [
+ {
id: 1,
name: 'Root',
username: 'root',
state: 'active',
avatar_url: null,
- path: '/root',
+ web_url: 'http://localhost:3000/root',
},
- current_user: {
- can_edit: true,
- },
- discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
- emoji_awardable: true,
- award_emoji: [],
- toggle_award_path: '/gitlab-org/gitlab-ce/notes/1437/toggle_award_emoji',
- report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1437&user_id=1',
- path: '/gitlab-org/gitlab-ce/notes/1437',
- }],
- individual_note: false,
-};
-
-export const loggedOutnoteableData = {
- "id": 98,
- "iid": 26,
- "author_id": 1,
- "description": "",
- "lock_version": 1,
- "milestone_id": null,
- "state": "opened",
- "title": "asdsa",
- "updated_by_id": 1,
- "created_at": "2017-02-07T10:11:18.395Z",
- "updated_at": "2017-08-08T10:22:51.564Z",
- "time_estimate": 0,
- "total_time_spent": 0,
- "human_time_estimate": null,
- "human_total_time_spent": null,
- "milestone": null,
- "labels": [],
- "branch_name": null,
- "confidential": false,
- "assignees": [{
- "id": 1,
- "name": "Root",
- "username": "root",
- "state": "active",
- "avatar_url": null,
- "web_url": "http://localhost:3000/root"
- }],
- "due_date": null,
- "moved_to_id": null,
- "project_id": 2,
- "web_url": "/gitlab-org/gitlab-ce/issues/26",
- "current_user": {
- "can_create_note": false,
- "can_update": false
+ ],
+ due_date: null,
+ moved_to_id: null,
+ project_id: 2,
+ web_url: '/gitlab-org/gitlab-ce/issues/26',
+ current_user: {
+ can_create_note: false,
+ can_update: false,
},
- "create_note_path": "/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue",
- "preview_note_path": "/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue"
-}
+ create_note_path: '/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue',
+ preview_note_path:
+ '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue',
+};
export const INDIVIDUAL_NOTE_RESPONSE_MAP = {
- 'GET': {
- '/gitlab-org/gitlab-ce/issues/26/discussions.json': [{
- "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
- "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
- "expanded": true,
- "notes": [{
- "id": 1390,
- "attachment": {
- "url": null,
- "filename": null,
- "image": false
- },
- "author": {
- "id": 1,
- "name": "Root",
- "username": "root",
- "state": "active",
- "avatar_url": null,
- "path": "/root"
- },
- "created_at": "2017-08-01T17:09:33.762Z",
- "updated_at": "2017-08-01T17:09:33.762Z",
- "system": false,
- "noteable_id": 98,
- "noteable_type": "Issue",
- "type": null,
- "human_access": "Owner",
- "note": "sdfdsaf",
- "note_html": "\u003cp dir=\"auto\"\u003esdfdsaf\u003c/p\u003e",
- "current_user": {
- "can_edit": true
- },
- "discussion_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
- "emoji_awardable": true,
- "award_emoji": [{
- "name": "baseball",
- "user": {
- "id": 1,
- "name": "Root",
- "username": "root"
- }
- }, {
- "name": "art",
- "user": {
- "id": 1,
- "name": "Root",
- "username": "root"
- }
- }],
- "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji",
- "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1",
- "path": "/gitlab-org/gitlab-ce/notes/1390"
- }],
- "individual_note": true
- }, {
- "id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
- "reply_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
- "expanded": true,
- "notes": [{
- "id": 1391,
- "attachment": {
- "url": null,
- "filename": null,
- "image": false
- },
- "author": {
- "id": 1,
- "name": "Root",
- "username": "root",
- "state": "active",
- "avatar_url": null,
- "path": "/root"
- },
- "created_at": "2017-08-02T10:51:38.685Z",
- "updated_at": "2017-08-02T10:51:38.685Z",
- "system": false,
- "noteable_id": 98,
- "noteable_type": "Issue",
- "type": null,
- "human_access": "Owner",
- "note": "New note!",
- "note_html": "\u003cp dir=\"auto\"\u003eNew note!\u003c/p\u003e",
- "current_user": {
- "can_edit": true
- },
- "discussion_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
- "emoji_awardable": true,
- "award_emoji": [],
- "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji",
- "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1",
- "path": "/gitlab-org/gitlab-ce/notes/1391"
- }],
- "individual_note": true
- }],
+ GET: {
+ '/gitlab-org/gitlab-ce/issues/26/discussions.json': [
+ {
+ id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+ reply_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+ expanded: true,
+ notes: [
+ {
+ id: 1390,
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ created_at: '2017-08-01T17:09:33.762Z',
+ updated_at: '2017-08-01T17:09:33.762Z',
+ system: false,
+ noteable_id: 98,
+ noteable_type: 'Issue',
+ type: null,
+ human_access: 'Owner',
+ note: 'sdfdsaf',
+ note_html: '\u003cp dir="auto"\u003esdfdsaf\u003c/p\u003e',
+ current_user: {
+ can_edit: true,
+ },
+ discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+ emoji_awardable: true,
+ award_emoji: [
+ {
+ name: 'baseball',
+ user: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ },
+ },
+ {
+ name: 'art',
+ user: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ },
+ },
+ ],
+ toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1',
+ path: '/gitlab-org/gitlab-ce/notes/1390',
+ },
+ ],
+ individual_note: true,
+ },
+ {
+ id: '70d5c92a4039a36c70100c6691c18c27e4b0a790',
+ reply_id: '70d5c92a4039a36c70100c6691c18c27e4b0a790',
+ expanded: true,
+ notes: [
+ {
+ id: 1391,
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ created_at: '2017-08-02T10:51:38.685Z',
+ updated_at: '2017-08-02T10:51:38.685Z',
+ system: false,
+ noteable_id: 98,
+ noteable_type: 'Issue',
+ type: null,
+ human_access: 'Owner',
+ note: 'New note!',
+ note_html: '\u003cp dir="auto"\u003eNew note!\u003c/p\u003e',
+ current_user: {
+ can_edit: true,
+ },
+ discussion_id: '70d5c92a4039a36c70100c6691c18c27e4b0a790',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1',
+ path: '/gitlab-org/gitlab-ce/notes/1391',
+ },
+ ],
+ individual_note: true,
+ },
+ ],
'/gitlab-org/gitlab-ce/noteable/issue/98/notes': {
last_fetched_at: 1512900838,
notes: [],
},
},
- 'PUT': {
+ PUT: {
'/gitlab-org/gitlab-ce/notes/1471': {
- "commands_changes": null,
- "valid": true,
- "id": 1471,
- "attachment": null,
- "author": {
- "id": 1,
- "name": "Root",
- "username": "root",
- "state": "active",
- "avatar_url": null,
- "path": "/root"
+ commands_changes: null,
+ valid: true,
+ id: 1471,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
},
- "created_at": "2017-08-08T16:53:00.666Z",
- "updated_at": "2017-12-10T11:03:21.876Z",
- "system": false,
- "noteable_id": 124,
- "noteable_type": "Issue",
- "noteable_iid": 29,
- "type": "DiscussionNote",
- "human_access": "Owner",
- "note": "Adding a comment",
- "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e",
- "last_edited_at": "2017-12-10T11:03:21.876Z",
- "last_edited_by": {
- "id": 1,
- "name": 'Root',
- "username": 'root',
- "state": 'active',
- "avatar_url": null,
- "path": '/root',
+ created_at: '2017-08-08T16:53:00.666Z',
+ updated_at: '2017-12-10T11:03:21.876Z',
+ system: false,
+ noteable_id: 124,
+ noteable_type: 'Issue',
+ noteable_iid: 29,
+ type: 'DiscussionNote',
+ human_access: 'Owner',
+ note: 'Adding a comment',
+ note_html: '\u003cp dir="auto"\u003eAdding a comment\u003c/p\u003e',
+ last_edited_at: '2017-12-10T11:03:21.876Z',
+ last_edited_by: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
},
- "current_user": {
- "can_edit": true
+ current_user: {
+ can_edit: true,
},
- "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
- "emoji_awardable": true,
- "award_emoji": [],
- "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji",
- "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1",
- "path": "/gitlab-org/gitlab-ce/notes/1471"
+ discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1',
+ path: '/gitlab-org/gitlab-ce/notes/1471',
},
- }
+ },
};
export const DISCUSSION_NOTE_RESPONSE_MAP = {
...INDIVIDUAL_NOTE_RESPONSE_MAP,
- 'GET': {
+ GET: {
...INDIVIDUAL_NOTE_RESPONSE_MAP.GET,
- '/gitlab-org/gitlab-ce/issues/26/discussions.json': [{
- "id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
- "reply_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
- "expanded": true,
- "notes": [{
- "id": 1471,
- "attachment": {
- "url": null,
- "filename": null,
- "image": false
- },
- "author": {
- "id": 1,
- "name": "Root",
- "username": "root",
- "state": "active",
- "avatar_url": null,
- "path": "/root"
- },
- "created_at": "2017-08-08T16:53:00.666Z",
- "updated_at": "2017-08-08T16:53:00.666Z",
- "system": false,
- "noteable_id": 124,
- "noteable_type": "Issue",
- "noteable_iid": 29,
- "type": "DiscussionNote",
- "human_access": "Owner",
- "note": "Adding a comment",
- "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e",
- "current_user": {
- "can_edit": true
- },
- "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
- "emoji_awardable": true,
- "award_emoji": [],
- "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji",
- "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1",
- "path": "/gitlab-org/gitlab-ce/notes/1471"
- }],
- "individual_note": false
- }],
+ '/gitlab-org/gitlab-ce/issues/26/discussions.json': [
+ {
+ id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
+ reply_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
+ expanded: true,
+ notes: [
+ {
+ id: 1471,
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ created_at: '2017-08-08T16:53:00.666Z',
+ updated_at: '2017-08-08T16:53:00.666Z',
+ system: false,
+ noteable_id: 124,
+ noteable_type: 'Issue',
+ noteable_iid: 29,
+ type: 'DiscussionNote',
+ human_access: 'Owner',
+ note: 'Adding a comment',
+ note_html: '\u003cp dir="auto"\u003eAdding a comment\u003c/p\u003e',
+ current_user: {
+ can_edit: true,
+ },
+ discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1',
+ path: '/gitlab-org/gitlab-ce/notes/1471',
+ },
+ ],
+ individual_note: false,
+ },
+ ],
},
};
export function individualNoteInterceptor(request, next) {
const body = INDIVIDUAL_NOTE_RESPONSE_MAP[request.method.toUpperCase()][request.url];
- next(request.respondWith(JSON.stringify(body), {
- status: 200,
- }));
+ next(
+ request.respondWith(JSON.stringify(body), {
+ status: 200,
+ }),
+ );
}
export function discussionNoteInterceptor(request, next) {
const body = DISCUSSION_NOTE_RESPONSE_MAP[request.method.toUpperCase()][request.url];
- next(request.respondWith(JSON.stringify(body), {
- status: 200,
- }));
+ next(
+ request.respondWith(JSON.stringify(body), {
+ status: 200,
+ }),
+ );
}
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index 91249b2c79e..520a25cc5c6 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -5,7 +5,13 @@ import * as actions from '~/notes/stores/actions';
import store from '~/notes/stores';
import testAction from '../../helpers/vuex_action_helper';
import { resetStore } from '../helpers';
-import { discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data';
+import {
+ discussionMock,
+ notesDataMock,
+ userDataMock,
+ noteableDataMock,
+ individualNote,
+} from '../mock_data';
describe('Actions Notes Store', () => {
afterEach(() => {
@@ -13,66 +19,103 @@ describe('Actions Notes Store', () => {
});
describe('setNotesData', () => {
- it('should set received notes data', (done) => {
- testAction(actions.setNotesData, null, { notesData: {} }, [
- { type: 'SET_NOTES_DATA', payload: notesDataMock },
- ], done);
+ it('should set received notes data', done => {
+ testAction(
+ actions.setNotesData,
+ notesDataMock,
+ { notesData: {} },
+ [{ type: 'SET_NOTES_DATA', payload: notesDataMock }],
+ [],
+ done,
+ );
});
});
describe('setNoteableData', () => {
- it('should set received issue data', (done) => {
- testAction(actions.setNoteableData, null, { noteableData: {} }, [
- { type: 'SET_NOTEABLE_DATA', payload: noteableDataMock },
- ], done);
+ it('should set received issue data', done => {
+ testAction(
+ actions.setNoteableData,
+ noteableDataMock,
+ { noteableData: {} },
+ [{ type: 'SET_NOTEABLE_DATA', payload: noteableDataMock }],
+ [],
+ done,
+ );
});
});
describe('setUserData', () => {
- it('should set received user data', (done) => {
- testAction(actions.setUserData, null, { userData: {} }, [
- { type: 'SET_USER_DATA', payload: userDataMock },
- ], done);
+ it('should set received user data', done => {
+ testAction(
+ actions.setUserData,
+ userDataMock,
+ { userData: {} },
+ [{ type: 'SET_USER_DATA', payload: userDataMock }],
+ [],
+ done,
+ );
});
});
describe('setLastFetchedAt', () => {
- it('should set received timestamp', (done) => {
- testAction(actions.setLastFetchedAt, null, { lastFetchedAt: {} }, [
- { type: 'SET_LAST_FETCHED_AT', payload: 'timestamp' },
- ], done);
+ it('should set received timestamp', done => {
+ testAction(
+ actions.setLastFetchedAt,
+ 'timestamp',
+ { lastFetchedAt: {} },
+ [{ type: 'SET_LAST_FETCHED_AT', payload: 'timestamp' }],
+ [],
+ done,
+ );
});
});
describe('setInitialNotes', () => {
- it('should set initial notes', (done) => {
- testAction(actions.setInitialNotes, null, { notes: [] }, [
- { type: 'SET_INITIAL_NOTES', payload: [individualNote] },
- ], done);
+ it('should set initial notes', done => {
+ testAction(
+ actions.setInitialNotes,
+ [individualNote],
+ { notes: [] },
+ [{ type: 'SET_INITIAL_NOTES', payload: [individualNote] }],
+ [],
+ done,
+ );
});
});
describe('setTargetNoteHash', () => {
- it('should set target note hash', (done) => {
- testAction(actions.setTargetNoteHash, null, { notes: [] }, [
- { type: 'SET_TARGET_NOTE_HASH', payload: 'hash' },
- ], done);
+ it('should set target note hash', done => {
+ testAction(
+ actions.setTargetNoteHash,
+ 'hash',
+ { notes: [] },
+ [{ type: 'SET_TARGET_NOTE_HASH', payload: 'hash' }],
+ [],
+ done,
+ );
});
});
describe('toggleDiscussion', () => {
- it('should toggle discussion', (done) => {
- testAction(actions.toggleDiscussion, null, { notes: [discussionMock] }, [
- { type: 'TOGGLE_DISCUSSION', payload: { discussionId: discussionMock.id } },
- ], done);
+ it('should toggle discussion', done => {
+ testAction(
+ actions.toggleDiscussion,
+ { discussionId: discussionMock.id },
+ { notes: [discussionMock] },
+ [{ type: 'TOGGLE_DISCUSSION', payload: { discussionId: discussionMock.id } }],
+ [],
+ done,
+ );
});
});
describe('async methods', () => {
const interceptor = (request, next) => {
- next(request.respondWith(JSON.stringify({}), {
- status: 200,
- }));
+ next(
+ request.respondWith(JSON.stringify({}), {
+ status: 200,
+ }),
+ );
};
beforeEach(() => {
@@ -84,8 +127,9 @@ describe('Actions Notes Store', () => {
});
describe('closeIssue', () => {
- it('sets state as closed', (done) => {
- store.dispatch('closeIssue', { notesData: { closeIssuePath: '' } })
+ it('sets state as closed', done => {
+ store
+ .dispatch('closeIssue', { notesData: { closeIssuePath: '' } })
.then(() => {
expect(store.state.noteableData.state).toEqual('closed');
expect(store.state.isToggleStateButtonLoading).toEqual(false);
@@ -96,8 +140,9 @@ describe('Actions Notes Store', () => {
});
describe('reopenIssue', () => {
- it('sets state as reopened', (done) => {
- store.dispatch('reopenIssue', { notesData: { reopenIssuePath: '' } })
+ it('sets state as reopened', done => {
+ store
+ .dispatch('reopenIssue', { notesData: { reopenIssuePath: '' } })
.then(() => {
expect(store.state.noteableData.state).toEqual('reopened');
expect(store.state.isToggleStateButtonLoading).toEqual(false);
@@ -110,7 +155,7 @@ describe('Actions Notes Store', () => {
describe('emitStateChangedEvent', () => {
it('emits an event on the document', () => {
- document.addEventListener('issuable_vue_app:change', (event) => {
+ document.addEventListener('issuable_vue_app:change', event => {
expect(event.detail.data).toEqual({ id: '1', state: 'closed' });
expect(event.detail.isClosed).toEqual(false);
});
@@ -120,40 +165,47 @@ describe('Actions Notes Store', () => {
});
describe('toggleStateButtonLoading', () => {
- it('should set loading as true', (done) => {
- testAction(actions.toggleStateButtonLoading, true, {}, [
- { type: 'TOGGLE_STATE_BUTTON_LOADING', payload: true },
- ], done);
+ it('should set loading as true', done => {
+ testAction(
+ actions.toggleStateButtonLoading,
+ true,
+ {},
+ [{ type: 'TOGGLE_STATE_BUTTON_LOADING', payload: true }],
+ [],
+ done,
+ );
});
- it('should set loading as false', (done) => {
- testAction(actions.toggleStateButtonLoading, false, {}, [
- { type: 'TOGGLE_STATE_BUTTON_LOADING', payload: false },
- ], done);
+ it('should set loading as false', done => {
+ testAction(
+ actions.toggleStateButtonLoading,
+ false,
+ {},
+ [{ type: 'TOGGLE_STATE_BUTTON_LOADING', payload: false }],
+ [],
+ done,
+ );
});
});
describe('toggleIssueLocalState', () => {
- it('sets issue state as closed', (done) => {
- testAction(actions.toggleIssueLocalState, 'closed', {}, [
- { type: 'CLOSE_ISSUE', payload: 'closed' },
- ], done);
+ it('sets issue state as closed', done => {
+ testAction(actions.toggleIssueLocalState, 'closed', {}, [{ type: 'CLOSE_ISSUE' }], [], done);
});
- it('sets issue state as reopened', (done) => {
- testAction(actions.toggleIssueLocalState, 'reopened', {}, [
- { type: 'REOPEN_ISSUE', payload: 'reopened' },
- ], done);
+ it('sets issue state as reopened', done => {
+ testAction(actions.toggleIssueLocalState, 'reopened', {}, [{ type: 'REOPEN_ISSUE' }], [], done);
});
});
describe('poll', () => {
- beforeEach((done) => {
+ beforeEach(done => {
jasmine.clock().install();
spyOn(Vue.http, 'get').and.callThrough();
- store.dispatch('setNotesData', notesDataMock)
+ store
+ .dispatch('setNotesData', notesDataMock)
.then(done)
.catch(done.fail);
});
@@ -162,23 +214,29 @@ describe('Actions Notes Store', () => {
jasmine.clock().uninstall();
});
- it('calls service with last fetched state', (done) => {
+ it('calls service with last fetched state', done => {
const interceptor = (request, next) => {
- next(request.respondWith(JSON.stringify({
- notes: [],
- last_fetched_at: '123456',
- }), {
- status: 200,
- headers: {
- 'poll-interval': '1000',
- },
- }));
+ next(
+ request.respondWith(
+ JSON.stringify({
+ notes: [],
+ last_fetched_at: '123456',
+ }),
+ {
+ status: 200,
+ headers: {
+ 'poll-interval': '1000',
+ },
+ },
+ ),
+ );
};
Vue.http.interceptors.push(interceptor);
Vue.http.interceptors.push(headersInterceptor);
- store.dispatch('poll')
+ store
+ .dispatch('poll')
.then(() => new Promise(resolve => requestAnimationFrame(resolve)))
.then(() => {
expect(Vue.http.get).toHaveBeenCalledWith(jasmine.anything(), {
@@ -192,9 +250,12 @@ describe('Actions Notes Store', () => {
jasmine.clock().tick(1500);
})
- .then(() => new Promise((resolve) => {
- requestAnimationFrame(resolve);
- }))
+ .then(
+ () =>
+ new Promise(resolve => {
+ requestAnimationFrame(resolve);
+ }),
+ )
.then(() => {
expect(Vue.http.get.calls.count()).toBe(2);
expect(Vue.http.get.calls.mostRecent().args[1].headers).toEqual({
diff --git a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js b/spec/javascripts/pages/labels/components/promote_label_modal_spec.js
index 080158a8ee0..a24f8204fe1 100644
--- a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js
+++ b/spec/javascripts/pages/labels/components/promote_label_modal_spec.js
@@ -12,6 +12,7 @@ describe('Promote label modal', () => {
labelColor: '#5cb85c',
labelTextColor: '#ffffff',
url: `${gl.TEST_HOST}/dummy/promote/labels`,
+ groupName: 'group',
};
describe('Modal title and description', () => {
@@ -24,7 +25,7 @@ describe('Promote label modal', () => {
});
it('contains the proper description', () => {
- expect(vm.text).toContain('Promoting this label will make it available for all projects inside the group');
+ expect(vm.text).toContain(`Promoting ${labelMockData.labelTitle} will make it available for all projects inside ${labelMockData.groupName}`);
});
it('contains a label span with the color', () => {
diff --git a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js
index 22956929e7b..8b220423637 100644
--- a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js
+++ b/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js
@@ -10,6 +10,7 @@ describe('Promote milestone modal', () => {
const milestoneMockData = {
milestoneTitle: 'v1.0',
url: `${gl.TEST_HOST}/dummy/promote/milestones`,
+ groupName: 'group',
};
describe('Modal title and description', () => {
@@ -22,7 +23,7 @@ describe('Promote milestone modal', () => {
});
it('contains the proper description', () => {
- expect(vm.text).toContain('Promoting this milestone will make it available for all projects inside the group.');
+ expect(vm.text).toContain(`Promoting ${milestoneMockData.milestoneTitle} will make it available for all projects inside ${milestoneMockData.groupName}.`);
});
it('contains the correct title', () => {
diff --git a/spec/javascripts/pipelines/graph/mock_data.js b/spec/javascripts/pipelines/graph/mock_data.js
index b9494f86d74..70eba98e939 100644
--- a/spec/javascripts/pipelines/graph/mock_data.js
+++ b/spec/javascripts/pipelines/graph/mock_data.js
@@ -1,232 +1,261 @@
-/* eslint-disable quote-props, quotes, comma-dangle */
export default {
- "id": 123,
- "user": {
- "name": "Root",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": null,
- "web_url": "http://localhost:3000/root"
+ id: 123,
+ user: {
+ name: 'Root',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: null,
+ web_url: 'http://localhost:3000/root',
},
- "active": false,
- "coverage": null,
- "path": "/root/ci-mock/pipelines/123",
- "details": {
- "status": {
- "icon": "icon_status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "has_details": true,
- "details_path": "/root/ci-mock/pipelines/123",
- "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico"
+ active: false,
+ coverage: null,
+ path: '/root/ci-mock/pipelines/123',
+ details: {
+ status: {
+ icon: 'icon_status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/pipelines/123',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
},
- "duration": 9,
- "finished_at": "2017-04-19T14:30:27.542Z",
- "stages": [{
- "name": "test",
- "title": "test: passed",
- "groups": [{
- "name": "test",
- "size": 1,
- "status": {
- "icon": "icon_status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "has_details": true,
- "details_path": "/root/ci-mock/builds/4153",
- "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/root/ci-mock/builds/4153/retry",
- "method": "post"
- }
+ duration: 9,
+ finished_at: '2017-04-19T14:30:27.542Z',
+ stages: [
+ {
+ name: 'test',
+ title: 'test: passed',
+ groups: [
+ {
+ name: 'test',
+ size: 1,
+ status: {
+ icon: 'icon_status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/builds/4153',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/ci-mock/builds/4153/retry',
+ method: 'post',
+ },
+ },
+ jobs: [
+ {
+ id: 4153,
+ name: 'test',
+ build_path: '/root/ci-mock/builds/4153',
+ retry_path: '/root/ci-mock/builds/4153/retry',
+ playable: false,
+ created_at: '2017-04-13T09:25:18.959Z',
+ updated_at: '2017-04-13T09:25:23.118Z',
+ status: {
+ icon: 'icon_status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/builds/4153',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/ci-mock/builds/4153/retry',
+ method: 'post',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ status: {
+ icon: 'icon_status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/pipelines/123#test',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
},
- "jobs": [{
- "id": 4153,
- "name": "test",
- "build_path": "/root/ci-mock/builds/4153",
- "retry_path": "/root/ci-mock/builds/4153/retry",
- "playable": false,
- "created_at": "2017-04-13T09:25:18.959Z",
- "updated_at": "2017-04-13T09:25:23.118Z",
- "status": {
- "icon": "icon_status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "has_details": true,
- "details_path": "/root/ci-mock/builds/4153",
- "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/root/ci-mock/builds/4153/retry",
- "method": "post"
- }
- }
- }]
- }],
- "status": {
- "icon": "icon_status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "has_details": true,
- "details_path": "/root/ci-mock/pipelines/123#test",
- "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico"
+ path: '/root/ci-mock/pipelines/123#test',
+ dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=test',
},
- "path": "/root/ci-mock/pipelines/123#test",
- "dropdown_path": "/root/ci-mock/pipelines/123/stage.json?stage=test"
- }, {
- "name": "deploy",
- "title": "deploy: passed",
- "groups": [{
- "name": "deploy to production",
- "size": 1,
- "status": {
- "icon": "icon_status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "has_details": true,
- "details_path": "/root/ci-mock/builds/4166",
- "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/root/ci-mock/builds/4166/retry",
- "method": "post"
- }
+ {
+ name: 'deploy',
+ title: 'deploy: passed',
+ groups: [
+ {
+ name: 'deploy to production',
+ size: 1,
+ status: {
+ icon: 'icon_status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/builds/4166',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/ci-mock/builds/4166/retry',
+ method: 'post',
+ },
+ },
+ jobs: [
+ {
+ id: 4166,
+ name: 'deploy to production',
+ build_path: '/root/ci-mock/builds/4166',
+ retry_path: '/root/ci-mock/builds/4166/retry',
+ playable: false,
+ created_at: '2017-04-19T14:29:46.463Z',
+ updated_at: '2017-04-19T14:30:27.498Z',
+ status: {
+ icon: 'icon_status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/builds/4166',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/ci-mock/builds/4166/retry',
+ method: 'post',
+ },
+ },
+ },
+ ],
+ },
+ {
+ name: 'deploy to staging',
+ size: 1,
+ status: {
+ icon: 'icon_status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/builds/4159',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/ci-mock/builds/4159/retry',
+ method: 'post',
+ },
+ },
+ jobs: [
+ {
+ id: 4159,
+ name: 'deploy to staging',
+ build_path: '/root/ci-mock/builds/4159',
+ retry_path: '/root/ci-mock/builds/4159/retry',
+ playable: false,
+ created_at: '2017-04-18T16:32:08.420Z',
+ updated_at: '2017-04-18T16:32:12.631Z',
+ status: {
+ icon: 'icon_status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/builds/4159',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/ci-mock/builds/4159/retry',
+ method: 'post',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ status: {
+ icon: 'icon_status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/pipelines/123#deploy',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
},
- "jobs": [{
- "id": 4166,
- "name": "deploy to production",
- "build_path": "/root/ci-mock/builds/4166",
- "retry_path": "/root/ci-mock/builds/4166/retry",
- "playable": false,
- "created_at": "2017-04-19T14:29:46.463Z",
- "updated_at": "2017-04-19T14:30:27.498Z",
- "status": {
- "icon": "icon_status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "has_details": true,
- "details_path": "/root/ci-mock/builds/4166",
- "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/root/ci-mock/builds/4166/retry",
- "method": "post"
- }
- }
- }]
- }, {
- "name": "deploy to staging",
- "size": 1,
- "status": {
- "icon": "icon_status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "has_details": true,
- "details_path": "/root/ci-mock/builds/4159",
- "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/root/ci-mock/builds/4159/retry",
- "method": "post"
- }
- },
- "jobs": [{
- "id": 4159,
- "name": "deploy to staging",
- "build_path": "/root/ci-mock/builds/4159",
- "retry_path": "/root/ci-mock/builds/4159/retry",
- "playable": false,
- "created_at": "2017-04-18T16:32:08.420Z",
- "updated_at": "2017-04-18T16:32:12.631Z",
- "status": {
- "icon": "icon_status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "has_details": true,
- "details_path": "/root/ci-mock/builds/4159",
- "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/root/ci-mock/builds/4159/retry",
- "method": "post"
- }
- }
- }]
- }],
- "status": {
- "icon": "icon_status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "has_details": true,
- "details_path": "/root/ci-mock/pipelines/123#deploy",
- "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico"
+ path: '/root/ci-mock/pipelines/123#deploy',
+ dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=deploy',
+ },
+ ],
+ artifacts: [],
+ manual_actions: [
+ {
+ name: 'deploy to production',
+ path: '/root/ci-mock/builds/4166/play',
+ playable: false,
},
- "path": "/root/ci-mock/pipelines/123#deploy",
- "dropdown_path": "/root/ci-mock/pipelines/123/stage.json?stage=deploy"
- }],
- "artifacts": [],
- "manual_actions": [{
- "name": "deploy to production",
- "path": "/root/ci-mock/builds/4166/play",
- "playable": false
- }]
+ ],
},
- "flags": {
- "latest": true,
- "triggered": false,
- "stuck": false,
- "yaml_errors": false,
- "retryable": false,
- "cancelable": false
+ flags: {
+ latest: true,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: false,
+ cancelable: false,
},
- "ref": {
- "name": "master",
- "path": "/root/ci-mock/tree/master",
- "tag": false,
- "branch": true
+ ref: {
+ name: 'master',
+ path: '/root/ci-mock/tree/master',
+ tag: false,
+ branch: true,
},
- "commit": {
- "id": "798e5f902592192afaba73f4668ae30e56eae492",
- "short_id": "798e5f90",
- "title": "Merge branch 'new-branch' into 'master'\r",
- "created_at": "2017-04-13T10:25:17.000+01:00",
- "parent_ids": ["54d483b1ed156fbbf618886ddf7ab023e24f8738", "c8e2d38a6c538822e81c57022a6e3a0cfedebbcc"],
- "message": "Merge branch 'new-branch' into 'master'\r\n\r\nAdd new file\r\n\r\nSee merge request !1",
- "author_name": "Root",
- "author_email": "admin@example.com",
- "authored_date": "2017-04-13T10:25:17.000+01:00",
- "committer_name": "Root",
- "committer_email": "admin@example.com",
- "committed_date": "2017-04-13T10:25:17.000+01:00",
- "author": {
- "name": "Root",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": null,
- "web_url": "http://localhost:3000/root"
+ commit: {
+ id: '798e5f902592192afaba73f4668ae30e56eae492',
+ short_id: '798e5f90',
+ title: "Merge branch 'new-branch' into 'master'\r",
+ created_at: '2017-04-13T10:25:17.000+01:00',
+ parent_ids: [
+ '54d483b1ed156fbbf618886ddf7ab023e24f8738',
+ 'c8e2d38a6c538822e81c57022a6e3a0cfedebbcc',
+ ],
+ message:
+ "Merge branch 'new-branch' into 'master'\r\n\r\nAdd new file\r\n\r\nSee merge request !1",
+ author_name: 'Root',
+ author_email: 'admin@example.com',
+ authored_date: '2017-04-13T10:25:17.000+01:00',
+ committer_name: 'Root',
+ committer_email: 'admin@example.com',
+ committed_date: '2017-04-13T10:25:17.000+01:00',
+ author: {
+ name: 'Root',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: null,
+ web_url: 'http://localhost:3000/root',
},
- "author_gravatar_url": null,
- "commit_url": "http://localhost:3000/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492",
- "commit_path": "/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492"
+ author_gravatar_url: null,
+ commit_url:
+ 'http://localhost:3000/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492',
+ commit_path: '/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492',
},
- "created_at": "2017-04-13T09:25:18.881Z",
- "updated_at": "2017-04-19T14:30:27.561Z"
+ created_at: '2017-04-13T09:25:18.881Z',
+ updated_at: '2017-04-19T14:30:27.561Z',
};
diff --git a/spec/javascripts/registry/stores/actions_spec.js b/spec/javascripts/registry/stores/actions_spec.js
index 3c9da4f107b..bc4c444655a 100644
--- a/spec/javascripts/registry/stores/actions_spec.js
+++ b/spec/javascripts/registry/stores/actions_spec.js
@@ -29,57 +29,96 @@ describe('Actions Registry Store', () => {
describe('fetchRepos', () => {
beforeEach(() => {
interceptor = (request, next) => {
- next(request.respondWith(JSON.stringify(reposServerResponse), {
- status: 200,
- }));
+ next(
+ request.respondWith(JSON.stringify(reposServerResponse), {
+ status: 200,
+ }),
+ );
};
Vue.http.interceptors.push(interceptor);
});
- it('should set receveived repos', (done) => {
- testAction(actions.fetchRepos, null, mockedState, [
- { type: types.TOGGLE_MAIN_LOADING },
- { type: types.SET_REPOS_LIST, payload: reposServerResponse },
- ], done);
+ it('should set receveived repos', done => {
+ testAction(
+ actions.fetchRepos,
+ null,
+ mockedState,
+ [
+ { type: types.TOGGLE_MAIN_LOADING },
+ { type: types.TOGGLE_MAIN_LOADING },
+ { type: types.SET_REPOS_LIST, payload: reposServerResponse },
+ ],
+ [],
+ done,
+ );
});
});
describe('fetchList', () => {
beforeEach(() => {
interceptor = (request, next) => {
- next(request.respondWith(JSON.stringify(registryServerResponse), {
- status: 200,
- }));
+ next(
+ request.respondWith(JSON.stringify(registryServerResponse), {
+ status: 200,
+ }),
+ );
};
Vue.http.interceptors.push(interceptor);
});
- it('should set received list', (done) => {
+ it('should set received list', done => {
mockedState.repos = parsedReposServerResponse;
- testAction(actions.fetchList, { repo: mockedState.repos[1] }, mockedState, [
- { type: types.TOGGLE_REGISTRY_LIST_LOADING },
- { type: types.SET_REGISTRY_LIST, payload: registryServerResponse },
- ], done);
+ const repo = mockedState.repos[1];
+
+ testAction(
+ actions.fetchList,
+ { repo },
+ mockedState,
+ [
+ { type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo },
+ { type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo },
+ {
+ type: types.SET_REGISTRY_LIST,
+ payload: {
+ repo,
+ resp: registryServerResponse,
+ headers: jasmine.anything(),
+ },
+ },
+ ],
+ [],
+ done,
+ );
});
});
});
describe('setMainEndpoint', () => {
- it('should commit set main endpoint', (done) => {
- testAction(actions.setMainEndpoint, 'endpoint', mockedState, [
- { type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' },
- ], done);
+ it('should commit set main endpoint', done => {
+ testAction(
+ actions.setMainEndpoint,
+ 'endpoint',
+ mockedState,
+ [{ type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' }],
+ [],
+ done,
+ );
});
});
describe('toggleLoading', () => {
- it('should commit toggle main loading', (done) => {
- testAction(actions.toggleLoading, null, mockedState, [
- { type: types.TOGGLE_MAIN_LOADING },
- ], done);
+ it('should commit toggle main loading', done => {
+ testAction(
+ actions.toggleLoading,
+ null,
+ mockedState,
+ [{ type: types.TOGGLE_MAIN_LOADING }],
+ [],
+ done,
+ );
});
});
});
diff --git a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
index 88a33caf2e3..0c173062835 100644
--- a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
+++ b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
@@ -62,4 +62,22 @@ describe('Confidential Issue Sidebar Block', () => {
done();
});
});
+
+ it('displays the edit form when opened from collapsed state', (done) => {
+ expect(vm1.edit).toBe(false);
+
+ vm1.$el.querySelector('.sidebar-collapsed-icon').click();
+
+ expect(vm1.edit).toBe(true);
+
+ setTimeout(() => {
+ expect(
+ vm1.$el
+ .innerHTML
+ .includes('You are going to turn off the confidentiality.'),
+ ).toBe(true);
+
+ done();
+ });
+ });
});
diff --git a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
index 696fca516bc..9abc3daf221 100644
--- a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
+++ b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
@@ -68,4 +68,22 @@ describe('LockIssueSidebar', () => {
done();
});
});
+
+ it('displays the edit form when opened from collapsed state', (done) => {
+ expect(vm1.isLockDialogOpen).toBe(false);
+
+ vm1.$el.querySelector('.sidebar-collapsed-icon').click();
+
+ expect(vm1.isLockDialogOpen).toBe(true);
+
+ setTimeout(() => {
+ expect(
+ vm1.$el
+ .innerHTML
+ .includes('Unlock this issue?'),
+ ).toBe(true);
+
+ done();
+ });
+ });
});
diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js
index d9e84e35f69..8b6e8b24f00 100644
--- a/spec/javascripts/sidebar/mock_data.js
+++ b/spec/javascripts/sidebar/mock_data.js
@@ -1,7 +1,5 @@
-/* eslint-disable quote-props*/
-
const RESPONSE_MAP = {
- 'GET': {
+ GET: {
'/gitlab-org/gitlab-shell/issues/5.json': {
id: 45,
iid: 5,
@@ -27,7 +25,8 @@ const RESPONSE_MAP = {
username: 'user0',
id: 22,
state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/user0',
},
{
@@ -35,7 +34,8 @@ const RESPONSE_MAP = {
username: 'tajuana',
id: 18,
state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/tajuana',
},
{
@@ -43,7 +43,8 @@ const RESPONSE_MAP = {
username: 'michaele.will',
id: 16,
state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/michaele.will',
},
],
@@ -72,7 +73,8 @@ const RESPONSE_MAP = {
username: 'user0',
id: 22,
state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
web_url: 'http://localhost:3001/user0',
},
{
@@ -80,7 +82,8 @@ const RESPONSE_MAP = {
username: 'tajuana',
id: 18,
state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
web_url: 'http://localhost:3001/tajuana',
},
{
@@ -88,7 +91,8 @@ const RESPONSE_MAP = {
username: 'michaele.will',
id: 16,
state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
web_url: 'http://localhost:3001/michaele.will',
},
],
@@ -100,7 +104,8 @@ const RESPONSE_MAP = {
username: 'user0',
id: 22,
state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
web_url: 'http://localhost:3001/user0',
},
{
@@ -108,7 +113,8 @@ const RESPONSE_MAP = {
username: 'tajuana',
id: 18,
state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
web_url: 'http://localhost:3001/tajuana',
},
{
@@ -116,7 +122,8 @@ const RESPONSE_MAP = {
username: 'michaele.will',
id: 16,
state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
web_url: 'http://localhost:3001/michaele.will',
},
],
@@ -126,20 +133,21 @@ const RESPONSE_MAP = {
},
'/autocomplete/projects?project_id=15': [
{
- 'id': 0,
- 'name_with_namespace': 'No project',
- }, {
- 'id': 20,
- 'name_with_namespace': 'foo / bar',
+ id: 0,
+ name_with_namespace: 'No project',
+ },
+ {
+ id: 20,
+ name_with_namespace: 'foo / bar',
},
],
},
- 'PUT': {
+ PUT: {
'/gitlab-org/gitlab-shell/issues/5.json': {
data: {},
},
},
- 'POST': {
+ POST: {
'/gitlab-org/gitlab-shell/issues/5/move': {
id: 123,
iid: 5,
@@ -182,7 +190,8 @@ const mockData = {
id: 1,
name: 'Administrator',
username: 'root',
- avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
},
rootPath: '/',
fullPath: '/gitlab-org/gitlab-shell',
@@ -201,12 +210,14 @@ const mockData = {
},
};
-mockData.sidebarMockInterceptor = function (request, next) {
+mockData.sidebarMockInterceptor = function(request, next) {
const body = this.responseMap[request.method.toUpperCase()][request.url];
- next(request.respondWith(JSON.stringify(body), {
- status: 200,
- }));
+ next(
+ request.respondWith(JSON.stringify(body), {
+ status: 200,
+ }),
+ );
}.bind(mockData);
export default mockData;
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
index 235c33fac0d..9b9c9656979 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
@@ -17,46 +17,58 @@ describe('MRWidgetHeader', () => {
describe('computed', () => {
describe('shouldShowCommitsBehindText', () => {
it('return true when there are divergedCommitsCount', () => {
- vm = mountComponent(Component, { mr: {
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
- targetBranch: 'master',
- } });
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ statusPath: 'abc',
+ },
+ });
expect(vm.shouldShowCommitsBehindText).toEqual(true);
});
it('returns false where there are no divergedComits count', () => {
- vm = mountComponent(Component, { mr: {
- divergedCommitsCount: 0,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
- targetBranch: 'master',
- } });
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 0,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ statusPath: 'abc',
+ },
+ });
expect(vm.shouldShowCommitsBehindText).toEqual(false);
});
});
describe('commitsText', () => {
it('returns singular when there is one commit', () => {
- vm = mountComponent(Component, { mr: {
- divergedCommitsCount: 1,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
- targetBranch: 'master',
- } });
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 1,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ statusPath: 'abc',
+ },
+ });
expect(vm.commitsText).toEqual('1 commit behind');
});
it('returns plural when there is more than one commit', () => {
- vm = mountComponent(Component, { mr: {
- divergedCommitsCount: 2,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
- targetBranch: 'master',
- } });
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 2,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ statusPath: 'abc',
+ },
+ });
expect(vm.commitsText).toEqual('2 commits behind');
});
@@ -66,24 +78,27 @@ describe('MRWidgetHeader', () => {
describe('template', () => {
describe('common elements', () => {
beforeEach(() => {
- vm = mountComponent(Component, { mr: {
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
- sourceBranchRemoved: false,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- targetBranch: 'master',
- isOpen: true,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- } });
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ statusPath: 'abc',
+ },
+ });
});
it('renders source branch link', () => {
- expect(
- vm.$el.querySelector('.js-source-branch').innerHTML,
- ).toEqual('<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>');
+ expect(vm.$el.querySelector('.js-source-branch').innerHTML).toEqual(
+ '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ );
});
it('renders clipboard button', () => {
@@ -101,18 +116,21 @@ describe('MRWidgetHeader', () => {
});
beforeEach(() => {
- vm = mountComponent(Component, { mr: {
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
- sourceBranchRemoved: false,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- targetBranch: 'master',
- isOpen: true,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- } });
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ statusPath: 'abc',
+ },
+ });
});
it('renders checkout branch button with modal trigger', () => {
@@ -123,39 +141,49 @@ describe('MRWidgetHeader', () => {
expect(button.getAttribute('data-toggle')).toEqual('modal');
});
+ it('renders web ide button', () => {
+ const button = vm.$el.querySelector('.js-web-ide');
+
+ expect(button.textContent.trim()).toEqual('Web IDE');
+ expect(button.getAttribute('href')).toEqual('undefined/-/ide/projectabc');
+ });
+
it('renders download dropdown with links', () => {
- expect(
- vm.$el.querySelector('.js-download-email-patches').textContent.trim(),
- ).toEqual('Email patches');
+ expect(vm.$el.querySelector('.js-download-email-patches').textContent.trim()).toEqual(
+ 'Email patches',
+ );
- expect(
- vm.$el.querySelector('.js-download-email-patches').getAttribute('href'),
- ).toEqual('/mr/email-patches');
+ expect(vm.$el.querySelector('.js-download-email-patches').getAttribute('href')).toEqual(
+ '/mr/email-patches',
+ );
- expect(
- vm.$el.querySelector('.js-download-plain-diff').textContent.trim(),
- ).toEqual('Plain diff');
+ expect(vm.$el.querySelector('.js-download-plain-diff').textContent.trim()).toEqual(
+ 'Plain diff',
+ );
- expect(
- vm.$el.querySelector('.js-download-plain-diff').getAttribute('href'),
- ).toEqual('/mr/plainDiffPath');
+ expect(vm.$el.querySelector('.js-download-plain-diff').getAttribute('href')).toEqual(
+ '/mr/plainDiffPath',
+ );
});
});
describe('with a closed merge request', () => {
beforeEach(() => {
- vm = mountComponent(Component, { mr: {
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
- sourceBranchRemoved: false,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- targetBranch: 'master',
- isOpen: false,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- } });
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: false,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ statusPath: 'abc',
+ },
+ });
});
it('does not render checkout branch button with modal trigger', () => {
@@ -165,30 +193,29 @@ describe('MRWidgetHeader', () => {
});
it('does not render download dropdown with links', () => {
- expect(
- vm.$el.querySelector('.js-download-email-patches'),
- ).toEqual(null);
+ expect(vm.$el.querySelector('.js-download-email-patches')).toEqual(null);
- expect(
- vm.$el.querySelector('.js-download-plain-diff'),
- ).toEqual(null);
+ expect(vm.$el.querySelector('.js-download-plain-diff')).toEqual(null);
});
});
describe('without diverged commits', () => {
beforeEach(() => {
- vm = mountComponent(Component, { mr: {
- divergedCommitsCount: 0,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
- sourceBranchRemoved: false,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- targetBranch: 'master',
- isOpen: true,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- } });
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 0,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ statusPath: 'abc',
+ },
+ });
});
it('does not render diverged commits info', () => {
@@ -198,22 +225,27 @@ describe('MRWidgetHeader', () => {
describe('with diverged commits', () => {
beforeEach(() => {
- vm = mountComponent(Component, { mr: {
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
- sourceBranchRemoved: false,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- targetBranch: 'master',
- isOpen: true,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- } });
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ statusPath: 'abc',
+ },
+ });
});
it('renders diverged commits info', () => {
- expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual('(12 commits behind)');
+ expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual(
+ '(12 commits behind)',
+ );
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 3dd75307484..3fc7663b9c2 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -1,213 +1,218 @@
-/* eslint-disable */
-
export default {
- "id": 132,
- "iid": 22,
- "assignee_id": null,
- "author_id": 1,
- "description": "",
- "lock_version": null,
- "milestone_id": null,
- "position": 0,
- "state": "merged",
- "title": "Update README.md",
- "updated_by_id": null,
- "created_at": "2017-04-07T12:27:26.718Z",
- "updated_at": "2017-04-07T15:39:25.852Z",
- "time_estimate": 0,
- "total_time_spent": 0,
- "human_time_estimate": null,
- "human_total_time_spent": null,
- "in_progress_merge_commit_sha": null,
- "merge_commit_sha": "53027d060246c8f47e4a9310fb332aa52f221775",
- "merge_error": null,
- "merge_params": {
- "force_remove_source_branch": null
+ id: 132,
+ iid: 22,
+ assignee_id: null,
+ author_id: 1,
+ description: '',
+ lock_version: null,
+ milestone_id: null,
+ position: 0,
+ state: 'merged',
+ title: 'Update README.md',
+ updated_by_id: null,
+ created_at: '2017-04-07T12:27:26.718Z',
+ updated_at: '2017-04-07T15:39:25.852Z',
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ in_progress_merge_commit_sha: null,
+ merge_commit_sha: '53027d060246c8f47e4a9310fb332aa52f221775',
+ merge_error: null,
+ merge_params: {
+ force_remove_source_branch: null,
},
- "merge_status": "can_be_merged",
- "merge_user_id": null,
- "merge_when_pipeline_succeeds": false,
- "source_branch": "daaaa",
- "source_branch_link": "daaaa",
- "source_project_id": 19,
- "target_branch": "master",
- "target_project_id": 19,
- "metrics": {
- "merged_by": {
- "name": "Administrator",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://localhost:3000/root"
+ merge_status: 'can_be_merged',
+ merge_user_id: null,
+ merge_when_pipeline_succeeds: false,
+ source_branch: 'daaaa',
+ source_branch_link: 'daaaa',
+ source_project_id: 19,
+ target_branch: 'master',
+ target_project_id: 19,
+ metrics: {
+ merged_by: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://localhost:3000/root',
},
- "merged_at": "2017-04-07T15:39:25.696Z",
- "closed_by": null,
- "closed_at": null
+ merged_at: '2017-04-07T15:39:25.696Z',
+ closed_by: null,
+ closed_at: null,
},
- "author": {
- "name": "Administrator",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://localhost:3000/root"
+ author: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://localhost:3000/root',
},
- "merge_user": null,
- "diff_head_sha": "104096c51715e12e7ae41f9333e9fa35b73f385d",
- "diff_head_commit_short_id": "104096c5",
- "merge_commit_message": "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
- "pipeline": {
- "id": 172,
- "user": {
- "name": "Administrator",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://localhost:3000/root"
+ merge_user: null,
+ diff_head_sha: '104096c51715e12e7ae41f9333e9fa35b73f385d',
+ diff_head_commit_short_id: '104096c5',
+ merge_commit_message:
+ "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
+ pipeline: {
+ id: 172,
+ user: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://localhost:3000/root',
},
- "active": false,
- "coverage": "92.16",
- "path": "/root/acets-app/pipelines/172",
- "details": {
- "status": {
- "icon": "icon_status_success",
- "favicon": "favicon_status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "has_details": true,
- "details_path": "/root/acets-app/pipelines/172"
+ active: false,
+ coverage: '92.16',
+ path: '/root/acets-app/pipelines/172',
+ details: {
+ status: {
+ icon: 'icon_status_success',
+ favicon: 'favicon_status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/acets-app/pipelines/172',
},
- "duration": null,
- "finished_at": "2017-04-07T14:00:14.256Z",
- "stages": [
+ duration: null,
+ finished_at: '2017-04-07T14:00:14.256Z',
+ stages: [
{
- "name": "build",
- "title": "build: failed",
- "status": {
- "icon": "icon_status_failed",
- "favicon": "favicon_status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "has_details": true,
- "details_path": "/root/acets-app/pipelines/172#build"
+ name: 'build',
+ title: 'build: failed',
+ status: {
+ icon: 'icon_status_failed',
+ favicon: 'favicon_status_failed',
+ text: 'failed',
+ label: 'failed',
+ group: 'failed',
+ has_details: true,
+ details_path: '/root/acets-app/pipelines/172#build',
},
- "path": "/root/acets-app/pipelines/172#build",
- "dropdown_path": "/root/acets-app/pipelines/172/stage.json?stage=build"
+ path: '/root/acets-app/pipelines/172#build',
+ dropdown_path: '/root/acets-app/pipelines/172/stage.json?stage=build',
},
{
- "name": "review",
- "title": "review: skipped",
- "status": {
- "icon": "icon_status_skipped",
- "favicon": "favicon_status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "has_details": true,
- "details_path": "/root/acets-app/pipelines/172#review"
+ name: 'review',
+ title: 'review: skipped',
+ status: {
+ icon: 'icon_status_skipped',
+ favicon: 'favicon_status_skipped',
+ text: 'skipped',
+ label: 'skipped',
+ group: 'skipped',
+ has_details: true,
+ details_path: '/root/acets-app/pipelines/172#review',
},
- "path": "/root/acets-app/pipelines/172#review",
- "dropdown_path": "/root/acets-app/pipelines/172/stage.json?stage=review"
- }
- ],
- "artifacts": [
-
+ path: '/root/acets-app/pipelines/172#review',
+ dropdown_path: '/root/acets-app/pipelines/172/stage.json?stage=review',
+ },
],
- "manual_actions": [
+ artifacts: [],
+ manual_actions: [
{
- "name": "stop_review",
- "path": "/root/acets-app/builds/1427/play",
- "playable": false
- }
- ]
+ name: 'stop_review',
+ path: '/root/acets-app/builds/1427/play',
+ playable: false,
+ },
+ ],
},
- "flags": {
- "latest": false,
- "triggered": false,
- "stuck": false,
- "yaml_errors": false,
- "retryable": true,
- "cancelable": false
+ flags: {
+ latest: false,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: false,
},
- "ref": {
- "name": "daaaa",
- "path": "/root/acets-app/tree/daaaa",
- "tag": false,
- "branch": true
+ ref: {
+ name: 'daaaa',
+ path: '/root/acets-app/tree/daaaa',
+ tag: false,
+ branch: true,
},
- "commit": {
- "id": "104096c51715e12e7ae41f9333e9fa35b73f385d",
- "short_id": "104096c5",
- "title": "Update README.md",
- "created_at": "2017-04-07T15:27:18.000+03:00",
- "parent_ids": [
- "2396536178668d8930c29d904e53bd4d06228b32"
- ],
- "message": "Update README.md",
- "author_name": "Administrator",
- "author_email": "admin@example.com",
- "authored_date": "2017-04-07T15:27:18.000+03:00",
- "committer_name": "Administrator",
- "committer_email": "admin@example.com",
- "committed_date": "2017-04-07T15:27:18.000+03:00",
- "author": {
- "name": "Administrator",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://localhost:3000/root"
+ commit: {
+ id: '104096c51715e12e7ae41f9333e9fa35b73f385d',
+ short_id: '104096c5',
+ title: 'Update README.md',
+ created_at: '2017-04-07T15:27:18.000+03:00',
+ parent_ids: ['2396536178668d8930c29d904e53bd4d06228b32'],
+ message: 'Update README.md',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ authored_date: '2017-04-07T15:27:18.000+03:00',
+ committer_name: 'Administrator',
+ committer_email: 'admin@example.com',
+ committed_date: '2017-04-07T15:27:18.000+03:00',
+ author: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://localhost:3000/root',
},
- "author_gravatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "commit_url": "http://localhost:3000/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d",
- "commit_path": "/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d"
+ author_gravatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ commit_url:
+ 'http://localhost:3000/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d',
+ commit_path: '/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d',
},
- "retry_path": "/root/acets-app/pipelines/172/retry",
- "created_at": "2017-04-07T12:27:19.520Z",
- "updated_at": "2017-04-07T15:28:44.800Z"
+ retry_path: '/root/acets-app/pipelines/172/retry',
+ created_at: '2017-04-07T12:27:19.520Z',
+ updated_at: '2017-04-07T15:28:44.800Z',
},
- "work_in_progress": false,
- "source_branch_exists": false,
- "mergeable_discussions_state": true,
- "conflicts_can_be_resolved_in_ui": false,
- "branch_missing": true,
- "commits_count": 1,
- "has_conflicts": false,
- "can_be_merged": true,
- "has_ci": true,
- "ci_status": "success",
- "pipeline_status_path": "/root/acets-app/merge_requests/22/pipeline_status",
- "issues_links": {
- "closing": "",
- "mentioned_but_not_closing": ""
+ work_in_progress: false,
+ source_branch_exists: false,
+ mergeable_discussions_state: true,
+ conflicts_can_be_resolved_in_ui: false,
+ branch_missing: true,
+ commits_count: 1,
+ has_conflicts: false,
+ can_be_merged: true,
+ has_ci: true,
+ ci_status: 'success',
+ pipeline_status_path: '/root/acets-app/merge_requests/22/pipeline_status',
+ issues_links: {
+ closing: '',
+ mentioned_but_not_closing: '',
},
- "current_user": {
- "can_resolve_conflicts": true,
- "can_remove_source_branch": false,
- "can_revert_on_current_merge_request": true,
- "can_cherry_pick_on_current_merge_request": true
+ current_user: {
+ can_resolve_conflicts: true,
+ can_remove_source_branch: false,
+ can_revert_on_current_merge_request: true,
+ can_cherry_pick_on_current_merge_request: true,
},
- "target_branch_path": "/root/acets-app/branches/master",
- "source_branch_path": "/root/acets-app/branches/daaaa",
- "conflict_resolution_ui_path": "/root/acets-app/merge_requests/22/conflicts",
- "remove_wip_path": "/root/acets-app/merge_requests/22/remove_wip",
- "cancel_merge_when_pipeline_succeeds_path": "/root/acets-app/merge_requests/22/cancel_merge_when_pipeline_succeeds",
- "create_issue_to_resolve_discussions_path": "/root/acets-app/issues/new?merge_request_to_resolve_discussions_of=22",
- "merge_path": "/root/acets-app/merge_requests/22/merge",
- "cherry_pick_in_fork_path": "/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+revert+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1",
- "revert_in_fork_path": "/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+cherry-pick+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1",
- "email_patches_path": "/root/acets-app/merge_requests/22.patch",
- "plain_diff_path": "/root/acets-app/merge_requests/22.diff",
- "status_path": "/root/acets-app/merge_requests/22.json",
- "merge_check_path": "/root/acets-app/merge_requests/22/merge_check",
- "ci_environments_status_url": "/root/acets-app/merge_requests/22/ci_environments_status",
- "project_archived": false,
- "merge_commit_message_with_description": "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
- "diverged_commits_count": 0,
- "only_allow_merge_if_pipeline_succeeds": false,
- "commit_change_content_path": "/root/acets-app/merge_requests/22/commit_change_content"
-}
+ target_branch_path: '/root/acets-app/branches/master',
+ source_branch_path: '/root/acets-app/branches/daaaa',
+ conflict_resolution_ui_path: '/root/acets-app/merge_requests/22/conflicts',
+ remove_wip_path: '/root/acets-app/merge_requests/22/remove_wip',
+ cancel_merge_when_pipeline_succeeds_path:
+ '/root/acets-app/merge_requests/22/cancel_merge_when_pipeline_succeeds',
+ create_issue_to_resolve_discussions_path:
+ '/root/acets-app/issues/new?merge_request_to_resolve_discussions_of=22',
+ merge_path: '/root/acets-app/merge_requests/22/merge',
+ cherry_pick_in_fork_path:
+ '/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+revert+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1',
+ revert_in_fork_path:
+ '/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+cherry-pick+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1',
+ email_patches_path: '/root/acets-app/merge_requests/22.patch',
+ plain_diff_path: '/root/acets-app/merge_requests/22.diff',
+ status_path: '/root/acets-app/merge_requests/22.json',
+ merge_check_path: '/root/acets-app/merge_requests/22/merge_check',
+ ci_environments_status_url: '/root/acets-app/merge_requests/22/ci_environments_status',
+ project_archived: false,
+ merge_commit_message_with_description:
+ "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
+ diverged_commits_count: 0,
+ only_allow_merge_if_pipeline_succeeds: false,
+ commit_change_content_path: '/root/acets-app/merge_requests/22/commit_change_content',
+};
diff --git a/spec/javascripts/vue_shared/components/mock_data.js b/spec/javascripts/vue_shared/components/mock_data.js
index 0d781bdca74..15b56c58c33 100644
--- a/spec/javascripts/vue_shared/components/mock_data.js
+++ b/spec/javascripts/vue_shared/components/mock_data.js
@@ -1,5 +1,3 @@
-/* eslint-disable */
-
export const mockMetrics = [
[1493716685, '4.30859375'],
[1493716745, '4.30859375'],
diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb
new file mode 100644
index 00000000000..14d055cbcc1
--- /dev/null
+++ b/spec/lib/backup/files_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Backup::Files do
+ let(:progress) { StringIO.new }
+ let!(:project) { create(:project) }
+
+ before do
+ allow(progress).to receive(:puts)
+ allow(progress).to receive(:print)
+ allow(FileUtils).to receive(:mkdir_p).and_return(true)
+ allow(FileUtils).to receive(:mv).and_return(true)
+ allow(File).to receive(:exist?).and_return(true)
+ allow(File).to receive(:realpath).with("/var/gitlab-registry").and_return("/var/gitlab-registry")
+ allow(File).to receive(:realpath).with("/var/gitlab-registry/..").and_return("/var")
+
+ allow_any_instance_of(String).to receive(:color) do |string, _color|
+ string
+ end
+
+ allow_any_instance_of(described_class).to receive(:progress).and_return(progress)
+ end
+
+ describe '#restore' do
+ subject { described_class.new('registry', '/var/gitlab-registry') }
+ let(:timestamp) { Time.utc(2017, 3, 22) }
+
+ around do |example|
+ Timecop.freeze(timestamp) { example.run }
+ end
+
+ describe 'folders with permission' do
+ before do
+ allow(subject).to receive(:run_pipeline!).and_return(true)
+ allow(subject).to receive(:backup_existing_files).and_return(true)
+ allow(Dir).to receive(:glob).with("/var/gitlab-registry/*", File::FNM_DOTMATCH).and_return(["/var/gitlab-registry/.", "/var/gitlab-registry/..", "/var/gitlab-registry/sample1"])
+ end
+
+ it 'moves all necessary files' do
+ allow(subject).to receive(:backup_existing_files).and_call_original
+ expect(FileUtils).to receive(:mv).with(["/var/gitlab-registry/sample1"], File.join(Gitlab.config.backup.path, "tmp", "registry.#{Time.now.to_i}"))
+ subject.restore
+ end
+
+ it 'raises no errors' do
+ expect { subject.restore }.not_to raise_error
+ end
+
+ it 'calls tar command with unlink' do
+ expect(subject).to receive(:run_pipeline!).with([%w(gzip -cd), %w(tar --unlink-first --recursive-unlink -C /var/gitlab-registry -xf -)], any_args)
+ subject.restore
+ end
+ end
+
+ describe 'folders without permissions' do
+ before do
+ allow(FileUtils).to receive(:mv).and_raise(Errno::EACCES)
+ allow(subject).to receive(:run_pipeline!).and_return(true)
+ end
+
+ it 'shows error message' do
+ expect(subject).to receive(:access_denied_error).with("/var/gitlab-registry")
+ subject.restore
+ end
+ end
+ end
+end
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb
index 03573c304aa..e4c1c9bafc0 100644
--- a/spec/lib/backup/repository_spec.rb
+++ b/spec/lib/backup/repository_spec.rb
@@ -7,6 +7,8 @@ describe Backup::Repository do
before do
allow(progress).to receive(:puts)
allow(progress).to receive(:print)
+ allow(FileUtils).to receive(:mkdir_p).and_return(true)
+ allow(FileUtils).to receive(:mv).and_return(true)
allow_any_instance_of(String).to receive(:color) do |string, _color|
string
@@ -68,6 +70,17 @@ describe Backup::Repository do
end
end
end
+
+ describe 'folders without permissions' do
+ before do
+ allow(FileUtils).to receive(:mv).and_raise(Errno::EACCES)
+ end
+
+ it 'shows error message' do
+ expect(subject).to receive(:access_denied_error)
+ subject.restore
+ end
+ end
end
describe '#empty_repo?' do
diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb
index cbb0089bde7..a50329473ad 100644
--- a/spec/lib/banzai/filter/autolink_filter_spec.rb
+++ b/spec/lib/banzai/filter/autolink_filter_spec.rb
@@ -167,6 +167,15 @@ describe Banzai::Filter::AutolinkFilter do
expect(actual).to eq(expected_complicated_link)
end
+ it 'does not double-encode HTML entities' do
+ encoded_link = "#{link}?foo=bar&amp;baz=quux"
+ expected_encoded_link = %Q{<a href="#{encoded_link}">#{encoded_link}</a>}
+ actual = unescape(filter(encoded_link).to_html)
+
+ expect(actual).to eq(Rinku.auto_link(encoded_link))
+ expect(actual).to eq(expected_encoded_link)
+ end
+
it 'does not include trailing HTML entities' do
doc = filter("See &lt;&lt;&lt;#{link}&gt;&gt;&gt;")
diff --git a/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb b/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb
index e112e9e9e3d..5ce84c61042 100644
--- a/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb
@@ -51,4 +51,20 @@ describe Gitlab::BackgroundMigration::MigrateBuildStage, :migration, schema: 201
expect { described_class.new.perform(1, 6) }
.to raise_error ActiveRecord::RecordNotUnique
end
+
+ context 'when invalid class can be loaded due to single table inheritance' do
+ let(:commit_status) do
+ jobs.create!(id: 7, commit_id: 1, project_id: 123, stage_idx: 4,
+ stage: 'post-deploy', status: :failed)
+ end
+
+ before do
+ commit_status.update_column(:type, 'SomeClass')
+ end
+
+ it 'does ignore single table inheritance type' do
+ expect { described_class.new.perform(1, 7) }.not_to raise_error
+ expect(jobs.find(7)).to have_attributes(stage_id: (a_value > 0))
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
new file mode 100644
index 00000000000..2ce858836e3
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Policy::Variables do
+ set(:project) { create(:project) }
+
+ let(:pipeline) do
+ build(:ci_empty_pipeline, project: project, ref: 'master', source: :push)
+ end
+
+ let(:ci_build) do
+ build(:ci_build, pipeline: pipeline, project: project, ref: 'master')
+ end
+
+ let(:seed) { double('build seed', to_resource: ci_build) }
+
+ before do
+ pipeline.variables.build(key: 'CI_PROJECT_NAME', value: '')
+ end
+
+ describe '#satisfied_by?' do
+ it 'is satisfied by at least one matching statement' do
+ policy = described_class.new(['$CI_PROJECT_ID', '$UNDEFINED'])
+
+ expect(policy).to be_satisfied_by(pipeline, seed)
+ end
+
+ it 'is not satisfied by an overriden empty variable' do
+ policy = described_class.new(['$CI_PROJECT_NAME'])
+
+ expect(policy).not_to be_satisfied_by(pipeline, seed)
+ end
+
+ it 'is satisfied by a truthy pipeline expression' do
+ policy = described_class.new([%($CI_PIPELINE_SOURCE == "push")])
+
+ expect(policy).to be_satisfied_by(pipeline, seed)
+ end
+
+ it 'is not satisfied by a falsy pipeline expression' do
+ policy = described_class.new([%($CI_PIPELINE_SOURCE == "invalid source")])
+
+ expect(policy).not_to be_satisfied_by(pipeline, seed)
+ end
+
+ it 'is satisfied by a truthy expression using undefined variable' do
+ policy = described_class.new(['$UNDEFINED == null'])
+
+ expect(policy).to be_satisfied_by(pipeline, seed)
+ end
+
+ it 'is not satisfied by a falsy expression using undefined variable' do
+ policy = described_class.new(['$UNDEFINED'])
+
+ expect(policy).not_to be_satisfied_by(pipeline, seed)
+ end
+
+ it 'allows to evaluate regular secret variables' do
+ create(:ci_variable, project: project, key: 'SECRET', value: 'my secret')
+
+ policy = described_class.new(["$SECRET == 'my secret'"])
+
+ expect(policy).to be_satisfied_by(pipeline, seed)
+ end
+
+ it 'does not persist neither pipeline nor build' do
+ described_class.new('$VAR').satisfied_by?(pipeline, seed)
+
+ expect(pipeline).not_to be_persisted
+ expect(seed.to_resource).not_to be_persisted
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/step_spec.rb b/spec/lib/gitlab/ci/build/step_spec.rb
index 5a21282712a..cce4efaa069 100644
--- a/spec/lib/gitlab/ci/build/step_spec.rb
+++ b/spec/lib/gitlab/ci/build/step_spec.rb
@@ -5,10 +5,14 @@ describe Gitlab::Ci::Build::Step do
shared_examples 'has correct script' do
subject { described_class.from_commands(job) }
+ before do
+ job.run!
+ end
+
it 'fabricates an object' do
expect(subject.name).to eq(:script)
expect(subject.script).to eq(script)
- expect(subject.timeout).to eq(job.timeout)
+ expect(subject.timeout).to eq(job.metadata_timeout)
expect(subject.when).to eq('on_success')
expect(subject.allow_failure).to be_falsey
end
@@ -47,6 +51,10 @@ describe Gitlab::Ci::Build::Step do
subject { described_class.from_after_script(job) }
+ before do
+ job.run!
+ end
+
context 'when after_script is empty' do
it 'doesn not fabricate an object' do
is_expected.to be_nil
@@ -59,7 +67,7 @@ describe Gitlab::Ci::Build::Step do
it 'fabricates an object' do
expect(subject.name).to eq(:after_script)
expect(subject.script).to eq(['ls -la', 'date'])
- expect(subject.timeout).to eq(job.timeout)
+ expect(subject.timeout).to eq(job.metadata_timeout)
expect(subject.when).to eq('always')
expect(subject.allow_failure).to be_truthy
end
diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
index 5e83abf645b..08718c382b9 100644
--- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
@@ -83,6 +83,39 @@ describe Gitlab::Ci::Config::Entry::Policy do
end
end
+ context 'when specifying valid variables expressions policy' do
+ let(:config) { { variables: ['$VAR == null'] } }
+
+ it 'is a correct configuraton' do
+ expect(entry).to be_valid
+ expect(entry.value).to eq(config)
+ end
+ end
+
+ context 'when specifying variables expressions in invalid format' do
+ let(:config) { { variables: '$MY_VAR' } }
+
+ it 'reports an error about invalid format' do
+ expect(entry.errors).to include /should be an array of strings/
+ end
+ end
+
+ context 'when specifying invalid variables expressions statement' do
+ let(:config) { { variables: ['$MY_VAR =='] } }
+
+ it 'reports an error about invalid statement' do
+ expect(entry.errors).to include /invalid expression syntax/
+ end
+ end
+
+ context 'when specifying invalid variables expressions token' do
+ let(:config) { { variables: ['$MY_VAR == 123'] } }
+
+ it 'reports an error about invalid statement' do
+ expect(entry.errors).to include /invalid expression syntax/
+ end
+ end
+
context 'when specifying unknown policy' do
let(:config) { { refs: ['master'], invalid: :something } }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
index 2258ae83f38..8312fa47cfa 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
@@ -6,7 +6,8 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
let(:pipeline) do
build(:ci_pipeline_with_one_job, project: project,
- ref: 'master')
+ ref: 'master',
+ user: user)
end
let(:command) do
@@ -42,6 +43,10 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
expect(pipeline.stages.first.builds).to be_one
expect(pipeline.stages.first.builds.first).not_to be_persisted
end
+
+ it 'correctly assigns user' do
+ expect(pipeline.builds).to all(have_attributes(user: user))
+ end
end
context 'when pipeline is empty' do
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb
index 86234dfb9e5..1ccb792d1da 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb
@@ -73,6 +73,22 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::String do
expect(token).not_to be_nil
expect(token.build.evaluate).to eq 'some " string'
end
+
+ it 'allows to use an empty string inside single quotes' do
+ scanner = StringScanner.new(%(''))
+
+ token = described_class.scan(scanner)
+
+ expect(token.build.evaluate).to eq ''
+ end
+
+ it 'allow to use an empty string inside double quotes' do
+ scanner = StringScanner.new(%(""))
+
+ token = described_class.scan(scanner)
+
+ expect(token.build.evaluate).to eq ''
+ end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
index 472a58599d8..6685bf5385b 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
@@ -1,14 +1,23 @@
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Statement do
- let(:pipeline) { build(:ci_pipeline) }
-
subject do
- described_class.new(text, pipeline)
+ described_class.new(text, variables)
+ end
+
+ let(:variables) do
+ { 'PRESENT_VARIABLE' => 'my variable',
+ EMPTY_VARIABLE: '' }
end
- before do
- pipeline.variables.build([key: 'VARIABLE', value: 'my variable'])
+ describe '.new' do
+ context 'when variables are not provided' do
+ it 'allows to properly initializes the statement' do
+ statement = described_class.new('$PRESENT_VARIABLE')
+
+ expect(statement.evaluate).to be_nil
+ end
+ end
end
describe '#parse_tree' do
@@ -23,18 +32,26 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
context 'when expression grammar is incorrect' do
table = [
- '$VAR "text"', # missing operator
- '== "123"', # invalid right side
- "'single quotes'", # single quotes string
- '$VAR ==', # invalid right side
- '12345', # unknown syntax
- '' # empty statement
+ '$VAR "text"', # missing operator
+ '== "123"', # invalid left side
+ '"some string"', # only string provided
+ '$VAR ==', # invalid right side
+ '12345', # unknown syntax
+ '' # empty statement
]
table.each do |syntax|
- it "raises an error when syntax is `#{syntax}`" do
- expect { described_class.new(syntax, pipeline).parse_tree }
- .to raise_error described_class::StatementError
+ context "when expression grammar is #{syntax.inspect}" do
+ let(:text) { syntax }
+
+ it 'aises a statement error exception' do
+ expect { subject.parse_tree }
+ .to raise_error described_class::StatementError
+ end
+
+ it 'is an invalid statement' do
+ expect(subject).not_to be_valid
+ end
end
end
end
@@ -47,10 +64,14 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
expect(subject.parse_tree)
.to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals
end
+
+ it 'is a valid statement' do
+ expect(subject).to be_valid
+ end
end
context 'when using a single token' do
- let(:text) { '$VARIABLE' }
+ let(:text) { '$PRESENT_VARIABLE' }
it 'returns a single token instance' do
expect(subject.parse_tree)
@@ -62,14 +83,17 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
describe '#evaluate' do
statements = [
- ['$VARIABLE == "my variable"', true],
- ["$VARIABLE == 'my variable'", true],
- ['"my variable" == $VARIABLE', true],
- ['$VARIABLE == null', false],
- ['$VAR == null', true],
- ['null == $VAR', true],
- ['$VARIABLE', 'my variable'],
- ['$VAR', nil]
+ ['$PRESENT_VARIABLE == "my variable"', true],
+ ["$PRESENT_VARIABLE == 'my variable'", true],
+ ['"my variable" == $PRESENT_VARIABLE', true],
+ ['$PRESENT_VARIABLE == null', false],
+ ['$EMPTY_VARIABLE == null', false],
+ ['"" == $EMPTY_VARIABLE', true],
+ ['$EMPTY_VARIABLE', ''],
+ ['$UNDEFINED_VARIABLE == null', true],
+ ['null == $UNDEFINED_VARIABLE', true],
+ ['$PRESENT_VARIABLE', 'my variable'],
+ ['$UNDEFINED_VARIABLE', nil]
]
statements.each do |expression, value|
@@ -82,4 +106,25 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
end
end
end
+
+ describe '#truthful?' do
+ statements = [
+ ['$PRESENT_VARIABLE == "my variable"', true],
+ ["$PRESENT_VARIABLE == 'no match'", false],
+ ['$UNDEFINED_VARIABLE == null', true],
+ ['$PRESENT_VARIABLE', true],
+ ['$UNDEFINED_VARIABLE', false],
+ ['$EMPTY_VARIABLE', false]
+ ]
+
+ statements.each do |expression, value|
+ context "when using expression `#{expression}`" do
+ let(:text) { expression }
+
+ it "returns `#{value.inspect}`" do
+ expect(subject.truthful?).to eq value
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 116573379e0..fffa727c2ed 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -21,16 +21,6 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
end
end
- describe '#user=' do
- let(:user) { build(:user) }
-
- it 'assignes user to a build' do
- subject.user = user
-
- expect(subject.attributes).to include(user: user)
- end
- end
-
describe '#to_resource' do
it 'returns a valid build resource' do
expect(subject.to_resource).to be_a(::Ci::Build)
diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
index 8f0bf40d624..eb1b285c7bd 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
@@ -95,16 +95,6 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do
end
end
- describe '#user=' do
- let(:user) { build(:user) }
-
- it 'assignes relevant pipeline attributes' do
- subject.user = user
-
- expect(subject.seeds.map(&:attributes)).to all(include(user: user))
- end
- end
-
describe '#to_resource' do
it 'builds a valid stage object with all builds' do
subject.to_resource.save!
diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
index cc1257484d2..bf9208f1ff4 100644
--- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
@@ -46,9 +46,13 @@ describe Gitlab::Ci::Variables::Collection::Item do
end
end
- describe '#to_hash' do
- it 'returns a hash representation of a collection item' do
- expect(described_class.new(**variable).to_hash).to eq variable
+ describe '#to_runner_variable' do
+ it 'returns a runner-compatible hash representation' do
+ runner_variable = described_class
+ .new(**variable)
+ .to_runner_variable
+
+ expect(runner_variable).to eq variable
end
end
end
diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb
index 90b6e178242..cb2f7718c9c 100644
--- a/spec/lib/gitlab/ci/variables/collection_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection_spec.rb
@@ -7,7 +7,7 @@ describe Gitlab::Ci::Variables::Collection do
collection = described_class.new([variable])
- expect(collection.first.to_hash).to eq variable
+ expect(collection.first.to_runner_variable).to eq variable
end
it 'can be initialized without an argument' do
@@ -96,4 +96,19 @@ describe Gitlab::Ci::Variables::Collection do
.to eq [{ key: 'TEST', value: 1, public: true }]
end
end
+
+ describe '#to_hash' do
+ it 'returns regular hash in valid order without duplicates' do
+ collection = described_class.new
+ .append(key: 'TEST1', value: 'test-1')
+ .append(key: 'TEST2', value: 'test-2')
+ .append(key: 'TEST1', value: 'test-3')
+
+ expect(collection.to_hash).to eq('TEST1' => 'test-3',
+ 'TEST2' => 'test-2')
+
+ expect(collection.to_hash).to include(TEST1: 'test-3')
+ expect(collection.to_hash).not_to include(TEST1: 'test-1')
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index fbc2af29b98..ecb16daec96 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -1311,6 +1311,14 @@ module Gitlab
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
end
+
+ it 'returns errors if pipeline variables expression is invalid' do
+ config = YAML.dump({ rspec: { script: 'test', only: { variables: ['== null'] } } })
+
+ expect { Gitlab::Ci::YamlProcessor.new(config) }
+ .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:only variables invalid expression syntax')
+ end
end
describe "Validate configuration templates" do
diff --git a/spec/lib/gitlab/git/gitmodules_parser_spec.rb b/spec/lib/gitlab/git/gitmodules_parser_spec.rb
index 143aa2218c9..6fd2b33486b 100644
--- a/spec/lib/gitlab/git/gitmodules_parser_spec.rb
+++ b/spec/lib/gitlab/git/gitmodules_parser_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::Git::GitmodulesParser do
it 'should parse a .gitmodules file correctly' do
- parser = described_class.new(<<-'GITMODULES'.strip_heredoc)
+ data = <<~GITMODULES
[submodule "vendor/libgit2"]
path = vendor/libgit2
[submodule "vendor/libgit2"]
@@ -16,6 +16,7 @@ describe Gitlab::Git::GitmodulesParser do
url = https://example.com/another/project
GITMODULES
+ parser = described_class.new(data.gsub("\n", "\r\n"))
modules = parser.parse
expect(modules).to eq({
diff --git a/spec/lib/gitlab/git/env_spec.rb b/spec/lib/gitlab/git/hook_env_spec.rb
index 03836d49518..e6aa5ad8c90 100644
--- a/spec/lib/gitlab/git/env_spec.rb
+++ b/spec/lib/gitlab/git/hook_env_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
-describe Gitlab::Git::Env do
+describe Gitlab::Git::HookEnv do
+ let(:gl_repository) { 'project-123' }
+
describe ".set" do
context 'with RequestStore.store disabled' do
before do
@@ -8,9 +10,9 @@ describe Gitlab::Git::Env do
end
it 'does not store anything' do
- described_class.set(GIT_OBJECT_DIRECTORY: 'foo')
+ described_class.set(gl_repository, GIT_OBJECT_DIRECTORY_RELATIVE: 'foo')
- expect(described_class.all).to be_empty
+ expect(described_class.all(gl_repository)).to be_empty
end
end
@@ -21,15 +23,19 @@ describe Gitlab::Git::Env do
it 'whitelist some `GIT_*` variables and stores them using RequestStore' do
described_class.set(
- GIT_OBJECT_DIRECTORY: 'foo',
- GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar',
+ gl_repository,
+ GIT_OBJECT_DIRECTORY_RELATIVE: 'foo',
+ GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: 'bar',
GIT_EXEC_PATH: 'baz',
PATH: '~/.bin:/bin')
- expect(described_class[:GIT_OBJECT_DIRECTORY]).to eq('foo')
- expect(described_class[:GIT_ALTERNATE_OBJECT_DIRECTORIES]).to eq('bar')
- expect(described_class[:GIT_EXEC_PATH]).to be_nil
- expect(described_class[:bar]).to be_nil
+ git_env = described_class.all(gl_repository)
+
+ expect(git_env[:GIT_OBJECT_DIRECTORY_RELATIVE]).to eq('foo')
+ expect(git_env[:GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE]).to eq('bar')
+ expect(git_env[:GIT_EXEC_PATH]).to be_nil
+ expect(git_env[:PATH]).to be_nil
+ expect(git_env[:bar]).to be_nil
end
end
end
@@ -39,14 +45,15 @@ describe Gitlab::Git::Env do
before do
allow(RequestStore).to receive(:active?).and_return(true)
described_class.set(
- GIT_OBJECT_DIRECTORY: 'foo',
- GIT_ALTERNATE_OBJECT_DIRECTORIES: ['bar'])
+ gl_repository,
+ GIT_OBJECT_DIRECTORY_RELATIVE: 'foo',
+ GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: ['bar'])
end
it 'returns an env hash' do
- expect(described_class.all).to eq({
- 'GIT_OBJECT_DIRECTORY' => 'foo',
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => ['bar']
+ expect(described_class.all(gl_repository)).to eq({
+ 'GIT_OBJECT_DIRECTORY_RELATIVE' => 'foo',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => ['bar']
})
end
end
@@ -56,8 +63,8 @@ describe Gitlab::Git::Env do
context 'with RequestStore.store enabled' do
using RSpec::Parameterized::TableSyntax
- let(:key) { 'GIT_OBJECT_DIRECTORY' }
- subject { described_class.to_env_hash }
+ let(:key) { 'GIT_OBJECT_DIRECTORY_RELATIVE' }
+ subject { described_class.to_env_hash(gl_repository) }
where(:input, :output) do
nil | nil
@@ -70,7 +77,7 @@ describe Gitlab::Git::Env do
with_them do
before do
allow(RequestStore).to receive(:active?).and_return(true)
- described_class.set(key.to_sym => input)
+ described_class.set(gl_repository, key.to_sym => input)
end
it 'puts the right value in the hash' do
@@ -84,47 +91,25 @@ describe Gitlab::Git::Env do
end
end
- describe ".[]" do
- context 'with RequestStore.store enabled' do
- before do
- allow(RequestStore).to receive(:active?).and_return(true)
- end
-
- before do
- described_class.set(
- GIT_OBJECT_DIRECTORY: 'foo',
- GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar')
- end
-
- it 'returns a stored value for an existing key' do
- expect(described_class[:GIT_OBJECT_DIRECTORY]).to eq('foo')
- end
-
- it 'returns nil for an non-existing key' do
- expect(described_class[:foo]).to be_nil
- end
- end
- end
-
describe 'thread-safety' do
context 'with RequestStore.store enabled' do
before do
allow(RequestStore).to receive(:active?).and_return(true)
- described_class.set(GIT_OBJECT_DIRECTORY: 'foo')
+ described_class.set(gl_repository, GIT_OBJECT_DIRECTORY_RELATIVE: 'foo')
end
it 'is thread-safe' do
another_thread = Thread.new do
- described_class.set(GIT_OBJECT_DIRECTORY: 'bar')
+ described_class.set(gl_repository, GIT_OBJECT_DIRECTORY_RELATIVE: 'bar')
Thread.stop
- described_class[:GIT_OBJECT_DIRECTORY]
+ described_class.all(gl_repository)[:GIT_OBJECT_DIRECTORY_RELATIVE]
end
# Ensure another_thread runs first
sleep 0.1 until another_thread.stop?
- expect(described_class[:GIT_OBJECT_DIRECTORY]).to eq('foo')
+ expect(described_class.all(gl_repository)[:GIT_OBJECT_DIRECTORY_RELATIVE]).to eq('foo')
another_thread.run
expect(another_thread.value).to eq('bar')
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 0e315b3f49e..5cbe2808d0b 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -120,7 +120,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe 'alternates keyword argument' do
context 'with no Git env stored' do
before do
- allow(Gitlab::Git::Env).to receive(:all).and_return({})
+ allow(Gitlab::Git::HookEnv).to receive(:all).and_return({})
end
it "is passed an empty array" do
@@ -132,7 +132,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
context 'with absolute and relative Git object dir envvars stored' do
before do
- allow(Gitlab::Git::Env).to receive(:all).and_return({
+ allow(Gitlab::Git::HookEnv).to receive(:all).and_return({
'GIT_OBJECT_DIRECTORY_RELATIVE' => './objects/foo',
'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => ['./objects/bar', './objects/baz'],
'GIT_OBJECT_DIRECTORY' => 'ignored',
@@ -148,22 +148,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
repository.rugged
end
end
-
- context 'with only absolute Git object dir envvars stored' do
- before do
- allow(Gitlab::Git::Env).to receive(:all).and_return({
- 'GIT_OBJECT_DIRECTORY' => 'foo',
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => %w[bar baz],
- 'GIT_OTHER' => 'another_env'
- })
- end
-
- it "is passed the absolute object dir envvars as is" do
- expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: %w[foo bar baz])
-
- repository.rugged
- end
- end
end
end
diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb
index 4e0ee206219..32ec1e029c8 100644
--- a/spec/lib/gitlab/git/rev_list_spec.rb
+++ b/spec/lib/gitlab/git/rev_list_spec.rb
@@ -3,17 +3,6 @@ require 'spec_helper'
describe Gitlab::Git::RevList do
let(:repository) { create(:project, :repository).repository.raw }
let(:rev_list) { described_class.new(repository, newrev: 'newrev') }
- let(:env_hash) do
- {
- 'GIT_OBJECT_DIRECTORY' => 'foo',
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
- }
- end
- let(:command_env) { { 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'foo:bar' } }
-
- before do
- allow(Gitlab::Git::Env).to receive(:all).and_return(env_hash)
- end
def args_for_popen(args_list)
[Gitlab.config.git.bin_path, 'rev-list', *args_list]
@@ -23,7 +12,7 @@ describe Gitlab::Git::RevList do
params = [
args_for_popen(additional_args),
repository.path,
- command_env,
+ {},
hash_including(lazy_block: with_lazy_block ? anything : nil)
]
diff --git a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
index 872377c93d8..f03c7e3f04b 100644
--- a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
@@ -58,4 +58,14 @@ describe Gitlab::GitalyClient::RemoteService do
client.update_remote_mirror(ref_name, only_branches_matching)
end
end
+
+ describe '.exists?' do
+ context "when the remote doesn't exist" do
+ let(:url) { 'https://gitlab.com/gitlab-org/ik-besta-niet-of-ik-word-geplaagd.git' }
+
+ it 'returns false' do
+ expect(described_class.exists?(url)).to be(false)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/util_spec.rb b/spec/lib/gitlab/gitaly_client/util_spec.rb
index d1e0136f8c1..550db6db6d9 100644
--- a/spec/lib/gitlab/gitaly_client/util_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/util_spec.rb
@@ -7,16 +7,19 @@ describe Gitlab::GitalyClient::Util do
let(:gl_repository) { 'project-1' }
let(:git_object_directory) { '.git/objects' }
let(:git_alternate_object_directory) { ['/dir/one', '/dir/two'] }
+ let(:git_env) do
+ {
+ 'GIT_OBJECT_DIRECTORY_RELATIVE' => git_object_directory,
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => git_alternate_object_directory
+ }
+ end
subject do
described_class.repository(repository_storage, relative_path, gl_repository)
end
it 'creates a Gitaly::Repository with the given data' do
- allow(Gitlab::Git::Env).to receive(:[]).with('GIT_OBJECT_DIRECTORY_RELATIVE')
- .and_return(git_object_directory)
- allow(Gitlab::Git::Env).to receive(:[]).with('GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE')
- .and_return(git_alternate_object_directory)
+ allow(Gitlab::Git::HookEnv).to receive(:all).with(gl_repository).and_return(git_env)
expect(subject).to be_a(Gitaly::Repository)
expect(subject.storage_name).to eq(repository_storage)
diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
index 5bedfc79dd3..1f0f1fdd7da 100644
--- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -38,8 +38,12 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
expect(project)
.to receive(:wiki_repository_exists?)
.and_return(false)
+ expect(Gitlab::GitalyClient::RemoteService)
+ .to receive(:exists?)
+ .with("foo.wiki.git")
+ .and_return(true)
- expect(importer.import_wiki?).to eq(true)
+ expect(importer.import_wiki?).to be(true)
end
it 'returns false if the GitHub wiki is disabled' do
diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb
index b0bc081a3c8..d0dadfa78da 100644
--- a/spec/lib/gitlab/http_spec.rb
+++ b/spec/lib/gitlab/http_spec.rb
@@ -12,11 +12,11 @@ describe Gitlab::HTTP do
end
it 'deny requests to localhost' do
- expect { described_class.get('http://localhost:3003') }.to raise_error(URI::InvalidURIError)
+ expect { described_class.get('http://localhost:3003') }.to raise_error(Gitlab::HTTP::BlockedUrlError)
end
it 'deny requests to private network' do
- expect { described_class.get('http://192.168.1.2:3003') }.to raise_error(URI::InvalidURIError)
+ expect { described_class.get('http://192.168.1.2:3003') }.to raise_error(Gitlab::HTTP::BlockedUrlError)
end
context 'if allow_local_requests set to true' do
@@ -41,7 +41,7 @@ describe Gitlab::HTTP do
context 'if allow_local_requests set to false' do
it 'override the global value and ban requests to localhost or private network' do
- expect { described_class.get('http://localhost:3003', allow_local_requests: false) }.to raise_error(URI::InvalidURIError)
+ expect { described_class.get('http://localhost:3003', allow_local_requests: false) }.to raise_error(Gitlab::HTTP::BlockedUrlError)
end
end
end
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
new file mode 100644
index 00000000000..ed54d87de4a
--- /dev/null
+++ b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy do
+ let!(:service) { described_class.new }
+ let!(:project) { create(:project, :with_export) }
+ let(:shared) { project.import_export_shared }
+ let!(:user) { create(:user) }
+
+ describe '#execute' do
+ before do
+ allow(service).to receive(:strategy_execute)
+ end
+
+ it 'returns if project exported file is not found' do
+ allow(project).to receive(:export_project_path).and_return(nil)
+
+ expect(service).not_to receive(:strategy_execute)
+
+ service.execute(user, project)
+ end
+
+ it 'creates a lock file in the export dir' do
+ allow(service).to receive(:delete_after_export_lock)
+
+ service.execute(user, project)
+
+ expect(lock_path_exist?).to be_truthy
+ end
+
+ context 'when the method succeeds' do
+ it 'removes the lock file' do
+ service.execute(user, project)
+
+ expect(lock_path_exist?).to be_falsey
+ end
+ end
+
+ context 'when the method fails' do
+ before do
+ allow(service).to receive(:strategy_execute).and_call_original
+ end
+
+ context 'when validation fails' do
+ before do
+ allow(service).to receive(:invalid?).and_return(true)
+ end
+
+ it 'does not create the lock file' do
+ expect(service).not_to receive(:create_or_update_after_export_lock)
+
+ service.execute(user, project)
+ end
+
+ it 'does not execute main logic' do
+ expect(service).not_to receive(:strategy_execute)
+
+ service.execute(user, project)
+ end
+
+ it 'logs validation errors in shared context' do
+ expect(service).to receive(:log_validation_errors)
+
+ service.execute(user, project)
+ end
+ end
+
+ context 'when an exception is raised' do
+ it 'removes the lock' do
+ expect { service.execute(user, project) }.to raise_error(NotImplementedError)
+
+ expect(lock_path_exist?).to be_falsey
+ end
+ end
+ end
+ end
+
+ describe '#log_validation_errors' do
+ it 'add the message to the shared context' do
+ errors = %w(test_message test_message2)
+
+ allow(service).to receive(:invalid?).and_return(true)
+ allow(service.errors).to receive(:full_messages).and_return(errors)
+
+ expect(shared).to receive(:add_error_message).twice.and_call_original
+
+ service.execute(user, project)
+
+ expect(shared.errors).to eq errors
+ end
+ end
+
+ describe '#to_json' do
+ it 'adds the current strategy class to the serialized attributes' do
+ params = { param1: 1 }
+ result = params.merge(klass: described_class.to_s).to_json
+
+ expect(described_class.new(params).to_json).to eq result
+ end
+ end
+
+ def lock_path_exist?
+ File.exist?(described_class.lock_file_path(project))
+ end
+end
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
new file mode 100644
index 00000000000..5fe57d9987b
--- /dev/null
+++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do
+ let(:example_url) { 'http://www.example.com' }
+ let(:strategy) { subject.new(url: example_url, http_method: 'post') }
+ let!(:project) { create(:project, :with_export) }
+ let!(:user) { build(:user) }
+
+ subject { described_class }
+
+ describe 'validations' do
+ it 'only POST and PUT method allowed' do
+ %w(POST post PUT put).each do |method|
+ expect(subject.new(url: example_url, http_method: method)).to be_valid
+ end
+
+ expect(subject.new(url: example_url, http_method: 'whatever')).not_to be_valid
+ end
+
+ it 'onyl allow urls as upload urls' do
+ expect(subject.new(url: example_url)).to be_valid
+ expect(subject.new(url: 'whatever')).not_to be_valid
+ end
+ end
+
+ describe '#execute' do
+ it 'removes the exported project file after the upload' do
+ allow(strategy).to receive(:send_file)
+ allow(strategy).to receive(:handle_response_error)
+
+ expect(project).to receive(:remove_exported_project_file)
+
+ strategy.execute(user, project)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/after_export_strategy_builder_spec.rb b/spec/lib/gitlab/import_export/after_export_strategy_builder_spec.rb
new file mode 100644
index 00000000000..bf727285a9f
--- /dev/null
+++ b/spec/lib/gitlab/import_export/after_export_strategy_builder_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::AfterExportStrategyBuilder do
+ let!(:strategies_namespace) { 'Gitlab::ImportExport::AfterExportStrategies' }
+
+ describe '.build!' do
+ context 'when klass param is' do
+ it 'null it returns the default strategy' do
+ expect(described_class.build!(nil).class).to eq described_class.default_strategy
+ end
+
+ it 'not a valid class it raises StrategyNotFoundError exception' do
+ expect { described_class.build!('Whatever') }.to raise_error(described_class::StrategyNotFoundError)
+ end
+
+ it 'not a descendant of AfterExportStrategy' do
+ expect { described_class.build!('User') }.to raise_error(described_class::StrategyNotFoundError)
+ end
+ end
+
+ it 'initializes strategy with attributes param' do
+ params = { param1: 1, param2: 2, param3: 3 }
+
+ strategy = described_class.build!("#{strategies_namespace}::DownloadNotificationStrategy", params)
+
+ params.each { |k, v| expect(strategy.public_send(k)).to eq v }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index a204a8f1ffe..b675d5dc031 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -18,6 +18,7 @@ issues:
- metrics
- timelogs
- issue_assignees
+- closed_by
events:
- author
- project
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 0716852f57f..f949a23ffbb 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -15,6 +15,7 @@ Issue:
- updated_by_id
- confidential
- closed_at
+- closed_by_id
- due_date
- moved_to_id
- lock_version
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index 2d35b026485..a3b3dc3be6d 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -74,13 +74,13 @@ describe Gitlab::UrlBlocker do
expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false
end
- context 'when allow_private_networks is' do
- let(:private_networks) { ['192.168.1.2', '10.0.0.2', '172.16.0.2'] }
+ context 'when allow_local_network is' do
+ let(:local_ips) { ['192.168.1.2', '10.0.0.2', '172.16.0.2'] }
let(:fake_domain) { 'www.fakedomain.fake' }
context 'true (default)' do
it 'does not block urls from private networks' do
- private_networks.each do |ip|
+ local_ips.each do |ip|
stub_domain_resolv(fake_domain, ip)
expect(described_class).not_to be_blocked_url("http://#{fake_domain}")
@@ -94,14 +94,14 @@ describe Gitlab::UrlBlocker do
context 'false' do
it 'blocks urls from private networks' do
- private_networks.each do |ip|
+ local_ips.each do |ip|
stub_domain_resolv(fake_domain, ip)
- expect(described_class).to be_blocked_url("http://#{fake_domain}", allow_private_networks: false)
+ expect(described_class).to be_blocked_url("http://#{fake_domain}", allow_local_network: false)
unstub_domain_resolv
- expect(described_class).to be_blocked_url("http://#{ip}", allow_private_networks: false)
+ expect(described_class).to be_blocked_url("http://#{ip}", allow_local_network: false)
end
end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 138d21ede97..9e6aa109a4b 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -12,6 +12,14 @@ describe Gitlab::UsageData do
create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true)
create(:service, project: projects[1], type: 'SlackService', active: true)
create(:service, project: projects[2], type: 'SlackService', active: true)
+
+ gcp_cluster = create(:cluster, :provided_by_gcp)
+ create(:cluster, :provided_by_user)
+ create(:cluster, :provided_by_user, :disabled)
+ create(:clusters_applications_helm, :installed, cluster: gcp_cluster)
+ create(:clusters_applications_ingress, :installed, cluster: gcp_cluster)
+ create(:clusters_applications_prometheus, :installed, cluster: gcp_cluster)
+ create(:clusters_applications_runner, :installed, cluster: gcp_cluster)
end
subject { described_class.data }
@@ -64,6 +72,12 @@ describe Gitlab::UsageData do
clusters
clusters_enabled
clusters_disabled
+ clusters_platforms_gke
+ clusters_platforms_user
+ clusters_applications_helm
+ clusters_applications_ingress
+ clusters_applications_prometheus
+ clusters_applications_runner
in_review_folder
groups
issues
@@ -97,6 +111,15 @@ describe Gitlab::UsageData do
expect(count_data[:projects_jira_active]).to eq(2)
expect(count_data[:projects_slack_notifications_active]).to eq(2)
expect(count_data[:projects_slack_slash_active]).to eq(1)
+
+ expect(count_data[:clusters_enabled]).to eq(6)
+ expect(count_data[:clusters_disabled]).to eq(1)
+ expect(count_data[:clusters_platforms_gke]).to eq(1)
+ expect(count_data[:clusters_platforms_user]).to eq(1)
+ expect(count_data[:clusters_applications_helm]).to eq(1)
+ expect(count_data[:clusters_applications_ingress]).to eq(1)
+ expect(count_data[:clusters_applications_prometheus]).to eq(1)
+ expect(count_data[:clusters_applications_runner]).to eq(1)
end
end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 1d0faf56f7c..2b3ffb2d7c0 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -55,7 +55,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_archive feature is disabled', :skip_gitaly_mock do
+ context 'when Gitaly workhorse_archive feature is disabled', :disable_gitaly do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
@@ -100,7 +100,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_send_git_patch feature is disabled', :skip_gitaly_mock do
+ context 'when Gitaly workhorse_send_git_patch feature is disabled', :disable_gitaly do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
@@ -173,7 +173,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_send_git_diff feature is disabled', :skip_gitaly_mock do
+ context 'when Gitaly workhorse_send_git_diff feature is disabled', :disable_gitaly do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
@@ -275,12 +275,14 @@ describe Gitlab::Workhorse do
describe '.git_http_ok' do
let(:user) { create(:user) }
+ let(:repo_path) { 'ignored but not allowed to be empty in gitlab-workhorse' }
let(:action) { 'info_refs' }
let(:params) do
{
GL_ID: "user-#{user.id}",
GL_USERNAME: user.username,
GL_REPOSITORY: "project-#{project.id}",
+ RepoPath: repo_path,
ShowAllRefs: false
}
end
@@ -295,6 +297,7 @@ describe Gitlab::Workhorse do
GL_ID: "user-#{user.id}",
GL_USERNAME: user.username,
GL_REPOSITORY: "wiki-#{project.id}",
+ RepoPath: repo_path,
ShowAllRefs: false
}
end
@@ -452,7 +455,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_raw_show feature is disabled', :skip_gitaly_mock do
+ context 'when Gitaly workhorse_raw_show feature is disabled', :disable_gitaly do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
diff --git a/spec/mailers/previews/notify_preview.rb b/spec/mailers/previews/notify_preview.rb
index 580f0d56a92..43c3c89f140 100644
--- a/spec/mailers/previews/notify_preview.rb
+++ b/spec/mailers/previews/notify_preview.rb
@@ -65,7 +65,7 @@ class NotifyPreview < ActionMailer::Preview
end
def merge_request
- @merge_request ||= project.merge_requests.find_by(source_branch: 'master', target_branch: 'feature')
+ @merge_request ||= project.merge_requests.first
end
def user
diff --git a/spec/models/ci/artifact_blob_spec.rb b/spec/models/ci/artifact_blob_spec.rb
index 4e72d9d748e..0014bbcf9f5 100644
--- a/spec/models/ci/artifact_blob_spec.rb
+++ b/spec/models/ci/artifact_blob_spec.rb
@@ -65,6 +65,19 @@ describe Ci::ArtifactBlob do
expect(url).not_to be_nil
expect(url).to eq("http://#{project.namespace.path}.#{Gitlab.config.pages.host}/-/#{project.path}/-/jobs/#{build.id}/artifacts/#{path}")
end
+
+ context 'when port is configured' do
+ let(:port) { 1234 }
+
+ it 'returns an URL with port number' do
+ allow(Gitlab.config.pages).to receive(:url).and_return("#{Gitlab.config.pages.url}:#{port}")
+
+ url = subject.external_url(build.project, build)
+
+ expect(url).not_to be_nil
+ expect(url).to eq("http://#{project.namespace.path}.#{Gitlab.config.pages.host}:#{port}/-/#{project.path}/-/jobs/#{build.id}/artifacts/#{path}")
+ end
+ end
end
end
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
new file mode 100644
index 00000000000..268561ee941
--- /dev/null
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe Ci::BuildMetadata do
+ set(:user) { create(:user) }
+ set(:group) { create(:group, :access_requestable) }
+ set(:project) { create(:project, :repository, group: group, build_timeout: 2000) }
+
+ set(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: project.commit.id,
+ ref: project.default_branch,
+ status: 'success')
+ end
+
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:build_metadata) { create(:ci_build_metadata, build: build) }
+
+ describe '#update_timeout_state' do
+ subject { build_metadata }
+
+ context 'when runner is not assigned to the job' do
+ it "doesn't change timeout value" do
+ expect { subject.update_timeout_state }.not_to change { subject.reload.timeout }
+ end
+
+ it "doesn't change timeout_source value" do
+ expect { subject.update_timeout_state }.not_to change { subject.reload.timeout_source }
+ end
+ end
+
+ context 'when runner is assigned to the job' do
+ before do
+ build.update_attributes(runner: runner)
+ end
+
+ context 'when runner timeout is lower than project timeout' do
+ let(:runner) { create(:ci_runner, maximum_timeout: 1900) }
+
+ it 'sets runner timeout' do
+ expect { subject.update_timeout_state }.to change { subject.reload.timeout }.to(1900)
+ end
+
+ it 'sets runner_timeout_source' do
+ expect { subject.update_timeout_state }.to change { subject.reload.timeout_source }.to('runner_timeout_source')
+ end
+ end
+
+ context 'when runner timeout is higher than project timeout' do
+ let(:runner) { create(:ci_runner, maximum_timeout: 2100) }
+
+ it 'sets project timeout' do
+ expect { subject.update_timeout_state }.to change { subject.reload.timeout }.to(2000)
+ end
+
+ it 'sets project_timeout_source' do
+ expect { subject.update_timeout_state }.to change { subject.reload.timeout_source }.to('project_timeout_source')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 7d935cf8d76..a12717835b0 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1271,12 +1271,6 @@ describe Ci::Build do
end
describe 'project settings' do
- describe '#timeout' do
- it 'returns project timeout configuration' do
- expect(build.timeout).to eq(project.build_timeout)
- end
- end
-
describe '#allow_git_fetch' do
it 'return project allow_git_fetch configuration' do
expect(build.allow_git_fetch).to eq(project.build_allow_git_fetch)
@@ -1469,24 +1463,24 @@ describe Ci::Build do
let(:container_registry_enabled) { false }
let(:predefined_variables) do
[
+ { key: 'CI_JOB_ID', value: build.id.to_s, public: true },
+ { key: 'CI_JOB_TOKEN', value: build.token, public: false },
+ { key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
+ { key: 'CI_BUILD_TOKEN', value: build.token, public: false },
+ { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
+ { key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false },
+ { 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: '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 },
- { key: 'CI_JOB_ID', value: build.id.to_s, public: true },
{ key: 'CI_JOB_NAME', value: 'test', public: true },
{ key: 'CI_JOB_STAGE', value: 'test', public: true },
- { key: 'CI_JOB_TOKEN', value: build.token, public: false },
{ key: 'CI_COMMIT_SHA', value: build.sha, public: true },
{ key: 'CI_COMMIT_REF_NAME', value: build.ref, public: true },
{ key: 'CI_COMMIT_REF_SLUG', value: build.ref_slug, public: true },
- { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
- { key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false },
- { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false },
- { key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
- { key: 'CI_BUILD_TOKEN', value: build.token, public: false },
{ key: 'CI_BUILD_REF', value: build.sha, public: true },
{ key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true },
{ key: 'CI_BUILD_REF_NAME', value: build.ref, public: true },
@@ -1951,6 +1945,7 @@ describe Ci::Build do
before do
allow(build).to receive(:predefined_variables) { [build_pre_var] }
allow(build).to receive(:yaml_variables) { [build_yaml_var] }
+ allow(build).to receive(:persisted_variables) { [] }
allow_any_instance_of(Project)
.to receive(:predefined_variables) { [project_pre_var] }
@@ -1999,6 +1994,106 @@ describe Ci::Build do
end
end
end
+
+ context 'when build has not been persisted yet' do
+ let(:build) do
+ described_class.new(
+ name: 'rspec',
+ stage: 'test',
+ ref: 'feature',
+ project: project,
+ pipeline: pipeline
+ )
+ end
+
+ it 'returns static predefined variables' do
+ expect(build.variables.size).to be >= 28
+ expect(build.variables)
+ .to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true)
+ expect(build).not_to be_persisted
+ end
+ end
+ end
+
+ describe '#scoped_variables' do
+ context 'when build has not been persisted yet' do
+ let(:build) do
+ described_class.new(
+ name: 'rspec',
+ stage: 'test',
+ ref: 'feature',
+ project: project,
+ pipeline: pipeline
+ )
+ end
+
+ it 'does not persist the build' do
+ expect(build).to be_valid
+ expect(build).not_to be_persisted
+
+ build.scoped_variables
+
+ expect(build).not_to be_persisted
+ end
+
+ it 'returns static predefined variables' do
+ keys = %w[CI_JOB_NAME
+ CI_COMMIT_SHA
+ CI_COMMIT_REF_NAME
+ CI_COMMIT_REF_SLUG
+ CI_JOB_STAGE]
+
+ variables = build.scoped_variables
+
+ variables.map { |env| env[:key] }.tap do |names|
+ expect(names).to include(*keys)
+ end
+
+ expect(variables)
+ .to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true)
+ end
+
+ it 'does not return prohibited variables' do
+ keys = %w[CI_JOB_ID
+ CI_JOB_TOKEN
+ CI_BUILD_ID
+ CI_BUILD_TOKEN
+ CI_REGISTRY_USER
+ CI_REGISTRY_PASSWORD
+ CI_REPOSITORY_URL
+ CI_ENVIRONMENT_URL]
+
+ build.scoped_variables.map { |env| env[:key] }.tap do |names|
+ expect(names).not_to include(*keys)
+ end
+ end
+ end
+ end
+
+ describe '#scoped_variables_hash' do
+ context 'when overriding secret variables' do
+ before do
+ project.variables.create!(key: 'MY_VAR', value: 'my value 1')
+ pipeline.variables.create!(key: 'MY_VAR', value: 'my value 2')
+ end
+
+ it 'returns a regular hash created using valid ordering' do
+ expect(build.scoped_variables_hash).to include('MY_VAR': 'my value 2')
+ expect(build.scoped_variables_hash).not_to include('MY_VAR': 'my value 1')
+ end
+ end
+
+ context 'when overriding user-provided variables' do
+ before do
+ pipeline.variables.build(key: 'MY_VAR', value: 'pipeline value')
+ build.yaml_variables = [{ key: 'MY_VAR', value: 'myvar', public: true }]
+ end
+
+ it 'returns a hash including variable with higher precedence' do
+ expect(build.scoped_variables_hash).to include('MY_VAR': 'pipeline value')
+ expect(build.scoped_variables_hash).not_to include('MY_VAR': 'myvar')
+ end
+ end
end
describe 'state transition: any => [:pending]' do
@@ -2011,6 +2106,70 @@ describe Ci::Build do
end
end
+ describe 'state transition: pending: :running' do
+ let(:runner) { create(:ci_runner) }
+ let(:job) { create(:ci_build, :pending, runner: runner) }
+
+ before do
+ job.project.update_attribute(:build_timeout, 1800)
+ end
+
+ def run_job_without_exception
+ job.run!
+ rescue StateMachines::InvalidTransition
+ end
+
+ shared_examples 'saves data on transition' do
+ it 'saves timeout' do
+ expect { job.run! }.to change { job.reload.ensure_metadata.timeout }.from(nil).to(expected_timeout)
+ end
+
+ it 'saves timeout_source' do
+ expect { job.run! }.to change { job.reload.ensure_metadata.timeout_source }.from('unknown_timeout_source').to(expected_timeout_source)
+ end
+
+ context 'when Ci::BuildMetadata#update_timeout_state fails update' do
+ before do
+ allow_any_instance_of(Ci::BuildMetadata).to receive(:update_timeout_state).and_return(false)
+ end
+
+ it "doesn't save timeout" do
+ expect { run_job_without_exception }.not_to change { job.reload.ensure_metadata.timeout_source }
+ end
+
+ 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
+
+ context 'when runner timeout overrides project timeout' do
+ let(:expected_timeout) { 900 }
+ let(:expected_timeout_source) { 'runner_timeout_source' }
+
+ before do
+ runner.update_attribute(:maximum_timeout, 900)
+ end
+
+ it_behaves_like 'saves data on transition'
+ end
+
+ context "when runner timeout doesn't override project timeout" do
+ let(:expected_timeout) { 1800 }
+ let(:expected_timeout_source) { 'project_timeout_source' }
+
+ before do
+ runner.update_attribute(:maximum_timeout, 3600)
+ end
+
+ it_behaves_like 'saves data on transition'
+ end
+ end
+
describe 'state transition: any => [:running]' do
shared_examples 'validation is active' do
context 'when depended job has not been completed yet' do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 92f00cfbc19..dd94515b0a4 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -346,6 +346,20 @@ describe Ci::Pipeline, :mailer do
end
end
end
+
+ context 'when variables policy is specified' do
+ let(:config) do
+ { unit: { script: 'minitest', only: { variables: ['$CI_PIPELINE_SOURCE'] } },
+ feature: { script: 'spinach', only: { variables: ['$UNDEFINED'] } } }
+ end
+
+ it 'returns stage seeds only when variables expression is truthy' do
+ seeds = pipeline.stage_seeds
+
+ expect(seeds.size).to eq 1
+ expect(seeds.dig(0, 0, :name)).to eq 'unit'
+ end
+ end
end
describe '#seeds_size' do
diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb
index ba7bad617b4..0eb1e3876e2 100644
--- a/spec/models/clusters/applications/helm_spec.rb
+++ b/spec/models/clusters/applications/helm_spec.rb
@@ -3,6 +3,18 @@ require 'rails_helper'
describe Clusters::Applications::Helm do
include_examples 'cluster application core specs', :clusters_applications_helm
+ describe '.installed' do
+ subject { described_class.installed }
+
+ let!(:cluster) { create(:clusters_applications_helm, :installed) }
+
+ before do
+ create(:clusters_applications_helm, :errored)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
describe '#install_command' do
let(:helm) { create(:clusters_applications_helm) }
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index 03f5b88a525..a47a07d908d 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -11,6 +11,18 @@ describe Clusters::Applications::Ingress do
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
end
+ describe '.installed' do
+ subject { described_class.installed }
+
+ let!(:cluster) { create(:clusters_applications_ingress, :installed) }
+
+ before do
+ create(:clusters_applications_ingress, :errored)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
describe '#make_installed!' do
before do
application.make_installed!
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index 2905b58066b..aeca6ee903a 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -4,6 +4,18 @@ describe Clusters::Applications::Prometheus do
include_examples 'cluster application core specs', :clusters_applications_prometheus
include_examples 'cluster application status specs', :cluster_application_prometheus
+ describe '.installed' do
+ subject { described_class.installed }
+
+ let!(:cluster) { create(:clusters_applications_prometheus, :installed) }
+
+ before do
+ create(:clusters_applications_prometheus, :errored)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
describe 'transition to installed' do
let(:project) { create(:project) }
let(:cluster) { create(:cluster, projects: [project]) }
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index a574779e39d..64d995a73c1 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -8,6 +8,18 @@ describe Clusters::Applications::Runner do
it { is_expected.to belong_to(:runner) }
+ describe '.installed' do
+ subject { described_class.installed }
+
+ let!(:cluster) { create(:clusters_applications_runner, :installed) }
+
+ before do
+ create(:clusters_applications_runner, :errored)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
describe '#install_command' do
let(:kubeclient) { double('kubernetes client') }
let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) }
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 8f12a0e3085..b942554d67b 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -39,6 +39,42 @@ describe Clusters::Cluster do
it { is_expected.to contain_exactly(cluster) }
end
+ describe '.user_provided' do
+ subject { described_class.user_provided }
+
+ let!(:cluster) { create(:cluster, :provided_by_user) }
+
+ before do
+ create(:cluster, :provided_by_gcp)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
+ describe '.gcp_provided' do
+ subject { described_class.gcp_provided }
+
+ let!(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ before do
+ create(:cluster, :provided_by_user)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
+ describe '.gcp_installed' do
+ subject { described_class.gcp_installed }
+
+ let!(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ before do
+ create(:cluster, :providing_by_gcp)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
describe 'validation' do
subject { cluster.valid? }
diff --git a/spec/models/concerns/chronic_duration_attribute_spec.rb b/spec/models/concerns/chronic_duration_attribute_spec.rb
new file mode 100644
index 00000000000..27c86e60e60
--- /dev/null
+++ b/spec/models/concerns/chronic_duration_attribute_spec.rb
@@ -0,0 +1,115 @@
+require 'spec_helper'
+
+shared_examples 'ChronicDurationAttribute reader' do
+ it 'contains dynamically created reader method' do
+ expect(subject.class).to be_public_method_defined(virtual_field)
+ end
+
+ it 'outputs chronic duration formatted value' do
+ subject.send("#{source_field}=", 120)
+
+ expect(subject.send(virtual_field)).to eq('2m')
+ end
+
+ context 'when value is set to nil' do
+ it 'outputs nil' do
+ subject.send("#{source_field}=", nil)
+
+ expect(subject.send(virtual_field)).to be_nil
+ end
+ end
+end
+
+shared_examples 'ChronicDurationAttribute writer' do
+ it 'contains dynamically created writer method' do
+ expect(subject.class).to be_public_method_defined("#{virtual_field}=")
+ end
+
+ before do
+ subject.send("#{virtual_field}=", '10m')
+ end
+
+ it 'parses chronic duration input' do
+ expect(subject.send(source_field)).to eq(600)
+ end
+
+ it 'passes validation' do
+ expect(subject.valid?).to be_truthy
+ end
+
+ context 'when negative input is used' do
+ before do
+ subject.send("#{source_field}=", 3600)
+ end
+
+ it "doesn't raise exception" do
+ expect { subject.send("#{virtual_field}=", '-10m') }.not_to raise_error(ChronicDuration::DurationParseError)
+ end
+
+ it "doesn't change value" do
+ expect { subject.send("#{virtual_field}=", '-10m') }.not_to change { subject.send(source_field) }
+ end
+
+ it "doesn't pass validation" do
+ subject.send("#{virtual_field}=", '-10m')
+
+ expect(subject.valid?).to be_falsey
+ expect(subject.errors&.messages).to include(virtual_field => ['is not a correct duration'])
+ end
+ end
+
+ context 'when empty input is used' do
+ before do
+ subject.send("#{virtual_field}=", '')
+ end
+
+ it 'writes nil' do
+ expect(subject.send(source_field)).to be_nil
+ end
+
+ it 'passes validation' do
+ expect(subject.valid?).to be_truthy
+ end
+ end
+
+ context 'when nil input is used' do
+ before do
+ subject.send("#{virtual_field}=", nil)
+ end
+
+ it 'writes nil' do
+ expect(subject.send(source_field)).to be_nil
+ end
+
+ it 'passes validation' do
+ expect(subject.valid?).to be_truthy
+ end
+
+ it "doesn't raise exception" do
+ expect { subject.send("#{virtual_field}=", nil) }.not_to raise_error(NoMethodError)
+ end
+ end
+end
+
+describe 'ChronicDurationAttribute' do
+ let(:source_field) {:maximum_timeout}
+ let(:virtual_field) {:maximum_timeout_human_readable}
+
+ subject { Ci::Runner.new }
+
+ it_behaves_like 'ChronicDurationAttribute reader'
+ it_behaves_like 'ChronicDurationAttribute writer'
+end
+
+describe 'ChronicDurationAttribute - reader' do
+ let(:source_field) {:timeout}
+ let(:virtual_field) {:timeout_human_readable}
+
+ subject {Ci::BuildMetadata.new}
+
+ it "doesn't contain dynamically created writer method" do
+ expect(subject.class).not_to be_public_method_defined("#{virtual_field}=")
+ end
+
+ it_behaves_like 'ChronicDurationAttribute reader'
+end
diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb
index 3d7283e2164..41440c6d288 100644
--- a/spec/models/deploy_key_spec.rb
+++ b/spec/models/deploy_key_spec.rb
@@ -17,4 +17,25 @@ describe DeployKey, :mailer do
should_not_email(user)
end
end
+
+ describe '#user' do
+ let(:deploy_key) { create(:deploy_key) }
+ let(:user) { create(:user) }
+
+ context 'when user is set' do
+ before do
+ deploy_key.user = user
+ end
+
+ it 'returns the user' do
+ expect(deploy_key.user).to be(user)
+ end
+ end
+
+ context 'when user is not set' do
+ it 'returns the ghost user' do
+ expect(deploy_key.user).to eq(User.ghost)
+ end
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 1a7a6e035ea..fef868ac0f2 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -224,14 +224,14 @@ describe Project do
project2 = build(:project, import_url: 'http://localhost:9000/t.git')
expect(project2).to be_invalid
- expect(project2.errors[:import_url]).to include('imports are not allowed from that URL')
+ expect(project2.errors[:import_url].first).to include('Requests to localhost are not allowed')
end
it "does not allow blocked import_url port" do
project2 = build(:project, import_url: 'http://github.com:25/t.git')
expect(project2).to be_invalid
- expect(project2.errors[:import_url]).to include('imports are not allowed from that URL')
+ expect(project2.errors[:import_url].first).to include('Only allowed ports are 22, 80, 443')
end
describe 'project pending deletion' do
@@ -1265,6 +1265,34 @@ describe Project do
end
end
+ describe '#pages_group_url' do
+ let(:group) { create :group, name: group_name }
+ let(:project) { create :project, namespace: group, name: project_name }
+ let(:domain) { 'Example.com' }
+ let(:port) { 1234 }
+
+ subject { project.pages_group_url }
+
+ before do
+ allow(Settings.pages).to receive(:host).and_return(domain)
+ allow(Gitlab.config.pages).to receive(:url).and_return("http://example.com:#{port}")
+ end
+
+ context 'group page' do
+ let(:group_name) { 'Group' }
+ let(:project_name) { 'group.example.com' }
+
+ it { is_expected.to eq("http://group.example.com:#{port}") }
+ end
+
+ context 'project page' do
+ let(:group_name) { 'Group' }
+ let(:project_name) { 'Project' }
+
+ it { is_expected.to eq("http://group.example.com:#{port}") }
+ end
+ end
+
describe '.search' do
let(:project) { create(:project, description: 'kitten mittens') }
@@ -2532,7 +2560,7 @@ describe Project do
end
end
- describe '#remove_exports' do
+ describe '#remove_export' do
let(:legacy_project) { create(:project, :legacy_storage, :with_export) }
let(:project) { create(:project, :with_export) }
@@ -2580,6 +2608,23 @@ describe Project do
end
end
+ describe '#remove_exported_project_file' do
+ let(:project) { create(:project, :with_export) }
+
+ it 'removes the exported project file' do
+ exported_file = project.export_project_path
+
+ expect(File.exist?(exported_file)).to be_truthy
+
+ allow(FileUtils).to receive(:rm_f).and_call_original
+ expect(FileUtils).to receive(:rm_f).with(exported_file).and_call_original
+
+ project.remove_exported_project_file
+
+ expect(File.exist?(exported_file)).to be_falsy
+ end
+ end
+
describe '#forks_count' do
it 'returns the number of forks' do
project = build(:project)
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 79f25dc4360..83ed3b203e6 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -58,6 +58,21 @@ describe Service do
end
describe "Template" do
+ describe '.build_from_template' do
+ context 'when template is invalid' do
+ it 'sets service template to inactive when template is invalid' do
+ project = create(:project)
+ template = JiraService.new(template: true, active: true)
+ template.save(validate: false)
+
+ service = described_class.build_from_template(project.id, template)
+
+ expect(service).to be_valid
+ expect(service.active).to be false
+ end
+ end
+ end
+
describe "for pushover service" do
let!(:service_template) do
PushoverService.create(
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index bbfdda23a31..100418da804 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -25,7 +25,7 @@ describe User do
it { is_expected.to have_many(:group_members) }
it { is_expected.to have_many(:groups) }
it { is_expected.to have_many(:keys).dependent(:destroy) }
- it { is_expected.to have_many(:deploy_keys).dependent(:destroy) }
+ it { is_expected.to have_many(:deploy_keys).dependent(:nullify) }
it { is_expected.to have_many(:events).dependent(:destroy) }
it { is_expected.to have_many(:issues).dependent(:destroy) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 852f67db958..8ad19e3f0f5 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -1141,4 +1141,33 @@ describe API::Commits do
end
end
end
+
+ describe 'GET /projects/:id/repository/commits/:sha/merge_requests' do
+ let!(:project) { create(:project, :repository, :private) }
+ let!(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') }
+ let(:commit) { merged_mr.merge_request_diff.commits.last }
+
+ it 'returns the correct merge request' do
+ get api("/projects/#{project.id}/repository/commits/#{commit.id}/merge_requests", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response.length).to eq(1)
+ expect(json_response[0]['id']).to eq(merged_mr.id)
+ end
+
+ it 'returns 403 for an unauthorized user' do
+ project.add_guest(user)
+
+ get api("/projects/#{project.id}/repository/commits/#{commit.id}/merge_requests", user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'responds 404 when the commit does not exist' do
+ get api("/projects/#{project.id}/repository/commits/a7d26f00c35b/merge_requests", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
end
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index 0772b3f2e64..ae9c0e9c304 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -91,6 +91,10 @@ describe API::DeployKeys do
expect do
post api("/projects/#{project.id}/deploy_keys", admin), key_attrs
end.to change { project.deploy_keys.count }.by(1)
+
+ new_key = project.deploy_keys.last
+ expect(new_key.key).to eq(key_attrs[:key])
+ expect(new_key.user).to eq(admin)
end
it 'returns an existing ssh key when attempting to add a duplicate' do
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 3cb90a1b8ef..db8c5f963d6 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -251,44 +251,23 @@ describe API::Internal do
end
context 'with env passed as a JSON' do
- context 'when relative path envs are not set' do
- it 'sets env in RequestStore' do
- expect(Gitlab::Git::Env).to receive(:set).with({
- 'GIT_OBJECT_DIRECTORY' => 'foo',
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
- })
-
- push(key, project.wiki, env: {
- GIT_OBJECT_DIRECTORY: 'foo',
- GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar'
- }.to_json)
+ let(:gl_repository) { project.gl_repository(is_wiki: true) }
- expect(response).to have_gitlab_http_status(200)
- end
- end
+ it 'sets env in RequestStore' do
+ obj_dir_relative = './objects'
+ alt_obj_dirs_relative = ['./alt-objects-1', './alt-objects-2']
- context 'when relative path envs are set' do
- it 'sets env in RequestStore' do
- obj_dir_relative = './objects'
- alt_obj_dirs_relative = ['./alt-objects-1', './alt-objects-2']
- repo_path = project.wiki.repository.path_to_repo
-
- expect(Gitlab::Git::Env).to receive(:set).with({
- 'GIT_OBJECT_DIRECTORY' => File.join(repo_path, obj_dir_relative),
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => alt_obj_dirs_relative.map { |d| File.join(repo_path, d) },
- 'GIT_OBJECT_DIRECTORY_RELATIVE' => obj_dir_relative,
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => alt_obj_dirs_relative
- })
-
- push(key, project.wiki, env: {
- GIT_OBJECT_DIRECTORY: 'foo',
- GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar',
- GIT_OBJECT_DIRECTORY_RELATIVE: obj_dir_relative,
- GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: alt_obj_dirs_relative
- }.to_json)
+ expect(Gitlab::Git::HookEnv).to receive(:set).with(gl_repository, {
+ 'GIT_OBJECT_DIRECTORY_RELATIVE' => obj_dir_relative,
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => alt_obj_dirs_relative
+ })
- expect(response).to have_gitlab_http_status(200)
- end
+ push(key, project.wiki, env: {
+ GIT_OBJECT_DIRECTORY_RELATIVE: obj_dir_relative,
+ GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: alt_obj_dirs_relative
+ }.to_json)
+
+ expect(response).to have_gitlab_http_status(200)
end
end
diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb
index 12583109b59..3834d27d0a9 100644
--- a/spec/requests/api/project_export_spec.rb
+++ b/spec/requests/api/project_export_spec.rb
@@ -5,6 +5,7 @@ describe API::ProjectExport do
set(:project_none) { create(:project) }
set(:project_started) { create(:project) }
set(:project_finished) { create(:project) }
+ set(:project_after_export) { create(:project) }
set(:user) { create(:user) }
set(:admin) { create(:admin) }
@@ -12,11 +13,13 @@ describe API::ProjectExport do
let(:path_none) { "/projects/#{project_none.id}/export" }
let(:path_started) { "/projects/#{project_started.id}/export" }
let(:path_finished) { "/projects/#{project_finished.id}/export" }
+ let(:path_after_export) { "/projects/#{project_after_export.id}/export" }
let(:download_path) { "/projects/#{project.id}/export/download" }
let(:download_path_none) { "/projects/#{project_none.id}/export/download" }
let(:download_path_started) { "/projects/#{project_started.id}/export/download" }
let(:download_path_finished) { "/projects/#{project_finished.id}/export/download" }
+ let(:download_path_export_action) { "/projects/#{project_after_export.id}/export/download" }
let(:export_path) { "#{Dir.tmpdir}/project_export_spec" }
@@ -29,6 +32,11 @@ describe API::ProjectExport do
# simulate exported
FileUtils.mkdir_p project_finished.export_path
FileUtils.touch File.join(project_finished.export_path, '_export.tar.gz')
+
+ # simulate in after export action
+ FileUtils.mkdir_p project_after_export.export_path
+ FileUtils.touch File.join(project_after_export.export_path, '_export.tar.gz')
+ FileUtils.touch Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy.lock_file_path(project_after_export)
end
after do
@@ -73,6 +81,14 @@ describe API::ProjectExport do
expect(json_response['export_status']).to eq('started')
end
+ it 'is after_export' do
+ get api(path_after_export, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/project/export_status')
+ expect(json_response['export_status']).to eq('after_export_action')
+ end
+
it 'is finished' do
get api(path_finished, user)
@@ -99,6 +115,7 @@ describe API::ProjectExport do
project_none.add_master(user)
project_started.add_master(user)
project_finished.add_master(user)
+ project_after_export.add_master(user)
end
it_behaves_like 'get project export status ok'
@@ -163,6 +180,36 @@ describe API::ProjectExport do
end
end
+ shared_examples_for 'get project export upload after action' do
+ context 'and is uploading' do
+ it 'downloads' do
+ get api(download_path_export_action, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context 'when upload complete' do
+ before do
+ FileUtils.rm_rf(project_after_export.export_path)
+ end
+
+ it_behaves_like '404 response' do
+ let(:request) { get api(download_path_export_action, user) }
+ end
+ end
+ end
+
+ shared_examples_for 'get project download by strategy' do
+ context 'when upload strategy set' do
+ it_behaves_like 'get project export upload after action'
+ end
+
+ context 'when download strategy set' do
+ it_behaves_like 'get project export download'
+ end
+ end
+
it_behaves_like 'when project export is disabled' do
let(:request) { get api(download_path, admin) }
end
@@ -171,7 +218,7 @@ describe API::ProjectExport do
context 'when user is an admin' do
let(:user) { admin }
- it_behaves_like 'get project export download'
+ it_behaves_like 'get project download by strategy'
end
context 'when user is a master' do
@@ -180,9 +227,10 @@ describe API::ProjectExport do
project_none.add_master(user)
project_started.add_master(user)
project_finished.add_master(user)
+ project_after_export.add_master(user)
end
- it_behaves_like 'get project export download'
+ it_behaves_like 'get project download by strategy'
end
context 'when user is a developer' do
@@ -229,10 +277,30 @@ describe API::ProjectExport do
end
shared_examples_for 'post project export start' do
- it 'starts' do
- post api(path, user)
+ context 'with upload strategy' do
+ context 'when params invalid' do
+ it_behaves_like '400 response' do
+ let(:request) { post(api(path, user), 'upload[url]' => 'whatever') }
+ end
+ end
+
+ it 'starts' do
+ allow_any_instance_of(Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy).to receive(:send_file)
+
+ post(api(path, user), 'upload[url]' => 'http://gitlab.com')
- expect(response).to have_gitlab_http_status(202)
+ expect(response).to have_gitlab_http_status(202)
+ end
+ end
+
+ context 'with download strategy' do
+ it 'starts' do
+ expect_any_instance_of(Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy).not_to receive(:send_file)
+
+ post api(path, user)
+
+ expect(response).to have_gitlab_http_status(202)
+ end
end
end
@@ -253,6 +321,7 @@ describe API::ProjectExport do
project_none.add_master(user)
project_started.add_master(user)
project_finished.add_master(user)
+ project_after_export.add_master(user)
end
it_behaves_like 'post project export start'
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index f3dd121faa9..5084b36c761 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -109,6 +109,26 @@ describe API::Runner do
end
end
+ context 'when maximum job timeout is specified' do
+ it 'creates runner' do
+ post api('/runners'), token: registration_token,
+ maximum_timeout: 9000
+
+ expect(response).to have_gitlab_http_status 201
+ expect(Ci::Runner.first.maximum_timeout).to eq(9000)
+ end
+
+ context 'when maximum job timeout is empty' do
+ it 'creates runner' do
+ post api('/runners'), token: registration_token,
+ maximum_timeout: ''
+
+ expect(response).to have_gitlab_http_status 201
+ expect(Ci::Runner.first.maximum_timeout).to be_nil
+ end
+ end
+ end
+
%w(name version revision platform architecture).each do |param|
context "when info parameter '#{param}' info is present" do
let(:value) { "#{param}_value" }
@@ -340,12 +360,12 @@ describe API::Runner do
let(:expected_steps) do
[{ 'name' => 'script',
'script' => %w(ls date),
- 'timeout' => job.timeout,
+ 'timeout' => job.metadata_timeout,
'when' => 'on_success',
'allow_failure' => false },
{ 'name' => 'after_script',
'script' => %w(ls date),
- 'timeout' => job.timeout,
+ 'timeout' => job.metadata_timeout,
'when' => 'always',
'allow_failure' => true }]
end
@@ -648,6 +668,41 @@ describe API::Runner do
end
end
end
+
+ describe 'timeout support' do
+ context 'when project specifies job timeout' do
+ let(:project) { create(:project, shared_runners_enabled: false, build_timeout: 1234) }
+
+ it 'contains info about timeout taken from project' do
+ request_job
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['runner_info']).to include({ 'timeout' => 1234 })
+ end
+
+ context 'when runner specifies lower timeout' do
+ let(:runner) { create(:ci_runner, maximum_timeout: 1000) }
+
+ it 'contains info about timeout overridden by runner' do
+ request_job
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['runner_info']).to include({ 'timeout' => 1000 })
+ end
+ end
+
+ context 'when runner specifies bigger timeout' do
+ let(:runner) { create(:ci_runner, maximum_timeout: 2000) }
+
+ it 'contains info about timeout not overridden by runner' do
+ request_job
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['runner_info']).to include({ 'timeout' => 1234 })
+ end
+ end
+ end
+ end
end
def request_job(token = runner.token, **params)
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index ec5cad4f4fd..d30f0cf36e2 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -123,6 +123,7 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(200)
expect(json_response['description']).to eq(shared_runner.description)
+ expect(json_response['maximum_timeout']).to be_nil
end
end
@@ -192,7 +193,8 @@ describe API::Runners do
tag_list: ['ruby2.1', 'pgsql', 'mysql'],
run_untagged: 'false',
locked: 'true',
- access_level: 'ref_protected')
+ access_level: 'ref_protected',
+ maximum_timeout: 1234)
shared_runner.reload
expect(response).to have_gitlab_http_status(200)
@@ -204,6 +206,7 @@ describe API::Runners do
expect(shared_runner.ref_protected?).to be_truthy
expect(shared_runner.ensure_runner_queue_value)
.not_to eq(runner_queue_value)
+ expect(shared_runner.maximum_timeout).to eq(1234)
end
end
diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb
index 16431ed4188..70402bac2e2 100644
--- a/spec/serializers/status_entity_spec.rb
+++ b/spec/serializers/status_entity_spec.rb
@@ -25,5 +25,10 @@ describe StatusEntity do
allow(Rails.env).to receive(:development?) { true }
expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/dev/favicon_status_success.ico')
end
+
+ it 'contains a canary namespaced favicon if canary env' do
+ stub_env('CANARY', 'true')
+ expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/canary/favicon_status_success.ico')
+ end
end
end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index b86a3d72bb4..8de0bdf92e2 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -29,7 +29,8 @@ describe Ci::RetryBuildService do
commit_id deployments erased_by_id last_deployment project_id
runner_id tag_taggings taggings tags trigger_request_id
user_id auto_canceled_by_id retried failure_reason
- artifacts_file_store artifacts_metadata_store].freeze
+ artifacts_file_store artifacts_metadata_store
+ metadata].freeze
shared_examples 'build duplication' do
let(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 47c1ebbeb81..7ae49c06896 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -67,6 +67,10 @@ describe Issues::CloseService do
expect(issue).to be_closed
end
+ it 'records closed user' do
+ expect(issue.closed_by_id).to be(user.id)
+ end
+
it 'sends email to user2 about assign of new issue' do
email = ActionMailer::Base.deliveries.last
expect(email.to.first).to eq(user2.email)
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 4413c6ef83e..2cacb97a293 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -70,6 +70,16 @@ describe Projects::CreateService, '#execute' do
opts[:default_branch] = 'master'
expect(create_project(user, opts)).to eq(nil)
end
+
+ it 'sets invalid service as inactive' do
+ create(:service, type: 'JiraService', project: nil, template: true, active: true)
+
+ project = create_project(user, opts)
+ service = project.services.first
+
+ expect(project).to be_persisted
+ expect(service.active).to be false
+ end
end
context 'wiki_enabled creates repository directory' do
@@ -232,14 +242,15 @@ describe Projects::CreateService, '#execute' do
end
context 'when a bad service template is created' do
- it 'reports an error in the imported project' do
+ it 'sets service to be inactive' do
opts[:import_url] = 'http://www.gitlab.com/gitlab-org/gitlab-ce'
create(:service, type: 'DroneCiService', project: nil, template: true, active: true)
project = create_project(user, opts)
+ service = project.services.first
- expect(project.errors.full_messages_for(:base).first).to match(/Unable to save project. Error: Unable to save DroneCiService/)
- expect(project.services.count).to eq 0
+ expect(project).to be_persisted
+ expect(service.active).to be false
end
end
diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb
new file mode 100644
index 00000000000..51491c7d529
--- /dev/null
+++ b/spec/services/projects/import_export/export_service_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+describe Projects::ImportExport::ExportService do
+ describe '#execute' do
+ let!(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:shared) { project.import_export_shared }
+ let(:service) { described_class.new(project, user) }
+ let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new }
+
+ context 'when all saver services succeed' do
+ before do
+ allow(service).to receive(:save_services).and_return(true)
+ end
+
+ it 'saves the project in the file system' do
+ expect(Gitlab::ImportExport::Saver).to receive(:save).with(project: project, shared: shared)
+
+ service.execute
+ end
+
+ it 'calls the after export strategy' do
+ expect(after_export_strategy).to receive(:execute)
+
+ service.execute(after_export_strategy)
+ end
+
+ context 'when after export strategy fails' do
+ before do
+ allow(after_export_strategy).to receive(:execute).and_return(false)
+ end
+
+ after do
+ service.execute(after_export_strategy)
+ end
+
+ it 'removes the remaining exported data' do
+ allow(shared).to receive(:export_path).and_return('whatever')
+ allow(FileUtils).to receive(:rm_rf)
+
+ expect(FileUtils).to receive(:rm_rf).with(shared.export_path)
+ end
+
+ it 'notifies the user' do
+ expect_any_instance_of(NotificationService).to receive(:project_not_exported)
+ end
+
+ it 'notifies logger' do
+ allow(Rails.logger).to receive(:error)
+
+ expect(Rails.logger).to receive(:error)
+ end
+ end
+ end
+
+ context 'when saver services fail' do
+ before do
+ allow(service).to receive(:save_services).and_return(false)
+ end
+
+ after do
+ expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
+ end
+
+ it 'removes the remaining exported data' do
+ allow(shared).to receive(:export_path).and_return('whatever')
+ allow(FileUtils).to receive(:rm_rf)
+
+ expect(FileUtils).to receive(:rm_rf).with(shared.export_path)
+ end
+
+ it 'notifies the user' do
+ expect_any_instance_of(NotificationService).to receive(:project_not_exported)
+ end
+
+ it 'notifies logger' do
+ expect(Rails.logger).to receive(:error)
+ end
+
+ it 'the after export strategy is not called' do
+ expect(service).not_to receive(:execute_after_export_action)
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index bf7facaec99..30c89ebd821 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -156,7 +156,7 @@ describe Projects::ImportService do
result = described_class.new(project, user).execute
expect(result[:status]).to eq :error
- expect(result[:message]).to end_with 'Blocked import URL.'
+ expect(result[:message]).to include('Requests to localhost are not allowed')
end
it 'fails with port 25' do
@@ -165,7 +165,7 @@ describe Projects::ImportService do
result = described_class.new(project, user).execute
expect(result[:status]).to eq :error
- expect(result[:message]).to end_with 'Blocked import URL.'
+ expect(result[:message]).to include('Only allowed ports are 22, 80, 443')
end
end
diff --git a/spec/support/cookie_helper.rb b/spec/support/cookie_helper.rb
index d72925e1838..5ff7b0b68c9 100644
--- a/spec/support/cookie_helper.rb
+++ b/spec/support/cookie_helper.rb
@@ -2,12 +2,25 @@
#
module CookieHelper
def set_cookie(name, value, options = {})
+ case page.driver
+ when Capybara::RackTest::Driver
+ rack_set_cookie(name, value)
+ else
+ selenium_set_cookie(name, value, options)
+ end
+ end
+
+ def selenium_set_cookie(name, value, options = {})
# Selenium driver will not set cookies for a given domain when the browser is at `about:blank`.
# It also doesn't appear to allow overriding the cookie path. loading `/` is the most inclusive.
visit options.fetch(:path, '/') unless on_a_page?
page.driver.browser.manage.add_cookie(name: name, value: value, **options)
end
+ def rack_set_cookie(name, value)
+ page.driver.browser.set_cookie("#{name}=#{value}")
+ end
+
def get_cookie(name)
page.driver.browser.manage.cookie_named(name)
end
diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb
index c7e8a39a617..9cf541372b5 100644
--- a/spec/support/gitaly.rb
+++ b/spec/support/gitaly.rb
@@ -1,11 +1,13 @@
RSpec.configure do |config|
config.before(:each) do |example|
if example.metadata[:disable_gitaly]
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false)
+ # Use 'and_wrap_original' to make sure the arguments are valid
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original { |m, *args| m.call(*args) && false }
else
next if example.metadata[:skip_gitaly_mock]
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
+ # Use 'and_wrap_original' to make sure the arguments are valid
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original { |m, *args| m.call(*args) || true }
end
end
end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index d08183846a0..db34090e971 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -140,6 +140,10 @@ module LoginHelpers
end
allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config)
stub_omniauth_setting(messages)
+ stub_saml_authorize_path_helpers
+ end
+
+ def stub_saml_authorize_path_helpers
allow_any_instance_of(Object).to receive(:user_saml_omniauth_authorize_path).and_return('/users/auth/saml')
allow_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml')
end
diff --git a/spec/support/migrations_helpers.rb b/spec/support/migrations_helpers.rb
index 6bf976a2cf9..5d6f662e8fe 100644
--- a/spec/support/migrations_helpers.rb
+++ b/spec/support/migrations_helpers.rb
@@ -1,6 +1,9 @@
module MigrationsHelpers
def table(name)
- Class.new(ActiveRecord::Base) { self.table_name = name }
+ Class.new(ActiveRecord::Base) do
+ self.table_name = name
+ self.inheritance_column = :_type_disabled
+ end
end
def migrations_paths
diff --git a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
index cd9974cd6e2..6352f1527cd 100644
--- a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
+++ b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
@@ -61,12 +61,18 @@ shared_examples "migrates" do |to_store:, from_store: nil|
expect { migrate(to) }.not_to change { file.exists? }
end
- context 'when migrate! is not oqqupied by another process' do
+ context 'when migrate! is not occupied by another process' do
it 'executes migrate!' do
expect(subject).to receive(:object_store=).at_least(1)
migrate(to)
end
+
+ it 'executes use_file' do
+ expect(subject).to receive(:unsafe_use_file).once
+
+ subject.use_file
+ end
end
context 'when migrate! is occupied by another process' do
@@ -79,7 +85,13 @@ shared_examples "migrates" do |to_store:, from_store: nil|
it 'does not execute migrate!' do
expect(subject).not_to receive(:unsafe_migrate!)
- expect { migrate(to) }.to raise_error('Already running')
+ expect { migrate(to) }.to raise_error('exclusive lease already taken')
+ end
+
+ it 'does not execute use_file' do
+ expect(subject).not_to receive(:unsafe_use_file)
+
+ expect { subject.use_file }.to raise_error('exclusive lease already taken')
end
after do
diff --git a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
index b778d26060d..6fcfae358ec 100644
--- a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
+++ b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
@@ -1,10 +1,9 @@
require 'rake_helper'
describe 'gitlab:uploads:migrate rake tasks' do
- let!(:projects) { create_list(:project, 10, :with_avatar) }
- let(:model_class) { Project }
- let(:uploader_class) { AvatarUploader }
- let(:mounted_as) { :avatar }
+ let(:model_class) { nil }
+ let(:uploader_class) { nil }
+ let(:mounted_as) { nil }
let(:batch_size) { 3 }
before do
@@ -20,9 +19,125 @@ describe 'gitlab:uploads:migrate rake tasks' do
run_rake_task("gitlab:uploads:migrate", *args)
end
- it 'enqueue jobs in batch' do
- expect(ObjectStorage::MigrateUploadsWorker).to receive(:enqueue!).exactly(4).times
+ shared_examples 'enqueue jobs in batch' do |batch:|
+ it do
+ expect(ObjectStorage::MigrateUploadsWorker)
+ .to receive(:perform_async).exactly(batch).times
+ .and_return("A fake job.")
- run
+ run
+ end
+ end
+
+ context "for AvatarUploader" do
+ let(:uploader_class) { AvatarUploader }
+ let(:mounted_as) { :avatar }
+
+ context "for Project" do
+ let(:model_class) { Project }
+ let!(:projects) { create_list(:project, 10, :with_avatar) }
+
+ it_behaves_like 'enqueue jobs in batch', batch: 4
+
+ context 'Upload has store = nil' do
+ before do
+ Upload.where(model: projects).update_all(store: nil)
+ end
+
+ it_behaves_like 'enqueue jobs in batch', batch: 4
+ end
+ end
+
+ context "for Group" do
+ let(:model_class) { Group }
+
+ before do
+ create_list(:group, 10, :with_avatar)
+ end
+
+ it_behaves_like 'enqueue jobs in batch', batch: 4
+ end
+
+ context "for User" do
+ let(:model_class) { User }
+
+ before do
+ create_list(:user, 10, :with_avatar)
+ end
+
+ it_behaves_like 'enqueue jobs in batch', batch: 4
+ end
+ end
+
+ context "for AttachmentUploader" do
+ let(:uploader_class) { AttachmentUploader }
+
+ context "for Note" do
+ let(:model_class) { Note }
+ let(:mounted_as) { :attachment }
+
+ before do
+ create_list(:note, 10, :with_attachment)
+ end
+
+ it_behaves_like 'enqueue jobs in batch', batch: 4
+ end
+
+ context "for Appearance" do
+ let(:model_class) { Appearance }
+ let(:mounted_as) { :logo }
+
+ before do
+ create(:appearance, :with_logos)
+ end
+
+ %i(logo header_logo).each do |mount|
+ it_behaves_like 'enqueue jobs in batch', batch: 1 do
+ let(:mounted_as) { mount }
+ end
+ end
+ end
+ end
+
+ context "for FileUploader" do
+ let(:uploader_class) { FileUploader }
+ let(:model_class) { Project }
+
+ before do
+ create_list(:project, 10) do |model|
+ uploader_class.new(model)
+ .store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
+ end
+ end
+
+ it_behaves_like 'enqueue jobs in batch', batch: 4
+ end
+
+ context "for PersonalFileUploader" do
+ let(:uploader_class) { PersonalFileUploader }
+ let(:model_class) { PersonalSnippet }
+
+ before do
+ create_list(:personal_snippet, 10) do |model|
+ uploader_class.new(model)
+ .store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
+ end
+ end
+
+ it_behaves_like 'enqueue jobs in batch', batch: 4
+ end
+
+ context "for NamespaceFileUploader" do
+ let(:uploader_class) { NamespaceFileUploader }
+ let(:model_class) { Snippet }
+
+ before do
+ create_list(:snippet, 10) do |model|
+ uploader_class.new(model)
+ .store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
+ end
+ end
+
+ it_behaves_like 'enqueue jobs in batch', batch: 4
end
end
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index 1d406c71955..59e02fecbce 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -308,6 +308,30 @@ describe ObjectStorage do
it { is_expected.to eq(remote_directory) }
end
+ context 'when file is in use' do
+ def when_file_is_in_use
+ uploader.use_file do
+ yield
+ end
+ end
+
+ it 'cannot migrate' do
+ when_file_is_in_use do
+ expect(uploader).not_to receive(:unsafe_migrate!)
+
+ expect { uploader.migrate!(described_class::Store::REMOTE) }.to raise_error('exclusive lease already taken')
+ end
+ end
+
+ it 'cannot use_file' do
+ when_file_is_in_use do
+ expect(uploader).not_to receive(:unsafe_use_file)
+
+ expect { uploader.use_file }.to raise_error('exclusive lease already taken')
+ end
+ end
+ end
+
describe '#fog_credentials' do
let(:connection) { Settingslogic.new("provider" => "AWS") }
diff --git a/spec/uploaders/workers/object_storage/background_move_worker_spec.rb b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
new file mode 100644
index 00000000000..b34f427fd8a
--- /dev/null
+++ b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
@@ -0,0 +1,146 @@
+require 'spec_helper'
+
+describe ObjectStorage::BackgroundMoveWorker do
+ let(:local) { ObjectStorage::Store::LOCAL }
+ let(:remote) { ObjectStorage::Store::REMOTE }
+
+ def perform
+ described_class.perform_async(uploader_class.name, subject_class, file_field, subject_id)
+ end
+
+ context 'for LFS' do
+ let!(:lfs_object) { create(:lfs_object, :with_file, file_store: local) }
+ let(:uploader_class) { LfsObjectUploader }
+ let(:subject_class) { LfsObject }
+ let(:file_field) { :file }
+ let(:subject_id) { lfs_object.id }
+
+ context 'when object storage is enabled' do
+ before do
+ stub_lfs_object_storage(background_upload: true)
+ end
+
+ it 'uploads object to storage' do
+ expect { perform }.to change { lfs_object.reload.file_store }.from(local).to(remote)
+ end
+
+ context 'when background upload is disabled' do
+ before do
+ allow(Gitlab.config.lfs.object_store).to receive(:background_upload) { false }
+ end
+
+ it 'is skipped' do
+ expect { perform }.not_to change { lfs_object.reload.file_store }
+ end
+ end
+ end
+
+ context 'when object storage is disabled' do
+ before do
+ stub_lfs_object_storage(enabled: false)
+ end
+
+ it "doesn't migrate files" do
+ perform
+
+ expect(lfs_object.reload.file_store).to eq(local)
+ end
+ end
+ end
+
+ context 'for legacy artifacts' do
+ let(:build) { create(:ci_build, :legacy_artifacts) }
+ let(:uploader_class) { LegacyArtifactUploader }
+ let(:subject_class) { Ci::Build }
+ let(:file_field) { :artifacts_file }
+ let(:subject_id) { build.id }
+
+ context 'when local storage is used' do
+ let(:store) { local }
+
+ context 'and remote storage is defined' do
+ before do
+ stub_artifacts_object_storage(background_upload: true)
+ end
+
+ it "migrates file to remote storage" do
+ perform
+
+ expect(build.reload.artifacts_file_store).to eq(remote)
+ end
+
+ context 'for artifacts_metadata' do
+ let(:file_field) { :artifacts_metadata }
+
+ it 'migrates metadata to remote storage' do
+ perform
+
+ expect(build.reload.artifacts_metadata_store).to eq(remote)
+ end
+ end
+ end
+ end
+ end
+
+ context 'for job artifacts' do
+ let(:artifact) { create(:ci_job_artifact, :archive) }
+ let(:uploader_class) { JobArtifactUploader }
+ let(:subject_class) { Ci::JobArtifact }
+ let(:file_field) { :file }
+ let(:subject_id) { artifact.id }
+
+ context 'when local storage is used' do
+ let(:store) { local }
+
+ context 'and remote storage is defined' do
+ before do
+ stub_artifacts_object_storage(background_upload: true)
+ end
+
+ it "migrates file to remote storage" do
+ perform
+
+ expect(artifact.reload.file_store).to eq(remote)
+ end
+ end
+ end
+ end
+
+ context 'for uploads' do
+ let!(:project) { create(:project, :with_avatar) }
+ let(:uploader_class) { AvatarUploader }
+ let(:file_field) { :avatar }
+
+ context 'when local storage is used' do
+ let(:store) { local }
+
+ context 'and remote storage is defined' do
+ before do
+ stub_uploads_object_storage(uploader_class, background_upload: true)
+ end
+
+ describe 'supports using the model' do
+ let(:subject_class) { project.class }
+ let(:subject_id) { project.id }
+
+ it "migrates file to remote storage" do
+ perform
+
+ expect(project.reload.avatar.file_storage?).to be_falsey
+ end
+ end
+
+ describe 'supports using the Upload' do
+ let(:subject_class) { Upload }
+ let(:subject_id) { project.avatar.upload.id }
+
+ it "migrates file to remote storage" do
+ perform
+
+ expect(project.reload.avatar.file_storage?).to be_falsey
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
new file mode 100644
index 00000000000..7a7dcb71680
--- /dev/null
+++ b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
@@ -0,0 +1,119 @@
+require 'spec_helper'
+
+describe ObjectStorage::MigrateUploadsWorker, :sidekiq do
+ shared_context 'sanity_check! fails' do
+ before do
+ expect(described_class).to receive(:sanity_check!).and_raise(described_class::SanityCheckError)
+ end
+ end
+
+ let!(:projects) { create_list(:project, 10, :with_avatar) }
+ let(:uploads) { Upload.all }
+ let(:model_class) { Project }
+ let(:mounted_as) { :avatar }
+ let(:to_store) { ObjectStorage::Store::REMOTE }
+
+ before do
+ stub_uploads_object_storage(AvatarUploader)
+ end
+
+ describe '.enqueue!' do
+ def enqueue!
+ described_class.enqueue!(uploads, Project, mounted_as, to_store)
+ end
+
+ it 'is guarded by .sanity_check!' do
+ expect(described_class).to receive(:perform_async)
+ expect(described_class).to receive(:sanity_check!)
+
+ enqueue!
+ end
+
+ context 'sanity_check! fails' do
+ include_context 'sanity_check! fails'
+
+ it 'does not enqueue a job' do
+ expect(described_class).not_to receive(:perform_async)
+
+ expect { enqueue! }.to raise_error(described_class::SanityCheckError)
+ end
+ end
+ end
+
+ describe '.sanity_check!' do
+ shared_examples 'raises a SanityCheckError' do
+ let(:mount_point) { nil }
+
+ it do
+ expect { described_class.sanity_check!(uploads, model_class, mount_point) }
+ .to raise_error(described_class::SanityCheckError)
+ end
+ end
+
+ context 'uploader types mismatch' do
+ let!(:outlier) { create(:upload, uploader: 'FileUploader') }
+
+ include_examples 'raises a SanityCheckError'
+ end
+
+ context 'model types mismatch' do
+ let!(:outlier) { create(:upload, model_type: 'Potato') }
+
+ include_examples 'raises a SanityCheckError'
+ end
+
+ context 'mount point not found' do
+ include_examples 'raises a SanityCheckError' do
+ let(:mount_point) { :potato }
+ end
+ end
+ end
+
+ describe '#perform' do
+ def perform
+ described_class.new.perform(uploads.ids, model_class.to_s, mounted_as, to_store)
+ rescue ObjectStorage::MigrateUploadsWorker::Report::MigrationFailures
+ # swallow
+ end
+
+ shared_examples 'outputs correctly' do |success: 0, failures: 0|
+ total = success + failures
+
+ if success > 0
+ it 'outputs the reports' do
+ expect(Rails.logger).to receive(:info).with(%r{Migrated #{success}/#{total} files})
+
+ perform
+ end
+ end
+
+ if failures > 0
+ it 'outputs upload failures' do
+ expect(Rails.logger).to receive(:warn).with(/Error .* I am a teapot/)
+
+ perform
+ end
+ end
+ end
+
+ it_behaves_like 'outputs correctly', success: 10
+
+ it 'migrates files' do
+ perform
+
+ aggregate_failures do
+ projects.each do |project|
+ expect(project.reload.avatar.upload.local?).to be_falsey
+ end
+ end
+ end
+
+ context 'migration is unsuccessful' do
+ before do
+ allow_any_instance_of(ObjectStorage::Concern).to receive(:migrate!).and_raise(CarrierWave::UploadError, "I am a teapot.")
+ end
+
+ it_behaves_like 'outputs correctly', failures: 10
+ end
+ end
+end
diff --git a/spec/workers/project_export_worker_spec.rb b/spec/workers/project_export_worker_spec.rb
new file mode 100644
index 00000000000..8899969c178
--- /dev/null
+++ b/spec/workers/project_export_worker_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe ProjectExportWorker do
+ let!(:user) { create(:user) }
+ let!(:project) { create(:project) }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ context 'when it succeeds' do
+ it 'calls the ExportService' do
+ expect_any_instance_of(::Projects::ImportExport::ExportService).to receive(:execute)
+
+ subject.perform(user.id, project.id, { 'klass' => 'Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy' })
+ end
+ end
+
+ context 'when it fails' do
+ it 'raises an exception when params are invalid' do
+ expect_any_instance_of(::Projects::ImportExport::ExportService).not_to receive(:execute)
+
+ expect { subject.perform(1234, project.id, {}) }.to raise_exception(ActiveRecord::RecordNotFound)
+ expect { subject.perform(user.id, 1234, {}) }.to raise_exception(ActiveRecord::RecordNotFound)
+ expect { subject.perform(user.id, project.id, { 'klass' => 'Whatever' }) }.to raise_exception(Gitlab::ImportExport::AfterExportStrategyBuilder::StrategyNotFoundError)
+ end
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 584951b5da0..af7bda5d562 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3605,7 +3605,7 @@ fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
-fsevents@^1.0.0, fsevents@^1.1.3:
+fsevents@^1.0.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8"
dependencies: