summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml11
-rw-r--r--Gemfile5
-rw-r--r--Gemfile.lock12
-rw-r--r--app/assets/javascripts/api.js146
-rw-r--r--app/assets/javascripts/ide/components/editor_mode_dropdown.vue87
-rw-r--r--app/assets/javascripts/ide/components/ide.vue85
-rw-r--r--app/assets/javascripts/ide/components/mr_file_icon.vue23
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue29
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue24
-rw-r--r--app/assets/javascripts/ide/components/repo_file_status_icon.vue40
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue71
-rw-r--r--app/assets/javascripts/ide/ide_router.js60
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js28
-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.js11
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js73
-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.js18
-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/lib/utils/url_utility.js10
-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/sidebar.scss1
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/assets/stylesheets/pages/repo.scss25
-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/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/models/ci/artifact_blob.rb7
-rw-r--r--app/models/ci/build.rb127
-rw-r--r--app/models/clusters/cluster.rb4
-rw-r--r--app/models/clusters/concerns/application_status.rb2
-rw-r--r--app/models/issue.rb6
-rw-r--r--app/models/project.rb35
-rw-r--r--app/services/boards/list_service.rb6
-rw-r--r--app/services/issues/close_service.rb1
-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/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/groups/show.html.haml2
-rw-r--r--app/views/layouts/devise.html.haml4
-rw-r--r--app/views/layouts/devise_empty.html.haml2
-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/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/settings/ci_cd/show.html.haml8
-rw-r--r--app/views/projects/show.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-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/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-pages-port.yml5
-rw-r--r--changelogs/unreleased/add-milestone-path-to-dashboard-milestones-breadcrumb-link.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-bump-html-pipeline-gem.yml5
-rw-r--r--changelogs/unreleased/feature-gb-variables-expressions-in-only-except.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/sh-cleanup-pages-worker.yml5
-rw-r--r--db/migrate/20180209165249_add_closed_by_to_issues.rb20
-rw-r--r--db/schema.rb2
-rw-r--r--doc/administration/issue_closing_pattern.md4
-rw-r--r--doc/api/commits.md5
-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/ci/examples/browser_performance.md93
-rw-r--r--doc/ci/pipelines.md8
-rw-r--r--doc/ci/quick_start/README.md5
-rw-r--r--doc/ci/runners/README.md19
-rw-r--r--doc/ci/variables/README.md66
-rw-r--r--doc/ci/yaml/README.md30
-rw-r--r--doc/integration/google.md2
-rw-r--r--doc/policy/maintenance.md2
-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/pipelines/settings.md2
-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/steps/project/project.rb2
-rw-r--r--lib/api/entities.rb1
-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/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/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/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/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/gitaly_client/util.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/tasks/gitlab/uploads/migrate.rake5
-rw-r--r--locale/gitlab.pot129
-rw-r--r--package.json3
-rw-r--r--spec/features/admin/admin_settings_spec.rb70
-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/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/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/notes/mock_data.js863
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js199
-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/gitlab/ci/build/policy/variables_spec.rb72
-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/util_spec.rb11
-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/models/ci/artifact_blob_spec.rb13
-rw-r--r--spec/models/ci/build_spec.rb115
-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/project_spec.rb51
-rw-r--r--spec/requests/api/internal_spec.rb49
-rw-r--r--spec/requests/api/project_export_spec.rb79
-rw-r--r--spec/services/issues/close_service_spec.rb4
-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/login_helpers.rb4
-rw-r--r--spec/tasks/gitlab/uploads/migrate_rake_spec.rb129
-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
240 files changed, 6685 insertions, 2523 deletions
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/Gemfile b/Gemfile
index a1e43700260..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 ---
@@ -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
diff --git a/Gemfile.lock b/Gemfile.lock
index bb510f6b539..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)
@@ -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)
@@ -1012,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)
@@ -1083,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)
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/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 015e750525a..5e44af01241 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,51 +1,51 @@
<script>
- import { mapState, mapGetters } from 'vuex';
- import ideSidebar from './ide_side_bar.vue';
- import ideContextbar from './ide_context_bar.vue';
- import repoTabs from './repo_tabs.vue';
- import repoFileButtons from './repo_file_buttons.vue';
- import ideStatusBar from './ide_status_bar.vue';
- import repoEditor from './repo_editor.vue';
+import { mapState, mapGetters } from 'vuex';
+import ideSidebar from './ide_side_bar.vue';
+import ideContextbar from './ide_context_bar.vue';
+import repoTabs from './repo_tabs.vue';
+import repoFileButtons from './repo_file_buttons.vue';
+import ideStatusBar from './ide_status_bar.vue';
+import repoEditor from './repo_editor.vue';
- export default {
- components: {
- ideSidebar,
- ideContextbar,
- repoTabs,
- repoFileButtons,
- ideStatusBar,
- repoEditor,
+export default {
+ components: {
+ ideSidebar,
+ ideContextbar,
+ repoTabs,
+ repoFileButtons,
+ ideStatusBar,
+ repoEditor,
+ },
+ props: {
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
},
- props: {
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- noChangesStateSvgPath: {
- type: String,
- required: true,
- },
- committedStateSvgPath: {
- type: String,
- required: true,
- },
+ noChangesStateSvgPath: {
+ type: String,
+ required: true,
},
- computed: {
- ...mapState(['changedFiles', 'openFiles', 'viewer']),
- ...mapGetters(['activeFile', 'hasChanges']),
+ committedStateSvgPath: {
+ type: String,
+ required: true,
},
- mounted() {
- const returnValue = 'Are you sure you want to lose unsaved changes?';
- window.onbeforeunload = e => {
- if (!this.changedFiles.length) return undefined;
+ },
+ computed: {
+ ...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
+ ...mapGetters(['activeFile', 'hasChanges']),
+ },
+ mounted() {
+ const returnValue = 'Are you sure you want to lose unsaved changes?';
+ window.onbeforeunload = e => {
+ if (!this.changedFiles.length) return undefined;
- Object.assign(e, {
- returnValue,
- });
- return returnValue;
- };
- },
- };
+ Object.assign(e, {
+ returnValue,
+ });
+ return returnValue;
+ };
+ },
+};
</script>
<template>
@@ -63,6 +63,7 @@
: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 e73d1ce839f..b6f8f8a1c99 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,9 +64,14 @@ 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('editor') : Promise.resolve();
+ const viewerPromise = this.delayViewerUpdated
+ ? this.updateViewer('editor')
+ : Promise.resolve();
return viewerPromise;
})
@@ -78,7 +79,7 @@ export default {
this.updateDelayViewerUpdated(false);
this.createEditorInstance();
})
- .catch((err) => {
+ .catch(err => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err;
});
@@ -101,9 +102,13 @@ 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) => {
+ this.model.onChange(model => {
const { file } = model;
if (file.active) {
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 9a7c83ea7b9..fab53592e4e 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);
}
@@ -102,12 +101,17 @@ export default {
:file="file"
/>
</span>
- <changed-file-icon
- v-if="file.changed || file.tempFile || file.staged"
- :file="file"
- :show-tooltip="true"
- class="prepend-top-5 pull-right"
- />
+ <span class="pull-right">
+ <mr-file-icon
+ v-if="file.mrChange"
+ />
+ <changed-file-icon
+ v-if="file.changed || file.tempFile || file.staged"
+ :file="file"
+ :show-tooltip="true"
+ class="prepend-top-5 pull-right"
+ />
+ </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 8ea64ddf84a..a44e418b2eb 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -1,42 +1,46 @@
<script>
- import { mapActions } from 'vuex';
- import RepoTab from './repo_tab.vue';
- import EditorMode from './editor_mode_dropdown.vue';
+import { mapActions } from 'vuex';
+import RepoTab from './repo_tab.vue';
+import EditorMode from './editor_mode_dropdown.vue';
- export default {
- components: {
- RepoTab,
- EditorMode,
+export default {
+ components: {
+ RepoTab,
+ EditorMode,
+ },
+ props: {
+ files: {
+ type: Array,
+ required: true,
},
- props: {
- files: {
- type: Array,
- required: true,
- },
- viewer: {
- type: String,
- required: true,
- },
- hasChanges: {
- type: Boolean,
- required: true,
- },
+ viewer: {
+ type: String,
+ required: true,
},
- data() {
- return {
- showShadow: false,
- };
+ hasChanges: {
+ type: Boolean,
+ required: true,
},
- updated() {
- if (!this.$refs.tabsScroller) return;
-
- this.showShadow =
- this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
- },
- methods: {
- ...mapActions(['updateViewer']),
+ mergeRequestId: {
+ type: String,
+ required: false,
+ default: '',
},
- };
+ },
+ data() {
+ return {
+ showShadow: false,
+ };
+ },
+ updated() {
+ if (!this.$refs.tabsScroller) return;
+
+ this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
+ },
+ methods: {
+ ...mapActions(['updateViewer']),
+ },
+};
</script>
<template>
@@ -55,6 +59,7 @@
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
+ :merge-request-id="mergeRequestId"
@click="updateViewer"
/>
</div>
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index db89c1d44db..be2c12c0487 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,
},
],
@@ -76,9 +76,7 @@ router.beforeEach((to, from, next) => {
.then(() => {
if (to.params[0]) {
const path =
- to.params[0].slice(-1) === '/'
- ? to.params[0].slice(0, -1)
- : to.params[0];
+ to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
const treeEntry = store.state.entries[path];
if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry);
@@ -96,6 +94,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 ef395b276af..f3c052d6903 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.path),
)),
);
+ 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();
@@ -28,10 +37,7 @@ export default class Model {
this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose);
- eventHub.$on(
- `editor.update.model.content.${this.file.path}`,
- this.updateContent,
- );
+ eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
get url() {
@@ -58,6 +64,10 @@ export default class Model {
return this.originalModel;
}
+ getBaseModel() {
+ return this.baseModel;
+ }
+
setValue(value) {
this.getModel().setValue(value);
}
@@ -81,13 +91,7 @@ export default class Model {
this.disposable.dispose();
this.events.clear();
- eventHub.$off(
- `editor.update.model.dispose.${this.file.path}`,
- this.dispose,
- );
- eventHub.$off(
- `editor.update.model.content.${this.file.path}`,
- this.updateContent,
- );
+ eventHub.$off(`editor.update.model.dispose.${this.file.path}`, this.dispose);
+ eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
}
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 880fa9ab8f7..3e512e79bba 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -7,8 +7,7 @@ import FilesDecoratorWorker from './workers/files_decorator_worker';
export const redirectToUrl = (_, url) => visitUrl(url);
-export const setInitialData = ({ commit }, data) =>
- commit(types.SET_INITIAL_DATA, data);
+export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const discardAllChanges = ({ state, commit, dispatch }) => {
state.changedFiles.forEach(file => {
@@ -60,14 +59,11 @@ export const createTempEntry = (
) =>
new Promise(resolve => {
const worker = new FilesDecoratorWorker();
- const fullName =
- name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
+ const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
if (state.entries[name]) {
flash(
- `The name "${name
- .split('/')
- .pop()}" is already taken in this directory.`,
+ `The name "${name.split('/').pop()}" is already taken in this directory.`,
'alert',
document,
null,
@@ -144,3 +140,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 0615f0fe90b..470d168e3e3 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -46,53 +46,63 @@ 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(() => {
commit(types.TOGGLE_LOADING, { entry: file });
- flash(
- 'Error loading file data. Please try again.',
- 'alert',
- document,
- null,
- false,
- true,
- );
+ flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
});
};
-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];
@@ -119,10 +129,7 @@ export const setFileEOL = ({ getters, commit }, { eol }) => {
}
};
-export const setEditorPosition = (
- { getters, commit },
- { editorRow, editorColumn },
-) => {
+export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn }) => {
if (getters.activeFile) {
commit(types.SET_FILE_POSITION, {
file: getters.activeFile,
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 72902146f64..9413698db34 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,13 +21,21 @@ 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 collapseButtonIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
-export const hasChanges = state =>
- !!state.changedFiles.length || !!state.stagedFiles.length;
+export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
// eslint-disable-next-line no-confusing-arrow
export const collapseButtonTooltip = state =>
state.rightPanelCollapsed ? 'Expand sidebar' : 'Collapse sidebar';
+
+export const hasMergeRequest = state => !!state.currentMergeRequestId;
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index 8be9f6a86e4..118b4df33f8 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 2a165f778ae..25ce3e70781 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, {
@@ -88,9 +86,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),
});
}
},
@@ -120,6 +116,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 78cae136476..f4a9354b024 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -28,6 +28,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 }) {
@@ -35,6 +37,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;
@@ -59,6 +66,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 6c09324e4ba..34975ac3144 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: [],
stagedFiles: [],
endpoints: {},
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/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/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 cf8f6202611..8f79ea90cc5 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -19,7 +19,7 @@
.ide-view {
display: flex;
height: calc(100vh - #{$header-height});
- margin-top: 40px;
+ margin-top: 0;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
@@ -53,6 +53,7 @@
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
+ max-width: inherit;
svg {
vertical-align: middle;
@@ -460,6 +461,8 @@
display: flex;
flex-direction: column;
flex: 1;
+ max-height: 100%;
+ overflow: auto;
}
.ide-commity-empty-state {
@@ -474,7 +477,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;
@@ -689,8 +692,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;
}
@@ -700,7 +709,7 @@
}
.content-wrapper {
- margin-top: $header-height;
+ margin-top: 0;
padding-bottom: 0;
}
@@ -724,11 +733,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;
}
@@ -737,10 +746,6 @@
}
&.flash-shown {
- .content-wrapper {
- margin-top: 0;
- }
-
.ide-view {
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/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/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 ed02af05e3d..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)
@@ -25,15 +26,17 @@ module Ci
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
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
+ ##
+ # 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
@@ -212,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?
@@ -258,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
@@ -459,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
@@ -558,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')
@@ -566,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?
@@ -583,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)
@@ -612,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/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/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/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/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/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/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/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/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/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/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/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/_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/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-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-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/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/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-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/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/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/schema.rb b/db/schema.rb
index 9aaefcf1c8d..06fc1a9d7e9 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -932,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
@@ -2095,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/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 a6b96ba539f..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
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/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/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 b91aa334ff3..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
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/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/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/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index 1052c9efa25..6cead7b9961 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -29,7 +29,7 @@ if the job surpasses the threshold, it is marked as failed.
### Timeout overriding on Runner level
-> - [Introduced][ce-17221] in GitLab 10.6.
+> - [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].
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/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/entities.rb b/lib/api/entities.rb
index 38161d1f127..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
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/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/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/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/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/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/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/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/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/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/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/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 ceb0416aff8..73ea7960485 100644
--- a/spec/javascripts/ide/components/repo_tabs_spec.js
+++ b/spec/javascripts/ide/components/repo_tabs_spec.js
@@ -17,6 +17,7 @@ describe('RepoTabs', () => {
files: openedFiles,
viewer: 'editor',
hasChanges: false,
+ hasMergeRequest: false,
});
openedFiles[0].active = true;
@@ -56,6 +57,7 @@ describe('RepoTabs', () => {
files: [],
viewer: 'editor',
hasChanges: false,
+ 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 adc6a93c06b..7cd990adb53 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 5de09f1faae..08d39fe1ca4 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -8,7 +8,7 @@ import eventHub from '~/ide/eventhub';
import { file, resetStore } from '../../helpers';
import testAction from '../../../helpers/vuex_action_helper';
-describe('Multi-file store file actions', () => {
+describe('IDE store file actions', () => {
beforeEach(() => {
spyOn(router, 'push');
});
@@ -192,7 +192,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');
@@ -203,7 +203,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');
@@ -214,7 +214,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');
@@ -225,7 +225,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();
@@ -234,9 +234,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);
@@ -259,7 +270,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);
@@ -270,7 +281,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');
@@ -278,6 +289,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 0c6fca82b1f..8d04b83928c 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(() => {
@@ -44,4 +44,24 @@ describe('Multi-file store getters', () => {
expect(getters.collapseButtonIcon(localState)).toBe('angle-double-left');
});
});
+
+ 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 3023b90e2e4..6e497949862 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;
@@ -65,6 +65,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();
});
});
@@ -79,6 +81,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';
@@ -115,6 +128,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/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/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/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/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/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/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/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_spec.rb b/spec/models/ci/build_spec.rb
index f5534d22a54..a12717835b0 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1463,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 },
@@ -1945,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] }
@@ -1993,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
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/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/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/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/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/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/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/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: