diff options
20 files changed, 233 insertions, 161 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 295216a6fea..fec78218958 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,9 +24,6 @@ variables: ES_JAVA_OPTS: "-Xms256m -Xmx256m" ELASTIC_URL: "http://elastic:changeme@elasticsearch:9200" -after_script: - - date - include: - local: .gitlab/ci/cache-repo.gitlab-ci.yml - local: .gitlab/ci/cng.gitlab-ci.yml diff --git a/.gitlab/ci/releases.gitlab-ci.yml b/.gitlab/ci/releases.gitlab-ci.yml index 8ca4041e6be..0f6753aa274 100644 --- a/.gitlab/ci/releases.gitlab-ci.yml +++ b/.gitlab/ci/releases.gitlab-ci.yml @@ -1,4 +1,10 @@ ---- +.releases:rules:canonical-dot-com-gitlab-stable-branch-only: + rules: + - if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAME == "gitlab-org/gitlab" && $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable-ee$/' + +.releases:rules:canonical-dot-com-security-gitlab-stable-branch-only: + rules: + - if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAME == "gitlab-org/security/gitlab" && $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable-ee$/' # Syncs any changes pushed to a stable branch to the corresponding # gitlab-foss/CE stable branch. We run this prior to any tests so that random @@ -10,27 +16,21 @@ stage: sync before_script: - apk add --no-cache --update curl bash jq - after_script: [] script: - bash scripts/sync-stable-branch.sh - only: - variables: - - $CI_SERVER_HOST == "gitlab.com" sync-stable-branch: - extends: .merge-train-sync + extends: + - .releases:rules:canonical-dot-com-gitlab-stable-branch-only + - .merge-train-sync variables: SOURCE_PROJECT: gitlab-org/gitlab TARGET_PROJECT: gitlab-org/gitlab-foss - only: - refs: - - /^[\d-]+-stable-ee$/@gitlab-org/gitlab sync-security-branch: - extends: .merge-train-sync + extends: + - .releases:rules:canonical-dot-com-security-gitlab-stable-branch-only + - .merge-train-sync variables: SOURCE_PROJECT: gitlab-org/security/gitlab TARGET_PROJECT: gitlab-org/security/gitlab-foss - only: - refs: - - /^[\d-]+-stable-ee$/@gitlab-org/security/gitlab diff --git a/changelogs/unreleased/mk-fix-node-name-vs-url.yml b/changelogs/unreleased/mk-fix-node-name-vs-url.yml new file mode 100644 index 00000000000..ea204a2f943 --- /dev/null +++ b/changelogs/unreleased/mk-fix-node-name-vs-url.yml @@ -0,0 +1,5 @@ +--- +title: 'Geo: Fix GeoNode name in geo:update_primary_node_url rake task' +merge_request: 24649 +author: +type: fixed diff --git a/db/post_migrate/20200212052620_readd_template_column_to_services.rb b/db/post_migrate/20200212052620_readd_template_column_to_services.rb new file mode 100644 index 00000000000..e54b9e39277 --- /dev/null +++ b/db/post_migrate/20200212052620_readd_template_column_to_services.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class ReaddTemplateColumnToServices < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + return if column_exists? :services, :template + + # The migration to drop the template column never actually shipped + # to production, so we should be okay to re-add it without worrying + # about doing a data migration. If we needed to restore the value + # of `template`, we would look for entries with `project_id IS NULL`. + add_column_with_default :services, :template, :boolean, default: false, allow_null: true + end + + def down + # NOP since the column is expected to exist + end +end diff --git a/db/schema.rb b/db/schema.rb index 07ff6ff0828..90f2bb5cf0f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_02_11_152410) do +ActiveRecord::Schema.define(version: 2020_02_12_052620) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md index 2999e0b6f1d..5455e5914e1 100644 --- a/doc/administration/geo/disaster_recovery/index.md +++ b/doc/administration/geo/disaster_recovery/index.md @@ -205,6 +205,25 @@ secondary domain, like changing Git remotes and API URLs. This command will use the changed `external_url` configuration defined in `/etc/gitlab/gitlab.rb`. +1. For GitLab 11.11 through 12.7 only, you may need to update the primary + node's name in the database. This bug has been fixed in GitLab 12.8. + + To determine if you need to do this, search for the + `gitlab_rails["geo_node_name"]` setting in your `/etc/gitlab/gitlab.rb` + file. If it is commented out with `#` or not found at all, then you will + need to update the primary node's name in the database. You can search for it + like so: + + ```shell + grep "geo_node_name" /etc/gitlab/gitlab.rb + ``` + + To update the primary node's name in the database: + + ```shell + gitlab-rails runner 'Gitlab::Geo.primary_node.update!(name: GeoNode.current_node_name)' + ``` + 1. Verify you can connect to the newly promoted **primary** using its URL. If you updated the DNS records for the primary domain, these changes may not have yet propagated depending on the previous DNS records TTL. diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md index 10da59ee9e0..32017a284d5 100644 --- a/doc/development/gitaly.md +++ b/doc/development/gitaly.md @@ -298,27 +298,12 @@ Here are the steps to gate a new feature in Gitaly behind a feature flag. ### GitLab Rails -1. In GitLab Rails: - - 1. Add the feature flag to `SERVER_FEATURE_FLAGS` in `lib/feature/gitaly.rb`: - - ```ruby - SERVER_FEATURE_FLAGS = %w[go-find-all-tags].freeze - ``` - - 1. Search for `["gitaly"]["features"]` (currently in `spec/requests/api/internal/base_spec.rb`) - and fix the expected results for the tests by adding the new feature flag into it: - - ```ruby - expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-get-all-lfs-pointers-go' => 'true', 'gitaly-feature-go-find-all-tags' => 'true') - ``` - 1. Test in a Rails console by setting the feature flag: NOTE: **Note:** Pay attention to the name of the flag and the one used in the Rails console. There is a difference between them (dashes replaced by underscores and name - prefix is changed). + prefix is changed). Make sure to prefix all flags with `gitaly_`. ```ruby Feature.enable('gitaly_go_find_all_tags') diff --git a/lib/banzai/filter/inline_metrics_filter.rb b/lib/banzai/filter/inline_metrics_filter.rb index c5a328c21b2..c1f4bf1f97f 100644 --- a/lib/banzai/filter/inline_metrics_filter.rb +++ b/lib/banzai/filter/inline_metrics_filter.rb @@ -25,7 +25,7 @@ module Banzai # Regular expression matching metrics urls def link_pattern - Gitlab::Metrics::Dashboard::Url.regex + Gitlab::Metrics::Dashboard::Url.metrics_regex end private diff --git a/lib/banzai/filter/inline_metrics_redactor_filter.rb b/lib/banzai/filter/inline_metrics_redactor_filter.rb index c70897fccbf..ae830831a27 100644 --- a/lib/banzai/filter/inline_metrics_redactor_filter.rb +++ b/lib/banzai/filter/inline_metrics_redactor_filter.rb @@ -59,7 +59,7 @@ module Banzai embed = Embed.new url = node.attribute('data-dashboard-url').to_s - set_path_and_permission(embed, url, URL.regex, :read_environment) + set_path_and_permission(embed, url, URL.metrics_regex, :read_environment) set_path_and_permission(embed, url, URL.grafana_regex, :read_project) unless embed.permission embeds[node] = embed if embed.permission diff --git a/lib/feature.rb b/lib/feature.rb index 543512b1598..aadc2c64957 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -32,6 +32,8 @@ class Feature end def persisted_names + return [] unless Gitlab::Database.exists? + Gitlab::SafeRequestStore[:flipper_persisted_names] ||= begin # We saw on GitLab.com, this database request was called 2300 diff --git a/lib/feature/gitaly.rb b/lib/feature/gitaly.rb index 7e3b9378d10..d327162b34e 100644 --- a/lib/feature/gitaly.rb +++ b/lib/feature/gitaly.rb @@ -1,34 +1,25 @@ # frozen_string_literal: true -require 'set' - class Feature class Gitaly - # Server feature flags should use '_' to separate words. - SERVER_FEATURE_FLAGS = - %w[ - cache_invalidator - inforef_uploadpack_cache - commit_without_batch_check - use_core_delta_islands - use_git_protocol_v2 - ].freeze - - DEFAULT_ON_FLAGS = Set.new([]).freeze + PREFIX = "gitaly_" class << self def enabled?(feature_flag) return false unless Feature::FlipperFeature.table_exists? - default_on = DEFAULT_ON_FLAGS.include?(feature_flag) - Feature.enabled?("gitaly_#{feature_flag}", default_enabled: default_on) + Feature.enabled?("#{PREFIX}#{feature_flag}") rescue ActiveRecord::NoDatabaseError, PG::ConnectionBad false end def server_feature_flags - SERVER_FEATURE_FLAGS.map do |f| - ["gitaly-feature-#{f.tr('_', '-')}", enabled?(f).to_s] + Feature.persisted_names + .select { |f| f.start_with?(PREFIX) } + .map do |f| + flag = f.delete_prefix(PREFIX) + + ["gitaly-feature-#{flag.tr('_', '-')}", enabled?(flag).to_s] end.to_h end end diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index 27522f89a5b..67fb0ab9608 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -203,36 +203,6 @@ module Gitlab start_repository: start_repository) end - # DEPRECATED: https://gitlab.com/gitlab-org/gitaly/issues/1628 - def user_rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:) - request = Gitaly::UserRebaseRequest.new( - repository: @gitaly_repo, - user: Gitlab::Git::User.from_gitlab(user).to_gitaly, - rebase_id: rebase_id.to_s, - branch: encode_binary(branch), - branch_sha: branch_sha, - remote_repository: remote_repository.gitaly_repository, - remote_branch: encode_binary(remote_branch) - ) - - response = GitalyClient.call( - @repository.storage, - :operation_service, - :user_rebase, - request, - timeout: GitalyClient.long_timeout, - remote_storage: remote_repository.storage - ) - - if response.pre_receive_error.presence - raise Gitlab::Git::PreReceiveError, response.pre_receive_error - elsif response.git_error.presence - raise Gitlab::Git::Repository::GitError, response.git_error - else - response.rebase_sha - end - end - def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:, push_options: []) request_enum = QueueEnumerator.new rebase_sha = nil diff --git a/lib/gitlab/metrics/dashboard/url.rb b/lib/gitlab/metrics/dashboard/url.rb index 712f769bbeb..b3cbfde828c 100644 --- a/lib/gitlab/metrics/dashboard/url.rb +++ b/lib/gitlab/metrics/dashboard/url.rb @@ -6,41 +6,36 @@ module Gitlab module Dashboard class Url class << self + include Gitlab::Utils::StrongMemoize # Matches urls for a metrics dashboard. This could be # either the /metrics endpoint or the /metrics_dashboard # endpoint. # # EX - https://<host>/<namespace>/<project>/environments/<env_id>/metrics - def regex - %r{ - (?<url> - #{gitlab_pattern} - #{project_pattern} - (?:\/\-)? - \/environments - \/(?<environment>\d+) - \/metrics - #{query_pattern} - #{anchor_pattern} + def metrics_regex + strong_memoize(:metrics_regex) do + regex_for_project_metrics( + %r{ + /environments + /(?<environment>\d+) + /metrics + }x ) - }x + end end # Matches dashboard urls for a Grafana embed. # # EX - https://<host>/<namespace>/<project>/grafana/metrics_dashboard def grafana_regex - %r{ - (?<url> - #{gitlab_pattern} - #{project_pattern} - (?:\/\-)? - \/grafana - \/metrics_dashboard - #{query_pattern} - #{anchor_pattern} + strong_memoize(:grafana_regex) do + regex_for_project_metrics( + %r{ + /grafana + /metrics_dashboard + }x ) - }x + end end # Parses query params out from full url string into hash. @@ -62,11 +57,24 @@ module Gitlab private - def gitlab_pattern + def regex_for_project_metrics(path_suffix_pattern) + %r{ + (?<url> + #{gitlab_host_pattern} + #{project_path_pattern} + (?:/-)? + #{path_suffix_pattern} + #{query_pattern} + #{anchor_pattern} + ) + }x + end + + def gitlab_host_pattern Regexp.escape(Gitlab.config.gitlab.url) end - def project_pattern + def project_path_pattern "\/#{Project.reference_pattern}" end @@ -82,3 +90,5 @@ module Gitlab end end end + +Gitlab::Metrics::Dashboard::Url.extend_if_ee('::EE::Gitlab::Metrics::Dashboard::Url') diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb index 58caacc1ad1..c036f188ea2 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context 'Verify', :docker, quarantine: 'https://gitlab.com/gitlab-org/gitlab/issues/202149' do + context 'Verify', :docker, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/issues/202149', type: :flaky } do describe 'Pipeline creation and processing' do let(:executor) { "qa-runner-#{Time.now.to_i}" } let(:max_wait) { 30 } diff --git a/spec/lib/feature/gitaly_spec.rb b/spec/lib/feature/gitaly_spec.rb index 4e07acf9c1a..afb522d05e1 100644 --- a/spec/lib/feature/gitaly_spec.rb +++ b/spec/lib/feature/gitaly_spec.rb @@ -5,10 +5,6 @@ require 'spec_helper' describe Feature::Gitaly do let(:feature_flag) { "mep_mep" } - before do - stub_const("#{described_class}::SERVER_FEATURE_FLAGS", [feature_flag]) - end - describe ".enabled?" do context 'when the gate is closed' do before do @@ -28,15 +24,13 @@ describe Feature::Gitaly do end describe ".server_feature_flags" do - context 'when one flag is disabled' do - before do - stub_feature_flags(gitaly_mep_mep: false) - end + before do + allow(Feature).to receive(:persisted_names).and_return(%w[gitaly_mep_mep foo]) + end - subject { described_class.server_feature_flags } + subject { described_class.server_feature_flags } - it { is_expected.to be_a(Hash) } - it { is_expected.to eq("gitaly-feature-mep-mep" => "false") } - end + it { is_expected.to be_a(Hash) } + it { is_expected.to eq("gitaly-feature-mep-mep" => "true") } end end diff --git a/spec/lib/gitlab/metrics/dashboard/url_spec.rb b/spec/lib/gitlab/metrics/dashboard/url_spec.rb index daaf66cba46..d98aa5e3697 100644 --- a/spec/lib/gitlab/metrics/dashboard/url_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/url_spec.rb @@ -3,38 +3,6 @@ require 'spec_helper' describe Gitlab::Metrics::Dashboard::Url do - shared_examples_for 'a regex which matches the expected url' do - it { is_expected.to be_a Regexp } - - it 'matches a metrics dashboard link with named params' do - expect(subject).to match url - - subject.match(url) do |m| - expect(m.named_captures).to eq expected_params - end - end - end - - shared_examples_for 'does not match non-matching urls' do - it 'does not match other gitlab urls that contain the term metrics' do - url = Gitlab::Routing.url_helpers.active_common_namespace_project_prometheus_metrics_url('foo', 'bar', :json) - - expect(subject).not_to match url - end - - it 'does not match other gitlab urls' do - url = Gitlab.config.gitlab.url - - expect(subject).not_to match url - end - - it 'does not match non-gitlab urls' do - url = 'https://www.super_awesome_site.com/' - - expect(subject).not_to match url - end - end - describe '#regex' do let(:url) do Gitlab::Routing.url_helpers.metrics_namespace_project_environment_url( @@ -59,10 +27,9 @@ describe Gitlab::Metrics::Dashboard::Url do } end - subject { described_class.regex } + subject { described_class.metrics_regex } - it_behaves_like 'a regex which matches the expected url' - it_behaves_like 'does not match non-matching urls' + it_behaves_like 'regex which matches url when expected' end describe '#grafana_regex' do @@ -89,15 +56,14 @@ describe Gitlab::Metrics::Dashboard::Url do subject { described_class.grafana_regex } - it_behaves_like 'a regex which matches the expected url' - it_behaves_like 'does not match non-matching urls' + it_behaves_like 'regex which matches url when expected' end describe '#build_dashboard_url' do it 'builds the url for the dashboard endpoint' do url = described_class.build_dashboard_url('foo', 'bar', 1) - expect(url).to match described_class.regex + expect(url).to match described_class.metrics_regex end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 3d817065963..1e558131dc6 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1622,7 +1622,6 @@ describe Repository do it 'executes the new Gitaly RPC' do expect_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:rebase) - expect_any_instance_of(Gitlab::GitalyClient::OperationService).not_to receive(:user_rebase) repository.rebase(user, merge_request) end diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index 677ea071012..733f0446cf4 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -313,6 +313,10 @@ describe API::Internal::Base do end context "git pull" do + before do + allow(Feature).to receive(:persisted_names).and_return(%w[gitaly_mep_mep]) + end + it "has the correct payload" do pull(key, project) @@ -326,7 +330,7 @@ describe API::Internal::Base do expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path) expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage)) expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage)) - expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true', 'gitaly-feature-use-core-delta-islands' => 'true', 'gitaly-feature-use-git-protocol-v2' => 'true') + expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-mep-mep' => 'true') expect(user.reload.last_activity_on).to eql(Date.today) end end @@ -346,7 +350,6 @@ describe API::Internal::Base do expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path) expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage)) expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage)) - expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true', 'gitaly-feature-use-core-delta-islands' => 'true', 'gitaly-feature-use-git-protocol-v2' => 'true') expect(user.reload.last_activity_on).to be_nil end end @@ -594,7 +597,6 @@ describe API::Internal::Base do expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path) expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage)) expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage)) - expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true', 'gitaly-feature-use-core-delta-islands' => 'true', 'gitaly-feature-use-git-protocol-v2' => 'true') end end diff --git a/spec/support/shared_examples/metrics/url_shared_examples.rb b/spec/support/shared_examples/metrics/url_shared_examples.rb new file mode 100644 index 00000000000..67742aecb87 --- /dev/null +++ b/spec/support/shared_examples/metrics/url_shared_examples.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'regex which matches url when expected' do + it { is_expected.to be_a Regexp } + + it 'matches a metrics dashboard link with named params' do + expect(subject).to match url + + subject.match(url) do |m| + expect(m.named_captures).to eq expected_params + end + end + + it 'does not match other gitlab urls that contain the term metrics' do + url = Gitlab::Routing.url_helpers.active_common_namespace_project_prometheus_metrics_url('foo', 'bar', :json) + + expect(subject).not_to match url + end + + it 'does not match other gitlab urls' do + url = Gitlab.config.gitlab.url + + expect(subject).not_to match url + end + + it 'does not match non-gitlab urls' do + url = 'https://www.super_awesome_site.com/' + + expect(subject).not_to match url + end +end diff --git a/spec/views/shared/projects/_list.html.haml_spec.rb b/spec/views/shared/projects/_list.html.haml_spec.rb new file mode 100644 index 00000000000..d6043921fc8 --- /dev/null +++ b/spec/views/shared/projects/_list.html.haml_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'shared/projects/_list' do + let(:group) { create(:group) } + + before do + allow(view).to receive(:projects).and_return(projects) + allow(view).to receive(:project_list_cache_key).and_return('fake_cache_key') + end + + context 'with projects' do + let(:projects) { build_stubbed_list(:project, 1) } + + it 'renders the list of projects' do + render + + projects.each do |project| + expect(rendered).to have_content(project.name) + end + end + end + + context 'without projects' do + let(:projects) { [] } + + context 'when @contributed_projects is set' do + context 'and is empty' do + before do + @contributed_projects = [] + end + + it 'renders a no-content message' do + render + + expect(rendered).to have_content(s_('UserProfile|This user hasn\'t contributed to any projects')) + end + end + end + + context 'when @starred_projects is set' do + context 'and is empty' do + before do + @starred_projects = [] + end + + it 'renders a no-content message' do + render + + expect(rendered).to have_content(s_('UserProfile|This user hasn\'t starred any projects')) + end + end + end + + context 'and without a special instance variable' do + context 'for an explore_page' do + before do + allow(view).to receive(:explore_page).and_return(true) + end + + it 'renders a no-content message' do + render + + expect(rendered).to have_content(s_('UserProfile|Explore public groups to find projects to contribute to.')) + end + end + + context 'for a non-explore page' do + it 'renders a no-content message' do + render + + expect(rendered).to have_content(s_('UserProfile|This user doesn\'t have any personal projects')) + end + end + end + end +end |