diff options
72 files changed, 1485 insertions, 512 deletions
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml index c8ad888dc08..26e0b4fdf08 100644 --- a/.gitlab/ci/frontend.gitlab-ci.yml +++ b/.gitlab/ci/frontend.gitlab-ci.yml @@ -93,7 +93,7 @@ DOCKER_DRIVER: overlay2 DOCKER_HOST: tcp://docker:2375 cache: - key: "assets-compile:production:vendor_ruby:.yarn-cache:tmp_cache_assets_sprockets:tmp_cache_webpack:v7" + key: "assets-compile:production:vendor_ruby:.yarn-cache:tmp_cache_assets_sprockets:tmp_cache_webpack:v9" artifacts: name: webpack-report expire_in: 31d @@ -152,7 +152,7 @@ gitlab:assets:compile pull-cache: # we override the max_old_space_size to prevent OOM errors NODE_OPTIONS: --max_old_space_size=3584 cache: - key: "assets-compile:v8" + key: "assets-compile:v9" artifacts: expire_in: 7d paths: @@ -180,7 +180,7 @@ compile-assets pull-push-cache foss: when: on_success cache: policy: pull-push - key: "assets-compile:v8:foss" + key: "assets-compile:v9:foss" compile-assets pull-cache: extends: .compile-assets-metadata @@ -203,7 +203,7 @@ compile-assets pull-cache foss: when: on_success cache: policy: pull - key: "assets-compile:v8:foss" + key: "assets-compile:v9:foss" .frontend-job-base: extends: diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index f5b6a353079..c18453b778c 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -25,7 +25,7 @@ # Jobs that only need to pull cache .default-cache: cache: - key: "debian-stretch-ruby-2.6.5-node-12.x" + key: "debian-stretch-ruby-2.6.5-pg9.6-node-12.x" paths: - .go/pkg/mod - vendor/ruby diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index 1490338086d..48f5d0e3983 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -143,7 +143,7 @@ static-analysis: script: - scripts/static-analysis cache: - key: "debian-stretch-ruby-2.6-and-rubocop" + key: "debian-stretch-ruby-2.6-pg9.6-rubocop" paths: - vendor/ruby - tmp/rubocop_cache diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index 2e42aa46ac0..5d2c33d06b4 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -25,6 +25,39 @@ - ".dockerignore" - "qa/**/*" +.review:rules:mr-and-schedule: + rules: + - <<: *if-canonical-gitlab-merge-request + changes: *code-qa-patterns + when: on_success + - <<: *if-canonical-dot-com-gitlab-org-group-schedule + when: on_success + +.review:rules:mr-only-auto: + rules: + - <<: *if-canonical-gitlab-merge-request + changes: *code-qa-patterns + when: on_success + +.review:rules:mr-only-manual: + rules: + - <<: *if-canonical-gitlab-merge-request + changes: *code-qa-patterns + when: manual + +.review:rules:review-cleanup: + rules: + - <<: *if-canonical-gitlab-merge-request + changes: *code-qa-patterns + when: manual + - <<: *if-canonical-dot-com-gitlab-org-group-schedule + when: on_success + +.review:rules:danger: + rules: + - if: '$DANGER_GITLAB_API_TOKEN && $CI_MERGE_REQUEST_IID' + when: on_success + .review-docker: extends: - .default-tags @@ -41,14 +74,10 @@ GITLAB_EDITION: "ce" build-qa-image: - extends: .review-docker + extends: + - .review-docker + - .review:rules:mr-and-schedule stage: prepare - rules: - - <<: *if-canonical-gitlab-merge-request - changes: *code-qa-patterns - when: on_success - - <<: *if-canonical-dot-com-gitlab-org-group-schedule - when: on_success script: - '[[ ! -d "ee/" ]] || export GITLAB_EDITION="ee"' - export QA_MASTER_IMAGE="${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab/gitlab-${GITLAB_EDITION}-qa:master" @@ -58,11 +87,11 @@ build-qa-image: - time docker build --cache-from "${QA_MASTER_IMAGE}" --tag ${QA_IMAGE} --file ./qa/Dockerfile ./ - time docker push ${QA_IMAGE} -.base-review-cleanup: +review-cleanup: extends: - .default-tags - .default-retry - - .default-only + - .review:rules:review-cleanup stage: prepare image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base allow_failure: true @@ -75,45 +104,23 @@ build-qa-image: script: - ruby -rrubygems scripts/review_apps/automated_cleanup.rb -schedule:review-cleanup: - extends: - - .base-review-cleanup - - .only-review-schedules - -manual:review-cleanup: - extends: - - .base-review-cleanup - - .only:changes-code-qa - when: manual - -.review-build-cng-base: +review-build-cng: extends: - .default-tags - .default-retry - - .default-only + - .review:rules:mr-and-schedule image: ruby:2.6-alpine stage: review-prepare before_script: - source scripts/utils.sh - install_api_client_dependencies_with_apk - install_gitlab_gem - dependencies: [] + needs: + - job: gitlab:assets:compile pull-cache + artifacts: false script: - BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng -review-build-cng: - extends: - - .review-build-cng-base - - .only-review - - .only:changes-code-qa - needs: ["gitlab:assets:compile pull-cache"] - -schedule:review-build-cng: - extends: - - .review-build-cng-base - - .only-review-schedules - needs: ["gitlab:assets:compile pull-cache"] - .review-workflow-base: extends: - .default-tags @@ -130,8 +137,10 @@ schedule:review-build-cng: url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN} on_stop: review-stop -.review-deploy-base: - extends: .review-workflow-base +review-deploy: + extends: + - .review-workflow-base + - .review:rules:mr-and-schedule stage: review allow_failure: true before_script: @@ -140,7 +149,7 @@ schedule:review-build-cng: - export GITALY_VERSION=$(<GITALY_SERVER_VERSION) - export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION) - echo "${CI_ENVIRONMENT_URL}" > review_app_url.txt - - source scripts/utils.sh + - source ./scripts/utils.sh - install_api_client_dependencies_with_apk - source scripts/review_apps/review-apps.sh script: @@ -156,19 +165,7 @@ schedule:review-build-cng: expire_in: 2 days when: always -review-deploy: - extends: .review-deploy-base - rules: - - <<: *if-canonical-gitlab-merge-request - changes: *code-qa-patterns - when: on_success - -schedule:review-deploy: - extends: - - .review-deploy-base - - .only-review-schedules - -.base-review-stop: +.review-stop-base: extends: .review-workflow-base environment: action: stop @@ -183,22 +180,18 @@ schedule:review-deploy: - source ./scripts/review_apps/review-apps.sh review-stop-failed-deployment: - extends: .base-review-stop + extends: + - .review-stop-base + - .review:rules:mr-only-auto stage: prepare - rules: - - <<: *if-canonical-gitlab-merge-request - changes: *code-qa-patterns - when: on_success script: - delete_failed_release review-stop: - extends: .base-review-stop + extends: + - .review-stop-base + - .review:rules:mr-only-manual stage: review - rules: - - <<: *if-canonical-gitlab-merge-request - changes: *code-qa-patterns - when: manual allow_failure: true script: - delete_release @@ -206,8 +199,9 @@ review-stop: .review-qa-base: extends: .review-docker stage: qa - needs: ["review-deploy"] - dependencies: ["review-deploy"] + needs: + - job: review-deploy + artifacts: true allow_failure: true variables: QA_ARTIFACTS_DIR: "${CI_PROJECT_DIR}/qa" @@ -235,29 +229,30 @@ review-stop: when: always review-qa-smoke: - extends: .review-qa-base - rules: - - <<: *if-canonical-gitlab-merge-request - changes: *code-qa-patterns - when: on_success + extends: + - .review-qa-base + - .review:rules:mr-only-auto script: - gitlab-qa Test::Instance::Smoke "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}" review-qa-all: - extends: .review-qa-base - rules: - - <<: *if-canonical-gitlab-merge-request - changes: *code-qa-patterns - when: manual + extends: + - .review-qa-base + - .review:rules:mr-only-manual parallel: 5 script: - export KNAPSACK_REPORT_PATH=knapsack/master_report.json - export KNAPSACK_TEST_FILE_PATTERN=qa/specs/features/**/*_spec.rb - gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}" -- --format RspecJunitFormatter --out tmp/rspec-${CI_JOB_ID}.xml --format html --out tmp/rspec.htm --color --format documentation -.review-performance-base: - extends: .review-docker +review-performance: + extends: + - .review-docker + - .review:rules:mr-and-schedule stage: qa + needs: + - job: review-deploy + artifacts: true allow_failure: true before_script: - export CI_ENVIRONMENT_URL="$(cat review_app_url.txt)" @@ -275,58 +270,17 @@ review-qa-all: reports: performance: performance.json -review-performance: - extends: .review-performance-base - rules: - - <<: *if-canonical-gitlab-merge-request - changes: *code-qa-patterns - when: on_success - needs: ["review-deploy"] - dependencies: ["review-deploy"] - before_script: - - export CI_ENVIRONMENT_URL="$(cat review_app_url.txt)" - - echo "${CI_ENVIRONMENT_URL}" - - mkdir -p gitlab-exporter - - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js - - mkdir -p sitespeed-results - script: - - 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}" - after_script: - - mv sitespeed-results/data/performance.json performance.json - artifacts: - paths: - - sitespeed-results/ - reports: - performance: performance.json - -schedule:review-performance: - extends: - - .review-performance-base - - .only-review-schedules - needs: ["schedule:review-deploy"] - dependencies: ["schedule:review-deploy"] - parallel-spec-reports: extends: - .default-tags - - .default-only - - .only-review - - .only:changes-code-qa + - .review:rules:mr-only-manual image: ruby:2.6-alpine stage: post-qa dependencies: ["review-qa-all"] + allow_failure: true variables: NEW_PARALLEL_SPECS_REPORT: qa/report-new.html BASE_ARTIFACT_URL: "${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/file/qa/" - allow_failure: true - when: manual - artifacts: - when: always - paths: - - qa/report-new.html - - qa/gitlab-qa-run-* - reports: - junit: qa/gitlab-qa-run-*/**/rspec-*.xml script: - apk add --update build-base libxml2-dev libxslt-dev && rm -rf /var/cache/apk/* - gem install nokogiri --no-document @@ -335,20 +289,23 @@ parallel-spec-reports: - cd - - '[[ -f $NEW_PARALLEL_SPECS_REPORT ]] || echo "{}" > ${NEW_PARALLEL_SPECS_REPORT}' - scripts/merge-html-reports ${NEW_PARALLEL_SPECS_REPORT} ${BASE_ARTIFACT_URL}${ARTIFACT_DIRS} qa/gitlab-qa-run-*/**/rspec.htm + artifacts: + when: always + paths: + - qa/report-new.html + - qa/gitlab-qa-run-* + reports: + junit: qa/gitlab-qa-run-*/**/rspec-*.xml danger-review: extends: - .default-tags - .default-retry - .default-cache - - .default-only - - .except:refs-master-tags-stable-deploy + - .review:rules:danger image: registry.gitlab.com/gitlab-org/gitlab-build-images:danger stage: test dependencies: [] - only: - variables: - - $DANGER_GITLAB_API_TOKEN script: - git version - node --version diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml index 24267584393..73ec0a559fc 100644 --- a/.gitlab/ci/setup.gitlab-ci.yml +++ b/.gitlab/ci/setup.gitlab-ci.yml @@ -1,3 +1,90 @@ +# Make sure to update all the similar conditions in other CI config files if you modify these conditions +.if-not-canonical-namespace: &if-not-canonical-namespace + if: '$CI_PROJECT_NAMESPACE !~ /^gitlab(-org)?($|\/)/' + +# Make sure to update all the similar conditions in other CI config files if you modify these conditions +.if-not-foss: &if-not-foss + if: '$CI_PROJECT_NAME != "gitlab-foss" && $CI_PROJECT_NAME != "gitlab-ce" && $CI_PROJECT_NAME != "gitlabhq"' + +# Make sure to update all the similar conditions in other CI config files if you modify these conditions +.if-master-or-tag: &if-master-or-tag + if: '$CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_TAG' + +# Make sure to update all the similar conditions in other CI config files if you modify these conditions +.if-default-refs: &if-default-refs + if: '$CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable(-ee)?$/ || $CI_COMMIT_REF_NAME =~ /^\d+-\d+-auto-deploy-\d+$/ || $CI_COMMIT_REF_NAME =~ /^security\// || $CI_MERGE_REQUEST_IID || $CI_COMMIT_TAG' + +# Make sure to update all the similar patterns in other CI config files if you modify these patterns +.code-backstage-patterns: &code-backstage-patterns + - ".gitlab/ci/**/*" + - ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}" + - ".{codeclimate,eslintrc,gitlab-ci,haml-lint,haml-lint_todo,rubocop,rubocop_todo,scss-lint}.yml" + - ".csscomb.json" + - "Dockerfile.assets" + - "*_VERSION" + - "Gemfile{,.lock}" + - "Rakefile" + - "{babel.config,jest.config}.js" + - "config.ru" + - "{package.json,yarn.lock}" + - "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*" + - "doc/api/graphql/reference/*" # Files in this folder are auto-generated + # Backstage changes + - "Dangerfile" + - "danger/**/*" + - "{,ee/}fixtures/**/*" + - "{,ee/}rubocop/**/*" + - "{,ee/}spec/**/*" + - "doc/README.md" # Some RSpec test rely on this file + +# Make sure to update all the similar patterns in other CI config files if you modify these patterns +.code-backstage-qa-patterns: &code-backstage-qa-patterns + - ".gitlab/ci/**/*" + - ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}" + - ".{codeclimate,eslintrc,gitlab-ci,haml-lint,haml-lint_todo,rubocop,rubocop_todo,scss-lint}.yml" + - ".csscomb.json" + - "Dockerfile.assets" + - "*_VERSION" + - "Gemfile{,.lock}" + - "Rakefile" + - "{babel.config,jest.config}.js" + - "config.ru" + - "{package.json,yarn.lock}" + - "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*" + - "doc/api/graphql/reference/*" # Files in this folder are auto-generated + # Backstage changes + - "Dangerfile" + - "danger/**/*" + - "{,ee/}fixtures/**/*" + - "{,ee/}rubocop/**/*" + - "{,ee/}spec/**/*" + - "doc/README.md" # Some RSpec test rely on this file + # QA changes + - ".dockerignore" + - "qa/**/*" + +.setup:rules:cache-gems: + rules: + - <<: *if-not-canonical-namespace + when: never + - <<: *if-master-or-tag + changes: *code-backstage-qa-patterns + when: on_success + +.setup:rules:gitlab_git_test: + rules: + - <<: *if-default-refs + changes: *code-backstage-patterns + when: on_success + +.setup:rules:no_ee_check: + rules: + - <<: *if-not-foss + when: never + - <<: *if-default-refs + changes: *code-backstage-patterns + when: on_success + # Insurance in case a gem needed by one of our releases gets yanked from # rubygems.org in the future. cache gems: @@ -6,11 +93,11 @@ cache gems: - .default-retry - .default-cache - .default-before_script - - .only:variables-canonical-dot-com - - .only:changes-code-backstage-qa + - .setup:rules:cache-gems stage: test - dependencies: ["setup-test-env"] - needs: ["setup-test-env"] + needs: + - job: setup-test-env + artifacts: true variables: SETUP_DB: "false" script: @@ -18,30 +105,23 @@ cache gems: artifacts: paths: - vendor/cache - only: - refs: - - master - - tags .minimal-job: extends: - .default-tags - .default-retry - - .default-only - - .only:changes-code-backstage dependencies: [] gitlab_git_test: - extends: .minimal-job + extends: + - .minimal-job + - .setup:rules:gitlab_git_test script: - spec/support/prepare-gitlab-git-test-for-commit --check-for-changes no_ee_check: - extends: .minimal-job + extends: + - .minimal-job + - .setup:rules:no_ee_check script: - scripts/no-ee-check - only: - variables: - - $CI_PROJECT_NAME == "gitlab-foss" - - $CI_PROJECT_NAME == "gitlab-ce" # Support former project name for forks/mirrors - - $CI_PROJECT_NAME == "gitlabhq" # Support former project name for dev diff --git a/.gitlab/ci/test-metadata.gitlab-ci.yml b/.gitlab/ci/test-metadata.gitlab-ci.yml index 4ec0dc70ae6..2457498f84d 100644 --- a/.gitlab/ci/test-metadata.gitlab-ci.yml +++ b/.gitlab/ci/test-metadata.gitlab-ci.yml @@ -1,7 +1,57 @@ +# Make sure to update all the similar conditions in other CI config files if you modify these conditions +.if-default-refs: &if-default-refs + if: '$CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable(-ee)?$/ || $CI_COMMIT_REF_NAME =~ /^\d+-\d+-auto-deploy-\d+$/ || $CI_COMMIT_REF_NAME =~ /^security\// || $CI_MERGE_REQUEST_IID || $CI_COMMIT_TAG' + +# Make sure to update all the similar conditions in other CI config files if you modify these conditions +.if-merge-request: &if-merge-request + if: '$CI_MERGE_REQUEST_IID' + +# Make sure to update all the similar conditions in other CI config files if you modify these conditions +.if-canonical-dot-com-gitlab-schedule: &if-canonical-dot-com-gitlab-schedule + if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_PATH == "gitlab-org/gitlab" && $CI_PIPELINE_SOURCE == "schedule"' + +# Make sure to update all the similar patterns in other CI config files if you modify these patterns +.code-backstage-patterns: &code-backstage-patterns + - ".gitlab/ci/**/*" + - ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}" + - ".{codeclimate,eslintrc,gitlab-ci,haml-lint,haml-lint_todo,rubocop,rubocop_todo,scss-lint}.yml" + - ".csscomb.json" + - "Dockerfile.assets" + - "*_VERSION" + - "Gemfile{,.lock}" + - "Rakefile" + - "{babel.config,jest.config}.js" + - "config.ru" + - "{package.json,yarn.lock}" + - "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*" + - "doc/api/graphql/reference/*" # Files in this folder are auto-generated + # Backstage changes + - "Dangerfile" + - "danger/**/*" + - "{,ee/}fixtures/**/*" + - "{,ee/}rubocop/**/*" + - "{,ee/}spec/**/*" + - "doc/README.md" # Some RSpec test rely on this file + +.test-metadata:rules:retrieve-tests-metadata: + rules: + - <<: *if-default-refs + changes: *code-backstage-patterns + when: on_success + +.test-metadata:rules:update-tests-metadata: + rules: + - <<: *if-canonical-dot-com-gitlab-schedule + changes: *code-backstage-patterns + when: on_success + +.test-metadata:rules:flaky-examples-check: + rules: + - <<: *if-merge-request + changes: *code-backstage-patterns + when: on_success + .tests-metadata-state: - extends: - - .default-only - - .only:changes-code-backstage variables: TESTS_METADATA_S3_BUCKET: "gitlab-ce-cache" before_script: @@ -19,7 +69,9 @@ - rspec_profiling/ retrieve-tests-metadata: - extends: .tests-metadata-state + extends: + - .tests-metadata-state + - .test-metadata:rules:retrieve-tests-metadata stage: prepare cache: policy: pull @@ -28,7 +80,9 @@ retrieve-tests-metadata: - retrieve_tests_metadata update-tests-metadata: - extends: .tests-metadata-state + extends: + - .tests-metadata-state + - .test-metadata:rules:update-tests-metadata stage: post-test cache: policy: push @@ -36,27 +90,17 @@ update-tests-metadata: - retry gem install fog-aws mime-types activesupport rspec_profiling postgres-copy --no-document - source scripts/rspec_helpers.sh - update_tests_metadata - only: - refs: - - schedules - variables: - # Only update the Knapsack metadata on GitLab.com/gitlab-org/gitlab - - $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_PATH == "gitlab-org/gitlab" flaky-examples-check: extends: - .default-tags - .default-retry - - .default-only - - .only:changes-code-backstage + - .test-metadata:rules:flaky-examples-check image: ruby:2.6-alpine stage: post-test variables: NEW_FLAKY_SPECS_REPORT: rspec_flaky/report-new.json allow_failure: true - only: - refs: - - merge_requests artifacts: expire_in: 30d paths: diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue index 61a66513838..b7d9600ec40 100644 --- a/app/assets/javascripts/blob/components/blob_header.vue +++ b/app/assets/javascripts/blob/components/blob_header.vue @@ -2,8 +2,7 @@ import ViewerSwitcher from './blob_header_viewer_switcher.vue'; import DefaultActions from './blob_header_default_actions.vue'; import BlobFilepath from './blob_header_filepath.vue'; -import eventHub from '../event_hub'; -import { RICH_BLOB_VIEWER, SIMPLE_BLOB_VIEWER } from './constants'; +import { SIMPLE_BLOB_VIEWER } from './constants'; export default { components: { @@ -26,10 +25,15 @@ export default { required: false, default: false, }, + activeViewerType: { + type: String, + required: false, + default: SIMPLE_BLOB_VIEWER, + }, }, data() { return { - activeViewer: this.blob.richViewer ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER, + viewer: this.hideViewerSwitcher ? null : this.activeViewerType, }; }, computed: { @@ -40,19 +44,16 @@ export default { return !this.hideDefaultActions; }, }, - created() { - if (this.showViewerSwitcher) { - eventHub.$on('switch-viewer', this.setActiveViewer); - } - }, - beforeDestroy() { - if (this.showViewerSwitcher) { - eventHub.$off('switch-viewer', this.setActiveViewer); - } + watch: { + viewer(newVal, oldVal) { + if (!this.hideViewerSwitcher && newVal !== oldVal) { + this.$emit('viewer-changed', newVal); + } + }, }, methods: { - setActiveViewer(viewer) { - this.activeViewer = viewer; + proxyCopyRequest() { + this.$emit('copy'); }, }, }; @@ -66,11 +67,16 @@ export default { </blob-filepath> <div class="file-actions d-none d-sm-block"> - <viewer-switcher v-if="showViewerSwitcher" :blob="blob" :active-viewer="activeViewer" /> + <viewer-switcher v-if="showViewerSwitcher" v-model="viewer" /> <slot name="actions"></slot> - <default-actions v-if="showDefaultActions" :blob="blob" :active-viewer="activeViewer" /> + <default-actions + v-if="showDefaultActions" + :raw-path="blob.rawPath" + :active-viewer="viewer" + @copy="proxyCopyRequest" + /> </div> </div> </template> diff --git a/app/assets/javascripts/blob/components/blob_header_default_actions.vue b/app/assets/javascripts/blob/components/blob_header_default_actions.vue index e526fae0dba..f5157fba819 100644 --- a/app/assets/javascripts/blob/components/blob_header_default_actions.vue +++ b/app/assets/javascripts/blob/components/blob_header_default_actions.vue @@ -7,7 +7,6 @@ import { RICH_BLOB_VIEWER, SIMPLE_BLOB_VIEWER, } from './constants'; -import eventHub from '../event_hub'; export default { components: { @@ -19,8 +18,8 @@ export default { GlTooltip: GlTooltipDirective, }, props: { - blob: { - type: Object, + rawPath: { + type: String, required: true, }, activeViewer: { @@ -30,11 +29,8 @@ export default { }, }, computed: { - rawUrl() { - return this.blob.rawPath; - }, downloadUrl() { - return `${this.blob.rawPath}?inline=false`; + return `${this.rawPath}?inline=false`; }, copyDisabled() { return this.activeViewer === RICH_BLOB_VIEWER; @@ -42,7 +38,7 @@ export default { }, methods: { requestCopyContents() { - eventHub.$emit('copy'); + this.$emit('copy'); }, }, BTN_COPY_CONTENTS_TITLE, @@ -65,7 +61,7 @@ export default { v-gl-tooltip.hover :aria-label="$options.BTN_RAW_TITLE" :title="$options.BTN_RAW_TITLE" - :href="rawUrl" + :href="rawPath" target="_blank" > <gl-icon name="doc-code" :size="14" /> diff --git a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue index 13ea87c99b1..689fa7638f0 100644 --- a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue +++ b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue @@ -6,7 +6,6 @@ import { SIMPLE_BLOB_VIEWER, SIMPLE_BLOB_VIEWER_TITLE, } from './constants'; -import eventHub from '../event_hub'; export default { components: { @@ -18,11 +17,7 @@ export default { GlTooltip: GlTooltipDirective, }, props: { - blob: { - type: Object, - required: true, - }, - activeViewer: { + value: { type: String, default: SIMPLE_BLOB_VIEWER, required: false, @@ -30,16 +25,16 @@ export default { }, computed: { isSimpleViewer() { - return this.activeViewer === SIMPLE_BLOB_VIEWER; + return this.value === SIMPLE_BLOB_VIEWER; }, isRichViewer() { - return this.activeViewer === RICH_BLOB_VIEWER; + return this.value === RICH_BLOB_VIEWER; }, }, methods: { switchToViewer(viewer) { - if (viewer !== this.activeViewer) { - eventHub.$emit('switch-viewer', viewer); + if (viewer !== this.value) { + this.$emit('input', viewer); } }, }, diff --git a/app/assets/javascripts/blob/event_hub.js b/app/assets/javascripts/blob/event_hub.js deleted file mode 100644 index 0948c2e5352..00000000000 --- a/app/assets/javascripts/blob/event_hub.js +++ /dev/null @@ -1,3 +0,0 @@ -import Vue from 'vue'; - -export default new Vue(); diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index 8c84b98a108..0fab3ee0f3b 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -1,11 +1,11 @@ import Vue from 'vue'; -import VueRouter from 'vue-router'; +import IdeRouter from '~/ide/ide_router_extension'; import { joinPaths } from '~/lib/utils/url_utility'; import flash from '~/flash'; import store from './stores'; import { __ } from '~/locale'; -Vue.use(VueRouter); +Vue.use(IdeRouter); /** * Routes below /-/ide/: @@ -33,7 +33,7 @@ const EmptyRouterComponent = { }, }; -const router = new VueRouter({ +const router = new IdeRouter({ mode: 'history', base: joinPaths(gon.relative_url_root || '', '/-/ide/'), routes: [ diff --git a/app/assets/javascripts/ide/ide_router_extension.js b/app/assets/javascripts/ide/ide_router_extension.js new file mode 100644 index 00000000000..a146aca7283 --- /dev/null +++ b/app/assets/javascripts/ide/ide_router_extension.js @@ -0,0 +1,21 @@ +import VueRouter from 'vue-router'; +import { escapeFileUrl } from '~/lib/utils/url_utility'; + +// To allow special characters (like "#," for example) in the branch names, we +// should encode all the locations before those get processed by History API. +// Otherwise, paths get messed up so that the router receives incorrect +// branchid. The only way to do it consistently and in a more or less +// future-proof manner is, unfortunately, to monkey-patch VueRouter or, as +// suggested here, achieve the same more reliably by subclassing VueRouter and +// update the methods, used in WebIDE. +// +// More context: https://gitlab.com/gitlab-org/gitlab/issues/35473 + +export default class IDERouter extends VueRouter { + push(location, onComplete, onAbort) { + super.push(escapeFileUrl(location), onComplete, onAbort); + } + resolve(to, current, append) { + return super.resolve(escapeFileUrl(to), current, append); + } +} diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 267b49e9d98..1ff4f7bab97 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -194,12 +194,14 @@ export function redirectTo(url) { return window.location.assign(url); } +export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/'); + export function webIDEUrl(route = undefined) { let returnUrl = `${gon.relative_url_root || ''}/-/ide/`; if (route) { returnUrl += `project${route.replace(new RegExp(`^${gon.relative_url_root || ''}`), '')}`; } - return returnUrl; + return escapeFileUrl(returnUrl); } /** @@ -313,8 +315,6 @@ export const setUrlParams = (params, url = window.location.href, clearParams = f return urlObj.toString(); }; -export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/'); - export function urlIsDifferent(url, compare = String(window.location)) { return url !== compare; } diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js index 1b94fb06107..803f4e37705 100644 --- a/app/assets/javascripts/pages/projects/graphs/charts/index.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -1,47 +1,15 @@ -import $ from 'jquery'; -import Chart from 'chart.js'; -import { barChartOptions, pieChartOptions } from '~/lib/utils/chart_utils'; +import Vue from 'vue'; +import { __ } from '~/locale'; +import { GlColumnChart } from '@gitlab/ui/dist/charts'; +import SeriesDataMixin from './series_data_mixin'; document.addEventListener('DOMContentLoaded', () => { - const projectChartData = JSON.parse(document.getElementById('projectChartData').innerHTML); + const languagesContainer = document.getElementById('js-languages-chart'); + const monthContainer = document.getElementById('js-month-chart'); + const weekdayContainer = document.getElementById('js-weekday-chart'); + const hourContainer = document.getElementById('js-hour-chart'); - const barChart = (selector, data) => { - // get selector by context - const ctx = selector.get(0).getContext('2d'); - // pointing parent container to make chart.js inherit its width - const container = $(selector).parent(); - selector.attr('width', $(container).width()); - - // Scale fonts if window width lower than 768px (iPad portrait) - const shouldAdjustFontSize = window.innerWidth < 768; - return new Chart(ctx, { - type: 'bar', - data, - options: barChartOptions(shouldAdjustFontSize), - }); - }; - - const pieChart = (context, data) => { - const options = pieChartOptions(); - - return new Chart(context, { - type: 'pie', - data, - options, - }); - }; - - const chartData = data => ({ - labels: Object.keys(data), - datasets: [ - { - backgroundColor: 'rgba(220,220,220,0.5)', - borderColor: 'rgba(220,220,220,1)', - borderWidth: 1, - data: Object.values(data), - }, - ], - }); + const LANGUAGE_CHART_HEIGHT = 300; const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => { if (firstDayOfWeek === 0) { @@ -58,28 +26,115 @@ document.addEventListener('DOMContentLoaded', () => { }, {}); }; - const hourData = chartData(projectChartData.hour); - barChart($('#hour-chart'), hourData); - - const weekDays = reorderWeekDays(projectChartData.weekDays, gon.first_day_of_week); - const dayData = chartData(weekDays); - barChart($('#weekday-chart'), dayData); + // eslint-disable-next-line no-new + new Vue({ + el: languagesContainer, + components: { + GlColumnChart, + }, + data() { + return { + chartData: JSON.parse(languagesContainer.dataset.chartData), + }; + }, + computed: { + seriesData() { + return { full: this.chartData.map(d => [d.label, d.value]) }; + }, + }, + render(h) { + return h(GlColumnChart, { + props: { + data: this.seriesData, + xAxisTitle: __('Used programming language'), + yAxisTitle: __('Percentage'), + xAxisType: 'category', + }, + attrs: { + height: LANGUAGE_CHART_HEIGHT, + }, + }); + }, + }); - const monthData = chartData(projectChartData.month); - barChart($('#month-chart'), monthData); + // eslint-disable-next-line no-new + new Vue({ + el: monthContainer, + components: { + GlColumnChart, + }, + mixins: [SeriesDataMixin], + data() { + return { + chartData: JSON.parse(monthContainer.dataset.chartData), + }; + }, + render(h) { + return h(GlColumnChart, { + props: { + data: this.seriesData, + xAxisTitle: __('Day of month'), + yAxisTitle: __('No. of commits'), + xAxisType: 'category', + }, + }); + }, + }); - const data = { - datasets: [ - { - data: projectChartData.languages.map(x => x.value), - backgroundColor: projectChartData.languages.map(x => x.color), - hoverBackgroundColor: projectChartData.languages.map(x => x.highlight), + // eslint-disable-next-line no-new + new Vue({ + el: weekdayContainer, + components: { + GlColumnChart, + }, + data() { + return { + chartData: JSON.parse(weekdayContainer.dataset.chartData), + }; + }, + computed: { + seriesData() { + const weekDays = reorderWeekDays(this.chartData, gon.first_day_of_week); + const data = Object.keys(weekDays).reduce((acc, key) => { + acc.push([key, weekDays[key]]); + return acc; + }, []); + return { full: data }; }, - ], - labels: projectChartData.languages.map(x => x.label), - }; - const ctx = $('#languages-chart') - .get(0) - .getContext('2d'); - pieChart(ctx, data); + }, + render(h) { + return h(GlColumnChart, { + props: { + data: this.seriesData, + xAxisTitle: __('Weekday'), + yAxisTitle: __('No. of commits'), + xAxisType: 'category', + }, + }); + }, + }); + + // eslint-disable-next-line no-new + new Vue({ + el: hourContainer, + components: { + GlColumnChart, + }, + mixins: [SeriesDataMixin], + data() { + return { + chartData: JSON.parse(hourContainer.dataset.chartData), + }; + }, + render(h) { + return h(GlColumnChart, { + props: { + data: this.seriesData, + xAxisTitle: __('Hour (UTC)'), + yAxisTitle: __('No. of commits'), + xAxisType: 'category', + }, + }); + }, + }); }); diff --git a/app/assets/javascripts/pages/projects/graphs/charts/series_data_mixin.js b/app/assets/javascripts/pages/projects/graphs/charts/series_data_mixin.js new file mode 100644 index 00000000000..941427a1ac3 --- /dev/null +++ b/app/assets/javascripts/pages/projects/graphs/charts/series_data_mixin.js @@ -0,0 +1,11 @@ +export default { + computed: { + seriesData() { + const data = Object.keys(this.chartData).reduce((acc, key) => { + acc.push([key, this.chartData[key]]); + return acc; + }, []); + return { full: data }; + }, + }, +}; diff --git a/app/controllers/projects/alerting/notifications_controller.rb b/app/controllers/projects/alerting/notifications_controller.rb new file mode 100644 index 00000000000..1fe31863469 --- /dev/null +++ b/app/controllers/projects/alerting/notifications_controller.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Projects + module Alerting + class NotificationsController < Projects::ApplicationController + respond_to :json + + skip_before_action :verify_authenticity_token + skip_before_action :project + + prepend_before_action :repository, :project_without_auth + + def create + token = extract_alert_manager_token(request) + result = notify_service.execute(token) + + head(response_status(result)) + end + + private + + def project_without_auth + @project ||= Project + .find_by_full_path("#{params[:namespace_id]}/#{params[:project_id]}") + end + + def extract_alert_manager_token(request) + Doorkeeper::OAuth::Token.from_bearer_authorization(request) + end + + def notify_service + Projects::Alerting::NotifyService + .new(project, current_user, notification_payload) + end + + def response_status(result) + return :ok if result.success? + + result.http_status + end + + def notification_payload + params.permit![:notification] + end + end + end +end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 7c0f4da355d..77a320f8925 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -27,7 +27,7 @@ module BlobHelper "#{current_user.namespace.full_path}/#{project.path}" end - segments = [ide_path, 'project', project_path, 'edit', ref] + segments = [ide_path, 'project', project_path, 'edit', encode_ide_path(ref)] segments.concat(['-', encode_ide_path(path)]) if path.present? File.join(segments) end diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 152aa7b3218..fcbfda8fbc2 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -77,7 +77,11 @@ class ContainerRepository < ApplicationRecord end def delete_tag_by_digest(digest) - client.delete_repository_tag(self.path, digest) + client.delete_repository_tag_by_digest(self.path, digest) + end + + def delete_tag_by_name(name) + client.delete_repository_tag_by_name(self.path, name) end def self.build_from_path(path) diff --git a/app/models/project.rb b/app/models/project.rb index 44701ef792a..b2ac9c99ab6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -138,6 +138,7 @@ class Project < ApplicationRecord has_many :boards # Project services + has_one :alerts_service has_one :campfire_service has_one :discord_service has_one :drone_ci_service @@ -2330,6 +2331,10 @@ class Project < ApplicationRecord protected_branches.limit(limit) end + def alerts_service_activated? + false + end + private def closest_namespace_setting(name) diff --git a/app/models/project_services/alerts_service.rb b/app/models/project_services/alerts_service.rb new file mode 100644 index 00000000000..2f7902d9617 --- /dev/null +++ b/app/models/project_services/alerts_service.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'securerandom' + +class AlertsService < Service + has_one :data, class_name: 'AlertsServiceData', autosave: true, + inverse_of: :service, foreign_key: :service_id + + attribute :token, :string + delegate :token, :token=, :token_changed?, :token_was, to: :data + + validates :token, presence: true, if: :activated? + + before_validation :prevent_token_assignment + before_validation :ensure_token, if: :activated? + + def url + url_helpers.project_alerts_notify_url(project, format: :json) + end + + def json_fields + super + %w(token) + end + + def editable? + false + end + + def show_active_box? + false + end + + def can_test? + false + end + + def title + _('Alerts endpoint') + end + + def description + _('Receive alerts on GitLab from any source') + end + + def detailed_description + description + end + + def self.to_param + 'alerts' + end + + def self.supported_events + %w() + end + + def data + super || build_data + end + + private + + def prevent_token_assignment + self.token = token_was if token.present? && token_changed? + end + + def ensure_token + self.token = generate_token if token.blank? + end + + def generate_token + SecureRandom.hex + end + + def url_helpers + Gitlab::Routing.url_helpers + end +end diff --git a/app/models/project_services/alerts_service_data.rb b/app/models/project_services/alerts_service_data.rb new file mode 100644 index 00000000000..5a52ed83455 --- /dev/null +++ b/app/models/project_services/alerts_service_data.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'securerandom' + +class AlertsServiceData < ApplicationRecord + belongs_to :service, class_name: 'AlertsService' + + validates :service, presence: true + + attr_encrypted :token, + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-gcm' +end diff --git a/app/services/projects/alerting/notify_service.rb b/app/services/projects/alerting/notify_service.rb new file mode 100644 index 00000000000..4ca3b154e4b --- /dev/null +++ b/app/services/projects/alerting/notify_service.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Projects + module Alerting + class NotifyService < BaseService + include Gitlab::Utils::StrongMemoize + + def execute(token) + return forbidden unless alerts_service_activated? + return unauthorized unless valid_token?(token) + + process_incident_issues + + ServiceResponse.success + rescue Gitlab::Alerting::NotificationPayloadParser::BadPayloadError + bad_request + end + + private + + delegate :alerts_service, :alerts_service_activated?, to: :project + + def process_incident_issues + IncidentManagement::ProcessAlertWorker + .perform_async(project.id, parsed_payload) + end + + def parsed_payload + Gitlab::Alerting::NotificationPayloadParser.call(params.to_h) + end + + def valid_token?(token) + token == alerts_service.token + end + + def bad_request + ServiceResponse.error(message: 'Bad Request', http_status: 400) + end + + def unauthorized + ServiceResponse.error(message: 'Unauthorized', http_status: 401) + end + + def forbidden + ServiceResponse.error(message: 'Forbidden', http_status: 403) + end + end + end +end diff --git a/app/services/projects/container_repository/delete_tags_service.rb b/app/services/projects/container_repository/delete_tags_service.rb index 88ff3c2c9df..d19f275e928 100644 --- a/app/services/projects/container_repository/delete_tags_service.rb +++ b/app/services/projects/container_repository/delete_tags_service.rb @@ -14,12 +14,25 @@ module Projects private + # Delete tags by name with a single DELETE request. This is only supported + # by the GitLab Container Registry fork. See + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23325 for details. + def fast_delete(container_repository, tag_names) + deleted_tags = tag_names.select do |name| + container_repository.delete_tag_by_name(name) + end + + deleted_tags.any? ? success(deleted: deleted_tags) : error('could not delete tags') + end + # Replace a tag on the registry with a dummy tag. # This is a hack as the registry doesn't support deleting individual # tags. This code effectively pushes a dummy image and assigns the tag to it. # This way when the tag is deleted only the dummy image is affected. + # This is used to preverse compatibility with third-party registries that + # don't support fast delete. # See https://gitlab.com/gitlab-org/gitlab/issues/15737 for a discussion - def smart_delete(container_repository, tag_names) + def slow_delete(container_repository, tag_names) # generates the blobs for the dummy image dummy_manifest = container_repository.client.generate_empty_manifest(container_repository.path) return error('could not generate manifest') if dummy_manifest.nil? @@ -36,6 +49,15 @@ module Projects end end + def smart_delete(container_repository, tag_names) + fast_delete_enabled = Feature.enabled?(:container_registry_fast_tag_delete, default_enabled: true) + if fast_delete_enabled && container_repository.client.supports_tag_delete? + fast_delete(container_repository, tag_names) + else + slow_delete(container_repository, tag_names) + end + end + # update the manifests of the tags with the new dummy image def replace_tag_manifests(container_repository, dummy_manifest, tag_names) deleted_tags = {} diff --git a/app/services/projects/lsif_data_service.rb b/app/services/projects/lsif_data_service.rb index 00103f364bf..1282a0736e7 100644 --- a/app/services/projects/lsif_data_service.rb +++ b/app/services/projects/lsif_data_service.rb @@ -2,7 +2,8 @@ module Projects class LsifDataService - attr_reader :file, :project, :path, :commit_id + attr_reader :file, :project, :path, :commit_id, + :docs, :doc_ranges, :ranges, :def_refs CACHE_EXPIRE_IN = 1.hour @@ -14,19 +15,18 @@ module Projects end def execute - docs, doc_ranges, ranges = - fetch_data.values_at('docs', 'doc_ranges', 'ranges') - - doc_id = doc_id_from(docs) + fetch_data! doc_ranges[doc_id]&.map do |range_id| - line_data, column_data = ranges[range_id]['loc'] + location, ref_id = ranges[range_id].values_at('loc', 'ref_id') + line_data, column_data = location { start_line: line_data.first, end_line: line_data.last, start_char: column_data.first, - end_char: column_data.last + end_char: column_data.last, + definition_url: definition_url_for(def_refs[ref_id]) } end end @@ -47,8 +47,17 @@ module Projects end end - def doc_id_from(docs) - docs.reduce(nil) do |doc_id, (id, doc_path)| + def fetch_data! + data = fetch_data + + @docs = data['docs'] + @doc_ranges = data['doc_ranges'] + @ranges = data['ranges'] + @def_refs = data['def_refs'] + end + + def doc_id + @doc_id ||= docs.reduce(nil) do |doc_id, (id, doc_path)| next doc_id unless doc_path =~ /#{path}$/ if doc_id.nil? || docs[doc_id].size > doc_path.size @@ -58,5 +67,24 @@ module Projects doc_id end end + + def dir_absolute_path + @dir_absolute_path ||= docs[doc_id]&.delete_suffix(path) + end + + def definition_url_for(ref_id) + return unless range = ranges[ref_id] + + def_doc_id, location = range.values_at('doc_id', 'loc') + localized_doc_url = docs[def_doc_id].delete_prefix(dir_absolute_path) + + # location is stored as [[start_line, end_line], [start_char, end_char]] + start_line = location.first.first + + line_anchor = "L#{start_line + 1}" + definition_ref_path = [commit_id, localized_doc_url].join('/') + + Gitlab::Routing.url_helpers.project_blob_path(project, definition_ref_path, anchor: line_anchor) + end end end diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml index 93a43b5d1ea..b38449b3ab9 100644 --- a/app/views/projects/graphs/charts.html.haml +++ b/app/views/projects/graphs/charts.html.haml @@ -7,20 +7,7 @@ %p = _("Measured in bytes of code. Excludes generated and vendored code.") - .row - .col-md-4 - %ul.bordered-list - - @languages.each do |language| - %li - %span{ style: "color: #{language[:color]}" } - = icon('circle') - - = language[:label] - .float-right - = language[:value] - \% - .col-md-8 - %canvas#languages-chart{ height: 400 } + #js-languages-chart{ data: { chart_data: @languages.to_json.html_safe } } .repo-charts .sub-header-block.border-top @@ -60,27 +47,18 @@ %p.slead = _("Commits per day of month") %div - %canvas#month-chart + #js-month-chart{ data: { chart_data: @commits_per_month.to_json.html_safe } } .row .col-md-6 .col-md-6 %p.slead = _("Commits per weekday") %div - %canvas#weekday-chart + #js-weekday-chart{ data: { chart_data: @commits_per_week_days.to_json.html_safe } } .row .col-md-6 .col-md-6 %p.slead = _("Commits per day hour (UTC)") %div - %canvas#hour-chart - --# haml-lint:disable InlineJavaScript -%script#projectChartData{ type: "application/json" } - - projectChartData = {}; - - projectChartData['hour'] = @commits_per_time - - projectChartData['weekDays'] = @commits_per_week_days - - projectChartData['month'] = @commits_per_month - - projectChartData['languages'] = @languages - = projectChartData.to_json.html_safe + #js-hour-chart{ data: { chart_data: @commits_per_time.to_json.html_safe } } diff --git a/changelogs/unreleased/31832-improve-performance-of-the-container-registry-delete-tags-api.yml b/changelogs/unreleased/31832-improve-performance-of-the-container-registry-delete-tags-api.yml new file mode 100644 index 00000000000..a803f08b884 --- /dev/null +++ b/changelogs/unreleased/31832-improve-performance-of-the-container-registry-delete-tags-api.yml @@ -0,0 +1,5 @@ +--- +title: Improve performance of the Container Registry delete tags API +merge_request: 23325 +author: +type: performance diff --git a/changelogs/unreleased/35473-webide-hash-in-branch-name.yml b/changelogs/unreleased/35473-webide-hash-in-branch-name.yml new file mode 100644 index 00000000000..f49ead4fe28 --- /dev/null +++ b/changelogs/unreleased/35473-webide-hash-in-branch-name.yml @@ -0,0 +1,5 @@ +--- +title: 'WebIDE: Support # in branch names' +merge_request: 24717 +author: +type: changed diff --git a/changelogs/unreleased/improve-background-migration-check.yml b/changelogs/unreleased/improve-background-migration-check.yml new file mode 100644 index 00000000000..5734f53f86f --- /dev/null +++ b/changelogs/unreleased/improve-background-migration-check.yml @@ -0,0 +1,5 @@ +--- +title: Add more accurate way of counting remaining background migrations before upgrading +merge_request: +author: +type: fixed diff --git a/config/initializers/rack_timeout.rb b/config/initializers/rack_timeout.rb index 1f1264de208..5d5a5fcf980 100644 --- a/config/initializers/rack_timeout.rb +++ b/config/initializers/rack_timeout.rb @@ -12,6 +12,8 @@ if Gitlab::Runtime.puma? && !Rails.env.test? require 'rack/timeout/base' + Rack::Timeout::Logger.level = Logger::ERROR + Gitlab::Application.configure do |config| config.middleware.insert_before(Rack::Runtime, Rack::Timeout, service_timeout: ENV.fetch('GITLAB_RAILS_RACK_TIMEOUT', 60).to_i, diff --git a/config/routes/project.rb b/config/routes/project.rb index 73625416b4b..f153082f118 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -323,6 +323,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end + post 'alerts/notify', to: 'alerting/notifications#create' + resources :pipelines, only: [:index, :new, :create, :show, :destroy] do collection do resource :pipelines_settings, path: 'settings', only: [:show, :update] diff --git a/doc/administration/geo/replication/updating_the_geo_nodes.md b/doc/administration/geo/replication/updating_the_geo_nodes.md index 426eb54c66a..d094ccf3e54 100644 --- a/doc/administration/geo/replication/updating_the_geo_nodes.md +++ b/doc/administration/geo/replication/updating_the_geo_nodes.md @@ -11,6 +11,8 @@ Updating Geo nodes involves performing: Depending on which version of Geo you are updating to/from, there may be different steps. +- [Updating to GitLab 12.7](version_specific_updates.md#updating-to-gitlab-127) +- [Updating to GitLab 12.2](version_specific_updates.md#updating-to-gitlab-122) - [Updating to GitLab 12.1](version_specific_updates.md#updating-to-gitlab-121) - [Updating to GitLab 10.8](version_specific_updates.md#updating-to-gitlab-108) - [Updating to GitLab 10.6](version_specific_updates.md#updating-to-gitlab-106) diff --git a/doc/update/README.md b/doc/update/README.md index 9762ecffc6d..1bd2770b957 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -116,14 +116,14 @@ following command: **For Omnibus installations** ```shell -sudo gitlab-rails runner -e production 'puts Sidekiq::Queue.new("background_migration").size' +sudo gitlab-rails runner -e production 'puts Gitlab::BackgroundMigration.remaining' ``` **For installations from source** ``` cd /home/git/gitlab -sudo -u git -H bundle exec rails runner -e production 'puts Sidekiq::Queue.new("background_migration").size' +sudo -u git -H bundle exec rails runner -e production 'puts Gitlab::BackgroundMigration.remaining' ``` ## Upgrading to a new major version diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb index bc0347f6ea1..12f7f04634f 100644 --- a/lib/container_registry/client.rb +++ b/lib/container_registry/client.rb @@ -6,6 +6,8 @@ require 'digest' module ContainerRegistry class Client + include Gitlab::Utils::StrongMemoize + attr_accessor :uri DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE = 'application/vnd.docker.distribution.manifest.v2+json' @@ -35,10 +37,25 @@ module ContainerRegistry response.headers['docker-content-digest'] if response.success? end - def delete_repository_tag(name, reference) - result = faraday.delete("/v2/#{name}/manifests/#{reference}") + def delete_repository_tag_by_digest(name, reference) + delete_if_exists("/v2/#{name}/manifests/#{reference}") + end - result.success? || result.status == 404 + def delete_repository_tag_by_name(name, reference) + delete_if_exists("/v2/#{name}/tags/reference/#{reference}") + end + + # Check if the registry supports tag deletion. This is only supported by the + # GitLab registry fork. The fastest and safest way to check this is to send + # an OPTIONS request to /v2/<name>/tags/reference/<tag>, using a random + # repository name and tag (the registry won't check if they exist). + # Registries that support tag deletion will reply with a 200 OK and include + # the DELETE method in the Allow header. Others reply with an 404 Not Found. + def supports_tag_delete? + strong_memoize(:supports_tag_delete) do + response = faraday.run_request(:options, '/v2/name/tags/reference/tag', '', {}) + response.success? && response.headers['allow']&.include?('DELETE') + end end def upload_raw_blob(path, blob) @@ -86,9 +103,7 @@ module ContainerRegistry end def delete_blob(name, digest) - result = faraday.delete("/v2/#{name}/blobs/#{digest}") - - result.success? || result.status == 404 + delete_if_exists("/v2/#{name}/blobs/#{digest}") end def put_tag(name, reference, manifest) @@ -163,6 +178,12 @@ module ContainerRegistry conn.adapter :net_http end end + + def delete_if_exists(path) + result = faraday.delete(path) + + result.success? || result.status == 404 + end end end diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb index 3c308258a3f..e1a2891e43a 100644 --- a/lib/container_registry/tag.rb +++ b/lib/container_registry/tag.rb @@ -118,7 +118,7 @@ module ContainerRegistry def unsafe_delete return unless digest - client.delete_repository_tag(repository.path, digest) + client.delete_repository_tag_by_digest(repository.path, digest) end end end diff --git a/lib/gitlab/background_migration.rb b/lib/gitlab/background_migration.rb index ddd6b11eebb..6a16c37e880 100644 --- a/lib/gitlab/background_migration.rb +++ b/lib/gitlab/background_migration.rb @@ -58,6 +58,14 @@ module Gitlab migration_class_for(class_name).new.perform(*arguments) end + def self.remaining + scheduled = Sidekiq::ScheduledSet.new.count do |job| + job.queue == self.queue + end + + scheduled + Sidekiq::Queue.new(self.queue).size + end + def self.exists?(migration_class, additional_queues = []) enqueued = Sidekiq::Queue.new(self.queue) scheduled = Sidekiq::ScheduledSet.new diff --git a/locale/gitlab.pot b/locale/gitlab.pot index fceb327aed1..6c5d4e6785e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5996,6 +5996,9 @@ msgstr "" msgid "Date range cannot exceed %{maxDateRange} days." msgstr "" +msgid "Day of month" +msgstr "" + msgid "DayTitle|F" msgstr "" @@ -10004,6 +10007,9 @@ msgstr "" msgid "Hook was successfully updated." msgstr "" +msgid "Hour (UTC)" +msgstr "" + msgid "Housekeeping" msgstr "" @@ -12803,6 +12809,9 @@ msgstr "" msgid "No, not interested right now" msgstr "" +msgid "No. of commits" +msgstr "" + msgid "Nobody has starred this repository yet" msgstr "" @@ -13531,6 +13540,9 @@ msgstr "" msgid "People without permission will never get a notification." msgstr "" +msgid "Percentage" +msgstr "" + msgid "Perform advanced options such as changing path, transferring, or removing the group." msgstr "" @@ -20797,6 +20809,9 @@ msgstr "" msgid "Used by members to sign in to your group in GitLab" msgstr "" +msgid "Used programming language" +msgstr "" + msgid "Used to help configure your identity provider" msgstr "" @@ -21486,6 +21501,9 @@ msgstr "" msgid "Wednesday" msgstr "" +msgid "Weekday" +msgstr "" + msgid "Weeks" msgstr "" diff --git a/spec/controllers/projects/alerting/notifications_controller_spec.rb b/spec/controllers/projects/alerting/notifications_controller_spec.rb new file mode 100644 index 00000000000..a56ac59215f --- /dev/null +++ b/spec/controllers/projects/alerting/notifications_controller_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::Alerting::NotificationsController do + let_it_be(:project) { create(:project) } + let_it_be(:environment) { create(:environment, project: project) } + + describe 'POST #create' do + let(:service_response) { ServiceResponse.success } + let(:notify_service) { instance_double(Projects::Alerting::NotifyService, execute: service_response) } + + around do |example| + ForgeryProtection.with_forgery_protection { example.run } + end + + before do + allow(Projects::Alerting::NotifyService).to receive(:new).and_return(notify_service) + end + + def make_request(body = {}) + post :create, params: project_params, body: body.to_json, as: :json + end + + context 'when notification service succeeds' do + let(:payload) do + { + title: 'Alert title', + hosts: 'https://gitlab.com' + } + end + + let(:permitted_params) { ActionController::Parameters.new(payload).permit! } + + it 'responds with ok' do + make_request + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'does not pass excluded parameters to the notify service' do + make_request(payload) + + expect(Projects::Alerting::NotifyService) + .to have_received(:new) + .with(project, nil, permitted_params) + end + end + + context 'when notification service fails' do + let(:service_response) { ServiceResponse.error(message: 'Unauthorized', http_status: 401) } + + it 'responds with the service response' do + make_request + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'bearer token' do + context 'when set' do + it 'extracts bearer token' do + request.headers['HTTP_AUTHORIZATION'] = 'Bearer some token' + + expect(notify_service).to receive(:execute).with('some token') + + make_request + end + + it 'pass nil if cannot extract a non-bearer token' do + request.headers['HTTP_AUTHORIZATION'] = 'some token' + + expect(notify_service).to receive(:execute).with(nil) + + make_request + end + end + + context 'when missing' do + it 'passes nil' do + expect(notify_service).to receive(:execute).with(nil) + + make_request + end + end + end + end + + def project_params(opts = {}) + opts.reverse_merge(namespace_id: project.namespace, project_id: project) + end +end diff --git a/spec/factories/services.rb b/spec/factories/services.rb index 5d62b3cb9c9..ffa51abf26f 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -44,6 +44,16 @@ FactoryBot.define do end end + factory :alerts_service do + project + type { 'AlertsService' } + active { true } + + trait :inactive do + active { false } + end + end + factory :drone_ci_service do project active { true } diff --git a/spec/features/projects/graph_spec.rb b/spec/features/projects/graph_spec.rb index 562dbeb816a..6b2a9a6b852 100644 --- a/spec/features/projects/graph_spec.rb +++ b/spec/features/projects/graph_spec.rb @@ -22,20 +22,12 @@ describe 'Project Graph', :js do end end - shared_examples 'page should have languages graphs' do - it 'renders languages' do - expect(page).to have_content(/Ruby 66.* %/) - expect(page).to have_content(/JavaScript 22.* %/) - end - end - context 'commits graph' do before do visit commits_project_graph_path(project, 'master') end it_behaves_like 'page should have commits graphs' - it_behaves_like 'page should have languages graphs' end context 'languages graph' do @@ -44,7 +36,6 @@ describe 'Project Graph', :js do end it_behaves_like 'page should have commits graphs' - it_behaves_like 'page should have languages graphs' end context 'charts graph' do @@ -53,7 +44,6 @@ describe 'Project Graph', :js do end it_behaves_like 'page should have commits graphs' - it_behaves_like 'page should have languages graphs' end context 'chart graph with HTML escaped branch name' do diff --git a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap index b77ca28b9d8..2878ad492a4 100644 --- a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap +++ b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap @@ -12,13 +12,12 @@ exports[`Blob Header Default Actions rendering matches the snapshot 1`] = ` class="file-actions d-none d-sm-block" > <viewer-switcher-stub - activeviewer="rich" - blob="[object Object]" + value="simple" /> <default-actions-stub - activeviewer="rich" - blob="[object Object]" + activeviewer="simple" + rawpath="/flightjs/flight/snippets/51/raw" /> </div> </div> diff --git a/spec/frontend/blob/components/blob_header_default_actions_spec.js b/spec/frontend/blob/components/blob_header_default_actions_spec.js index fe0edffd12d..5da0d40ab14 100644 --- a/spec/frontend/blob/components/blob_header_default_actions_spec.js +++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js @@ -8,7 +8,6 @@ import { } from '~/blob/components/constants'; import { GlButtonGroup, GlButton } from '@gitlab/ui'; import { Blob } from './mock_data'; -import eventHub from '~/blob/event_hub'; describe('Blob Header Default Actions', () => { let wrapper; @@ -16,10 +15,10 @@ describe('Blob Header Default Actions', () => { let buttons; const hrefPrefix = 'http://localhost'; - function createComponent(blobProps = {}, propsData = {}) { + function createComponent(propsData = {}) { wrapper = mount(BlobHeaderActions, { propsData: { - blob: Object.assign({}, Blob, blobProps), + rawPath: Blob.rawPath, ...propsData, }, }); @@ -60,12 +59,9 @@ describe('Blob Header Default Actions', () => { }); it('renders "Copy file contents" button as disables if the viewer is Rich', () => { - createComponent( - {}, - { - activeViewer: RICH_BLOB_VIEWER, - }, - ); + createComponent({ + activeViewer: RICH_BLOB_VIEWER, + }); buttons = wrapper.findAll(GlButton); expect(buttons.at(0).attributes('disabled')).toBeTruthy(); @@ -74,10 +70,10 @@ describe('Blob Header Default Actions', () => { describe('functionally', () => { it('emits an event when a Copy Contents button is clicked', () => { - jest.spyOn(eventHub, '$emit'); + jest.spyOn(wrapper.vm, '$emit'); buttons.at(0).vm.$emit('click'); - expect(eventHub.$emit).toHaveBeenCalledWith('copy'); + expect(wrapper.vm.$emit).toHaveBeenCalledWith('copy'); }); }); }); diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js index 7d1443fb069..d410ef10fc9 100644 --- a/spec/frontend/blob/components/blob_header_spec.js +++ b/spec/frontend/blob/components/blob_header_spec.js @@ -3,7 +3,6 @@ import BlobHeader from '~/blob/components/blob_header.vue'; import ViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue'; import DefaultActions from '~/blob/components/blob_header_default_actions.vue'; import BlobFilepath from '~/blob/components/blob_header_filepath.vue'; -import eventHub from '~/blob/event_hub'; import { Blob } from './mock_data'; @@ -21,10 +20,6 @@ describe('Blob Header Default Actions', () => { }); } - beforeEach(() => { - createComponent(); - }); - afterEach(() => { wrapper.destroy(); }); @@ -96,37 +91,48 @@ describe('Blob Header Default Actions', () => { describe('functionality', () => { const newViewer = 'Foo Bar'; + const activeViewerType = 'Alpha Beta'; - it('listens to "switch-view" event when viewer switcher is shown and updates activeViewer', () => { - expect(wrapper.vm.showViewerSwitcher).toBe(true); - eventHub.$emit('switch-viewer', newViewer); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.activeViewer).toBe(newViewer); - }); - }); - - it('does not update active viewer if the switcher is not shown', () => { - const activeViewer = 'Alpha Beta'; + const factory = (hideViewerSwitcher = false) => { createComponent( {}, + {}, { - data() { - return { - activeViewer, - }; - }, - }, - { - hideViewerSwitcher: true, + activeViewerType, + hideViewerSwitcher, }, ); + }; + + it('by default sets viewer data based on activeViewerType', () => { + factory(); + expect(wrapper.vm.viewer).toBe(activeViewerType); + }); + + it('sets viewer to null if the viewer switcher should be hidden', () => { + factory(true); + expect(wrapper.vm.viewer).toBe(null); + }); + + it('watches the changes in viewer data and emits event when the change is registered', () => { + factory(); + jest.spyOn(wrapper.vm, '$emit'); + wrapper.vm.viewer = newViewer; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.$emit).toHaveBeenCalledWith('viewer-changed', newViewer); + }); + }); + + it('does not emit event if the switcher is not rendered', () => { + factory(true); expect(wrapper.vm.showViewerSwitcher).toBe(false); - eventHub.$emit('switch-viewer', newViewer); + jest.spyOn(wrapper.vm, '$emit'); + wrapper.vm.viewer = newViewer; return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.activeViewer).toBe(activeViewer); + expect(wrapper.vm.$emit).not.toHaveBeenCalled(); }); }); }); diff --git a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js index 88e9eeea994..f1a7ac8b21a 100644 --- a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js +++ b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js @@ -7,18 +7,13 @@ import { SIMPLE_BLOB_VIEWER_TITLE, } from '~/blob/components/constants'; import { GlButtonGroup, GlButton } from '@gitlab/ui'; -import { Blob } from './mock_data'; -import eventHub from '~/blob/event_hub'; describe('Blob Header Viewer Switcher', () => { let wrapper; - function createComponent(blobProps = {}, propsData = {}) { + function createComponent(propsData = {}) { wrapper = mount(BlobHeaderViewerSwitcher, { - propsData: { - blob: Object.assign({}, Blob, blobProps), - ...propsData, - }, + propsData, }); } @@ -29,7 +24,7 @@ describe('Blob Header Viewer Switcher', () => { describe('intiialization', () => { it('is initialized with simple viewer as active', () => { createComponent(); - expect(wrapper.vm.activeViewer).toBe(SIMPLE_BLOB_VIEWER); + expect(wrapper.vm.value).toBe(SIMPLE_BLOB_VIEWER); }); }); @@ -60,42 +55,42 @@ describe('Blob Header Viewer Switcher', () => { let simpleBtn; let richBtn; - function factory(propsOptions = {}) { - createComponent({}, propsOptions); + function factory(propsData = {}) { + createComponent(propsData); buttons = wrapper.findAll(GlButton); simpleBtn = buttons.at(0); richBtn = buttons.at(1); - jest.spyOn(eventHub, '$emit'); + jest.spyOn(wrapper.vm, '$emit'); } it('does not switch the viewer if the selected one is already active', () => { factory(); - expect(wrapper.vm.activeViewer).toBe(SIMPLE_BLOB_VIEWER); + expect(wrapper.vm.value).toBe(SIMPLE_BLOB_VIEWER); simpleBtn.vm.$emit('click'); - expect(wrapper.vm.activeViewer).toBe(SIMPLE_BLOB_VIEWER); - expect(eventHub.$emit).not.toHaveBeenCalled(); + expect(wrapper.vm.value).toBe(SIMPLE_BLOB_VIEWER); + expect(wrapper.vm.$emit).not.toHaveBeenCalled(); }); it('emits an event when a Rich Viewer button is clicked', () => { factory(); - expect(wrapper.vm.activeViewer).toBe(SIMPLE_BLOB_VIEWER); + expect(wrapper.vm.value).toBe(SIMPLE_BLOB_VIEWER); richBtn.vm.$emit('click'); return wrapper.vm.$nextTick().then(() => { - expect(eventHub.$emit).toHaveBeenCalledWith('switch-viewer', RICH_BLOB_VIEWER); + expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', RICH_BLOB_VIEWER); }); }); it('emits an event when a Simple Viewer button is clicked', () => { factory({ - activeViewer: RICH_BLOB_VIEWER, + value: RICH_BLOB_VIEWER, }); simpleBtn.vm.$emit('click'); return wrapper.vm.$nextTick().then(() => { - expect(eventHub.$emit).toHaveBeenCalledWith('switch-viewer', SIMPLE_BLOB_VIEWER); + expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', SIMPLE_BLOB_VIEWER); }); }); }); diff --git a/spec/frontend/ide/ide_router_extension_spec.js b/spec/frontend/ide/ide_router_extension_spec.js new file mode 100644 index 00000000000..3e29ecc4a90 --- /dev/null +++ b/spec/frontend/ide/ide_router_extension_spec.js @@ -0,0 +1,48 @@ +import VueRouter from 'vue-router'; +import IdeRouter from '~/ide/ide_router_extension'; + +jest.mock('vue-router'); + +describe('IDE overrides of VueRouter', () => { + const paths = branch => [ + `${branch}`, + `/${branch}`, + `/${branch}/-/`, + `/edit/${branch}`, + `/edit/${branch}/-/`, + `/blob/${branch}`, + `/blob/${branch}/-/`, + `/blob/${branch}/-/src/merge_requests/2`, + `/blob/${branch}/-/src/blob/`, + `/tree/${branch}/-/src/blob/`, + `/tree/${branch}/-/src/tree/`, + ]; + let router; + + beforeEach(() => { + VueRouter.mockClear(); + router = new IdeRouter({ + mode: 'history', + }); + }); + + it.each` + path | expected + ${'#-test'} | ${'%23-test'} + ${'#test'} | ${'%23test'} + ${'test#'} | ${'test%23'} + ${'test-#'} | ${'test-%23'} + ${'test-#-hash'} | ${'test-%23-hash'} + ${'test/hash#123'} | ${'test/hash%23123'} + `('finds project path when route is $path', ({ path, expected }) => { + paths(path).forEach(route => { + const expectedPath = route.replace(path, expected); + + router.push(route); + expect(VueRouter.prototype.push).toHaveBeenCalledWith(expectedPath, undefined, undefined); + + router.resolve(route); + expect(VueRouter.prototype.resolve).toHaveBeenCalledWith(expectedPath, undefined, undefined); + }); + }); +}); diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index 989de1a8337..d0abf2c03a9 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -28,6 +28,12 @@ describe('URL utility', () => { gon.relative_url_root = ''; }); + it('escapes special characters', () => { + expect(urlUtils.webIDEUrl('/gitlab-org/gitlab-#-foss/merge_requests/1')).toBe( + '/-/ide/project/gitlab-org/gitlab-%23-foss/merge_requests/1', + ); + }); + describe('without relative_url_root', () => { it('returns IDE path with route', () => { expect(urlUtils.webIDEUrl('/gitlab-org/gitlab-foss/merge_requests/1')).toBe( diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index 2d0c4723648..a9f4b03eba5 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -244,8 +244,8 @@ describe BlobHelper do it 'escapes special characters' do Rails.application.routes.default_url_options[:script_name] = nil - expect(helper.ide_edit_path(project, "testing/#hashes", "readme.md#test")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/testing/#hashes/-/readme.md%23test") - expect(helper.ide_edit_path(project, "testing/#hashes", "src#/readme.md#test")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/testing/#hashes/-/src%23/readme.md%23test") + expect(helper.ide_edit_path(project, "testing/#hashes", "readme.md#test")).to eq("/-/ide/project/#{project.full_path}/edit/testing/%23hashes/-/readme.md%23test") + expect(helper.ide_edit_path(project, "testing/#hashes", "src#/readme.md#test")).to eq("/-/ide/project/#{project.full_path}/edit/testing/%23hashes/-/src%23/readme.md%23test") end it 'does not escape "/" character' do diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb index a493b96b1e4..5d2334a6d8f 100644 --- a/spec/lib/container_registry/client_spec.rb +++ b/spec/lib/container_registry/client_spec.rb @@ -146,4 +146,57 @@ describe ContainerRegistry::Client do expect(subject).to eq 'sha256:123' end end + + describe '#delete_repository_tag_by_name' do + subject { client.delete_repository_tag_by_name('group/test', 'a') } + + context 'when the tag exists' do + before do + stub_request(:delete, "http://container-registry/v2/group/test/tags/reference/a") + .to_return(status: 200, body: "") + end + + it { is_expected.to be_truthy } + end + + context 'when the tag does not exist' do + before do + stub_request(:delete, "http://container-registry/v2/group/test/tags/reference/a") + .to_return(status: 404, body: "") + end + + it { is_expected.to be_truthy } + end + + context 'when an error occurs' do + before do + stub_request(:delete, "http://container-registry/v2/group/test/tags/reference/a") + .to_return(status: 500, body: "") + end + + it { is_expected.to be_falsey } + end + end + + describe '#supports_tag_delete?' do + subject { client.supports_tag_delete? } + + context 'when the server supports tag deletion' do + before do + stub_request(:options, "http://container-registry/v2/name/tags/reference/tag") + .to_return(status: 200, body: "", headers: { 'Allow' => 'DELETE' }) + end + + it { is_expected.to be_truthy } + end + + context 'when the server does not support tag deletion' do + before do + stub_request(:options, "http://container-registry/v2/name/tags/reference/tag") + .to_return(status: 404, body: "") + end + + it { is_expected.to be_falsey } + end + end end diff --git a/spec/lib/gitlab/background_migration_spec.rb b/spec/lib/gitlab/background_migration_spec.rb index 9546550bfc5..71959f54b38 100644 --- a/spec/lib/gitlab/background_migration_spec.rb +++ b/spec/lib/gitlab/background_migration_spec.rb @@ -165,6 +165,32 @@ describe Gitlab::BackgroundMigration do end end + describe '.remaining', :redis do + context 'when there are jobs remaining' do + let(:queue) { Array.new(12) } + + before do + allow(Sidekiq::Queue).to receive(:new) + .with(described_class.queue) + .and_return(Array.new(12)) + + Sidekiq::Testing.disable! do + BackgroundMigrationWorker.perform_in(10.minutes, 'Foo') + end + end + + it 'returns the enqueued jobs plus the scheduled jobs' do + expect(described_class.remaining).to eq(13) + end + end + + context 'when there are no jobs remaining' do + it 'returns zero' do + expect(described_class.remaining).to be_zero + end + end + end + describe '.exists?' do context 'when there are enqueued jobs present' do let(:queue) do diff --git a/spec/models/chat_name_spec.rb b/spec/models/chat_name_spec.rb index 82991937644..863c28a86fb 100644 --- a/spec/models/chat_name_spec.rb +++ b/spec/models/chat_name_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe ChatName do - set(:chat_name) { create(:chat_name) } + let_it_be(:chat_name) { create(:chat_name) } subject { chat_name } it { is_expected.to belong_to(:service) } diff --git a/spec/models/chat_team_spec.rb b/spec/models/chat_team_spec.rb index 76beb3d506b..107fdaccc68 100644 --- a/spec/models/chat_team_spec.rb +++ b/spec/models/chat_team_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe ChatTeam do - set(:chat_team) { create(:chat_team) } + let_it_be(:chat_team) { create(:chat_team) } subject { chat_team } # Associations diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index c9d6687f0ea..31e13122b95 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' describe Ci::Bridge do - set(:project) { create(:project) } - set(:target_project) { create(:project, name: 'project', namespace: create(:namespace, name: 'my')) } - set(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:project) { create(:project) } + let_it_be(:target_project) { create(:project, name: 'project', namespace: create(:namespace, name: 'my')) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } let(:bridge) do create(:ci_bridge, :variables, status: :created, diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb index da95a2d30f5..588e5872cc8 100644 --- a/spec/models/ci/build_metadata_spec.rb +++ b/spec/models/ci/build_metadata_spec.rb @@ -3,11 +3,11 @@ require 'spec_helper' describe Ci::BuildMetadata do - set(:user) { create(:user) } - set(:group) { create(:group) } - set(:project) { create(:project, :repository, group: group, build_timeout: 2000) } + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :repository, group: group, build_timeout: 2000) } - set(:pipeline) do + let_it_be(:pipeline) do create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch, diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 91185446488..8f2626037a1 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -3,11 +3,11 @@ require 'spec_helper' describe Ci::Build do - set(:user) { create(:user) } - set(:group) { create(:group) } - set(:project) { create(:project, :repository, group: group) } + let_it_be(:user) { create(:user) } + let_it_be(:group, reload: true) { create(:group) } + let_it_be(:project, reload: true) { create(:project, :repository, group: group) } - set(:pipeline) do + let_it_be(:pipeline, reload: true) do create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch, @@ -3612,7 +3612,7 @@ describe Ci::Build do end describe '.matches_tag_ids' do - set(:build) { create(:ci_build, project: project, user: user) } + let_it_be(:build, reload: true) { create(:ci_build, project: project, user: user) } let(:tag_ids) { ::ActsAsTaggableOn::Tag.named_any(tag_list).ids } subject { described_class.where(id: build).matches_tag_ids(tag_ids) } @@ -3659,7 +3659,7 @@ describe Ci::Build do end describe '.matches_tags' do - set(:build) { create(:ci_build, project: project, user: user) } + let_it_be(:build, reload: true) { create(:ci_build, project: project, user: user) } subject { described_class.where(id: build).with_any_tags } @@ -3685,7 +3685,7 @@ describe Ci::Build do end describe 'pages deployments' do - set(:build) { create(:ci_build, project: project, user: user) } + let_it_be(:build, reload: true) { create(:ci_build, project: project, user: user) } context 'when job is "pages"' do before do @@ -3852,7 +3852,7 @@ describe Ci::Build do end describe '#artifacts_metadata_entry' do - set(:build) { create(:ci_build, project: project) } + let_it_be(:build) { create(:ci_build, project: project) } let(:path) { 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' } around do |example| @@ -3952,7 +3952,7 @@ describe Ci::Build do end describe '#supported_runner?' do - set(:build) { create(:ci_build) } + let_it_be(:build) { create(:ci_build) } subject { build.supported_runner?(runner_features) } diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb index ca49233dde1..f08f05a09bf 100644 --- a/spec/models/ci/build_trace_chunk_spec.rb +++ b/spec/models/ci/build_trace_chunk_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do include ExclusiveLeaseHelpers - set(:build) { create(:ci_build, :running) } + let_it_be(:build) { create(:ci_build, :running) } let(:chunk_index) { 0 } let(:data_store) { :redis } let(:raw_data) { nil } @@ -24,7 +24,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do context 'FastDestroyAll' do let(:parent) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: parent) } - let(:build) { create(:ci_build, :running, :trace_live, pipeline: pipeline, project: parent) } + let!(:build) { create(:ci_build, :running, :trace_live, pipeline: pipeline, project: parent) } let(:subjects) { build.trace_chunks } describe 'Forbid #destroy and #destroy_all' do diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb index 370606a73bc..f3d743fc272 100644 --- a/spec/models/ci/processable_spec.rb +++ b/spec/models/ci/processable_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe Ci::Processable do - set(:project) { create(:project) } - set(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:project) { create(:project) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } describe '#aggregated_needs_names' do let(:with_aggregated_needs) { pipeline.processables.select_with_aggregated_needs(project) } diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 3e494d19233..0192c8ed17d 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -38,8 +38,8 @@ describe Ci::Runner do end context 'runner_type validations' do - set(:group) { create(:group) } - set(:project) { create(:project) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project) } let(:group_runner) { create(:ci_runner, :group, groups: [group]) } let(:project_runner) { create(:ci_runner, :project, projects: [project]) } let(:instance_runner) { create(:ci_runner, :instance) } @@ -322,7 +322,7 @@ describe Ci::Runner do end describe '#can_pick?' do - set(:pipeline) { create(:ci_pipeline) } + let_it_be(:pipeline) { create(:ci_pipeline) } let(:build) { create(:ci_build, pipeline: pipeline) } let(:runner_project) { build.project } let(:runner) { create(:ci_runner, :project, projects: [runner_project], tag_list: tag_list, run_untagged: run_untagged) } diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb index 0a3065140bf..5ed812652c5 100644 --- a/spec/models/container_repository_spec.rb +++ b/spec/models/container_repository_spec.rb @@ -85,7 +85,7 @@ describe ContainerRepository do context 'when action succeeds' do it 'returns status that indicates success' do expect(repository.client) - .to receive(:delete_repository_tag) + .to receive(:delete_repository_tag_by_digest) .twice .and_return(true) @@ -96,7 +96,7 @@ describe ContainerRepository do context 'when action fails' do it 'returns status that indicates failure' do expect(repository.client) - .to receive(:delete_repository_tag) + .to receive(:delete_repository_tag_by_digest) .twice .and_return(false) @@ -105,6 +105,36 @@ describe ContainerRepository do end end + describe '#delete_tag_by_name' do + let(:repository) do + create(:container_repository, name: 'my_image', + tags: { latest: '123', rc1: '234' }, + project: project) + end + + context 'when action succeeds' do + it 'returns status that indicates success' do + expect(repository.client) + .to receive(:delete_repository_tag_by_name) + .with(repository.path, "latest") + .and_return(true) + + expect(repository.delete_tag_by_name('latest')).to be_truthy + end + end + + context 'when action fails' do + it 'returns status that indicates failure' do + expect(repository.client) + .to receive(:delete_repository_tag_by_name) + .with(repository.path, "latest") + .and_return(false) + + expect(repository.delete_tag_by_name('latest')).to be_falsey + end + end + end + describe '#location' do context 'when registry is running on a custom port' do before do diff --git a/spec/models/diff_viewer/server_side_spec.rb b/spec/models/diff_viewer/server_side_spec.rb index 27de0584b8a..0a14eae26f3 100644 --- a/spec/models/diff_viewer/server_side_spec.rb +++ b/spec/models/diff_viewer/server_side_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe DiffViewer::ServerSide do - set(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } let(:commit) { project.commit_by(oid: '570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } let!(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } diff --git a/spec/models/event_collection_spec.rb b/spec/models/event_collection_spec.rb index c421ffa000d..e6f80a4c4d0 100644 --- a/spec/models/event_collection_spec.rb +++ b/spec/models/event_collection_spec.rb @@ -4,10 +4,10 @@ require 'spec_helper' describe EventCollection do describe '#to_a' do - set(:group) { create(:group) } - set(:project) { create(:project_empty_repo, group: group) } - set(:projects) { Project.where(id: project.id) } - set(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project_empty_repo, group: group) } + let_it_be(:projects) { Project.where(id: project.id) } + let_it_be(:user) { create(:user) } context 'with project events' do before do diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb index 93862e98172..57eb077031c 100644 --- a/spec/models/guest_spec.rb +++ b/spec/models/guest_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' describe Guest do - set(:public_project) { create(:project, :public) } - set(:private_project) { create(:project, :private) } - set(:internal_project) { create(:project, :internal) } + let_it_be(:public_project, reload: true) { create(:project, :public) } + let_it_be(:private_project) { create(:project, :private) } + let_it_be(:internal_project) { create(:project, :internal) } describe '.can_pull?' do context 'when project is private' do diff --git a/spec/models/list_user_preference_spec.rb b/spec/models/list_user_preference_spec.rb index 1335a3700dc..10a7bf41f4e 100644 --- a/spec/models/list_user_preference_spec.rb +++ b/spec/models/list_user_preference_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe ListUserPreference do - set(:user) { create(:user) } - set(:list) { create(:list) } + let_it_be(:user) { create(:user) } + let_it_be(:list) { create(:list) } before do list.update_preferences_for(user, { collapsed: true }) diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb index a88db3b87af..99b7c4f148a 100644 --- a/spec/models/pages_domain_spec.rb +++ b/spec/models/pages_domain_spec.rb @@ -352,9 +352,9 @@ describe PagesDomain do end context 'configuration updates when attributes change' do - set(:project1) { create(:project) } - set(:project2) { create(:project) } - set(:domain) { create(:pages_domain) } + let_it_be(:project1) { create(:project) } + let_it_be(:project2) { create(:project) } + let_it_be(:domain) { create(:pages_domain) } where(:attribute, :old_value, :new_value, :update_expected) do now = Time.now @@ -402,8 +402,8 @@ describe PagesDomain do end context 'TLS configuration' do - set(:domain_without_tls) { create(:pages_domain, :without_certificate, :without_key) } - set(:domain) { create(:pages_domain) } + let_it_be(:domain_without_tls) { create(:pages_domain, :without_certificate, :without_key) } + let_it_be(:domain) { create(:pages_domain) } let(:cert1) { domain.certificate } let(:cert2) { cert1 + ' ' } diff --git a/spec/models/project_services/alerts_service_spec.rb b/spec/models/project_services/alerts_service_spec.rb new file mode 100644 index 00000000000..4e63ece26d8 --- /dev/null +++ b/spec/models/project_services/alerts_service_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AlertsService do + let_it_be(:project) { create(:project) } + let(:service_params) { { project: project, active: active } } + let(:active) { true } + let(:service) { described_class.new(service_params) } + + shared_context 'when active' do + let(:active) { true } + end + + shared_context 'when inactive' do + let(:active) { false } + end + + shared_context 'when persisted' do + before do + service.save! + service.reload + end + end + + describe '#url' do + include Gitlab::Routing + + subject { service.url } + + it { is_expected.to eq(project_alerts_notify_url(project, format: :json)) } + end + + describe '#json_fields' do + subject { service.json_fields } + + it { is_expected.to eq(%w(active token)) } + end + + describe '#as_json' do + subject { service.as_json(only: service.json_fields) } + + it { is_expected.to eq('active' => true, 'token' => nil) } + end + + describe '#token' do + shared_context 'reset token' do + before do + service.token = '' + service.valid? + end + end + + shared_context 'assign token' do |token| + before do + service.token = token + service.valid? + end + end + + shared_examples 'valid token' do + it { is_expected.to match(/\A\h{32}\z/) } + end + + shared_examples 'no token' do + it { is_expected.to be_blank } + end + + subject { service.token } + + context 'when active' do + include_context 'when active' + + context 'when resetting' do + let!(:previous_token) { service.token } + + include_context 'reset token' + + it_behaves_like 'valid token' + + it { is_expected.not_to eq(previous_token) } + end + + context 'when assigning' do + include_context 'assign token', 'random token' + + it_behaves_like 'valid token' + end + end + + context 'when inactive' do + include_context 'when inactive' + + context 'when resetting' do + let!(:previous_token) { service.token } + + include_context 'reset token' + + it_behaves_like 'no token' + end + end + + context 'when persisted' do + include_context 'when persisted' + + it_behaves_like 'valid token' + end + end +end diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb index 83d3c8b3a70..d93b8a2cb40 100644 --- a/spec/models/project_services/microsoft_teams_service_spec.rb +++ b/spec/models/project_services/microsoft_teams_service_spec.rb @@ -37,9 +37,9 @@ describe MicrosoftTeamsService do end describe "#execute" do - let(:user) { create(:user) } + let(:user) { create(:user) } - set(:project) { create(:project, :repository, :wiki_repo) } + let_it_be(:project) { create(:project, :repository, :wiki_repo) } before do allow(chat_service).to receive_messages( diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb index 49005d8c681..1922bb065cf 100644 --- a/spec/models/project_services/prometheus_service_spec.rb +++ b/spec/models/project_services/prometheus_service_spec.rb @@ -169,7 +169,7 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do end context 'cluster belongs to projects group' do - set(:group) { create(:group) } + let_it_be(:group) { create(:group) } let(:project) { create(:prometheus_project, group: group) } let(:cluster) { create(:cluster_for_group, :with_installed_helm, groups: [group]) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f847cb63ddc..9dc362594dd 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -3901,7 +3901,7 @@ describe Project do end context 'legacy storage' do - set(:project) { create(:project, :repository, :legacy_storage) } + let_it_be(:project) { create(:project, :repository, :legacy_storage) } let(:gitlab_shell) { Gitlab::Shell.new } let(:project_storage) { project.send(:storage) } @@ -4000,7 +4000,7 @@ describe Project do end context 'hashed storage' do - set(:project) { create(:project, :repository, skip_disk_validation: true) } + let_it_be(:project) { create(:project, :repository, skip_disk_validation: true) } let(:gitlab_shell) { Gitlab::Shell.new } let(:hash) { Digest::SHA2.hexdigest(project.id.to_s) } let(:hashed_prefix) { File.join('@hashed', hash[0..1], hash[2..3]) } @@ -4090,7 +4090,7 @@ describe Project do end describe '#has_ci?' do - set(:project) { create(:project) } + let_it_be(:project, reload: true) { create(:project) } let(:repository) { double } before do @@ -4134,7 +4134,7 @@ describe Project do Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(0) end - set(:project) { create(:project) } + let_it_be(:project, reload: true) { create(:project) } subject { project.auto_devops_enabled? } @@ -4269,7 +4269,7 @@ describe Project do end describe '#has_auto_devops_implicitly_enabled?' do - set(:project) { create(:project) } + let_it_be(:project, reload: true) { create(:project) } context 'when disabled in settings' do before do @@ -4330,7 +4330,7 @@ describe Project do end describe '#has_auto_devops_implicitly_disabled?' do - set(:project) { create(:project) } + let_it_be(:project, reload: true) { create(:project) } before do allow(Feature).to receive(:enabled?).and_call_original @@ -4408,7 +4408,7 @@ describe Project do end describe '#api_variables' do - set(:project) { create(:project) } + let_it_be(:project) { create(:project) } it 'exposes API v4 URL' do expect(project.api_variables.first[:key]).to eq 'CI_API_V4_URL' @@ -4605,7 +4605,7 @@ describe Project do end describe '#write_repository_config' do - set(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } it 'writes full path in .git/config when key is missing' do project.write_repository_config @@ -4696,7 +4696,7 @@ describe Project do end describe '#has_active_hooks?' do - set(:project) { create(:project) } + let_it_be(:project) { create(:project) } it { expect(project.has_active_hooks?).to be_falsey } @@ -4723,7 +4723,7 @@ describe Project do end describe '#has_active_services?' do - set(:project) { create(:project) } + let_it_be(:project) { create(:project) } it { expect(project.has_active_services?).to be_falsey } @@ -5009,8 +5009,8 @@ describe Project do describe '#members_among' do let(:users) { create_list(:user, 3) } - set(:group) { create(:group) } - set(:project) { create(:project, namespace: group) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, namespace: group) } before do project.add_guest(users.first) @@ -5584,6 +5584,14 @@ describe Project do end end + describe '#alerts_service_activated?' do + let!(:project) { create(:project) } + + subject { project.alerts_service_activated? } + + it { is_expected.to be_falsey } + end + def rugged_config rugged_repo(project.repository).config end diff --git a/spec/models/releases/source_spec.rb b/spec/models/releases/source_spec.rb index c8ac8e31c97..d7af6fd90a6 100644 --- a/spec/models/releases/source_spec.rb +++ b/spec/models/releases/source_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Releases::Source do - set(:project) { create(:project, :repository, name: 'finance-cal') } + let_it_be(:project) { create(:project, :repository, name: 'finance-cal') } let(:tag_name) { 'v1.0' } describe '.all' do diff --git a/spec/requests/api/lsif_data_spec.rb b/spec/requests/api/lsif_data_spec.rb index ca3a30bd1d0..2e0670ded95 100644 --- a/spec/requests/api/lsif_data_spec.rb +++ b/spec/requests/api/lsif_data_spec.rb @@ -60,7 +60,8 @@ describe API::LsifData do 'end_char' => 18, 'end_line' => 8, 'start_char' => 13, - 'start_line' => 8 + 'start_line' => 8, + 'definition_url' => project_blob_path(project, "#{commit.id}/morestrings/reverse.go", anchor: 'L5') }) end diff --git a/spec/services/projects/alerting/notify_service_spec.rb b/spec/services/projects/alerting/notify_service_spec.rb new file mode 100644 index 00000000000..efd168a0a8a --- /dev/null +++ b/spec/services/projects/alerting/notify_service_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::Alerting::NotifyService do + let_it_be(:project, reload: true) { create(:project) } + + shared_examples 'does not process incident issues' do |http_status:| + it 'does not process issues' do + expect(IncidentManagement::ProcessAlertWorker) + .not_to receive(:perform_async) + + expect(subject.status).to eq(:error) + expect(subject.http_status).to eq(http_status) + end + end + + describe '#execute' do + let(:token) { 'invalid-token' } + let(:starts_at) { Time.now.change(usec: 0) } + let(:service) { described_class.new(project, nil, payload) } + let(:payload_raw) do + { + 'title' => 'alert title', + 'start_time' => starts_at.rfc3339 + } + end + let(:payload) { ActionController::Parameters.new(payload_raw).permit! } + + subject { service.execute(token) } + + it_behaves_like 'does not process incident issues', http_status: 403 + end +end diff --git a/spec/services/projects/container_repository/cleanup_tags_service_spec.rb b/spec/services/projects/container_repository/cleanup_tags_service_spec.rb index 78b969c8a0e..cd4d1e3fe67 100644 --- a/spec/services/projects/container_repository/cleanup_tags_service_spec.rb +++ b/spec/services/projects/container_repository/cleanup_tags_service_spec.rb @@ -41,7 +41,7 @@ describe Projects::ContainerRepository::CleanupTagsService do let(:params) { {} } it 'does not remove anything' do - expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag) + expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_digest) is_expected.to include(status: :success, deleted: []) end @@ -156,7 +156,7 @@ describe Projects::ContainerRepository::CleanupTagsService do def expect_delete(digest) expect_any_instance_of(ContainerRegistry::Client) - .to receive(:delete_repository_tag) + .to receive(:delete_repository_tag_by_digest) .with(repository.path, digest) { true } end end diff --git a/spec/services/projects/container_repository/delete_tags_service_spec.rb b/spec/services/projects/container_repository/delete_tags_service_spec.rb index decbbb7597f..e17e4b6f7c9 100644 --- a/spec/services/projects/container_repository/delete_tags_service_spec.rb +++ b/spec/services/projects/container_repository/delete_tags_service_spec.rb @@ -18,10 +18,6 @@ describe Projects::ContainerRepository::DeleteTagsService do stub_container_registry_tags( repository: repository.path, tags: %w(latest A Ba Bb C D E)) - - stub_tag_digest('latest', 'sha256:configA') - stub_tag_digest('A', 'sha256:configA') - stub_tag_digest('Ba', 'sha256:configB') end describe '#execute' do @@ -38,82 +34,178 @@ describe Projects::ContainerRepository::DeleteTagsService do project.add_developer(user) end - context 'when no params are specified' do - let(:params) { {} } + context 'when the registry supports fast delete' do + context 'and the feature is enabled' do + let_it_be(:project) { create(:project, :private) } + let_it_be(:repository) { create(:container_repository, :root, project: project) } - it 'does not remove anything' do - expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag) + before do + allow(repository.client).to receive(:supports_tag_delete?).and_return(true) + end - is_expected.to include(status: :error) - end - end + context 'with tags to delete' do + let_it_be(:tags) { %w[A Ba] } - context 'with empty tags' do - let(:tags) { [] } + it 'deletes the tags by name' do + stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/A") + .to_return(status: 200, body: "") - it 'does not remove anything' do - expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag) + stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/Ba") + .to_return(status: 200, body: "") - is_expected.to include(status: :error) - end - end + expect_delete_tag_by_name('A') + expect_delete_tag_by_name('Ba') + + is_expected.to include(status: :success) + end + + it 'succeeds when tag delete returns 404' do + stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/A") + .to_return(status: 200, body: "") + + stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/Ba") + .to_return(status: 404, body: "") + + is_expected.to include(status: :success) + end + + context 'with failures' do + context 'when the delete request fails' do + before do + stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/A") + .to_return(status: 500, body: "") + + stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/Ba") + .to_return(status: 500, body: "") + end - context 'with tags to delete' do - let(:tags) { %w[A Ba] } + it { is_expected.to include(status: :error) } + end + end + end + + context 'when no params are specified' do + let_it_be(:params) { {} } + + it 'does not remove anything' do + expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_name) + + is_expected.to include(status: :error) + end + end - it 'deletes the tags using a dummy image' do - stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3') + context 'with empty tags' do + let_it_be(:tags) { [] } - stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/A") - .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' }) + it 'does not remove anything' do + expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_name) - stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/Ba") - .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' }) + is_expected.to include(status: :error) + end + end + end + context 'and the feature is disabled' do + before do + stub_feature_flags(container_registry_fast_tag_delete: false) + end - expect_delete_tag('sha256:dummy') + it 'fallbacks to slow delete' do + expect(service).not_to receive(:fast_delete) + expect(service).to receive(:slow_delete).with(repository, tags) - is_expected.to include(status: :success) + subject + end end + end + context 'when the registry does not support fast delete' do + let_it_be(:project) { create(:project, :private) } + let_it_be(:repository) { create(:container_repository, :root, project: project) } - it 'succedes when tag delete returns 404' do - stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3') + before do + stub_tag_digest('latest', 'sha256:configA') + stub_tag_digest('A', 'sha256:configA') + stub_tag_digest('Ba', 'sha256:configB') - stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/A") - .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' }) + allow(repository.client).to receive(:supports_tag_delete?).and_return(false) + end - stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/Ba") - .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' }) + context 'when no params are specified' do + let_it_be(:params) { {} } - stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/sha256:dummy") - .to_return(status: 404, body: "", headers: {}) + it 'does not remove anything' do + expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_digest) - is_expected.to include(status: :success) + is_expected.to include(status: :error) + end end - context 'with failures' do - context 'when the dummy manifest generation fails' do - before do - stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3', success: false) - end + context 'with empty tags' do + let_it_be(:tags) { [] } + + it 'does not remove anything' do + expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_digest) + + is_expected.to include(status: :error) + end + end + + context 'with tags to delete' do + let_it_be(:tags) { %w[A Ba] } - it { is_expected.to include(status: :error) } + it 'deletes the tags using a dummy image' do + stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3') + + stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/A") + .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' }) + + stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/Ba") + .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' }) + + expect_delete_tag_by_digest('sha256:dummy') + + is_expected.to include(status: :success) end - context 'when updating the tags fails' do - before do - stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3') + it 'succeeds when tag delete returns 404' do + stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3') - stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/A") - .to_return(status: 500, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' }) + stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/A") + .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' }) - stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/Ba") - .to_return(status: 500, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' }) + stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/Ba") + .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' }) + + stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/sha256:dummy") + .to_return(status: 404, body: "", headers: {}) + + is_expected.to include(status: :success) + end - stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3") - .to_return(status: 200, body: "", headers: {}) + context 'with failures' do + context 'when the dummy manifest generation fails' do + before do + stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3', success: false) + end + + it { is_expected.to include(status: :error) } end - it { is_expected.to include(status: :error) } + context 'when updating the tags fails' do + before do + stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3') + + stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/A") + .to_return(status: 500, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' }) + + stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/Ba") + .to_return(status: 500, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' }) + + stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3") + .to_return(status: 200, body: "", headers: {}) + end + + it { is_expected.to include(status: :error) } + end end end end @@ -141,9 +233,21 @@ describe Projects::ContainerRepository::DeleteTagsService do .with(repository.path, content, digest) { double(success?: success ) } end - def expect_delete_tag(digest) + def expect_delete_tag_by_digest(digest) expect_any_instance_of(ContainerRegistry::Client) - .to receive(:delete_repository_tag) + .to receive(:delete_repository_tag_by_digest) .with(repository.path, digest) { true } + + expect_any_instance_of(ContainerRegistry::Client) + .not_to receive(:delete_repository_tag_by_name) + end + + def expect_delete_tag_by_name(name) + expect_any_instance_of(ContainerRegistry::Client) + .to receive(:delete_repository_tag_by_name) + .with(repository.path, name) { true } + + expect_any_instance_of(ContainerRegistry::Client) + .not_to receive(:delete_repository_tag_by_digest) end end diff --git a/spec/services/projects/lsif_data_service_spec.rb b/spec/services/projects/lsif_data_service_spec.rb index b3c37c01c4d..29a99a96c41 100644 --- a/spec/services/projects/lsif_data_service_spec.rb +++ b/spec/services/projects/lsif_data_service_spec.rb @@ -23,43 +23,51 @@ describe Projects::LsifDataService do end context 'for main.go' do + let(:path_prefix) { "/#{project.full_path}/-/blob/#{commit_id}" } + it 'returns lsif ranges for the file' do expect(service.execute).to eq([ { end_char: 9, end_line: 6, start_char: 5, - start_line: 6 + start_line: 6, + definition_url: "#{path_prefix}/main.go#L7" }, { end_char: 36, end_line: 3, start_char: 1, - start_line: 3 + start_line: 3, + definition_url: "#{path_prefix}/main.go#L4" }, { end_char: 12, end_line: 7, start_char: 1, - start_line: 7 + start_line: 7, + definition_url: "#{path_prefix}/main.go#L4" }, { end_char: 20, end_line: 7, start_char: 13, - start_line: 7 + start_line: 7, + definition_url: "#{path_prefix}/morestrings/reverse.go#L11" }, { end_char: 12, end_line: 8, start_char: 1, - start_line: 8 + start_line: 8, + definition_url: "#{path_prefix}/main.go#L4" }, { end_char: 18, end_line: 8, start_char: 13, - start_line: 8 + start_line: 8, + definition_url: "#{path_prefix}/morestrings/reverse.go#L5" } ]) end @@ -73,7 +81,8 @@ describe Projects::LsifDataService do end_char: 2, end_line: 11, start_char: 1, - start_line: 11 + start_line: 11, + definition_url: "/#{project.full_path}/-/blob/#{commit_id}/morestrings/reverse.go#L12" }) end end @@ -87,7 +96,7 @@ describe Projects::LsifDataService do end end - describe '#doc_id_from' do + describe '#doc_id' do context 'when the passed path matches multiple files' do let(:path) { 'check/main.go' } let(:docs) do @@ -100,7 +109,9 @@ describe Projects::LsifDataService do end it 'fetches the document with the shortest absolute path' do - expect(service.__send__(:doc_id_from, docs)).to eq(3) + service.instance_variable_set(:@docs, docs) + + expect(service.__send__(:doc_id)).to eq(3) end end end |