diff options
45 files changed, 705 insertions, 226 deletions
diff --git a/.gitlab/ci/memory.gitlab-ci.yml b/.gitlab/ci/memory.gitlab-ci.yml index 50b843df585..2f3907a331a 100644 --- a/.gitlab/ci/memory.gitlab-ci.yml +++ b/.gitlab/ci/memory.gitlab-ci.yml @@ -5,15 +5,15 @@ memory-static: # Loads each of gems in the Gemfile and checks how much memory they consume when they are required. # 'derailed_benchmarks' internally uses 'get_process_mem' - - scripts/memory-static 'tmp/memory_static_full_report.txt' 'tmp/memory_static_metrics.txt' + - bundle exec derailed bundle:mem > tmp/memory_bundle_mem.txt + - scripts/generate-gems-size-metrics-static tmp/memory_bundle_mem.txt >> 'tmp/memory_metrics.txt' # Outputs detailed information about objects created while gems are loaded. # 'derailed_benchmarks' internally uses 'memory_profiler' - - scripts/memory-static-objects 'tmp/memory_static_objects_full_report.txt' 'tmp/memory_static_metrics.txt' + - bundle exec derailed bundle:objects > tmp/memory_bundle_objects.txt + - scripts/generate-gems-memory-metrics-static tmp/memory_bundle_objects.txt >> 'tmp/memory_metrics.txt' artifacts: paths: - - tmp/memory_static_full_report.txt - - tmp/memory_static_objects_full_report.txt - - tmp/memory_static_metrics.txt + - tmp/memory_*.txt reports: - metrics: tmp/memory_static_metrics.txt + metrics: tmp/memory_metrics.txt diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index 9b764028be9..933af90c85a 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -77,6 +77,7 @@ schedule:review-build-cng: .review-deploy-base: &review-deploy-base <<: *review-base allow_failure: true + retry: 2 stage: review variables: HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}" @@ -95,10 +96,16 @@ schedule:review-build-cng: - install_api_client_dependencies_with_apk - source scripts/review_apps/review-apps.sh script: - - perform_review_app_deployment + - check_kube_domain + - ensure_namespace + - install_tiller + - install_external_dns + - download_chart + - deploy || display_deployment_debug + - wait_for_review_app_to_be_accessible + - add_license artifacts: - paths: - - review_app_url.txt + paths: [review_app_url.txt] expire_in: 2 days when: always @@ -108,8 +115,6 @@ review-deploy: schedule:review-deploy: <<: *review-deploy-base <<: *review-schedules-only - script: - - perform_review_app_deployment review-stop: <<: *review-base @@ -124,11 +129,11 @@ review-stop: script: - source scripts/review_apps/review-apps.sh - delete - - cleanup .review-qa-base: &review-qa-base <<: *review-docker allow_failure: true + retry: 2 stage: qa variables: <<: *review-docker-variables diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 9db5ea12f52..7f3a46a841e 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.48.0 +1.49.0 diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js index d0cc4897aeb..a4394ab7e92 100644 --- a/app/assets/javascripts/commons/polyfills.js +++ b/app/assets/javascripts/commons/polyfills.js @@ -12,6 +12,7 @@ import 'core-js/es/promise/finally'; import 'core-js/es/string/code-point-at'; import 'core-js/es/string/from-code-point'; import 'core-js/es/string/includes'; +import 'core-js/es/string/starts-with'; import 'core-js/es/symbol'; import 'core-js/es/map'; import 'core-js/es/weak-map'; diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 3b73dd83c9f..b308cd9c236 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -318,6 +318,7 @@ class GfmAutoComplete { } setupLabels($input) { + const instance = this; const fetchData = this.fetchData.bind(this); const LABEL_COMMAND = { LABEL: '/label', UNLABEL: '/unlabel', RELABEL: '/relabel' }; let command = ''; @@ -348,7 +349,6 @@ class GfmAutoComplete { })); }, matcher(flag, subtext) { - const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers); const subtextNodes = subtext .split(/\n+/g) .pop() @@ -366,6 +366,27 @@ class GfmAutoComplete { return null; }); + // If any label matches the inserted text after the last `~`, suggest those labels, + // even if any spaces or funky characters were typed. + // This allows matching labels like "Accepting merge requests". + const labels = instance.cachedData[flag]; + if (labels) { + if (!subtext.includes(flag)) { + // Do not match if there is no `~` before the cursor + return null; + } + const lastCandidate = subtext.split(flag).pop(); + if (labels.find(label => label.title.startsWith(lastCandidate))) { + return lastCandidate; + } + } else { + // Load all labels into the autocompleter. + // This needs to happen if e.g. editing a label in an existing comment, because normally + // label data would only be loaded only once you type `~`. + fetchData(this.$inputor, this.at); + } + + const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers); return match && match.length ? match[1] : null; }, filter(query, data, searchKey) { @@ -563,8 +584,9 @@ class GfmAutoComplete { const accentAChar = decodeURI('%C3%80'); const accentYChar = decodeURI('%C3%BF'); + // Holy regex, batman! const regexp = new RegExp( - `^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, + `^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-:]|[^\\x00-\\x7a])*)$`, 'gi', ); diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index 5429b834708..48aabaf9dcf 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -208,10 +208,6 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => { commit(types.DELETE_ENTRY, path); - if (entry.parentPath && state.entries[entry.parentPath].tree.length === 0) { - dispatch('deleteEntry', entry.parentPath); - } - dispatch('triggerFilesChange'); }; diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index db7fd8524c2..f0256ff4d41 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Runner < ApplicationRecord - VERSION = '0.5.2'.freeze + VERSION = '0.6.0'.freeze self.table_name = 'clusters_applications_runners' diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml index e7edb93f05b..5b657966909 100644 --- a/app/views/projects/pages_domains/_form.html.haml +++ b/app/views/projects/pages_domains/_form.html.haml @@ -11,7 +11,7 @@ - if Gitlab.config.pages.external_https - - auto_ssl_available = ::Gitlab::LetsEncrypt::Client.new.enabled? + - auto_ssl_available = ::Gitlab::LetsEncrypt.enabled?(@domain) - auto_ssl_enabled = @domain.auto_ssl_enabled? - auto_ssl_available_and_enabled = auto_ssl_available && auto_ssl_enabled diff --git a/app/workers/pages_domain_ssl_renewal_cron_worker.rb b/app/workers/pages_domain_ssl_renewal_cron_worker.rb index 4ca9db922b4..40c34d29970 100644 --- a/app/workers/pages_domain_ssl_renewal_cron_worker.rb +++ b/app/workers/pages_domain_ssl_renewal_cron_worker.rb @@ -5,9 +5,9 @@ class PagesDomainSslRenewalCronWorker include CronjobQueue def perform - return unless ::Gitlab::LetsEncrypt::Client.new.enabled? - PagesDomain.need_auto_ssl_renewal.find_each do |domain| + next unless ::Gitlab::LetsEncrypt.enabled?(domain) + PagesDomainSslRenewalWorker.perform_async(domain.id) end end diff --git a/app/workers/pages_domain_ssl_renewal_worker.rb b/app/workers/pages_domain_ssl_renewal_worker.rb index 00c9c4782d8..b32458ca777 100644 --- a/app/workers/pages_domain_ssl_renewal_worker.rb +++ b/app/workers/pages_domain_ssl_renewal_worker.rb @@ -4,11 +4,9 @@ class PagesDomainSslRenewalWorker include ApplicationWorker def perform(domain_id) - return unless ::Gitlab::LetsEncrypt::Client.new.enabled? - domain = PagesDomain.find_by_id(domain_id) - - return unless domain + return unless domain&.enabled? + return unless ::Gitlab::LetsEncrypt.enabled?(domain) ::PagesDomains::ObtainLetsEncryptCertificateService.new(domain).execute end diff --git a/changelogs/unreleased/60860-keep-empty-folders-in-tree.yml b/changelogs/unreleased/60860-keep-empty-folders-in-tree.yml new file mode 100644 index 00000000000..237d0fd6aef --- /dev/null +++ b/changelogs/unreleased/60860-keep-empty-folders-in-tree.yml @@ -0,0 +1,5 @@ +--- +title: Keep the empty folders in the tree +merge_request: 29196 +author: +type: fixed diff --git a/changelogs/unreleased/gitaly-version-v1.49.0.yml b/changelogs/unreleased/gitaly-version-v1.49.0.yml new file mode 100644 index 00000000000..8795bab0209 --- /dev/null +++ b/changelogs/unreleased/gitaly-version-v1.49.0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade to Gitaly v1.49.0 +merge_request: 29990 +author: +type: changed diff --git a/changelogs/unreleased/mh-colon-autocomplete.yml b/changelogs/unreleased/mh-colon-autocomplete.yml new file mode 100644 index 00000000000..8b169c22588 --- /dev/null +++ b/changelogs/unreleased/mh-colon-autocomplete.yml @@ -0,0 +1,5 @@ +--- +title: Allow auto-completing scoped labels +merge_request: 29749 +author: +type: added diff --git a/changelogs/unreleased/sh-quiet-backup-secrets-log.yml b/changelogs/unreleased/sh-quiet-backup-secrets-log.yml new file mode 100644 index 00000000000..cf3e90c0cb1 --- /dev/null +++ b/changelogs/unreleased/sh-quiet-backup-secrets-log.yml @@ -0,0 +1,5 @@ +--- +title: Silence backup warnings when CRON=1 in use +merge_request: 30033 +author: +type: fixed diff --git a/changelogs/unreleased/sh-recover-ee-schema-backport-migration-failure.yml b/changelogs/unreleased/sh-recover-ee-schema-backport-migration-failure.yml new file mode 100644 index 00000000000..1695e2827eb --- /dev/null +++ b/changelogs/unreleased/sh-recover-ee-schema-backport-migration-failure.yml @@ -0,0 +1,5 @@ +--- +title: Prevent EE backport migrations from running if CE is not migrated +merge_request: 30002 +author: +type: fixed diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-6-0.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-6-0.yml new file mode 100644 index 00000000000..6719fa94b19 --- /dev/null +++ b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-6-0.yml @@ -0,0 +1,5 @@ +--- +title: Update GitLab Runner Helm Chart to 0.6.0 +merge_request: 29982 +author: +type: other diff --git a/config/initializers/forbid_sidekiq_in_transactions.rb b/config/initializers/forbid_sidekiq_in_transactions.rb index deb94d7dbce..a69f1ba090e 100644 --- a/config/initializers/forbid_sidekiq_in_transactions.rb +++ b/config/initializers/forbid_sidekiq_in_transactions.rb @@ -17,7 +17,7 @@ module Sidekiq module NoEnqueueingFromTransactions %i(perform_async perform_at perform_in).each do |name| define_method(name) do |*args| - if !Sidekiq::Worker.skip_transaction_check && AfterCommitQueue.inside_transaction? + if !Sidekiq::Worker.skip_transaction_check && Gitlab::Database.inside_transaction? begin raise Sidekiq::Worker::EnqueueFromTransactionError, <<~MSG `#{self}.#{name}` cannot be called inside a transaction as this can lead to diff --git a/db/migrate/20190402150158_backport_enterprise_schema.rb b/db/migrate/20190402150158_backport_enterprise_schema.rb index 610a8808383..8762cc53ed7 100644 --- a/db/migrate/20190402150158_backport_enterprise_schema.rb +++ b/db/migrate/20190402150158_backport_enterprise_schema.rb @@ -117,6 +117,8 @@ class BackportEnterpriseSchema < ActiveRecord::Migration[5.0] end def up + check_schema! + create_missing_tables update_appearances @@ -868,6 +870,52 @@ class BackportEnterpriseSchema < ActiveRecord::Migration[5.0] remove_column_if_exists(:geo_nodes, :internal_url) end + # Some users may have upgraded to EE at some point but downgraded to + # CE v11.11.3. As a result, their EE tables may not be in the right + # state. Here we check for these such cases and attempt to guide the + # user into recovering from this state by upgrading to v11.11.3 EE + # before installing v12.0.0 CE. + def check_schema! + # The following cases will fail later when this migration attempts + # to add a foreign key for non-existent columns. + columns_to_check = [ + [:epics, :parent_id], # Added in GitLab 11.7 + [:geo_event_log, :cache_invalidation_event_id], # Added in GitLab 11.4 + [:vulnerability_feedback, :merge_request_id] # Added in GitLab 11.9 + ].freeze + + columns_to_check.each do |table, column| + check_ee_columns!(table, column) + end + end + + def check_ee_columns!(table, column) + return unless table_exists?(table) + return if column_exists?(table, column) + + raise_ee_migration_error!(table, column) + end + + def raise_ee_migration_error!(table, column) + message = "Your database is missing the '#{column}' column from the '#{table}' table that is present for GitLab EE." + + message += + if ::Gitlab.ee? + "\nUpgrade your GitLab instance to 11.11.3 EE first!" + else + <<~MSG + + Even though it looks like you're running a CE installation, it appears + you may have installed GitLab EE at some point. To migrate to GitLab 12.0: + + 1. Install GitLab 11.11.3 EE + 2. Install GitLab 12.0.x CE + MSG + end + + raise Exception.new(message) + end + def create_missing_tables create_table_if_not_exists "approval_merge_request_rule_sources", id: :bigserial do |t| t.bigint "approval_merge_request_rule_id", null: false diff --git a/db/migrate/20190513174947_enable_create_incident_issues_by_default.rb b/db/migrate/20190513174947_enable_create_incident_issues_by_default.rb new file mode 100644 index 00000000000..ecd466627fe --- /dev/null +++ b/db/migrate/20190513174947_enable_create_incident_issues_by_default.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class EnableCreateIncidentIssuesByDefault < ActiveRecord::Migration[5.1] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + change_default_for :create_issue, from: false, to: true + change_default_for :send_email, from: true, to: false + end + + private + + def change_default_for(column, from:, to:) + change_column_default :project_incident_management_settings, + column, from: from, to: to + end +end diff --git a/db/schema.rb b/db/schema.rb index 44213fddd86..a94e5142627 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -2503,9 +2503,9 @@ ActiveRecord::Schema.define(version: 20190620112608) do t.index ["project_id"], name: "index_project_import_data_on_project_id", using: :btree end - create_table "project_incident_management_settings", primary_key: "project_id", id: :integer, default: nil, force: :cascade do |t| - t.boolean "create_issue", default: false, null: false - t.boolean "send_email", default: true, null: false + create_table "project_incident_management_settings", primary_key: "project_id", id: :serial, force: :cascade do |t| + t.boolean "create_issue", default: true, null: false + t.boolean "send_email", default: false, null: false t.text "issue_template_key" end diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index 4c9d1684c00..fc9b175bc8a 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -28,8 +28,13 @@ we need to solve before being able to use Jest for all our needs. - Jest runs in a Node.js environment, not in a browser. Support for running Jest tests in a browser [is planned](https://gitlab.com/gitlab-org/gitlab-ce/issues/58205). - Because Jest runs in a Node.js environment, it uses [jsdom](https://github.com/jsdom/jsdom) by default. +- Jest does not have access to Webpack loaders or aliases. + The aliases used by Jest are defined in its [own config](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/jest.config.js). - All calls to `setTimeout` and `setInterval` are mocked away. See also [Jest Timer Mocks](https://jestjs.io/docs/en/timer-mocks). - `rewire` is not required because Jest supports mocking modules. See also [Manual Mocks](https://jestjs.io/docs/en/manual-mocks). +- No [context object](https://jasmine.github.io/tutorials/your_first_suite#section-The_%3Ccode%3Ethis%3C/code%3E_keyword) is passed to tests in Jest. + This means sharing `this.something` between `beforeEach()` and `it()` for example does not work. + Instead you should declare shared variables in the context that they are needed (via `const` / `let`). - The following will cause tests to fail in Jest: - Unmocked requests. - Unhandled Promise rejections. diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index dc86e66cb4f..ea8b96eb24d 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -46,13 +46,16 @@ this is enabled by default. The following languages and dependency managers are supported. -| Language (package managers) | Scan tool | -|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------| -| JavaScript ([npm](https://www.npmjs.com/), [yarn](https://yarnpkg.com/en/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general), [Retire.js](https://retirejs.github.io/retire.js) | -| Python ([pip](https://pip.pypa.io/en/stable/)) (only `requirements.txt` supported) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general) | -| Ruby ([gem](https://rubygems.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general), [bundler-audit](https://github.com/rubysec/bundler-audit) | -| Java ([Maven](https://maven.apache.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general) | -| PHP ([Composer](https://getcomposer.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general) | +| Language (package managers) | Supported | Scan tool(s) | +|----------------------------- | --------- | ------------ | +| JavaScript ([npm](https://www.npmjs.com/), [yarn](https://yarnpkg.com/en/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [Retire.js](https://retirejs.github.io/retire.js) | +| Python ([pip](https://pip.pypa.io/en/stable/)) (only `requirements.txt` supported) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) | +| Ruby ([gem](https://rubygems.org/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [bundler-audit](https://github.com/rubysec/bundler-audit) | +| Java ([Maven](https://maven.apache.org/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) | +| PHP ([Composer](https://getcomposer.org/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) | +| Python ([poetry](https://poetry.eustace.io/)) | no ([issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/7006 "Support Poetry in Dependency Scanning")) | not available | +| Python ([Pipfile](https://docs.pipenv.org/en/latest/basics/)) | no ([issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/11756 "Pipfile.lock support for Dependency Scanning"))| not available | +| Go ([Golang](https://golang.org/)) | no ([issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/7132 "Dependency Scanning for Go")) | not available | ## Remote checks diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index 751e8e44e60..aab7131e353 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -160,7 +160,7 @@ receivers: > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/4925) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.11. -Alerts can be used to trigger actions, like open an issue automatically. To configure the actions: +Alerts can be used to trigger actions, like open an issue automatically (enabled by default since `12.1`). To configure the actions: 1. Navigate to your project's **Settings > Operations > Incidents**. 1. Enable the option to create issues. diff --git a/lib/after_commit_queue.rb b/lib/after_commit_queue.rb index 6fb7985f955..6a180fdf338 100644 --- a/lib/after_commit_queue.rb +++ b/lib/after_commit_queue.rb @@ -15,7 +15,7 @@ module AfterCommitQueue end def run_after_commit_or_now(&block) - if AfterCommitQueue.inside_transaction? + if Gitlab::Database.inside_transaction? if ActiveRecord::Base.connection.current_transaction.records.include?(self) run_after_commit(&block) else @@ -32,18 +32,6 @@ module AfterCommitQueue true end - def self.open_transactions_baseline - if ::Rails.env.test? - return DatabaseCleaner.connections.count { |conn| conn.strategy.is_a?(DatabaseCleaner::ActiveRecord::Transaction) } - end - - 0 - end - - def self.inside_transaction? - ActiveRecord::Base.connection.open_transactions > open_transactions_baseline - end - protected def _run_after_commit_queue diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 8da98cc3909..e4d4779ba9a 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -234,6 +234,7 @@ module Gitlab def self.connection ActiveRecord::Base.connection end + private_class_method :connection def self.cached_column_exists?(table_name, column_name) connection.schema_cache.columns_hash(table_name).has_key?(column_name.to_s) @@ -243,8 +244,6 @@ module Gitlab connection.schema_cache.data_source_exists?(table_name) end - private_class_method :connection - def self.database_version row = connection.execute("SELECT VERSION()").first @@ -272,5 +271,20 @@ module Gitlab end end end + + # inside_transaction? will return true if the caller is running within a transaction. Handles special cases + # when running inside a test environment, in which the entire test is running with a DatabaseCleaner transaction + def self.inside_transaction? + ActiveRecord::Base.connection.open_transactions > open_transactions_baseline + end + + def self.open_transactions_baseline + if ::Rails.env.test? + return DatabaseCleaner.connections.count { |conn| conn.strategy.is_a?(DatabaseCleaner::ActiveRecord::Transaction) } + end + + 0 + end + private_class_method :open_transactions_baseline end end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index a6739f12280..19b6aab1c4f 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -15,11 +15,6 @@ module Gitlab SEARCH_CONTEXT_LINES = 3 REV_LIST_COMMIT_LIMIT = 2_000 - # In https://gitlab.com/gitlab-org/gitaly/merge_requests/698 - # We copied these two prefixes into gitaly-go, so don't change these - # or things will break! (REBASE_WORKTREE_PREFIX and SQUASH_WORKTREE_PREFIX) - REBASE_WORKTREE_PREFIX = 'rebase'.freeze - SQUASH_WORKTREE_PREFIX = 'squash'.freeze GITALY_INTERNAL_URL = 'ssh://gitaly/internal.git'.freeze GITLAB_PROJECTS_TIMEOUT = Gitlab.config.gitlab_shell.git_timeout EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000'.freeze diff --git a/lib/gitlab/lets_encrypt.rb b/lib/gitlab/lets_encrypt.rb new file mode 100644 index 00000000000..cdf24f24647 --- /dev/null +++ b/lib/gitlab/lets_encrypt.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Gitlab + module LetsEncrypt + def self.enabled?(pages_domain = nil) + return false unless Gitlab::CurrentSettings.lets_encrypt_terms_of_service_accepted + + return false unless Feature.enabled?(:pages_auto_ssl) + + # If no domain is passed, just check whether we're enabled globally + return true unless pages_domain + + !!pages_domain.project && Feature.enabled?(:pages_auto_ssl_for_project, pages_domain.project) + end + end +end diff --git a/lib/gitlab/lets_encrypt/client.rb b/lib/gitlab/lets_encrypt/client.rb index 66aea137012..ad2921ed555 100644 --- a/lib/gitlab/lets_encrypt/client.rb +++ b/lib/gitlab/lets_encrypt/client.rb @@ -34,14 +34,6 @@ module Gitlab acme_client.terms_of_service end - def enabled? - return false unless Feature.enabled?(:pages_auto_ssl) - - return false unless private_key - - Gitlab::CurrentSettings.lets_encrypt_terms_of_service_accepted - end - private def acme_client @@ -65,7 +57,7 @@ module Gitlab end def ensure_account - raise 'Acme integration is disabled' unless enabled? + raise 'Acme integration is disabled' unless ::Gitlab::LetsEncrypt.enabled? @acme_account ||= acme_client.new_account(contact: contact, terms_of_service_agreed: true) end diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index c531eb1d216..2bf71701b57 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -21,10 +21,10 @@ namespace :gitlab do backup.cleanup backup.remove_old - puts "Warning: Your gitlab.rb and gitlab-secrets.json files contain sensitive data \n" \ + progress.puts "Warning: Your gitlab.rb and gitlab-secrets.json files contain sensitive data \n" \ "and are not included in this backup. You will need these files to restore a backup.\n" \ "Please back them up manually.".color(:red) - puts "Backup task is done." + progress.puts "Backup task is done." end # Restore backup of GitLab system diff --git a/scripts/generate-gems-memory-metrics-static b/scripts/generate-gems-memory-metrics-static new file mode 100755 index 00000000000..aa7ce3615bf --- /dev/null +++ b/scripts/generate-gems-memory-metrics-static @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +abort "usage: #{__FILE__} <memory_bundle_objects_file_name>" unless ARGV.length == 1 +memory_bundle_objects_file_name = ARGV.first + +full_report = File.readlines(memory_bundle_objects_file_name) + +allocated_str = full_report[1] +retained_str = full_report[2] +allocated_stats = /Total allocated: (?<bytes>.*) bytes \((?<objects>.*) objects\)/.match(allocated_str) +retained_stats = /Total retained: (?<bytes>.*) bytes \((?<objects>.*) objects\)/.match(retained_str) + +abort 'failed to process the benchmark output' unless allocated_stats && retained_stats + +puts "memory_static_objects_allocated_mb #{(allocated_stats[:bytes].to_f / (1024 * 1024)).round(1)}" +puts "memory_static_objects_retained_mb #{(retained_stats[:bytes].to_f / (1024 * 1024)).round(1)}" +puts "memory_static_objects_allocated_items #{allocated_stats[:objects]}" +puts "memory_static_objects_retained_items #{retained_stats[:objects]}" diff --git a/scripts/generate-gems-size-metrics-static b/scripts/generate-gems-size-metrics-static new file mode 100755 index 00000000000..ceec8aaccf1 --- /dev/null +++ b/scripts/generate-gems-size-metrics-static @@ -0,0 +1,30 @@ +#!/usr/bin/env ruby + +abort "usage: #{__FILE__} <memory_bundle_mem_file_name>" unless ARGV.length == 1 +memory_bundle_mem_file_name = ARGV.first + +full_report = File.readlines(memory_bundle_mem_file_name) + +def total_size(memory_bundle_mem_report) + stats = /TOP: (?<total_mibs_str>.*) MiB/.match(memory_bundle_mem_report.first) + abort 'failed to process the benchmark output' unless stats + "gem_total_size_mb #{stats[:total_mibs_str].to_f.round(1)}" +end + +TOP_LEVEL_GEM_LOG_FORMAT = /^ (?<gem_name>\S.*):\s*(?<gem_size>\d[.\d]*)\s*MiB/.freeze +def all_gems(memory_bundle_mem_report) + memory_bundle_mem_report.map do |line| + TOP_LEVEL_GEM_LOG_FORMAT.match(line) + end.compact +end + +def gems_as_metrics(gems_match_data) + gems_match_data.map do |gem| + gem_name = gem[:gem_name] + gem_size_mb = gem[:gem_size].to_f.round(1) + "gem_size_mb{name=\"#{gem_name}\"} #{gem_size_mb}" + end +end + +puts total_size(full_report) +puts gems_as_metrics(all_gems(full_report)).sort(&:casecmp) diff --git a/scripts/memory-static b/scripts/memory-static deleted file mode 100755 index 54f147a7a91..00000000000 --- a/scripts/memory-static +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env ruby - -require_relative '../lib/gitlab/popen' - -full_report_filename, metrics_filename = ARGV -abort 'usage: memory-static <full_report_filename> <metrics_filename>' unless full_report_filename && metrics_filename - -full_report, status = Gitlab::Popen.popen(%w(bundle exec derailed bundle:mem)) -abort 'failed to execute the benchmark' unless status.zero? - -File.open(full_report_filename, 'w') do |f| - f.write(full_report) -end - -stats = /TOP: (?<total_mibs_str>.*) MiB/.match(full_report.lines.first) -abort 'failed to process the benchmark output' unless stats - -File.open(metrics_filename, 'a') do |f| - f.puts "memory_static_total_mb #{stats[:total_mibs_str].to_f.round(1)}" -end diff --git a/scripts/memory-static-objects b/scripts/memory-static-objects deleted file mode 100755 index 2ad38d9717c..00000000000 --- a/scripts/memory-static-objects +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env ruby - -require_relative '../lib/gitlab/popen' - -full_report_filename, metrics_filename = ARGV -abort 'usage: memory-static-objects <full_report_filename> <metrics_filename>' unless full_report_filename && metrics_filename - -full_report, status = Gitlab::Popen.popen(%w(bundle exec derailed bundle:objects)) -abort 'failed to execute the benchmark' unless status.zero? - -File.open(full_report_filename, 'w') do |f| - f.write(full_report) -end - -allocated_str = full_report.lines[1] -retained_str = full_report.lines[2] -allocated_stats = /Total allocated: (?<bytes>.*) bytes \((?<objects>.*) objects\)/.match(allocated_str) -retained_stats = /Total retained: (?<bytes>.*) bytes \((?<objects>.*) objects\)/.match(retained_str) - -abort 'failed to process the benchmark output' unless allocated_stats && retained_stats - -File.open(metrics_filename, 'a') do |f| - f.puts "memory_static_objects_allocated_mb #{(allocated_stats[:bytes].to_f / (1024 * 1024)).round(1)}" - f.puts "memory_static_objects_retained_mb #{(retained_stats[:bytes].to_f / (104 * 1024)).round(1)}" - f.puts "memory_static_objects_allocated_items #{allocated_stats[:objects]}" - f.puts "memory_static_objects_retained_items #{retained_stats[:objects]}" -end diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index 3bae2e08a6f..633ea28e96c 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -1,7 +1,7 @@ [[ "$TRACE" ]] && set -x export TILLER_NAMESPACE="$KUBE_NAMESPACE" -function deployExists() { +function deploy_exists() { local namespace="${1}" local deploy="${2}" echoinfo "Checking if ${deploy} exists in the ${namespace} namespace..." true @@ -13,8 +13,7 @@ function deployExists() { return $deploy_exists } -function previousDeployFailed() { - set +e +function previous_deploy_failed() { local deploy="${1}" echoinfo "Checking for previous deployment of ${deploy}" true @@ -34,7 +33,6 @@ function previousDeployFailed() { else echoerr "Previous deployment NOT found." fi - set -e return $status } @@ -51,49 +49,35 @@ function delete() { helm delete --purge "$name" } -function cleanup() { - if [ -z "$CI_ENVIRONMENT_SLUG" ]; then - echoerr "No release given, aborting the delete!" - return - fi - - echoinfo "Cleaning up '$CI_ENVIRONMENT_SLUG'..." true - - kubectl -n "$KUBE_NAMESPACE" delete \ - ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa \ - --now --ignore-not-found --include-uninitialized \ - -l release="$CI_ENVIRONMENT_SLUG" -} - function get_pod() { local app_name="${1}" local status="${2-Running}" get_pod_cmd="kubectl get pods -n ${KUBE_NAMESPACE} --field-selector=status.phase=${status} -lapp=${app_name},release=${CI_ENVIRONMENT_SLUG} --no-headers -o=custom-columns=NAME:.metadata.name" - echoinfo "Running '${get_pod_cmd}'" true + echoinfo "Waiting till '${app_name}' pod is ready" true + echoinfo "Running '${get_pod_cmd}'" + local interval=5 + local elapsed_seconds=0 + local max_seconds=$((2 * 60)) while true; do local pod_name pod_name="$(eval "${get_pod_cmd}")" [[ "${pod_name}" == "" ]] || break - echoinfo "Waiting till '${app_name}' pod is ready"; - sleep 5; + if [[ "${elapsed_seconds}" -gt "${max_seconds}" ]]; then + echoerr "The pod name couldn't be found after ${elapsed_seconds} seconds, aborting." + break + fi + + printf "." + let "elapsed_seconds+=interval" + sleep ${interval} done echoinfo "The pod name is '${pod_name}'." echo "${pod_name}" } -function perform_review_app_deployment() { - check_kube_domain - ensure_namespace - install_tiller - install_external_dns - time deploy - wait_for_review_app_to_be_accessible - add_license -} - function check_kube_domain() { echoinfo "Checking that Kube domain exists..." true @@ -119,9 +103,16 @@ function install_tiller() { echoinfo "Initiating the Helm client..." helm init --client-only + # Set toleration for Tiller to be installed on a specific node pool helm init \ + --wait \ --upgrade \ - --replicas 2 + --node-selectors "app=helm" \ + --replicas 3 \ + --override "spec.template.spec.tolerations[0].key"="dedicated" \ + --override "spec.template.spec.tolerations[0].operator"="Equal" \ + --override "spec.template.spec.tolerations[0].value"="helm" \ + --override "spec.template.spec.tolerations[0].effect"="NoSchedule" kubectl rollout status -n "$TILLER_NAMESPACE" -w "deployment/tiller-deploy" @@ -137,7 +128,7 @@ function install_external_dns() { domain=$(echo "${REVIEW_APPS_DOMAIN}" | awk -F. '{printf "%s.%s", $(NF-1), $NF}') echoinfo "Installing external DNS for domain ${domain}..." true - if ! deployExists "${KUBE_NAMESPACE}" "${release_name}" || previousDeployFailed "${release_name}" ; then + if ! deploy_exists "${KUBE_NAMESPACE}" "${release_name}" || previous_deploy_failed "${release_name}" ; then echoinfo "Installing external-dns Helm chart" helm repo update helm install stable/external-dns \ @@ -156,7 +147,7 @@ function install_external_dns() { fi } -function create_secret() { +function create_application_secret() { echoinfo "Creating the ${CI_ENVIRONMENT_SLUG}-gitlab-initial-root-password secret in the ${KUBE_NAMESPACE} namespace..." true kubectl create secret generic -n "$KUBE_NAMESPACE" \ @@ -165,7 +156,7 @@ function create_secret() { --dry-run -o json | kubectl apply -f - } -function download_gitlab_chart() { +function download_chart() { echoinfo "Downloading the GitLab chart..." true curl -o gitlab.tar.bz2 "https://gitlab.com/charts/gitlab/-/archive/${GITLAB_HELM_CHART_REF}/gitlab-${GITLAB_HELM_CHART_REF}.tar.bz2" @@ -194,14 +185,12 @@ function deploy() { gitlab_workhorse_image_repository="${IMAGE_REPOSITORY}/gitlab-workhorse-${IMAGE_VERSION}" # Cleanup and previous installs, as FAILED and PENDING_UPGRADE will cause errors with `upgrade` - if [ "$CI_ENVIRONMENT_SLUG" != "production" ] && previousDeployFailed "$CI_ENVIRONMENT_SLUG" ; then + if [ "$CI_ENVIRONMENT_SLUG" != "production" ] && previous_deploy_failed "$CI_ENVIRONMENT_SLUG" ; then echo "Deployment in bad state, cleaning up $CI_ENVIRONMENT_SLUG" delete - cleanup fi - create_secret - download_gitlab_chart + create_application_secret HELM_CMD=$(cat << EOF helm upgrade --install \ @@ -216,7 +205,7 @@ HELM_CMD=$(cat << EOF --set prometheus.install=false \ --set global.ingress.configureCertmanager=false \ --set global.ingress.tls.secretName=tls-cert \ - --set global.ingress.annotations."external-dns\.alpha\.kubernetes\.io/ttl"="10" + --set global.ingress.annotations."external-dns\.alpha\.kubernetes\.io/ttl"="10" \ --set nginx-ingress.controller.service.enableHttp=false \ --set nginx-ingress.defaultBackend.resources.requests.memory=7Mi \ --set nginx-ingress.controller.resources.requests.memory=440M \ @@ -252,14 +241,35 @@ EOF echoinfo "Deploying with:" echoinfo "${HELM_CMD}" - eval $HELM_CMD || true + eval "${HELM_CMD}" +} + +function display_deployment_debug() { + migrations_pod=$(get_pod "migrations"); + if [ -z "${migrations_pod}" ]; then + echoerr "Migrations pod not found." + else + echoinfo "Logs tail of the ${migrations_pod} pod..." + + kubectl logs -n "$KUBE_NAMESPACE" "${migrations_pod}" | sed "s/${REVIEW_APPS_ROOT_PASSWORD}/[REDACTED]/g" + fi + + unicorn_pod=$(get_pod "unicorn"); + if [ -z "${unicorn_pod}" ]; then + echoerr "Unicorn pod not found." + else + echoinfo "Logs tail of the ${unicorn_pod} pod..." + + kubectl logs -n "$KUBE_NAMESPACE" -c unicorn "${unicorn_pod}" | sed "s/${REVIEW_APPS_ROOT_PASSWORD}/[REDACTED]/g" + fi } function wait_for_review_app_to_be_accessible() { - # In case the Review App isn't completely available yet. Keep trying for 5 minutes. + echoinfo "Waiting for the Review App at ${CI_ENVIRONMENT_URL} to be accessible..." true + local interval=5 local elapsed_seconds=0 - local max_seconds=$((5 * 60)) + local max_seconds=$((2 * 60)) while true; do local review_app_http_code review_app_http_code=$(curl --silent --output /dev/null --max-time 5 --write-out "%{http_code}" "${CI_ENVIRONMENT_URL}/users/sign_in") @@ -272,10 +282,10 @@ function wait_for_review_app_to_be_accessible() { sleep ${interval} done - if [[ "${review_app_http_code}" == "200" ]]; then - echoinfo "The Review App at ${CI_ENVIRONMENT_URL} is ready!" + if [[ "${review_app_http_code}" -eq "200" ]]; then + echoinfo "The Review App at ${CI_ENVIRONMENT_URL} is ready after ${elapsed_seconds} seconds!" else - echoerr "The Review App at ${CI_ENVIRONMENT_URL} isn't ready after 5 minutes of polling..." + echoerr "The Review App at ${CI_ENVIRONMENT_URL} isn't ready after ${max_seconds} seconds of polling..." exit 1 fi } diff --git a/scripts/trigger-build b/scripts/trigger-build index 52bc61cac56..4d8110fce10 100755 --- a/scripts/trigger-build +++ b/scripts/trigger-build @@ -122,7 +122,14 @@ module Trigger end def ref - ENV['CNG_BRANCH'] || 'master' + default_ref = + if ENV['CI_COMMIT_REF_NAME'] =~ /^[\d-]+-stable(-ee)?$/ + ENV['CI_COMMIT_REF_NAME'] + else + 'master' + end + + ENV['CNG_BRANCH'] || default_ref end def trigger_token diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 8eb413bdd8d..40845ec48f9 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -3,14 +3,14 @@ require 'rails_helper' describe 'GFM autocomplete', :js do let(:issue_xss_title) { 'This will execute alert<img src=x onerror=alert(2)<img src=x onerror=alert(1)>' } let(:user_xss_title) { 'eve <img src=x onerror=alert(2)<img src=x onerror=alert(1)>' } - let(:label_xss_title) { 'alert label <img src=x onerror="alert(\'Hello xss\');" a'} + let(:label_xss_title) { 'alert label <img src=x onerror="alert(\'Hello xss\');" a' } let(:milestone_xss_title) { 'alert milestone <img src=x onerror="alert(\'Hello xss\');" a' } let(:user_xss) { create(:user, name: user_xss_title, username: 'xss.user') } - let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') } + let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') } let(:project) { create(:project) } let(:label) { create(:label, project: project, title: 'special+') } - let(:issue) { create(:issue, project: project) } + let(:issue) { create(:issue, project: project) } before do project.add_maintainer(user) @@ -293,6 +293,70 @@ describe 'GFM autocomplete', :js do expect(find('.atwho-view-ul').text).to have_content('alert label') end end + + it 'allows colons when autocompleting scoped labels' do + create(:label, project: project, title: 'scoped:label') + + note = find('#note-body') + type(note, '~scoped:') + + wait_for_requests + + page.within '.atwho-container #at-view-labels' do + expect(find('.atwho-view-ul').text).to have_content('scoped:label') + end + end + + it 'allows colons when autocompleting scoped labels with double colons' do + create(:label, project: project, title: 'scoped::label') + + note = find('#note-body') + type(note, '~scoped::') + + wait_for_requests + + page.within '.atwho-container #at-view-labels' do + expect(find('.atwho-view-ul').text).to have_content('scoped::label') + end + end + + it 'allows spaces when autocompleting multi-word labels' do + create(:label, project: project, title: 'Accepting merge requests') + + note = find('#note-body') + type(note, '~Accepting merge') + + wait_for_requests + + page.within '.atwho-container #at-view-labels' do + expect(find('.atwho-view-ul').text).to have_content('Accepting merge requests') + end + end + + it 'only autocompletes the latest label' do + create(:label, project: project, title: 'Accepting merge requests') + create(:label, project: project, title: 'Accepting job applicants') + + note = find('#note-body') + type(note, '~Accepting merge requests foo bar ~Accepting job') + + wait_for_requests + + page.within '.atwho-container #at-view-labels' do + expect(find('.atwho-view-ul').text).to have_content('Accepting job applicants') + end + end + + it 'does not autocomplete labels if no tilde is typed' do + create(:label, project: project, title: 'Accepting merge requests') + + note = find('#note-body') + type(note, 'Accepting merge') + + wait_for_requests + + expect(page).not_to have_css('.atwho-container #at-view-labels') + end end shared_examples 'autocomplete suggestions' do diff --git a/spec/frontend/helpers/vuex_action_helper.js b/spec/frontend/helpers/vuex_action_helper.js index 88652202a8e..6c3569a2247 100644 --- a/spec/frontend/helpers/vuex_action_helper.js +++ b/spec/frontend/helpers/vuex_action_helper.js @@ -20,7 +20,7 @@ const noop = () => {}; * // expected mutations * [ * { type: types.MUTATION} - * { type: types.MUTATION_1, payload: jasmine.any(Number)} + * { type: types.MUTATION_1, payload: expect.any(Number)} * ], * // expected actions * [ @@ -89,10 +89,7 @@ export default ( payload, ); - return new Promise(resolve => { - setImmediate(resolve); - }) - .then(() => result) + return (result || new Promise(resolve => setImmediate(resolve))) .catch(error => { validateResults(); throw error; diff --git a/spec/frontend/helpers/vuex_action_helper_spec.js b/spec/frontend/helpers/vuex_action_helper_spec.js new file mode 100644 index 00000000000..61d05762a04 --- /dev/null +++ b/spec/frontend/helpers/vuex_action_helper_spec.js @@ -0,0 +1,166 @@ +import MockAdapter from 'axios-mock-adapter'; +import { TEST_HOST } from 'helpers/test_constants'; +import axios from '~/lib/utils/axios_utils'; +import testAction from './vuex_action_helper'; + +describe('VueX test helper (testAction)', () => { + let originalExpect; + let assertion; + let mock; + const noop = () => {}; + + beforeEach(() => { + mock = new MockAdapter(axios); + /** + * In order to test the helper properly, we need to overwrite the Jest + * `expect` helper. We test that the testAction helper properly passes the + * dispatched actions/committed mutations to the Jest helper. + */ + originalExpect = expect; + assertion = null; + global.expect = actual => ({ + toEqual: () => { + originalExpect(actual).toEqual(assertion); + }, + }); + }); + + afterEach(() => { + mock.restore(); + global.expect = originalExpect; + }); + + it('properly passes state and payload to action', () => { + const exampleState = { FOO: 12, BAR: 3 }; + const examplePayload = { BAZ: 73, BIZ: 55 }; + + const action = ({ state }, payload) => { + originalExpect(state).toEqual(exampleState); + originalExpect(payload).toEqual(examplePayload); + }; + + assertion = { mutations: [], actions: [] }; + + testAction(action, examplePayload, exampleState); + }); + + describe('given a sync action', () => { + it('mocks committing mutations', () => { + const action = ({ commit }) => { + commit('MUTATION'); + }; + + assertion = { mutations: [{ type: 'MUTATION' }], actions: [] }; + + testAction(action, null, {}, assertion.mutations, assertion.actions, noop); + }); + + it('mocks dispatching actions', () => { + const action = ({ dispatch }) => { + dispatch('ACTION'); + }; + + assertion = { actions: [{ type: 'ACTION' }], mutations: [] }; + + testAction(action, null, {}, assertion.mutations, assertion.actions, noop); + }); + + it('works with done callback once finished', done => { + assertion = { mutations: [], actions: [] }; + + testAction(noop, null, {}, assertion.mutations, assertion.actions, done); + }); + + it('returns a promise', done => { + assertion = { mutations: [], actions: [] }; + + testAction(noop, null, {}, assertion.mutations, assertion.actions) + .then(done) + .catch(done.fail); + }); + }); + + describe('given an async action (returning a promise)', () => { + let lastError; + const data = { FOO: 'BAR' }; + + const asyncAction = ({ commit, dispatch }) => { + dispatch('ACTION'); + + return axios + .get(TEST_HOST) + .catch(error => { + commit('ERROR'); + lastError = error; + throw error; + }) + .then(() => { + commit('SUCCESS'); + return data; + }); + }; + + beforeEach(() => { + lastError = null; + }); + + it('works with done callback once finished', done => { + mock.onGet(TEST_HOST).replyOnce(200, 42); + + assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] }; + + testAction(asyncAction, null, {}, assertion.mutations, assertion.actions, done); + }); + + it('returns original data of successful promise while checking actions/mutations', done => { + mock.onGet(TEST_HOST).replyOnce(200, 42); + + assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] }; + + testAction(asyncAction, null, {}, assertion.mutations, assertion.actions) + .then(res => { + originalExpect(res).toEqual(data); + done(); + }) + .catch(done.fail); + }); + + it('returns original error of rejected promise while checking actions/mutations', done => { + mock.onGet(TEST_HOST).replyOnce(500, ''); + + assertion = { mutations: [{ type: 'ERROR' }], actions: [{ type: 'ACTION' }] }; + + testAction(asyncAction, null, {}, assertion.mutations, assertion.actions) + .then(done.fail) + .catch(error => { + originalExpect(error).toBe(lastError); + done(); + }); + }); + }); + + it('works with async actions not returning promises', done => { + const data = { FOO: 'BAR' }; + + const asyncAction = ({ commit, dispatch }) => { + dispatch('ACTION'); + + axios + .get(TEST_HOST) + .then(() => { + commit('SUCCESS'); + return data; + }) + .catch(error => { + commit('ERROR'); + throw error; + }); + }; + + mock.onGet(TEST_HOST).replyOnce(200, 42); + + assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] }; + + testAction(asyncAction, null, {}, assertion.mutations, assertion.actions, done); + }); +}); diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js index 37354283cab..537152f5eed 100644 --- a/spec/javascripts/ide/stores/actions_spec.js +++ b/spec/javascripts/ide/stores/actions_spec.js @@ -492,6 +492,33 @@ describe('Multi-file store actions', () => { done, ); }); + + it('does not delete a folder after it is emptied', done => { + const testFolder = { + type: 'tree', + tree: [], + }; + const testEntry = { + path: 'testFolder/entry-to-delete', + parentPath: 'testFolder', + opened: false, + tree: [], + }; + testFolder.tree.push(testEntry); + store.state.entries = { + testFolder, + 'testFolder/entry-to-delete': testEntry, + }; + + testAction( + deleteEntry, + 'testFolder/entry-to-delete', + store.state, + [{ type: types.DELETE_ENTRY, payload: 'testFolder/entry-to-delete' }], + [{ type: 'burstUnusedSeal' }, { type: 'triggerFilesChange' }], + done, + ); + }); }); describe('renameEntry', () => { diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index e72fb9c6fbc..cceeae8afe6 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -2038,24 +2038,24 @@ describe Gitlab::Git::Repository, :seed_helper do end describe '#clean_stale_repository_files' do - let(:worktree_path) { File.join(repository_path, 'worktrees', 'delete-me') } + let(:worktree_id) { 'rebase-1' } + let(:gitlab_worktree_path) { File.join(repository_path, 'gitlab-worktree', worktree_id) } + let(:admin_dir) { File.join(repository_path, 'worktrees') } it 'cleans up the files' do - create_worktree = %W[git -C #{repository_path} worktree add --detach #{worktree_path} master] + create_worktree = %W[git -C #{repository_path} worktree add --detach #{gitlab_worktree_path} master] raise 'preparation failed' unless system(*create_worktree, err: '/dev/null') - FileUtils.touch(worktree_path, mtime: Time.now - 8.hours) + FileUtils.touch(gitlab_worktree_path, mtime: Time.now - 8.hours) # git rev-list --all will fail in git 2.16 if HEAD is pointing to a non-existent object, # but the HEAD must be 40 characters long or git will ignore it. - File.write(File.join(worktree_path, 'HEAD'), Gitlab::Git::BLANK_SHA) - - # git 2.16 fails with "fatal: bad object HEAD" - expect(rev_list_all).to be false + File.write(File.join(admin_dir, worktree_id, 'HEAD'), Gitlab::Git::BLANK_SHA) + expect(rev_list_all).to be(false) repository.clean_stale_repository_files - expect(rev_list_all).to be true - expect(File.exist?(worktree_path)).to be_falsey + expect(rev_list_all).to be(true) + expect(File.exist?(gitlab_worktree_path)).to be_falsey end def rev_list_all diff --git a/spec/lib/gitlab/lets_encrypt/client_spec.rb b/spec/lib/gitlab/lets_encrypt/client_spec.rb index 5454d9c1af4..cbb862cb0c9 100644 --- a/spec/lib/gitlab/lets_encrypt/client_spec.rb +++ b/spec/lib/gitlab/lets_encrypt/client_spec.rb @@ -116,42 +116,6 @@ describe ::Gitlab::LetsEncrypt::Client do end end - describe '#enabled?' do - subject { client.enabled? } - - context 'when terms of service are accepted' do - it { is_expected.to eq(true) } - - context "when private_key isn't present and database is read only" do - before do - allow(::Gitlab::Database).to receive(:read_only?).and_return(true) - end - - it 'returns false' do - expect(::Gitlab::CurrentSettings.lets_encrypt_private_key).to eq(nil) - - is_expected.to eq(false) - end - end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(pages_auto_ssl: false) - end - - it { is_expected.to eq(false) } - end - end - - context 'when terms of service are not accepted' do - before do - stub_application_setting(lets_encrypt_terms_of_service_accepted: false) - end - - it { is_expected.to eq(false) } - end - end - describe '#terms_of_service_url' do subject { client.terms_of_service_url } diff --git a/spec/lib/gitlab/lets_encrypt_spec.rb b/spec/lib/gitlab/lets_encrypt_spec.rb new file mode 100644 index 00000000000..674b114e9d3 --- /dev/null +++ b/spec/lib/gitlab/lets_encrypt_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ::Gitlab::LetsEncrypt do + include LetsEncryptHelpers + + before do + stub_lets_encrypt_settings + end + + describe '.enabled?' do + let(:project) { create(:project) } + let(:pages_domain) { create(:pages_domain, project: project) } + + subject { described_class.enabled?(pages_domain) } + + context 'when terms of service are accepted' do + it { is_expected.to eq(true) } + + context 'when feature flag is disabled' do + before do + stub_feature_flags(pages_auto_ssl: false) + end + + it { is_expected.to eq(false) } + end + end + + context 'when terms of service are not accepted' do + before do + stub_application_setting(lets_encrypt_terms_of_service_accepted: false) + end + + it { is_expected.to eq(false) } + end + + context 'when feature flag for project is disabled' do + before do + stub_feature_flags(pages_auto_ssl_for_project: false) + end + + it 'returns false' do + is_expected.to eq(false) + end + end + + context 'when domain has not project' do + let(:pages_domain) { create(:pages_domain) } + + it 'returns false' do + is_expected.to eq(false) + end + end + end +end diff --git a/spec/migrations/backport_enterprise_schema_spec.rb b/spec/migrations/backport_enterprise_schema_spec.rb new file mode 100644 index 00000000000..8d2d9d4953a --- /dev/null +++ b/spec/migrations/backport_enterprise_schema_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require Rails.root.join('db', 'migrate', '20190402150158_backport_enterprise_schema.rb') + +describe BackportEnterpriseSchema, :migration, schema: 20190329085614 do + include MigrationsHelpers + + def drop_if_exists(table) + active_record_base.connection.drop_table(table) if active_record_base.connection.table_exists?(table) + end + + describe '#up' do + it 'creates new EE tables' do + migrate! + + expect(active_record_base.connection.table_exists?(:epics)).to be true + expect(active_record_base.connection.table_exists?(:geo_nodes)).to be true + end + + context 'missing EE columns' do + before do + drop_if_exists(:epics) + + active_record_base.connection.create_table "epics" do |t| + t.integer :group_id, null: false, index: true + t.integer :author_id, null: false, index: true + end + end + + after do + drop_if_exists(:epics) + end + + it 'flags an error' do + expect { migrate! }.to raise_error(/Your database is missing.*that is present for GitLab EE/) + end + end + end +end diff --git a/spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb b/spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb index 2ae4872f51d..08a3511f70b 100644 --- a/spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb +++ b/spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb @@ -12,15 +12,18 @@ describe PagesDomainSslRenewalCronWorker do end describe '#perform' do - let!(:domain) { create(:pages_domain) } - let!(:domain_with_enabled_auto_ssl) { create(:pages_domain, auto_ssl_enabled: true) } - let!(:domain_with_obtained_letsencrypt) { create(:pages_domain, :letsencrypt, auto_ssl_enabled: true) } + let(:project) { create :project } + let!(:domain) { create(:pages_domain, project: project) } + let!(:domain_with_enabled_auto_ssl) { create(:pages_domain, project: project, auto_ssl_enabled: true) } + let!(:domain_with_obtained_letsencrypt) do + create(:pages_domain, :letsencrypt, project: project, auto_ssl_enabled: true) + end let!(:domain_without_auto_certificate) do - create(:pages_domain, :without_certificate, :without_key, auto_ssl_enabled: true) + create(:pages_domain, :without_certificate, :without_key, project: project, auto_ssl_enabled: true) end let!(:domain_with_expired_auto_ssl) do - create(:pages_domain, :letsencrypt, :with_expired_certificate) + create(:pages_domain, :letsencrypt, :with_expired_certificate, project: project) end it 'enqueues a PagesDomainSslRenewalWorker for domains needing renewal' do diff --git a/spec/workers/pages_domain_ssl_renewal_worker_spec.rb b/spec/workers/pages_domain_ssl_renewal_worker_spec.rb index a3d33de1b40..3552ff0823a 100644 --- a/spec/workers/pages_domain_ssl_renewal_worker_spec.rb +++ b/spec/workers/pages_domain_ssl_renewal_worker_spec.rb @@ -7,7 +7,8 @@ describe PagesDomainSslRenewalWorker do subject(:worker) { described_class.new } - let(:domain) { create(:pages_domain) } + let(:project) { create(:project) } + let(:domain) { create(:pages_domain, project: project) } before do stub_lets_encrypt_settings @@ -22,14 +23,24 @@ describe PagesDomainSslRenewalWorker do worker.perform(domain.id) end + shared_examples 'does nothing' do + it 'does nothing' do + expect(::PagesDomains::ObtainLetsEncryptCertificateService).not_to receive(:new) + end + end + context 'when domain was deleted' do before do domain.destroy! end - it 'does nothing' do - expect(::PagesDomains::ObtainLetsEncryptCertificateService).not_to receive(:new) - end + include_examples 'does nothing' + end + + context 'when domain is disabled' do + let(:domain) { create(:pages_domain, :disabled) } + + include_examples 'does nothing' end end end |