Update upstream source from tag 'upstream/15.9.7+ds1'
Update to upstream version '15.9.7+ds1'
with Debian dir 98150fa7d5
This commit is contained in:
commit
ec5314ff10
126 changed files with 2705 additions and 974 deletions
|
@ -12,6 +12,7 @@ stages:
|
||||||
- post-qa
|
- post-qa
|
||||||
- pages
|
- pages
|
||||||
- notify
|
- notify
|
||||||
|
- release-environments
|
||||||
|
|
||||||
# always use `gitlab-org` runners, however
|
# always use `gitlab-org` runners, however
|
||||||
# in cases where jobs require Docker-in-Docker, the job
|
# in cases where jobs require Docker-in-Docker, the job
|
||||||
|
@ -32,6 +33,8 @@ default:
|
||||||
|
|
||||||
.ruby2-variables: &ruby2-variables
|
.ruby2-variables: &ruby2-variables
|
||||||
RUBY_VERSION: "2.7"
|
RUBY_VERSION: "2.7"
|
||||||
|
OMNIBUS_GITLAB_RUBY2_BUILD: "true"
|
||||||
|
OMNIBUS_GITLAB_CACHE_EDITION: "GITLAB_RUBY2"
|
||||||
|
|
||||||
.default-branch-incident-variables: &default-branch-incident-variables
|
.default-branch-incident-variables: &default-branch-incident-variables
|
||||||
CREATE_INCIDENT_FOR_PIPELINE_FAILURE: "true"
|
CREATE_INCIDENT_FOR_PIPELINE_FAILURE: "true"
|
||||||
|
@ -45,8 +48,8 @@ workflow:
|
||||||
# If `$FORCE_GITLAB_CI` is set, create a pipeline.
|
# If `$FORCE_GITLAB_CI` is set, create a pipeline.
|
||||||
- if: '$FORCE_GITLAB_CI'
|
- if: '$FORCE_GITLAB_CI'
|
||||||
variables:
|
variables:
|
||||||
<<: *ruby3-variables
|
<<: *ruby2-variables
|
||||||
PIPELINE_NAME: 'Ruby 3 forced pipeline'
|
PIPELINE_NAME: 'Ruby 2 forced pipeline'
|
||||||
# As part of the process of creating RCs automatically, we update stable
|
# As part of the process of creating RCs automatically, we update stable
|
||||||
# branches with the changes of the most recent production deployment. The
|
# branches with the changes of the most recent production deployment. The
|
||||||
# merge requests used for this merge a branch release-tools/X into a stable
|
# merge requests used for this merge a branch release-tools/X into a stable
|
||||||
|
@ -61,14 +64,14 @@ workflow:
|
||||||
PIPELINE_NAME: 'Ruby 2 $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline'
|
PIPELINE_NAME: 'Ruby 2 $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline'
|
||||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /Community contribution/'
|
- if: '$CI_MERGE_REQUEST_LABELS =~ /Community contribution/'
|
||||||
variables:
|
variables:
|
||||||
<<: *ruby3-variables
|
<<: *ruby2-variables
|
||||||
GITLAB_DEPENDENCY_PROXY_ADDRESS: ""
|
GITLAB_DEPENDENCY_PROXY_ADDRESS: ""
|
||||||
PIPELINE_NAME: 'Ruby 3 $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline (community contribution)'
|
PIPELINE_NAME: 'Ruby 2 $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline (community contribution)'
|
||||||
# For (detached) merge request pipelines.
|
# For (detached) merge request pipelines.
|
||||||
- if: '$CI_MERGE_REQUEST_IID'
|
- if: '$CI_MERGE_REQUEST_IID'
|
||||||
variables:
|
variables:
|
||||||
<<: *ruby3-variables
|
<<: *ruby2-variables
|
||||||
PIPELINE_NAME: 'Ruby 3 $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline'
|
PIPELINE_NAME: 'Ruby 2 $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline'
|
||||||
# For the scheduled pipelines, we set specific variables.
|
# For the scheduled pipelines, we set specific variables.
|
||||||
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "schedule"'
|
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "schedule"'
|
||||||
variables:
|
variables:
|
||||||
|
@ -168,6 +171,7 @@ variables:
|
||||||
|
|
||||||
ES_JAVA_OPTS: "-Xms256m -Xmx256m"
|
ES_JAVA_OPTS: "-Xms256m -Xmx256m"
|
||||||
ELASTIC_URL: "http://elastic:changeme@elasticsearch:9200"
|
ELASTIC_URL: "http://elastic:changeme@elasticsearch:9200"
|
||||||
|
BUNDLER_CHECKSUM_VERIFICATION_OPT_IN: "1"
|
||||||
CACHE_CLASSES: "true"
|
CACHE_CLASSES: "true"
|
||||||
CHECK_PRECOMPILED_ASSETS: "true"
|
CHECK_PRECOMPILED_ASSETS: "true"
|
||||||
FF_USE_FASTZIP: "true"
|
FF_USE_FASTZIP: "true"
|
||||||
|
@ -184,6 +188,7 @@ variables:
|
||||||
REVIEW_APPS_GCP_PROJECT: "gitlab-review-apps"
|
REVIEW_APPS_GCP_PROJECT: "gitlab-review-apps"
|
||||||
REVIEW_APPS_GCP_REGION: "us-central1"
|
REVIEW_APPS_GCP_REGION: "us-central1"
|
||||||
|
|
||||||
|
CACHE_ASSETS_AS_PACKAGE: "true"
|
||||||
BUILD_ASSETS_IMAGE: "true" # Set it to "false" to disable assets image building, used in `build-assets-image`
|
BUILD_ASSETS_IMAGE: "true" # Set it to "false" to disable assets image building, used in `build-assets-image`
|
||||||
SIMPLECOV: "true"
|
SIMPLECOV: "true"
|
||||||
|
|
||||||
|
|
23
.gitlab/ci/release-environments.gitlab-ci.yml
Normal file
23
.gitlab/ci/release-environments.gitlab-ci.yml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
start-release-environments-pipeline:
|
||||||
|
allow_failure: true
|
||||||
|
extends:
|
||||||
|
- .release-environments:rules:start-release-environments-pipeline
|
||||||
|
stage: release-environments
|
||||||
|
# We do not want to have ALL global variables passed as trigger variables,
|
||||||
|
# as they cannot be overridden. See this issue for more context:
|
||||||
|
#
|
||||||
|
# https://gitlab.com/gitlab-org/gitlab/-/issues/387183
|
||||||
|
inherit:
|
||||||
|
variables:
|
||||||
|
- RUBY_VERSION
|
||||||
|
|
||||||
|
# These variables are set in the pipeline schedules.
|
||||||
|
# They need to be explicitly passed on to the child pipeline.
|
||||||
|
# https://docs.gitlab.com/ee/ci/pipelines/multi_project_pipelines.html#pass-cicd-variables-to-a-downstream-pipeline-by-using-the-variables-keyword
|
||||||
|
variables:
|
||||||
|
# This is needed by `release-environments-build-cng-env` (`.gitlab/ci/release-environments/main.gitlab-ci.yml`).
|
||||||
|
PARENT_PIPELINE_ID: $CI_PIPELINE_ID
|
||||||
|
trigger:
|
||||||
|
strategy: depend
|
||||||
|
include: .gitlab/ci/release-environments/main.gitlab-ci.yml
|
62
.gitlab/ci/release-environments/main.gitlab-ci.yml
Normal file
62
.gitlab/ci/release-environments/main.gitlab-ci.yml
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
---
|
||||||
|
default:
|
||||||
|
interruptible: true
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- prepare
|
||||||
|
|
||||||
|
include:
|
||||||
|
- local: .gitlab/ci/global.gitlab-ci.yml
|
||||||
|
|
||||||
|
release-environments-build-cng-env:
|
||||||
|
allow_failure: true
|
||||||
|
image: ${GITLAB_DEPENDENCY_PROXY_ADDRESS}ruby:${RUBY_VERSION}-alpine3.16
|
||||||
|
stage: prepare
|
||||||
|
needs:
|
||||||
|
# We need this job because we need its `cached-assets-hash.txt` artifact, so that we can pass the assets image tag to the downstream CNG pipeline.
|
||||||
|
- pipeline: $PARENT_PIPELINE_ID
|
||||||
|
job: build-assets-image
|
||||||
|
variables:
|
||||||
|
BUILD_ENV: build.env
|
||||||
|
before_script:
|
||||||
|
- source ./scripts/utils.sh
|
||||||
|
- install_gitlab_gem
|
||||||
|
script:
|
||||||
|
- 'ruby -r./scripts/trigger-build.rb -e "puts Trigger.variables_for_env_file(Trigger::CNG.new.variables)" > $BUILD_ENV'
|
||||||
|
- echo "GITLAB_ASSETS_TAG=$(assets_image_tag)" >> $BUILD_ENV
|
||||||
|
- ruby -e 'puts "FULL_RUBY_VERSION=#{RUBY_VERSION}"' >> build.env
|
||||||
|
- cat $BUILD_ENV
|
||||||
|
artifacts:
|
||||||
|
reports:
|
||||||
|
dotenv: $BUILD_ENV
|
||||||
|
paths:
|
||||||
|
- $BUILD_ENV
|
||||||
|
expire_in: 7 days
|
||||||
|
when: always
|
||||||
|
|
||||||
|
release-environments-build-cng:
|
||||||
|
allow_failure: true
|
||||||
|
stage: prepare
|
||||||
|
needs: ["release-environments-build-cng-env"]
|
||||||
|
inherit:
|
||||||
|
variables: false
|
||||||
|
variables:
|
||||||
|
GITLAB_REF_SLUG: "${GITLAB_REF_SLUG}"
|
||||||
|
# CNG pipeline specific variables
|
||||||
|
GITLAB_VERSION: "${GITLAB_VERSION}"
|
||||||
|
GITLAB_TAG: "${GITLAB_TAG}"
|
||||||
|
GITLAB_ASSETS_TAG: "${GITLAB_ASSETS_TAG}"
|
||||||
|
FORCE_RAILS_IMAGE_BUILDS: "${FORCE_RAILS_IMAGE_BUILDS}"
|
||||||
|
CE_PIPELINE: "${CE_PIPELINE}" # Based on https://docs.gitlab.com/ee/ci/jobs/job_control.html#check-if-a-variable-exists, `if: '$CE_PIPELINE'` will evaluate to `false` when this variable is empty
|
||||||
|
EE_PIPELINE: "${EE_PIPELINE}" # Based on https://docs.gitlab.com/ee/ci/jobs/job_control.html#check-if-a-variable-exists, `if: '$EE_PIPELINE'` will evaluate to `false` when this variable is empty
|
||||||
|
GITLAB_ELASTICSEARCH_INDEXER_VERSION: "${GITLAB_ELASTICSEARCH_INDEXER_VERSION}"
|
||||||
|
GITLAB_KAS_VERSION: "${GITLAB_KAS_VERSION}"
|
||||||
|
GITLAB_METRICS_EXPORTER_VERSION: "${GITLAB_METRICS_EXPORTER_VERSION}"
|
||||||
|
GITLAB_PAGES_VERSION: "${GITLAB_PAGES_VERSION}"
|
||||||
|
GITLAB_SHELL_VERSION: "${GITLAB_SHELL_VERSION}"
|
||||||
|
GITALY_SERVER_VERSION: "${GITALY_SERVER_VERSION}"
|
||||||
|
RUBY_VERSION: "${FULL_RUBY_VERSION}"
|
||||||
|
trigger:
|
||||||
|
project: gitlab-org/build/CNG-mirror
|
||||||
|
branch: $TRIGGER_BRANCH
|
||||||
|
strategy: depend
|
|
@ -1901,6 +1901,13 @@
|
||||||
when: never
|
when: never
|
||||||
- if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_PATH == "gitlab-org/gitlab" && $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable-ee$/'
|
- if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_PATH == "gitlab-org/gitlab" && $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable-ee$/'
|
||||||
|
|
||||||
|
.releases:rules:canonical-dot-com-gitlab-stable-branch-only-setup-test-env-patterns:
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_MESSAGE =~ /\[merge-train skip\]/'
|
||||||
|
when: never
|
||||||
|
- if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_PATH == "gitlab-org/gitlab" && $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable-ee$/'
|
||||||
|
changes: *setup-test-env-patterns
|
||||||
|
|
||||||
.releases:rules:canonical-dot-com-security-gitlab-stable-branch-only:
|
.releases:rules:canonical-dot-com-security-gitlab-stable-branch-only:
|
||||||
rules:
|
rules:
|
||||||
- if: '$CI_COMMIT_MESSAGE =~ /\[merge-train skip\]/'
|
- if: '$CI_COMMIT_MESSAGE =~ /\[merge-train skip\]/'
|
||||||
|
@ -2295,3 +2302,14 @@
|
||||||
- <<: *if-dot-com-gitlab-org-merge-request
|
- <<: *if-dot-com-gitlab-org-merge-request
|
||||||
changes: *feature-flag-development-config-patterns
|
changes: *feature-flag-development-config-patterns
|
||||||
allow_failure: true # See https://gitlab.com/gitlab-org/gitlab/-/issues/351136
|
allow_failure: true # See https://gitlab.com/gitlab-org/gitlab/-/issues/351136
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# release-environments rules #
|
||||||
|
##############################
|
||||||
|
.release-environments:rules:start-release-environments-pipeline:
|
||||||
|
rules:
|
||||||
|
- <<: *if-not-ee
|
||||||
|
when: never
|
||||||
|
- <<: *if-merge-request-labels-pipeline-expedite
|
||||||
|
when: never
|
||||||
|
- !reference [".releases:rules:canonical-dot-com-gitlab-stable-branch-only-setup-test-env-patterns", rules]
|
||||||
|
|
35
.gitlab/merge_request_templates/Stable Branch.md
Normal file
35
.gitlab/merge_request_templates/Stable Branch.md
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<!--
|
||||||
|
Merging into stable branches in canonical projects is reserved for
|
||||||
|
GitLab patch releases https://docs.gitlab.com/ee/policy/maintenance.html#patch-releases
|
||||||
|
|
||||||
|
If you're backporting a security fix, please refer to the security merge request
|
||||||
|
template https://gitlab.com/gitlab-org/security/gitlab/blob/master/.gitlab/merge_request_templates/Security%20Release.md.
|
||||||
|
Security backport merge requests should not be opened on the GitLab canonical project.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## What does this MR do and why?
|
||||||
|
|
||||||
|
_Describe in detail what merge request is being backported and why_
|
||||||
|
|
||||||
|
## MR acceptance checklist
|
||||||
|
|
||||||
|
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.
|
||||||
|
|
||||||
|
* [ ] This MR is backporting a bug fix, documentation update, or spec fix, previously merged in the default branch.
|
||||||
|
* [ ] The original MR has been deployed to GitLab.com (not applicable for documentation or spec changes).
|
||||||
|
* [ ] This MR has a [severity label] assigned (if applicable).
|
||||||
|
* [ ] Ensure the `e2e:package-and-test` job has either succeeded or been approved by a Software Engineer in Test.
|
||||||
|
|
||||||
|
#### Note to the merge request author and maintainer
|
||||||
|
|
||||||
|
The process of backporting bug fixes into stable branches is tracked as part of an
|
||||||
|
[internal pilot]. If you have questions about this process, please:
|
||||||
|
|
||||||
|
* Refer to the [internal pilot] issue for feedback or questions.
|
||||||
|
* Refer to the [patch release runbook for engineers and maintainers] for guidance.
|
||||||
|
|
||||||
|
[severity label]: https://about.gitlab.com/handbook/engineering/quality/issue-triage/#severity
|
||||||
|
[internal pilot]: https://gitlab.com/gitlab-com/gl-infra/delivery/-/issues/2886
|
||||||
|
[patch release runbook for engineers and maintainers]: https://gitlab.com/gitlab-org/release/docs/-/blob/master/general/patch/process_new.md
|
||||||
|
|
||||||
|
/assign me
|
55
CHANGELOG.md
55
CHANGELOG.md
|
@ -2,6 +2,61 @@
|
||||||
documentation](doc/development/changelog.md) for instructions on adding your own
|
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||||
entry.
|
entry.
|
||||||
|
|
||||||
|
## 15.9.7 (2023-05-03)
|
||||||
|
|
||||||
|
### Security (1 change)
|
||||||
|
|
||||||
|
- [Only maintainers of projects should be able to assign runners to them](gitlab-org/security/gitlab@695748314b758ca2d9992df7509025a6ac868000) ([merge request](gitlab-org/security/gitlab!3236))
|
||||||
|
|
||||||
|
## 15.9.6 (2023-05-01)
|
||||||
|
|
||||||
|
### Security (8 changes)
|
||||||
|
|
||||||
|
- [Resolve ambiguous references for archive metadata](gitlab-org/security/gitlab@233b0f78baf8eb9adcfd77e4d1aa606d54472d34) ([merge request](gitlab-org/security/gitlab!3203))
|
||||||
|
- [Commit trailers now only match public user email addresses](gitlab-org/security/gitlab@e360774721bb9b5f6a2da9908ef08d92ad5a79cd) ([merge request](gitlab-org/security/gitlab!3209))
|
||||||
|
- [Handle invalid URLs in asset proxy](gitlab-org/security/gitlab@ee6df7196b14014b5416f090a684e3b6ba600b5a) ([merge request](gitlab-org/security/gitlab!3213))
|
||||||
|
- [Relay state to check for only allowing sub paths](gitlab-org/security/gitlab@c690eec0a2f8aa506b8ff3ffadf306aa91501648) ([merge request](gitlab-org/security/gitlab!3221))
|
||||||
|
- [Prohibit 40 character hex sets at beginning of path-based branch name](gitlab-org/security/gitlab@889683b6b1884bfc36208dfae899d0fb9437246c) ([merge request](gitlab-org/security/gitlab!3195))
|
||||||
|
- [Update policy to prevent banned members from accessing public projects](gitlab-org/security/gitlab@1abcbdc23881dab5f675e858afa31be87d5d47ce) ([merge request](gitlab-org/security/gitlab!3187))
|
||||||
|
- [Use dummy filename as filename when viewing raw xml files](gitlab-org/security/gitlab@33563159bcc7d46c95f013bf089ed94128f10379) ([merge request](gitlab-org/security/gitlab!3193))
|
||||||
|
- [Authorize access to vulnerabilitiesCountByDay resolver](gitlab-org/security/gitlab@4b0825f79b0a27eeddabaee0b3a7f627b2487706) ([merge request](gitlab-org/security/gitlab!3181))
|
||||||
|
|
||||||
|
## 15.9.5 (2023-04-21)
|
||||||
|
|
||||||
|
### Fixed (1 change)
|
||||||
|
|
||||||
|
- [Fix automatically-retried jobs stuck in pending state](gitlab-org/gitlab@752fbfcd613259b71af37f62a83321e8f573219b) ([merge request](gitlab-org/gitlab!117281))
|
||||||
|
|
||||||
|
## 15.9.4 (2023-03-30)
|
||||||
|
|
||||||
|
### Security (16 changes)
|
||||||
|
|
||||||
|
- [Add checks to remove open redirects from Observability URL](gitlab-org/security/gitlab@98b1bd243f454bd28c262131be616ee2060c3a78) ([merge request](gitlab-org/security/gitlab!3104))
|
||||||
|
- [Redirect to tree from project root on ref collision](gitlab-org/security/gitlab@0f0c0f21dffe300a56abf1e07a2fefb17160faeb) ([merge request](gitlab-org/security/gitlab!3133))
|
||||||
|
- [Fixes soft email confirmation alert vulnerability](gitlab-org/security/gitlab@12498f791f9c5fe833f5202b06cc818d4dcf965b) ([merge request](gitlab-org/security/gitlab!3124))
|
||||||
|
- [Restrict Prometheus API access on public projects](gitlab-org/security/gitlab@440a7989ff46ca333f86a38aefa47f74301e66fc) ([merge request](gitlab-org/security/gitlab!3163))
|
||||||
|
- [Verify that users have access to the parent of the fork](gitlab-org/security/gitlab@9dd0dff69d3941e827c461c67b9af10da07d69f8) ([merge request](gitlab-org/security/gitlab!3084))
|
||||||
|
- [Protect webhook secrets by resetting url_variables](gitlab-org/security/gitlab@cd20b44dd5b075827203330802e331b896448265) ([merge request](gitlab-org/security/gitlab!3140))
|
||||||
|
- [Replace Unicode space chars with spaces](gitlab-org/security/gitlab@76975082c41870265e1285fa8f4e053eb6ff11ae) ([merge request](gitlab-org/security/gitlab!3136))
|
||||||
|
- [Check access to parent when creating and updating epics](gitlab-org/security/gitlab@7fcc4a0d010d3a428e803f95ef47904c4c7178a8) ([merge request](gitlab-org/security/gitlab!3149))
|
||||||
|
- [Improve Gitlab::UrlSanitizer regex to match more URIs](gitlab-org/security/gitlab@4e7313536e4cdb3ecef37100b5a73720eabfbc79) ([merge request](gitlab-org/security/gitlab!3108))
|
||||||
|
- [Check access to target project before looking for branch](gitlab-org/security/gitlab@f55edf39e52af9eecb19caf8ed5d4cb8524ef64d) ([merge request](gitlab-org/security/gitlab!3040))
|
||||||
|
- [Fix the potential leak of internal notes](gitlab-org/security/gitlab@be73600e8c43c22cda1ace5910eb2052b2741972) ([merge request](gitlab-org/security/gitlab!3120))
|
||||||
|
- [Use UntrustedRegexp to limit scan of HTML comments](gitlab-org/security/gitlab@d5e65583debcae71787e171643275bc9b9d4393e) ([merge request](gitlab-org/security/gitlab!3142))
|
||||||
|
- [Filter namespace environments by feature visibility](gitlab-org/security/gitlab@54045b508a9ba9ae18f5992b77970240774b28a7) ([merge request](gitlab-org/security/gitlab!3111))
|
||||||
|
- [Check access to reorder issues in epic tree](gitlab-org/security/gitlab@bc033cd3a98c9a1468545811a8180604f7f8aee3) ([merge request](gitlab-org/security/gitlab!3101))
|
||||||
|
- [Fix security report authorization](gitlab-org/security/gitlab@a01cf9d8383ffc4c0e29514f71d49bf345e1f7c2) ([merge request](gitlab-org/security/gitlab!3106))
|
||||||
|
- [Prevent XSS attack in "Maximum page reached" page](gitlab-org/security/gitlab@3cefb16a5e369ee99f4c3ccbaa02cead6faf1a99) ([merge request](gitlab-org/security/gitlab!3130))
|
||||||
|
|
||||||
|
## 15.9.3 (2023-03-09)
|
||||||
|
|
||||||
|
### Fixed (4 changes)
|
||||||
|
|
||||||
|
- [Fix foreign_key_exists? migration helper](gitlab-org/gitlab@7b51239b18779acfe9876fb9467f1231f56d47b4) ([merge request](gitlab-org/gitlab!114005))
|
||||||
|
- [Enable Geo::RepositoryRegistrySyncWorker on Geo secondary site](gitlab-org/gitlab@57b542b4377bcc991b65f34a37397ac1d08846d9) ([merge request](gitlab-org/gitlab!114005)) **GitLab Enterprise Edition**
|
||||||
|
- [Guard against dropped columns when finalizing user details migration](gitlab-org/gitlab@939d646e2cbbbabf870e15fae384c0380d371111) ([merge request](gitlab-org/gitlab!114005))
|
||||||
|
- [Fix object deletion not working with Azure Blob Storage](gitlab-org/gitlab@9515c7a334a43c0e580543029a8da5061bdc19ce) ([merge request](gitlab-org/gitlab!114005))
|
||||||
|
|
||||||
## 15.9.2 (2023-03-02)
|
## 15.9.2 (2023-03-02)
|
||||||
|
|
||||||
### Security (12 changes)
|
### Security (12 changes)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
15.9.2
|
15.9.7
|
|
@ -1 +1 @@
|
||||||
15.9.2
|
15.9.7
|
2
Gemfile
2
Gemfile
|
@ -158,7 +158,7 @@ gem 'fog-rackspace', '~> 0.1.1'
|
||||||
# We may want to update this dependency if this is ever addressed upstream, e.g. via
|
# We may want to update this dependency if this is ever addressed upstream, e.g. via
|
||||||
# https://github.com/aliyun/aliyun-oss-ruby-sdk/pull/93
|
# https://github.com/aliyun/aliyun-oss-ruby-sdk/pull/93
|
||||||
gem 'fog-aliyun', '~> 0.4'
|
gem 'fog-aliyun', '~> 0.4'
|
||||||
gem 'gitlab-fog-azure-rm', '~> 1.4.0', require: 'fog/azurerm'
|
gem 'gitlab-fog-azure-rm', '~> 1.7.0', require: 'fog/azurerm'
|
||||||
|
|
||||||
# for Google storage
|
# for Google storage
|
||||||
gem 'google-cloud-storage', '~> 1.44.0'
|
gem 'google-cloud-storage', '~> 1.44.0'
|
||||||
|
|
|
@ -202,7 +202,7 @@
|
||||||
{"name":"gitlab-chronic","version":"0.10.5","platform":"ruby","checksum":"f80f18dc699b708870a80685243331290bc10cfeedb6b99c92219722f729c875"},
|
{"name":"gitlab-chronic","version":"0.10.5","platform":"ruby","checksum":"f80f18dc699b708870a80685243331290bc10cfeedb6b99c92219722f729c875"},
|
||||||
{"name":"gitlab-dangerfiles","version":"3.7.0","platform":"ruby","checksum":"35c5bc42e60c575ab5701192ca2384ab414b14c2963602b39e143b1aaeb7e54d"},
|
{"name":"gitlab-dangerfiles","version":"3.7.0","platform":"ruby","checksum":"35c5bc42e60c575ab5701192ca2384ab414b14c2963602b39e143b1aaeb7e54d"},
|
||||||
{"name":"gitlab-experiment","version":"0.7.1","platform":"ruby","checksum":"166dddb3aa83428bcaa93c35684ed01dc4d61f321fd2ae40b020806dc54a7824"},
|
{"name":"gitlab-experiment","version":"0.7.1","platform":"ruby","checksum":"166dddb3aa83428bcaa93c35684ed01dc4d61f321fd2ae40b020806dc54a7824"},
|
||||||
{"name":"gitlab-fog-azure-rm","version":"1.4.0","platform":"ruby","checksum":"af4163c32b028aa5208814a3f4765a5817d50527e6c61931f766bf18a2e0eb7e"},
|
{"name":"gitlab-fog-azure-rm","version":"1.7.0","platform":"ruby","checksum":"969c67943c54ad4c259a6acd040493f13922fbdf2211bb4eca00e71505263dc2"},
|
||||||
{"name":"gitlab-labkit","version":"0.30.1","platform":"ruby","checksum":"bdedbd86014c83dfd6a50d20dbc1709697bba2bb9e3666383e5f28cbd312b113"},
|
{"name":"gitlab-labkit","version":"0.30.1","platform":"ruby","checksum":"bdedbd86014c83dfd6a50d20dbc1709697bba2bb9e3666383e5f28cbd312b113"},
|
||||||
{"name":"gitlab-license","version":"2.2.1","platform":"ruby","checksum":"39fcf6be8b2887df8afe01b5dcbae8d08b7c5d937ff56b0fb40484a8c4f02d30"},
|
{"name":"gitlab-license","version":"2.2.1","platform":"ruby","checksum":"39fcf6be8b2887df8afe01b5dcbae8d08b7c5d937ff56b0fb40484a8c4f02d30"},
|
||||||
{"name":"gitlab-mail_room","version":"0.0.9","platform":"ruby","checksum":"6700374b5c0aa9d9ad4e711aeb677f0b7d415a6d01d3baa699efab25349d851c"},
|
{"name":"gitlab-mail_room","version":"0.0.9","platform":"ruby","checksum":"6700374b5c0aa9d9ad4e711aeb677f0b7d415a6d01d3baa699efab25349d851c"},
|
||||||
|
|
|
@ -577,7 +577,7 @@ GEM
|
||||||
gitlab-experiment (0.7.1)
|
gitlab-experiment (0.7.1)
|
||||||
activesupport (>= 3.0)
|
activesupport (>= 3.0)
|
||||||
request_store (>= 1.0)
|
request_store (>= 1.0)
|
||||||
gitlab-fog-azure-rm (1.4.0)
|
gitlab-fog-azure-rm (1.7.0)
|
||||||
azure-storage-blob (~> 2.0)
|
azure-storage-blob (~> 2.0)
|
||||||
azure-storage-common (~> 2.0)
|
azure-storage-common (~> 2.0)
|
||||||
fog-core (= 2.1.0)
|
fog-core (= 2.1.0)
|
||||||
|
@ -1678,7 +1678,7 @@ DEPENDENCIES
|
||||||
gitlab-chronic (~> 0.10.5)
|
gitlab-chronic (~> 0.10.5)
|
||||||
gitlab-dangerfiles (~> 3.7.0)
|
gitlab-dangerfiles (~> 3.7.0)
|
||||||
gitlab-experiment (~> 0.7.1)
|
gitlab-experiment (~> 0.7.1)
|
||||||
gitlab-fog-azure-rm (~> 1.4.0)
|
gitlab-fog-azure-rm (~> 1.7.0)
|
||||||
gitlab-labkit (~> 0.30.1)
|
gitlab-labkit (~> 0.30.1)
|
||||||
gitlab-license (~> 2.2.1)
|
gitlab-license (~> 2.2.1)
|
||||||
gitlab-mail_room (~> 0.0.9)
|
gitlab-mail_room (~> 0.0.9)
|
||||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
15.9.2
|
15.9.7
|
|
@ -7,23 +7,36 @@ export function getFrameSrc(url) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mountVueComponent = (element) => {
|
const mountVueComponent = (element) => {
|
||||||
const url = [element.dataset.frameUrl];
|
const { frameUrl, observabilityUrl } = element.dataset;
|
||||||
|
|
||||||
return new Vue({
|
try {
|
||||||
el: element,
|
if (
|
||||||
render(h) {
|
!observabilityUrl ||
|
||||||
return h('iframe', {
|
!frameUrl ||
|
||||||
style: {
|
new URL(frameUrl)?.host !== new URL(observabilityUrl).host
|
||||||
height: '366px',
|
)
|
||||||
width: '768px',
|
return;
|
||||||
},
|
|
||||||
attrs: {
|
// eslint-disable-next-line no-new
|
||||||
src: getFrameSrc(url),
|
new Vue({
|
||||||
frameBorder: '0',
|
el: element,
|
||||||
},
|
render(h) {
|
||||||
});
|
return h('iframe', {
|
||||||
},
|
style: {
|
||||||
});
|
height: '366px',
|
||||||
|
width: '768px',
|
||||||
|
},
|
||||||
|
attrs: {
|
||||||
|
src: getFrameSrc(frameUrl),
|
||||||
|
frameBorder: '0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function renderObservability(elements) {
|
export default function renderObservability(elements) {
|
||||||
|
|
|
@ -143,7 +143,7 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
skip() {
|
skip() {
|
||||||
return !this.workItemId || !this.workItemsMvcEnabled;
|
return !this.workItemId;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
workItemTypes: {
|
workItemTypes: {
|
||||||
|
@ -156,15 +156,9 @@ export default {
|
||||||
update(data) {
|
update(data) {
|
||||||
return data.workspace?.workItemTypes?.nodes;
|
return data.workspace?.workItemTypes?.nodes;
|
||||||
},
|
},
|
||||||
skip() {
|
|
||||||
return !this.workItemsMvcEnabled;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
workItemsMvcEnabled() {
|
|
||||||
return this.glFeatures.workItemsMvc;
|
|
||||||
},
|
|
||||||
taskWorkItemType() {
|
taskWorkItemType() {
|
||||||
return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id;
|
return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id;
|
||||||
},
|
},
|
||||||
|
@ -194,8 +188,7 @@ export default {
|
||||||
|
|
||||||
this.renderGFM();
|
this.renderGFM();
|
||||||
this.updateTaskStatusText();
|
this.updateTaskStatusText();
|
||||||
|
if (this.workItemId) {
|
||||||
if (this.workItemId && this.workItemsMvcEnabled) {
|
|
||||||
const taskLink = this.$el.querySelector(
|
const taskLink = this.$el.querySelector(
|
||||||
`.gfm-issue[data-issue="${getIdFromGraphQLId(this.workItemId)}"]`,
|
`.gfm-issue[data-issue="${getIdFromGraphQLId(this.workItemId)}"]`,
|
||||||
);
|
);
|
||||||
|
@ -228,9 +221,7 @@ export default {
|
||||||
|
|
||||||
this.renderSortableLists();
|
this.renderSortableLists();
|
||||||
|
|
||||||
if (this.workItemsMvcEnabled) {
|
this.renderTaskListItemActions();
|
||||||
this.renderTaskListItemActions();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderSortableLists() {
|
renderSortableLists() {
|
||||||
|
|
|
@ -110,9 +110,10 @@ export default class Project {
|
||||||
const urlParams = { [fieldName]: ref };
|
const urlParams = { [fieldName]: ref };
|
||||||
if (params.group === BRANCH_GROUP_NAME) {
|
if (params.group === BRANCH_GROUP_NAME) {
|
||||||
urlParams.ref_type = BRANCH_REF_TYPE;
|
urlParams.ref_type = BRANCH_REF_TYPE;
|
||||||
} else {
|
} else if (params.group === TAG_GROUP_NAME) {
|
||||||
urlParams.ref_type = TAG_REF_TYPE;
|
urlParams.ref_type = TAG_REF_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
link.href = mergeUrlParams(urlParams, linkTarget);
|
link.href = mergeUrlParams(urlParams, linkTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { GlButton } from '@gitlab/ui';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||||
import { escapeFileUrl, visitUrl } from '~/lib/utils/url_utility';
|
import { joinPaths, escapeFileUrl, visitUrl } from '~/lib/utils/url_utility';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
|
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
|
||||||
import PerformancePlugin from '~/performance/vue_performance_plugin';
|
import PerformancePlugin from '~/performance/vue_performance_plugin';
|
||||||
|
@ -121,7 +121,7 @@ export default function setupVueRepositoryList() {
|
||||||
|
|
||||||
if (!refSwitcherEl) return false;
|
if (!refSwitcherEl) return false;
|
||||||
|
|
||||||
const { projectId, projectRootPath } = refSwitcherEl.dataset;
|
const { projectId, projectRootPath, refType } = refSwitcherEl.dataset;
|
||||||
|
|
||||||
return new Vue({
|
return new Vue({
|
||||||
el: refSwitcherEl,
|
el: refSwitcherEl,
|
||||||
|
@ -129,7 +129,8 @@ export default function setupVueRepositoryList() {
|
||||||
return createElement(RefSelector, {
|
return createElement(RefSelector, {
|
||||||
props: {
|
props: {
|
||||||
projectId,
|
projectId,
|
||||||
value: ref,
|
value: refType ? joinPaths('refs', refType, ref) : ref,
|
||||||
|
useSymbolicRefNames: true,
|
||||||
},
|
},
|
||||||
on: {
|
on: {
|
||||||
input(selectedRef) {
|
input(selectedRef) {
|
||||||
|
|
|
@ -16,22 +16,29 @@ const getNamespaceTargetRegex = (ref) => new RegExp(`(/-/(blob|tree))/${ref}/(.*
|
||||||
* @param {string} selectedRef - The selected ref from the ref dropdown.
|
* @param {string} selectedRef - The selected ref from the ref dropdown.
|
||||||
*/
|
*/
|
||||||
export function generateRefDestinationPath(projectRootPath, ref, selectedRef) {
|
export function generateRefDestinationPath(projectRootPath, ref, selectedRef) {
|
||||||
const currentPath = window.location.pathname;
|
const url = new URL(window.location.href);
|
||||||
const encodedHash = '%23';
|
const currentPath = url.pathname;
|
||||||
|
let refType = null;
|
||||||
let namespace = '/-/tree';
|
let namespace = '/-/tree';
|
||||||
let target;
|
let target;
|
||||||
|
let actualRef = selectedRef;
|
||||||
|
|
||||||
|
const matches = selectedRef.match(/^refs\/(heads|tags)\/(.+)/);
|
||||||
|
if (matches) {
|
||||||
|
[, refType, actualRef] = matches;
|
||||||
|
}
|
||||||
|
if (refType) {
|
||||||
|
url.searchParams.set('ref_type', refType);
|
||||||
|
} else {
|
||||||
|
url.searchParams.delete('ref_type');
|
||||||
|
}
|
||||||
|
|
||||||
const NAMESPACE_TARGET_REGEX = getNamespaceTargetRegex(ref);
|
const NAMESPACE_TARGET_REGEX = getNamespaceTargetRegex(ref);
|
||||||
const match = NAMESPACE_TARGET_REGEX.exec(currentPath);
|
const match = NAMESPACE_TARGET_REGEX.exec(currentPath);
|
||||||
if (match) {
|
if (match) {
|
||||||
[, namespace, , target] = match;
|
[, namespace, , target] = match;
|
||||||
}
|
}
|
||||||
|
url.pathname = joinPaths(projectRootPath, namespace, actualRef, target);
|
||||||
|
|
||||||
const destinationPath = joinPaths(
|
return url.toString();
|
||||||
projectRootPath,
|
|
||||||
namespace,
|
|
||||||
encodeURI(selectedRef).replace(/#/g, encodedHash),
|
|
||||||
target,
|
|
||||||
);
|
|
||||||
|
|
||||||
return `${destinationPath}${window.location.hash}`;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ConfirmEmailWarning
|
module ConfirmEmailWarning
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
|
@ -17,11 +18,9 @@ module ConfirmEmailWarning
|
||||||
return unless current_user
|
return unless current_user
|
||||||
return if current_user.confirmed?
|
return if current_user.confirmed?
|
||||||
|
|
||||||
email = current_user.unconfirmed_email || current_user.email
|
|
||||||
|
|
||||||
flash.now[:warning] = format(
|
flash.now[:warning] = format(
|
||||||
confirm_warning_message,
|
confirm_warning_message,
|
||||||
email: email,
|
email: email_to_display,
|
||||||
resend_link: view_context.link_to(_('Resend it'), user_confirmation_path(user: { email: email }), method: :post),
|
resend_link: view_context.link_to(_('Resend it'), user_confirmation_path(user: { email: email }), method: :post),
|
||||||
update_link: view_context.link_to(_('Update it'), profile_path)
|
update_link: view_context.link_to(_('Update it'), profile_path)
|
||||||
).html_safe
|
).html_safe
|
||||||
|
@ -29,7 +28,16 @@ module ConfirmEmailWarning
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def email
|
||||||
|
current_user.unconfirmed_email || current_user.email
|
||||||
|
end
|
||||||
|
strong_memoize_attr :email
|
||||||
|
|
||||||
def confirm_warning_message
|
def confirm_warning_message
|
||||||
_("Please check your email (%{email}) to verify that you own this address and unlock the power of CI/CD. Didn't receive it? %{resend_link}. Wrong email address? %{update_link}.")
|
_("Please check your email (%{email}) to verify that you own this address and unlock the power of CI/CD. Didn't receive it? %{resend_link}. Wrong email address? %{update_link}.")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def email_to_display
|
||||||
|
html_escape(email)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,6 +31,7 @@ class Projects::BlobController < Projects::ApplicationController
|
||||||
before_action :authorize_edit_tree!, only: [:new, :create, :update, :destroy]
|
before_action :authorize_edit_tree!, only: [:new, :create, :update, :destroy]
|
||||||
|
|
||||||
before_action :commit, except: [:new, :create]
|
before_action :commit, except: [:new, :create]
|
||||||
|
before_action :check_for_ambiguous_ref, only: [:show]
|
||||||
before_action :blob, except: [:new, :create]
|
before_action :blob, except: [:new, :create]
|
||||||
before_action :require_branch_head, only: [:edit, :update]
|
before_action :require_branch_head, only: [:edit, :update]
|
||||||
before_action :editor_variables, except: [:show, :preview, :diff]
|
before_action :editor_variables, except: [:show, :preview, :diff]
|
||||||
|
@ -145,6 +146,15 @@ class Projects::BlobController < Projects::ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_for_ambiguous_ref
|
||||||
|
@ref_type = ref_type
|
||||||
|
|
||||||
|
if @ref_type == ExtractsRef::BRANCH_REF_TYPE && ambiguous_ref?(@project, @ref)
|
||||||
|
branch = @project.repository.find_branch(@ref)
|
||||||
|
redirect_to project_blob_path(@project, File.join(branch.target, @path))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def commit
|
def commit
|
||||||
@commit ||= @repository.commit(@ref)
|
@commit ||= @repository.commit(@ref)
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ class Projects::RefsController < Projects::ApplicationController
|
||||||
when "tree"
|
when "tree"
|
||||||
project_tree_path(@project, @id)
|
project_tree_path(@project, @id)
|
||||||
when "blob"
|
when "blob"
|
||||||
project_blob_path(@project, @id)
|
project_blob_path(@project, @id, ref_type: ref_type)
|
||||||
when "graph"
|
when "graph"
|
||||||
project_network_path(@project, @id, ref_type: ref_type)
|
project_network_path(@project, @id, ref_type: ref_type)
|
||||||
when "graphs"
|
when "graphs"
|
||||||
|
|
|
@ -28,6 +28,15 @@ class Projects::TreeController < Projects::ApplicationController
|
||||||
def show
|
def show
|
||||||
return render_404 unless @commit
|
return render_404 unless @commit
|
||||||
|
|
||||||
|
@ref_type = ref_type
|
||||||
|
if @ref_type == BRANCH_REF_TYPE && ambiguous_ref?(@project, @ref)
|
||||||
|
branch = @project.repository.find_branch(@ref)
|
||||||
|
if branch
|
||||||
|
redirect_to project_tree_path(@project, branch.target)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if tree.entries.empty?
|
if tree.entries.empty?
|
||||||
if @repository.blob_at(@commit.id, @path)
|
if @repository.blob_at(@commit.id, @path)
|
||||||
redirect_to project_blob_path(@project, File.join(@ref, @path))
|
redirect_to project_blob_path(@project, File.join(@ref, @path))
|
||||||
|
|
|
@ -171,11 +171,19 @@ class ProjectsController < Projects::ApplicationController
|
||||||
flash.now[:alert] = _("Project '%{project_name}' queued for deletion.") % { project_name: @project.name }
|
flash.now[:alert] = _("Project '%{project_name}' queued for deletion.") % { project_name: @project.name }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if ambiguous_ref?(@project, @ref)
|
||||||
|
branch = @project.repository.find_branch(@ref)
|
||||||
|
|
||||||
|
# The files view would render a ref other than the default branch
|
||||||
|
# This redirect can be removed once the view is fixed
|
||||||
|
redirect_to(project_tree_path(@project, branch.target), alert: _("The default branch of this project clashes with another ref"))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
@notification_setting = current_user.notification_settings_for(@project) if current_user
|
@notification_setting = current_user.notification_settings_for(@project) if current_user
|
||||||
@project = @project.present(current_user: current_user)
|
@project = @project.present(current_user: current_user)
|
||||||
|
|
||||||
render_landing_page
|
render_landing_page
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -32,18 +32,9 @@ module Environments
|
||||||
end
|
end
|
||||||
|
|
||||||
def namespace_environments
|
def namespace_environments
|
||||||
# We assume reporter access is needed for the :read_environment permission
|
|
||||||
# here. This expection is also present in
|
|
||||||
# IssuableFinder::Params#min_access_level, which is used for filtering out
|
|
||||||
# merge requests that don't have the right permissions.
|
|
||||||
#
|
|
||||||
# We use this approach so we don't need to load every project into memory
|
|
||||||
# just to verify if we can see their environments. Doing so would not be
|
|
||||||
# efficient, and possibly mess up pagination if certain projects are not
|
|
||||||
# meant to be visible.
|
|
||||||
projects = project_or_group
|
projects = project_or_group
|
||||||
.all_projects
|
.all_projects
|
||||||
.public_or_visible_to_user(current_user, Gitlab::Access::REPORTER)
|
.filter_by_feature_visibility(:environments, current_user)
|
||||||
|
|
||||||
Environment.for_project(projects)
|
Environment.for_project(projects)
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,6 +30,7 @@ class NotesFinder
|
||||||
notes = init_collection
|
notes = init_collection
|
||||||
notes = since_fetch_at(notes)
|
notes = since_fetch_at(notes)
|
||||||
notes = notes.with_notes_filter(@params[:notes_filter]) if notes_filter?
|
notes = notes.with_notes_filter(@params[:notes_filter]) if notes_filter?
|
||||||
|
notes = redact_internal(notes)
|
||||||
sort(notes)
|
sort(notes)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -181,6 +182,13 @@ class NotesFinder
|
||||||
|
|
||||||
notes.order_by(sort)
|
notes.order_by(sort)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def redact_internal(notes)
|
||||||
|
subject = @project || target
|
||||||
|
return notes if Ability.allowed?(@current_user, :read_internal_note, subject)
|
||||||
|
|
||||||
|
notes.not_internal
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
NotesFinder.prepend_mod_with('NotesFinder')
|
NotesFinder.prepend_mod_with('NotesFinder')
|
||||||
|
|
|
@ -116,7 +116,7 @@ module AvatarsHelper
|
||||||
private
|
private
|
||||||
|
|
||||||
def avatar_icon_by_user_email_or_gravatar(email, size, scale, only_path:)
|
def avatar_icon_by_user_email_or_gravatar(email, size, scale, only_path:)
|
||||||
user = User.find_by_any_email(email)
|
user = User.with_public_email(email).first
|
||||||
|
|
||||||
if user
|
if user
|
||||||
avatar_icon_for_user(user, size, scale, only_path: only_path)
|
avatar_icon_for_user(user, size, scale, only_path: only_path)
|
||||||
|
|
|
@ -24,25 +24,37 @@ module Taskable
|
||||||
(\s.+) # followed by whitespace and some text.
|
(\s.+) # followed by whitespace and some text.
|
||||||
}x.freeze
|
}x.freeze
|
||||||
|
|
||||||
|
ITEM_PATTERN_UNTRUSTED =
|
||||||
|
'^' \
|
||||||
|
'(?:(?:>\s{0,4})*)' \
|
||||||
|
'(?P<prefix>(?:\s*(?:[-+*]|(?:\d+\.)))+)' \
|
||||||
|
'\s+' \
|
||||||
|
'(?P<checkbox>' \
|
||||||
|
"#{COMPLETE_PATTERN.source}|#{INCOMPLETE_PATTERN.source}" \
|
||||||
|
')' \
|
||||||
|
'(?P<label>\s.+)'.freeze
|
||||||
|
|
||||||
# ignore tasks in code or html comment blocks. HTML blocks
|
# ignore tasks in code or html comment blocks. HTML blocks
|
||||||
# are ok as we allow tasks inside <detail> blocks
|
# are ok as we allow tasks inside <detail> blocks
|
||||||
REGEX = %r{
|
REGEX =
|
||||||
#{::Gitlab::Regex.markdown_code_or_html_comments}
|
"#{::Gitlab::Regex.markdown_code_or_html_comments_untrusted}" \
|
||||||
|
|
"|" \
|
||||||
(?<task_item>
|
"(?P<task_item>" \
|
||||||
#{ITEM_PATTERN}
|
"#{ITEM_PATTERN_UNTRUSTED}" \
|
||||||
)
|
")".freeze
|
||||||
}mx.freeze
|
|
||||||
|
|
||||||
def self.get_tasks(content)
|
def self.get_tasks(content)
|
||||||
items = []
|
items = []
|
||||||
|
|
||||||
content.to_s.scan(REGEX) do
|
regex = Gitlab::UntrustedRegexp.new(REGEX, multiline: true)
|
||||||
next unless $~[:task_item]
|
regex.scan(content.to_s).each do |match|
|
||||||
|
next unless regex.extract_named_group(:task_item, match)
|
||||||
|
|
||||||
$~[:task_item].scan(ITEM_PATTERN) do |prefix, checkbox, label|
|
prefix = regex.extract_named_group(:prefix, match)
|
||||||
items << TaskList::Item.new("#{prefix.strip} #{checkbox}", label.strip)
|
checkbox = regex.extract_named_group(:checkbox, match)
|
||||||
end
|
label = regex.extract_named_group(:label, match)
|
||||||
|
|
||||||
|
items << TaskList::Item.new("#{prefix.strip} #{checkbox}", label.strip)
|
||||||
end
|
end
|
||||||
|
|
||||||
items
|
items
|
||||||
|
|
|
@ -41,7 +41,7 @@ class WebHook < ApplicationRecord
|
||||||
after_initialize :initialize_url_variables
|
after_initialize :initialize_url_variables
|
||||||
|
|
||||||
before_validation :reset_token
|
before_validation :reset_token
|
||||||
before_validation :reset_url_variables, unless: ->(hook) { hook.is_a?(ServiceHook) }
|
before_validation :reset_url_variables, unless: ->(hook) { hook.is_a?(ServiceHook) }, on: :update
|
||||||
before_validation :set_branch_filter_nil, if: :branch_filter_strategy_all_branches?
|
before_validation :set_branch_filter_nil, if: :branch_filter_strategy_all_branches?
|
||||||
validates :push_events_branch_filter, untrusted_regexp: true, if: :branch_filter_strategy_regex?
|
validates :push_events_branch_filter, untrusted_regexp: true, if: :branch_filter_strategy_regex?
|
||||||
validates :push_events_branch_filter, "web_hooks/wildcard_branch_filter": true, if: :branch_filter_strategy_wildcard?
|
validates :push_events_branch_filter, "web_hooks/wildcard_branch_filter": true, if: :branch_filter_strategy_wildcard?
|
||||||
|
@ -150,7 +150,7 @@ class WebHook < ApplicationRecord
|
||||||
# See app/validators/json_schemas/web_hooks_url_variables.json
|
# See app/validators/json_schemas/web_hooks_url_variables.json
|
||||||
VARIABLE_REFERENCE_RE = /\{([A-Za-z]+[0-9]*(?:[._-][A-Za-z0-9]+)*)\}/.freeze
|
VARIABLE_REFERENCE_RE = /\{([A-Za-z]+[0-9]*(?:[._-][A-Za-z0-9]+)*)\}/.freeze
|
||||||
|
|
||||||
def interpolated_url
|
def interpolated_url(url = self.url, url_variables = self.url_variables)
|
||||||
return url unless url.include?('{')
|
return url unless url.include?('{')
|
||||||
|
|
||||||
vars = url_variables
|
vars = url_variables
|
||||||
|
@ -176,7 +176,19 @@ class WebHook < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset_url_variables
|
def reset_url_variables
|
||||||
self.url_variables = {} if url_changed? && !encrypted_url_variables_changed?
|
interpolated_url_was = interpolated_url(decrypt_url_was, url_variables_were)
|
||||||
|
|
||||||
|
return if url_variables_were.empty? || interpolated_url_was == interpolated_url
|
||||||
|
|
||||||
|
self.url_variables = {} if url_changed? && url_variables_were.to_a.intersection(url_variables.to_a).any?
|
||||||
|
end
|
||||||
|
|
||||||
|
def decrypt_url_was
|
||||||
|
self.class.decrypt_url(encrypted_url_was, iv: Base64.decode64(encrypted_url_iv_was))
|
||||||
|
end
|
||||||
|
|
||||||
|
def url_variables_were
|
||||||
|
self.class.decrypt_url_variables(encrypted_url_variables_was, iv: encrypted_url_variables_iv_was)
|
||||||
end
|
end
|
||||||
|
|
||||||
def next_failure_count
|
def next_failure_count
|
||||||
|
|
|
@ -36,7 +36,8 @@ class ProjectFeature < ApplicationRecord
|
||||||
merge_requests: Gitlab::Access::REPORTER,
|
merge_requests: Gitlab::Access::REPORTER,
|
||||||
metrics_dashboard: Gitlab::Access::REPORTER,
|
metrics_dashboard: Gitlab::Access::REPORTER,
|
||||||
container_registry: Gitlab::Access::REPORTER,
|
container_registry: Gitlab::Access::REPORTER,
|
||||||
package_registry: Gitlab::Access::REPORTER
|
package_registry: Gitlab::Access::REPORTER,
|
||||||
|
environments: Gitlab::Access::REPORTER
|
||||||
}.freeze
|
}.freeze
|
||||||
PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT = { repository: Gitlab::Access::REPORTER }.freeze
|
PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT = { repository: Gitlab::Access::REPORTER }.freeze
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,9 @@ module Uploads
|
||||||
private
|
private
|
||||||
|
|
||||||
def delete_object(key)
|
def delete_object(key)
|
||||||
connection.delete_object(bucket_name, key)
|
return unless available?
|
||||||
|
|
||||||
|
connection.delete_object(bucket_name, object_key(key))
|
||||||
|
|
||||||
# So far, only GoogleCloudStorage raises an exception when the file is not found.
|
# So far, only GoogleCloudStorage raises an exception when the file is not found.
|
||||||
# Other providers support idempotent requests and does not raise an error
|
# Other providers support idempotent requests and does not raise an error
|
||||||
|
@ -35,11 +37,16 @@ module Uploads
|
||||||
end
|
end
|
||||||
|
|
||||||
def bucket_name
|
def bucket_name
|
||||||
return unless available?
|
|
||||||
|
|
||||||
object_store.remote_directory
|
object_store.remote_directory
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def object_key(key)
|
||||||
|
# We allow administrators to create "sub buckets" by setting a prefix.
|
||||||
|
# This makes it possible to deploy GitLab with only one object storage
|
||||||
|
# bucket. This mirrors the implementation in app/uploaders/object_storage.rb.
|
||||||
|
File.join([object_store.bucket_prefix, key].compact)
|
||||||
|
end
|
||||||
|
|
||||||
def connection
|
def connection
|
||||||
return unless available?
|
return unless available?
|
||||||
|
|
||||||
|
|
|
@ -412,7 +412,6 @@ class ProjectPolicy < BasePolicy
|
||||||
end
|
end
|
||||||
|
|
||||||
rule { can?(:metrics_dashboard) }.policy do
|
rule { can?(:metrics_dashboard) }.policy do
|
||||||
enable :read_prometheus
|
|
||||||
enable :read_deployment
|
enable :read_deployment
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ module Ci
|
||||||
end
|
end
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def clone!(job, variables: [], enqueue_if_actionable: false)
|
def clone!(job, variables: [], enqueue_if_actionable: false, start_pipeline: false)
|
||||||
# Cloning a job requires a strict type check to ensure
|
# Cloning a job requires a strict type check to ensure
|
||||||
# the attributes being used for the clone are taken straight
|
# the attributes being used for the clone are taken straight
|
||||||
# from the model and not overridden by other abstractions.
|
# from the model and not overridden by other abstractions.
|
||||||
|
@ -32,7 +32,11 @@ module Ci
|
||||||
new_job.set_enqueue_immediately!
|
new_job.set_enqueue_immediately!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
start_pipeline_proc = -> { start_pipeline(job, new_job) } if start_pipeline
|
||||||
|
|
||||||
new_job.run_after_commit do
|
new_job.run_after_commit do
|
||||||
|
start_pipeline_proc&.call
|
||||||
|
|
||||||
::Ci::CopyCrossDatabaseAssociationsService.new.execute(job, new_job)
|
::Ci::CopyCrossDatabaseAssociationsService.new.execute(job, new_job)
|
||||||
|
|
||||||
::Deployments::CreateForBuildService.new.execute(new_job)
|
::Deployments::CreateForBuildService.new.execute(new_job)
|
||||||
|
@ -59,15 +63,12 @@ module Ci
|
||||||
def check_assignable_runners!(job); end
|
def check_assignable_runners!(job); end
|
||||||
|
|
||||||
def retry_job(job, variables: [])
|
def retry_job(job, variables: [])
|
||||||
clone!(job, variables: variables, enqueue_if_actionable: true).tap do |new_job|
|
clone!(job, variables: variables, enqueue_if_actionable: true, start_pipeline: true).tap do |new_job|
|
||||||
check_assignable_runners!(new_job) if new_job.is_a?(Ci::Build)
|
check_assignable_runners!(new_job) if new_job.is_a?(Ci::Build)
|
||||||
|
|
||||||
next if new_job.failed?
|
next if new_job.failed?
|
||||||
|
|
||||||
ResetSkippedJobsService.new(project, current_user).execute(job)
|
ResetSkippedJobsService.new(project, current_user).execute(job)
|
||||||
|
|
||||||
Ci::PipelineCreation::StartPipelineService.new(job.pipeline).execute
|
|
||||||
new_job.reset
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -76,6 +77,11 @@ module Ci
|
||||||
raise Gitlab::Access::AccessDeniedError, '403 Forbidden'
|
raise Gitlab::Access::AccessDeniedError, '403 Forbidden'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def start_pipeline(job, new_job)
|
||||||
|
Ci::PipelineCreation::StartPipelineService.new(job.pipeline).execute
|
||||||
|
new_job.reset
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -33,15 +33,9 @@ module Ci
|
||||||
current_project_ids = runner.projects.ids
|
current_project_ids = runner.projects.ids
|
||||||
# rubocop:enable CodeReuse/ActiveRecord
|
# rubocop:enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
unless associate_new_projects(new_project_ids, current_project_ids)
|
response = associate_new_projects(new_project_ids, current_project_ids)
|
||||||
response = ServiceResponse.error(message: 'failed to assign projects to runner')
|
response = disassociate_old_projects(new_project_ids, current_project_ids) if response.success?
|
||||||
raise ActiveRecord::Rollback, response.errors
|
raise ActiveRecord::Rollback, response.errors unless response.success?
|
||||||
end
|
|
||||||
|
|
||||||
unless disassociate_old_projects(new_project_ids, current_project_ids)
|
|
||||||
response = ServiceResponse.error(message: 'failed to destroy runner project')
|
|
||||||
raise ActiveRecord::Rollback, response.errors
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
response
|
response
|
||||||
|
@ -49,16 +43,29 @@ module Ci
|
||||||
|
|
||||||
def associate_new_projects(new_project_ids, current_project_ids)
|
def associate_new_projects(new_project_ids, current_project_ids)
|
||||||
missing_projects = Project.id_in(new_project_ids - current_project_ids)
|
missing_projects = Project.id_in(new_project_ids - current_project_ids)
|
||||||
missing_projects.all? { |project| runner.assign_to(project, current_user) }
|
|
||||||
|
unless missing_projects.all? { |project| current_user.can?(:register_project_runners, project) }
|
||||||
|
return ServiceResponse.error(message: 'user is not authorized to add runners to project')
|
||||||
|
end
|
||||||
|
|
||||||
|
unless missing_projects.all? { |project| runner.assign_to(project, current_user) }
|
||||||
|
return ServiceResponse.error(message: 'failed to assign projects to runner')
|
||||||
|
end
|
||||||
|
|
||||||
|
ServiceResponse.success
|
||||||
end
|
end
|
||||||
|
|
||||||
def disassociate_old_projects(new_project_ids, current_project_ids)
|
def disassociate_old_projects(new_project_ids, current_project_ids)
|
||||||
projects_to_be_deleted = current_project_ids - new_project_ids
|
projects_to_be_deleted = current_project_ids - new_project_ids
|
||||||
return true if projects_to_be_deleted.empty?
|
return ServiceResponse.success if projects_to_be_deleted.empty?
|
||||||
|
|
||||||
Ci::RunnerProject
|
all_destroyed =
|
||||||
.destroy_by(project_id: projects_to_be_deleted)
|
Ci::RunnerProject
|
||||||
.all?(&:destroyed?)
|
.destroy_by(project_id: projects_to_be_deleted)
|
||||||
|
.all?(&:destroyed?)
|
||||||
|
return ServiceResponse.success if all_destroyed
|
||||||
|
|
||||||
|
ServiceResponse.error(message: 'failed to destroy runner project')
|
||||||
end
|
end
|
||||||
|
|
||||||
attr_reader :runner, :current_user, :project_ids
|
attr_reader :runner, :current_user, :project_ids
|
||||||
|
|
|
@ -54,7 +54,15 @@ module MergeRequests
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_service
|
def validate_service
|
||||||
errors << 'User is required' if current_user.nil?
|
if current_user.nil?
|
||||||
|
errors << 'User is required'
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
unless current_user&.can?(:read_code, target_project)
|
||||||
|
errors << 'User access was denied'
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
unless target_project.merge_requests_enabled?
|
unless target_project.merge_requests_enabled?
|
||||||
errors << "Merge requests are not enabled for project #{target_project.full_path}"
|
errors << "Merge requests are not enabled for project #{target_project.full_path}"
|
||||||
|
|
|
@ -18,5 +18,5 @@
|
||||||
%h5= _("Maximum page reached")
|
%h5= _("Maximum page reached")
|
||||||
%p= _("Sorry, you have exceeded the maximum browsable page number. Please use the API to explore further.")
|
%p= _("Sorry, you have exceeded the maximum browsable page number. Please use the API to explore further.")
|
||||||
|
|
||||||
= render Pajamas::ButtonComponent.new(href: request.params.merge(page: @max_page_number)) do
|
= render Pajamas::ButtonComponent.new(href: safe_params.merge(page: @max_page_number)) do
|
||||||
= _("Back to page %{number}") % { number: @max_page_number }
|
= _("Back to page %{number}") % { number: @max_page_number }
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
.tree-ref-container.gl-display-flex.gl-flex-wrap.gl-gap-2.mb-2.mb-md-0
|
.tree-ref-container.gl-display-flex.gl-flex-wrap.gl-gap-2.mb-2.mb-md-0
|
||||||
.tree-ref-holder.gl-max-w-26
|
.tree-ref-holder.gl-max-w-26
|
||||||
#js-tree-ref-switcher{ data: { project_id: @project.id, project_root_path: project_path(@project) } }
|
#js-tree-ref-switcher{ data: { project_id: @project.id, ref_type: @ref_type.to_s, project_root_path: project_path(@project) } }
|
||||||
|
|
||||||
#js-repo-breadcrumb{ data: breadcrumb_data_attributes }
|
#js-repo-breadcrumb{ data: breadcrumb_data_attributes }
|
||||||
|
|
||||||
|
|
|
@ -628,11 +628,16 @@ production: &base
|
||||||
geo_secondary_registry_consistency_worker:
|
geo_secondary_registry_consistency_worker:
|
||||||
cron: "* * * * *"
|
cron: "* * * * *"
|
||||||
|
|
||||||
# GitLab Geo registry sync worker (for backfilling)
|
# GitLab Geo blob registry sync worker (for backfilling)
|
||||||
# NOTE: This will only take effect if Geo is enabled (secondary nodes only)
|
# NOTE: This will only take effect if Geo is enabled (secondary nodes only)
|
||||||
geo_registry_sync_worker:
|
geo_registry_sync_worker:
|
||||||
cron: "*/1 * * * *"
|
cron: "*/1 * * * *"
|
||||||
|
|
||||||
|
# GitLab Geo repository registry sync worker (for backfilling)
|
||||||
|
# NOTE: This will only take effect if Geo is enabled (secondary nodes only)
|
||||||
|
geo_repository_registry_sync_worker:
|
||||||
|
cron: "*/1 * * * *"
|
||||||
|
|
||||||
# Elasticsearch bulk updater for incremental updates.
|
# Elasticsearch bulk updater for incremental updates.
|
||||||
# NOTE: This will only take effect if elasticsearch is enabled.
|
# NOTE: This will only take effect if elasticsearch is enabled.
|
||||||
elastic_index_bulk_cron_worker:
|
elastic_index_bulk_cron_worker:
|
||||||
|
|
|
@ -721,6 +721,9 @@ Gitlab.ee do
|
||||||
Settings.cron_jobs['geo_registry_sync_worker'] ||= Settingslogic.new({})
|
Settings.cron_jobs['geo_registry_sync_worker'] ||= Settingslogic.new({})
|
||||||
Settings.cron_jobs['geo_registry_sync_worker']['cron'] ||= '*/1 * * * *'
|
Settings.cron_jobs['geo_registry_sync_worker']['cron'] ||= '*/1 * * * *'
|
||||||
Settings.cron_jobs['geo_registry_sync_worker']['job_class'] ||= 'Geo::RegistrySyncWorker'
|
Settings.cron_jobs['geo_registry_sync_worker']['job_class'] ||= 'Geo::RegistrySyncWorker'
|
||||||
|
Settings.cron_jobs['geo_repository_registry_sync_worker'] ||= Settingslogic.new({})
|
||||||
|
Settings.cron_jobs['geo_repository_registry_sync_worker']['cron'] ||= '*/1 * * * *'
|
||||||
|
Settings.cron_jobs['geo_repository_registry_sync_worker']['job_class'] ||= 'Geo::RepositoryRegistrySyncWorker'
|
||||||
Settings.cron_jobs['geo_metrics_update_worker'] ||= Settingslogic.new({})
|
Settings.cron_jobs['geo_metrics_update_worker'] ||= Settingslogic.new({})
|
||||||
Settings.cron_jobs['geo_metrics_update_worker']['cron'] ||= '*/1 * * * *'
|
Settings.cron_jobs['geo_metrics_update_worker']['cron'] ||= '*/1 * * * *'
|
||||||
Settings.cron_jobs['geo_metrics_update_worker']['job_class'] ||= 'Geo::MetricsUpdateWorker'
|
Settings.cron_jobs['geo_metrics_update_worker']['job_class'] ||= 'Geo::MetricsUpdateWorker'
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
return if helper.stable_branch?
|
||||||
|
|
||||||
data_qa_selectors = /qa_selector|data-qa-selector/
|
data_qa_selectors = /qa_selector|data-qa-selector/
|
||||||
|
|
||||||
deprecated_qa_selectors = /(?!.*\bdata-qa-)(?=class=.*qa-.*|class: .*qa-.*)/
|
deprecated_qa_selectors = /(?!.*\bdata-qa-)(?=class=.*qa-.*|class: .*qa-.*)/
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
if stable_branch.non_security_stable_branch?
|
if stable_branch.encourage_package_and_qa_execution?
|
||||||
markdown(<<~MARKDOWN)
|
markdown(<<~MARKDOWN)
|
||||||
### QA `e2e:package-and-test`
|
## QA `e2e:package-and-test`
|
||||||
|
|
||||||
**@#{helper.mr_author}, the `package-and-test` job must complete before merging this merge request.***
|
**@#{helper.mr_author}, the `package-and-test` job must complete before merging this merge request.***
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,3 @@ has_milestone = !gitlab.mr_json["milestone"].nil?
|
||||||
unless has_milestone || (helper.security_mr? && helper.mr_target_branch == default_branch)
|
unless has_milestone || (helper.security_mr? && helper.mr_target_branch == default_branch)
|
||||||
warn "This merge request does not refer to an existing milestone.", sticky: false
|
warn "This merge request does not refer to an existing milestone.", sticky: false
|
||||||
end
|
end
|
||||||
|
|
||||||
has_pick_into_stable_label = helper.mr_labels.find { |label| label.start_with?('Pick into') }
|
|
||||||
|
|
||||||
if helper.mr_target_branch != default_branch && !has_pick_into_stable_label && !helper.security_mr?
|
|
||||||
warn "Most of the time, merge requests should target `#{default_branch}`. Otherwise, please set the relevant `Pick into X.Y` label."
|
|
||||||
end
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ require './spec/support/sidekiq_middleware'
|
||||||
|
|
||||||
Sidekiq::Testing.inline! do
|
Sidekiq::Testing.inline! do
|
||||||
Gitlab::Seeder.quiet do
|
Gitlab::Seeder.quiet do
|
||||||
User.not_mass_generated.sample(10).each do |user|
|
User.humans.not_mass_generated.sample(10).each do |user|
|
||||||
source_project = Project.not_mass_generated.public_only.sample
|
source_project = Project.not_mass_generated.public_only.sample
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class FinalizeBackfillUserDetailsFields < Gitlab::Database::Migration[2.0]
|
||||||
|
BACKFILL_MIGRATION = 'BackfillUserDetailsFields'
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||||
|
|
||||||
|
def up
|
||||||
|
# If the 20230116160904_remove_user_details_fields_from_user.rb migration already ran,
|
||||||
|
# finalizing this background migration will fail.
|
||||||
|
return unless column_exists?(:users, :linkedin)
|
||||||
|
|
||||||
|
ensure_batched_background_migration_is_finished(
|
||||||
|
job_class_name: BACKFILL_MIGRATION,
|
||||||
|
table_name: :users,
|
||||||
|
column_name: :id,
|
||||||
|
job_arguments: [],
|
||||||
|
finalize: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down; end
|
||||||
|
end
|
|
@ -1,13 +1,11 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class RemoveTempIndexForUserDetailsFields < Gitlab::Database::Migration[2.0]
|
class RemoveTempIndexForUserDetailsFields < Gitlab::Database::Migration[2.0]
|
||||||
BACKFILL_MIGRATION = 'BackfillUserDetailsFields'
|
|
||||||
INDEX_NAME = 'tmp_idx_where_user_details_fields_filled'
|
INDEX_NAME = 'tmp_idx_where_user_details_fields_filled'
|
||||||
|
|
||||||
disable_ddl_transaction!
|
disable_ddl_transaction!
|
||||||
|
|
||||||
def up
|
def up
|
||||||
finalize_background_migration BACKFILL_MIGRATION
|
|
||||||
remove_concurrent_index_by_name :users, INDEX_NAME
|
remove_concurrent_index_by_name :users, INDEX_NAME
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class NullifyLastErrorFromProjectMirrorData < Gitlab::Database::Migration[2.1]
|
||||||
|
MIGRATION = 'NullifyLastErrorFromProjectMirrorData'
|
||||||
|
INTERVAL = 2.minutes
|
||||||
|
BATCH_SIZE = 10_000
|
||||||
|
SUB_BATCH_SIZE = 1_000
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||||
|
|
||||||
|
def up
|
||||||
|
queue_batched_background_migration(
|
||||||
|
MIGRATION,
|
||||||
|
:project_mirror_data,
|
||||||
|
:id,
|
||||||
|
job_interval: INTERVAL,
|
||||||
|
batch_size: BATCH_SIZE,
|
||||||
|
sub_batch_size: SUB_BATCH_SIZE
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
delete_batched_background_migration(MIGRATION, :project_mirror_data, :id, [])
|
||||||
|
end
|
||||||
|
end
|
1
db/schema_migrations/20221102231130
Normal file
1
db/schema_migrations/20221102231130
Normal file
|
@ -0,0 +1 @@
|
||||||
|
8678040a9fa8da1d455489db89e00084943d1dced6dd01cbf3517afd1a47bac5
|
1
db/schema_migrations/20230208131808
Normal file
1
db/schema_migrations/20230208131808
Normal file
|
@ -0,0 +1 @@
|
||||||
|
784f8f189eee7b5cf3136f0a859874a1d170d2b148f4c260f968b144816f1322
|
|
@ -1363,6 +1363,7 @@ The bug causes all wildcard domains (`.example.com`) to be ignored except for th
|
||||||
gitaly['env'] = {
|
gitaly['env'] = {
|
||||||
"no_proxy" => "sever.yourdomain.org, .yourdomain.com",
|
"no_proxy" => "sever.yourdomain.org, .yourdomain.com",
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
1. Reconfigure GitLab:
|
1. Reconfigure GitLab:
|
||||||
|
|
||||||
|
|
|
@ -258,7 +258,7 @@ One way to generate the initial list is to run the Rake task `rubocop:todo:gener
|
||||||
bundle exec rake rubocop:todo:generate
|
bundle exec rake rubocop:todo:generate
|
||||||
```
|
```
|
||||||
|
|
||||||
To generate TODO list for specific RuboCop rules, pass them comma-seperated as
|
To generate TODO list for specific RuboCop rules, pass them comma-separated as
|
||||||
argument to the Rake task:
|
argument to the Rake task:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
|
|
@ -117,20 +117,14 @@ Install the required packages (needed to compile Ruby and native extensions to R
|
||||||
```shell
|
```shell
|
||||||
sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libre2-dev \
|
sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libre2-dev \
|
||||||
libreadline-dev libncurses5-dev libffi-dev curl openssh-server libxml2-dev libxslt-dev \
|
libreadline-dev libncurses5-dev libffi-dev curl openssh-server libxml2-dev libxslt-dev \
|
||||||
libcurl4-openssl-dev libicu-dev logrotate rsync python3-docutils pkg-config cmake runit-systemd
|
libcurl4-openssl-dev libicu-dev libkrb5-dev logrotate rsync python3-docutils pkg-config cmake \
|
||||||
|
runit-systemd
|
||||||
```
|
```
|
||||||
|
|
||||||
NOTE:
|
NOTE:
|
||||||
GitLab requires OpenSSL version 1.1. If your Linux distribution includes a different version of OpenSSL,
|
GitLab requires OpenSSL version 1.1. If your Linux distribution includes a different version of OpenSSL,
|
||||||
you might have to install 1.1 manually.
|
you might have to install 1.1 manually.
|
||||||
|
|
||||||
If you want to use Kerberos for user authentication, install `libkrb5-dev`
|
|
||||||
(if you don't know what Kerberos is, you can assume you don't need it):
|
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo apt-get install libkrb5-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Git
|
### Git
|
||||||
|
|
||||||
From GitLab 13.6, we recommend you use the
|
From GitLab 13.6, we recommend you use the
|
||||||
|
|
|
@ -3115,7 +3115,7 @@ such as the following:
|
||||||
| Encrypt SAML assertion | Optional | Uses TLS between your identity provider, the user's browser, and GitLab. |
|
| Encrypt SAML assertion | Optional | Uses TLS between your identity provider, the user's browser, and GitLab. |
|
||||||
| Sign SAML assertion | Optional | Validates the integrity of a SAML assertion. When active, signs the whole response. |
|
| Sign SAML assertion | Optional | Validates the integrity of a SAML assertion. When active, signs the whole response. |
|
||||||
| Check SAML request signature | Optional | Checks the signature on the SAML response. |
|
| Check SAML request signature | Optional | Checks the signature on the SAML response. |
|
||||||
| Default RelayState | Optional | Specifies the URL users should end up on after successfully signing in through SAML at your IdP. |
|
| Default RelayState | Optional | Specifies the sub-paths of the base URL that users should end up on after successfully signing in through SAML at your IdP. |
|
||||||
| NameID format | Persistent | See [NameID format details](../user/group/saml_sso/index.md#nameid-format). |
|
| NameID format | Persistent | See [NameID format details](../user/group/saml_sso/index.md#nameid-format). |
|
||||||
| Additional URLs | Optional | May include the issuer, identifier, or assertion consumer service URL in other fields on some providers. |
|
| Additional URLs | Optional | May include the issuer, identifier, or assertion consumer service URL in other fields on some providers. |
|
||||||
|
|
||||||
|
|
|
@ -266,7 +266,7 @@ arguments until the status query returns no rows.
|
||||||
|
|
||||||
1. Run a reconfigure:
|
1. Run a reconfigure:
|
||||||
|
|
||||||
```plaintext
|
```plaintext
|
||||||
sudo gitlab-ctl reconfigure
|
sudo gitlab-ctl reconfigure
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -325,6 +325,37 @@ The results from the query can be plugged into the command:
|
||||||
sudo gitlab-rake gitlab:background_migrations:finalize[CopyColumnUsingBackgroundMigrationJob,events,id,'[["id"]\, ["id_convert_to_bigint"]]']
|
sudo gitlab-rake gitlab:background_migrations:finalize[CopyColumnUsingBackgroundMigrationJob,events,id,'[["id"]\, ["id_convert_to_bigint"]]']
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Mark a batched migration finished
|
||||||
|
|
||||||
|
There can be cases where the background migration fails: when jumping too many version upgrades,
|
||||||
|
or backward-incompatible database schema changes. (For an example, see [issue 393216](https://gitlab.com/gitlab-org/gitlab/-/issues/393216)).
|
||||||
|
Failed background migrations prevent further application upgrades.
|
||||||
|
|
||||||
|
When the background migration is determined to be "safe" to skip, the migration can be manually marked finished:
|
||||||
|
|
||||||
|
WARNING:
|
||||||
|
Make sure you create a backup before proceeding.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Start the rails console
|
||||||
|
|
||||||
|
connection = ApplicationRecord.connection # or Ci::ApplicationRecord.connection, depending on which DB was the migration scheduled
|
||||||
|
|
||||||
|
Gitlab::Database::SharedModel.using_connection(connection) do
|
||||||
|
migration = Gitlab::Database::BackgroundMigration::BatchedMigration.find_for_configuration(
|
||||||
|
Gitlab::Database.gitlab_schemas_for_connection(connection),
|
||||||
|
'BackfillUserDetailsFields',
|
||||||
|
:users,
|
||||||
|
:id,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
# mark all jobs completed
|
||||||
|
migration.batched_jobs.update_all(status: Gitlab::Database::BackgroundMigration::BatchedJob.state_machine.states['succeeded'].value)
|
||||||
|
migration.update_attribute(:status, Gitlab::Database::BackgroundMigration::BatchedMigration.state_machine.states[:finished].value)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
### The `BackfillNamespaceIdForNamespaceRoute` batched migration job fails
|
### The `BackfillNamespaceIdForNamespaceRoute` batched migration job fails
|
||||||
|
|
||||||
In GitLab 14.8, the `BackfillNamespaceIdForNamespaceRoute` batched background migration job
|
In GitLab 14.8, the `BackfillNamespaceIdForNamespaceRoute` batched background migration job
|
||||||
|
|
|
@ -524,6 +524,7 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
|
||||||
|
|
||||||
### 15.4.6
|
### 15.4.6
|
||||||
|
|
||||||
|
- Due to a [bug introduced in curl in GitLab 15.4.6](https://github.com/curl/curl/issues/10122), the [`no_proxy` environment variable may not work properly](../administration/geo/replication/troubleshooting.md#secondary-site-returns-received-http-code-403-from-proxy-after-connect). Either downgrade to GitLab 15.4.5, or upgrade to GitLab 15.5.7 or a later version.
|
||||||
- Due to [a bug introduced in GitLab 15.4](https://gitlab.com/gitlab-org/gitlab/-/issues/390155), if one or more Git repositories in Gitaly Cluster is [unavailable](../administration/gitaly/recovery.md#unavailable-repositories), then [Repository checks](../administration/repository_checks.md#repository-checks) and [Geo replication and verification](../administration/geo/index.md) stop running for all project or project wiki repositories in the affected Gitaly Cluster. The bug was fixed by [reverting the change in GitLab 15.9.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110823). Before upgrading to this version, check if you have any "unavailable" repositories. See [the bug issue](https://gitlab.com/gitlab-org/gitlab/-/issues/390155) for more information.
|
- Due to [a bug introduced in GitLab 15.4](https://gitlab.com/gitlab-org/gitlab/-/issues/390155), if one or more Git repositories in Gitaly Cluster is [unavailable](../administration/gitaly/recovery.md#unavailable-repositories), then [Repository checks](../administration/repository_checks.md#repository-checks) and [Geo replication and verification](../administration/geo/index.md) stop running for all project or project wiki repositories in the affected Gitaly Cluster. The bug was fixed by [reverting the change in GitLab 15.9.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110823). Before upgrading to this version, check if you have any "unavailable" repositories. See [the bug issue](https://gitlab.com/gitlab-org/gitlab/-/issues/390155) for more information.
|
||||||
|
|
||||||
### 15.4.5
|
### 15.4.5
|
||||||
|
|
|
@ -422,6 +422,14 @@ Example:
|
||||||
Additional instructions here.
|
Additional instructions here.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
### 15.9.0
|
||||||
|
|
||||||
|
With the addition of `gitlab-sshd` the Kerberos headers are needed to build GitLab Shell.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo apt install libkrb5-dev
|
||||||
|
```
|
||||||
|
|
||||||
### 15.0.0
|
### 15.0.0
|
||||||
|
|
||||||
Support for more than one database has been added to GitLab. [As part of this](https://gitlab.com/gitlab-org/gitlab/-/issues/338182),
|
Support for more than one database has been added to GitLab. [As part of this](https://gitlab.com/gitlab-org/gitlab/-/issues/338182),
|
||||||
|
|
|
@ -549,6 +549,77 @@ To enable group file templates:
|
||||||
1. Choose a project to act as the template repository.
|
1. Choose a project to act as the template repository.
|
||||||
1. Select **Save changes**.
|
1. Select **Save changes**.
|
||||||
|
|
||||||
|
## Group merge checks settings **(PREMIUM)**
|
||||||
|
|
||||||
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/372040) in GitLab 15.9 [with a flag](../../administration/feature_flags.md) name `support_group_level_merge_checks_setting`. Disabled by default.
|
||||||
|
|
||||||
|
FLAG:
|
||||||
|
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to
|
||||||
|
[enable the feature flag](../../administration/feature_flags.md) named `support_group_level_merge_checks_setting`. On GitLab.com, this feature is not
|
||||||
|
available.
|
||||||
|
|
||||||
|
Group owners can set up merge request checks on a top-level group, which apply to all subgroups and projects.
|
||||||
|
|
||||||
|
If the settings are inherited by a subgroup or project, they cannot be changed in the subgroup or project
|
||||||
|
that inherited them.
|
||||||
|
|
||||||
|
### Require a successful pipeline for merge
|
||||||
|
|
||||||
|
You can configure all child projects in your group to require a complete and successful pipeline before
|
||||||
|
merge.
|
||||||
|
|
||||||
|
See also [the project-level setting](../project/merge_requests/merge_when_pipeline_succeeds.md#require-a-successful-pipeline-for-merge).
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- You must be the owner of the group.
|
||||||
|
|
||||||
|
To enable this setting:
|
||||||
|
|
||||||
|
1. On the top bar, select **Main menu > Groups** and find your group.
|
||||||
|
1. On the left sidebar, select **Settings > General**.
|
||||||
|
1. Expand **Merge requests**.
|
||||||
|
1. Under **Merge checks**, select **Pipelines must succeed**.
|
||||||
|
This setting also prevents merge requests from being merged if there is no pipeline.
|
||||||
|
1. Select **Save changes**.
|
||||||
|
|
||||||
|
#### Allow merge after skipped pipelines
|
||||||
|
|
||||||
|
You can configure [skipped pipelines](../../ci/pipelines/index.md#skip-a-pipeline) from preventing merge requests from being merged.
|
||||||
|
|
||||||
|
See also [the project-level setting](../project/merge_requests/merge_when_pipeline_succeeds.md#allow-merge-after-skipped-pipelines).
|
||||||
|
|
||||||
|
Prerequisite:
|
||||||
|
|
||||||
|
- You must be the owner of the group.
|
||||||
|
|
||||||
|
To change this behavior:
|
||||||
|
|
||||||
|
1. On the top bar, select **Main menu > Groups** and find your group.
|
||||||
|
1. On the left sidebar, select **Settings > General**.
|
||||||
|
1. Expand **Merge requests**.
|
||||||
|
1. Under **Merge checks**:
|
||||||
|
- Select **Pipelines must succeed**.
|
||||||
|
- Select **Skipped pipelines are considered successful**.
|
||||||
|
1. Select **Save changes**.
|
||||||
|
|
||||||
|
### Prevent merge unless all threads are resolved
|
||||||
|
|
||||||
|
You can prevent merge requests from being merged until all threads are resolved. When this setting is enabled, for all child projects in your group, the
|
||||||
|
**Unresolved threads** count in a merge request is shown in orange when at least one thread remains unresolved.
|
||||||
|
|
||||||
|
Prerequisite:
|
||||||
|
|
||||||
|
- You must be the owner of the group.
|
||||||
|
|
||||||
|
To enable this setting:
|
||||||
|
|
||||||
|
1. On the top bar, select **Main menu > Groups** and find your group.
|
||||||
|
1. On the left sidebar, select **Settings > General**.
|
||||||
|
1. Expand **Merge requests**.
|
||||||
|
1. Under **Merge checks**, select **All threads must be resolved**.
|
||||||
|
1. Select **Save changes**.
|
||||||
|
|
||||||
## Group merge request approval settings **(PREMIUM)**
|
## Group merge request approval settings **(PREMIUM)**
|
||||||
|
|
||||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285458) in GitLab 13.9. [Deployed behind the `group_merge_request_approval_settings_feature_flag` flag](../../administration/feature_flags.md), disabled by default.
|
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285458) in GitLab 13.9. [Deployed behind the `group_merge_request_approval_settings_feature_flag` flag](../../administration/feature_flags.md), disabled by default.
|
||||||
|
|
|
@ -24,13 +24,7 @@ To edit an issue:
|
||||||
|
|
||||||
### Remove a task list item
|
### Remove a task list item
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/377307) in GitLab 15.9 [with a flag](../../../administration/feature_flags.md) named `work_items_mvc`. Disabled by default.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/377307) in GitLab 15.9.
|
||||||
|
|
||||||
FLAG:
|
|
||||||
On self-managed GitLab, by default this feature is not available.
|
|
||||||
To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `work_items_mvc`.
|
|
||||||
On GitLab.com, this feature is not available.
|
|
||||||
The feature is not ready for production use.
|
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|
||||||
|
|
|
@ -58,15 +58,9 @@ To create a task:
|
||||||
1. Enter the task title.
|
1. Enter the task title.
|
||||||
1. Select **Create task**.
|
1. Select **Create task**.
|
||||||
|
|
||||||
### Create a task from a task list item
|
### From a task list item
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/377307) in GitLab 15.9 [with a flag](../administration/feature_flags.md) named `work_items_mvc`. Disabled by default.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/377307) in GitLab 15.9.
|
||||||
|
|
||||||
FLAG:
|
|
||||||
On self-managed GitLab, by default this feature is not available.
|
|
||||||
To make it available, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `work_items_mvc`.
|
|
||||||
On GitLab.com, this feature is not available.
|
|
||||||
The feature is not ready for production use.
|
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|
||||||
|
|
|
@ -7028,7 +7028,7 @@ references and their corresponding code points.</p>
|
||||||
<copy-code></copy-code>
|
<copy-code></copy-code>
|
||||||
</div>
|
</div>
|
||||||
<div class="gl-relative markdown-code-block js-markdown-code">
|
<div class="gl-relative markdown-code-block js-markdown-code">
|
||||||
<pre data-sourcepos="7591:1-7595:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext"><p> &amp; © Æ Ď</span>
|
<pre data-sourcepos="7591:1-7595:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext"><p> &amp; © Æ Ď</span>
|
||||||
<span id="LC2" class="line" lang="plaintext">¾ ℋ ⅆ</span>
|
<span id="LC2" class="line" lang="plaintext">¾ ℋ ⅆ</span>
|
||||||
<span id="LC3" class="line" lang="plaintext">∲ ≧̸</p></span></code></pre>
|
<span id="LC3" class="line" lang="plaintext">∲ ≧̸</p></span></code></pre>
|
||||||
<copy-code></copy-code>
|
<copy-code></copy-code>
|
||||||
|
@ -7344,11 +7344,11 @@ stripped in this way:</p>
|
||||||
<div>
|
<div>
|
||||||
<div><a href="#example-343">Example 343</a></div>
|
<div><a href="#example-343">Example 343</a></div>
|
||||||
<div class="gl-relative markdown-code-block js-markdown-code">
|
<div class="gl-relative markdown-code-block js-markdown-code">
|
||||||
<pre data-sourcepos="7960:1-7962:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">` b `</span></code></pre>
|
<pre data-sourcepos="7960:1-7962:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">` b `</span></code></pre>
|
||||||
<copy-code></copy-code>
|
<copy-code></copy-code>
|
||||||
</div>
|
</div>
|
||||||
<div class="gl-relative markdown-code-block js-markdown-code">
|
<div class="gl-relative markdown-code-block js-markdown-code">
|
||||||
<pre data-sourcepos="7964:1-7966:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext"><p><code> b </code></p></span></code></pre>
|
<pre data-sourcepos="7964:1-7966:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext"><p><code> b </code></p></span></code></pre>
|
||||||
<copy-code></copy-code>
|
<copy-code></copy-code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7356,12 +7356,12 @@ stripped in this way:</p>
|
||||||
<div>
|
<div>
|
||||||
<div><a href="#example-344">Example 344</a></div>
|
<div><a href="#example-344">Example 344</a></div>
|
||||||
<div class="gl-relative markdown-code-block js-markdown-code">
|
<div class="gl-relative markdown-code-block js-markdown-code">
|
||||||
<pre data-sourcepos="7974:1-7977:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">` `</span>
|
<pre data-sourcepos="7974:1-7977:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">` `</span>
|
||||||
<span id="LC2" class="line" lang="plaintext">` `</span></code></pre>
|
<span id="LC2" class="line" lang="plaintext">` `</span></code></pre>
|
||||||
<copy-code></copy-code>
|
<copy-code></copy-code>
|
||||||
</div>
|
</div>
|
||||||
<div class="gl-relative markdown-code-block js-markdown-code">
|
<div class="gl-relative markdown-code-block js-markdown-code">
|
||||||
<pre data-sourcepos="7979:1-7982:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext"><p><code> </code></span>
|
<pre data-sourcepos="7979:1-7982:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext"><p><code> </code></span>
|
||||||
<span id="LC2" class="line" lang="plaintext"><code> </code></p></span></code></pre>
|
<span id="LC2" class="line" lang="plaintext"><code> </code></p></span></code></pre>
|
||||||
<copy-code></copy-code>
|
<copy-code></copy-code>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7832,11 +7832,11 @@ not part of a [left-flanking delimiter run]:</p>
|
||||||
<div>
|
<div>
|
||||||
<div><a href="#example-363">Example 363</a></div>
|
<div><a href="#example-363">Example 363</a></div>
|
||||||
<div class="gl-relative markdown-code-block js-markdown-code">
|
<div class="gl-relative markdown-code-block js-markdown-code">
|
||||||
<pre data-sourcepos="8485:1-8487:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">* a *</span></code></pre>
|
<pre data-sourcepos="8485:1-8487:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">* a *</span></code></pre>
|
||||||
<copy-code></copy-code>
|
<copy-code></copy-code>
|
||||||
</div>
|
</div>
|
||||||
<div class="gl-relative markdown-code-block js-markdown-code">
|
<div class="gl-relative markdown-code-block js-markdown-code">
|
||||||
<pre data-sourcepos="8489:1-8491:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext"><p>* a *</p></span></code></pre>
|
<pre data-sourcepos="8489:1-8491:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext"><p>* a *</p></span></code></pre>
|
||||||
<copy-code></copy-code>
|
<copy-code></copy-code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9790,7 +9790,7 @@ Other [Unicode whitespace] like non-breaking space doesn't work.</p>
|
||||||
<div>
|
<div>
|
||||||
<div><a href="#example-515">Example 515</a></div>
|
<div><a href="#example-515">Example 515</a></div>
|
||||||
<div class="gl-relative markdown-code-block js-markdown-code">
|
<div class="gl-relative markdown-code-block js-markdown-code">
|
||||||
<pre data-sourcepos="10823:1-10825:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">[link](/url "title")</span></code></pre>
|
<pre data-sourcepos="10823:1-10825:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">[link](/url "title")</span></code></pre>
|
||||||
<copy-code></copy-code>
|
<copy-code></copy-code>
|
||||||
</div>
|
</div>
|
||||||
<div class="gl-relative markdown-code-block js-markdown-code">
|
<div class="gl-relative markdown-code-block js-markdown-code">
|
||||||
|
|
|
@ -203,6 +203,10 @@ module API
|
||||||
render_api_error!("Target project id:#{params[:from_project_id]} is not a fork of project id:#{params[:id]}", 400)
|
render_api_error!("Target project id:#{params[:from_project_id]} is not a fork of project id:#{params[:id]}", 400)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
unless can?(current_user, :read_code, target_project)
|
||||||
|
forbidden!("You don't have access to this fork's parent project")
|
||||||
|
end
|
||||||
|
|
||||||
cache_key = compare_cache_key(current_user, user_project, target_project, declared_params)
|
cache_key = compare_cache_key(current_user, user_project, target_project, declared_params)
|
||||||
|
|
||||||
cache_action(cache_key, expires_in: 1.minute) do
|
cache_action(cache_key, expires_in: 1.minute) do
|
||||||
|
|
|
@ -6,11 +6,35 @@ module Banzai
|
||||||
# as well as hiding the customer's IP address when requesting images.
|
# as well as hiding the customer's IP address when requesting images.
|
||||||
# Copies the original img `src` to `data-canonical-src` then replaces the
|
# Copies the original img `src` to `data-canonical-src` then replaces the
|
||||||
# `src` with a new url to the proxy server.
|
# `src` with a new url to the proxy server.
|
||||||
class AssetProxyFilter < HTML::Pipeline::CamoFilter
|
#
|
||||||
|
# Based on https://github.com/gjtorikian/html-pipeline/blob/v2.14.3/lib/html/pipeline/camo_filter.rb
|
||||||
|
class AssetProxyFilter < HTML::Pipeline::Filter
|
||||||
def initialize(text, context = nil, result = nil)
|
def initialize(text, context = nil, result = nil)
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
return doc unless asset_proxy_enabled?
|
||||||
|
|
||||||
|
doc.search('img').each do |element|
|
||||||
|
original_src = element['src']
|
||||||
|
next unless original_src
|
||||||
|
|
||||||
|
begin
|
||||||
|
uri = URI.parse(original_src)
|
||||||
|
rescue StandardError
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
next if uri.host.nil? && !original_src.start_with?('///')
|
||||||
|
next if asset_host_allowed?(uri.host)
|
||||||
|
|
||||||
|
element['src'] = asset_proxy_url(original_src)
|
||||||
|
element['data-canonical-src'] = original_src
|
||||||
|
end
|
||||||
|
doc
|
||||||
|
end
|
||||||
|
|
||||||
def validate
|
def validate
|
||||||
needs(:asset_proxy, :asset_proxy_secret_key) if asset_proxy_enabled?
|
needs(:asset_proxy, :asset_proxy_secret_key) if asset_proxy_enabled?
|
||||||
end
|
end
|
||||||
|
@ -63,6 +87,24 @@ module Banzai
|
||||||
application_settings.try(:asset_proxy_whitelist).presence ||
|
application_settings.try(:asset_proxy_whitelist).presence ||
|
||||||
[Gitlab.config.gitlab.host]
|
[Gitlab.config.gitlab.host]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def asset_proxy_enabled?
|
||||||
|
!context[:disable_asset_proxy]
|
||||||
|
end
|
||||||
|
|
||||||
|
def asset_proxy_url(url)
|
||||||
|
"#{context[:asset_proxy]}/#{asset_url_hash(url)}/#{hexencode(url)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def asset_url_hash(url)
|
||||||
|
OpenSSL::HMAC.hexdigest('sha1', context[:asset_proxy_secret_key], url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def hexencode(str)
|
||||||
|
str.unpack1('H*')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'uri'
|
||||||
|
|
||||||
module Banzai
|
module Banzai
|
||||||
module Filter
|
module Filter
|
||||||
class InlineObservabilityFilter < ::Banzai::Filter::InlineEmbedsFilter
|
class InlineObservabilityFilter < ::Banzai::Filter::InlineEmbedsFilter
|
||||||
|
@ -15,7 +17,8 @@ module Banzai
|
||||||
doc.document.create_element(
|
doc.document.create_element(
|
||||||
'div',
|
'div',
|
||||||
class: 'js-render-observability',
|
class: 'js-render-observability',
|
||||||
'data-frame-url': url
|
'data-frame-url': url,
|
||||||
|
'data-observability-url': Gitlab::Observability.observability_url
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -28,8 +31,15 @@ module Banzai
|
||||||
# obtained from the target link
|
# obtained from the target link
|
||||||
def element_to_embed(node)
|
def element_to_embed(node)
|
||||||
url = node['href']
|
url = node['href']
|
||||||
|
uri = URI.parse(url)
|
||||||
|
observability_uri = URI.parse(Gitlab::Observability.observability_url)
|
||||||
|
|
||||||
create_element(url)
|
if uri.scheme == observability_uri.scheme &&
|
||||||
|
uri.port == observability_uri.port &&
|
||||||
|
uri.host.casecmp?(observability_uri.host) &&
|
||||||
|
uri.path.downcase.exclude?("auth/start")
|
||||||
|
create_element(url)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
# Can be extended for different types of repository object, e.g. Project or Snippet
|
# Can be extended for different types of repository object, e.g. Project or Snippet
|
||||||
module ExtractsRef
|
module ExtractsRef
|
||||||
InvalidPathError = Class.new(StandardError)
|
InvalidPathError = Class.new(StandardError)
|
||||||
|
BRANCH_REF_TYPE = 'heads'
|
||||||
|
TAG_REF_TYPE = 'tags'
|
||||||
# Given a string containing both a Git tree-ish, such as a branch or tag, and
|
# Given a string containing both a Git tree-ish, such as a branch or tag, and
|
||||||
# a filesystem path joined by forward slashes, attempts to separate the two.
|
# a filesystem path joined by forward slashes, attempts to separate the two.
|
||||||
#
|
#
|
||||||
|
@ -91,7 +92,7 @@ module ExtractsRef
|
||||||
def ref_type
|
def ref_type
|
||||||
return unless params[:ref_type].present?
|
return unless params[:ref_type].present?
|
||||||
|
|
||||||
params[:ref_type] == 'tags' ? 'tags' : 'heads'
|
params[:ref_type] == TAG_REF_TYPE ? TAG_REF_TYPE : BRANCH_REF_TYPE
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -154,4 +155,13 @@ module ExtractsRef
|
||||||
def repository_container
|
def repository_container
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ambiguous_ref?(project, ref)
|
||||||
|
return true if project.repository.ambiguous_ref?(ref)
|
||||||
|
|
||||||
|
return false unless ref&.starts_with?('refs/')
|
||||||
|
|
||||||
|
unprefixed_ref = ref.sub(%r{^refs/(heads|tags)/}, '')
|
||||||
|
project.repository.commit(unprefixed_ref).present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module BackgroundMigration
|
||||||
|
# Nullifies last_error value from project_mirror_data table as they
|
||||||
|
# potentially included sensitive data.
|
||||||
|
# https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/3041
|
||||||
|
class NullifyLastErrorFromProjectMirrorData < BatchedMigrationJob
|
||||||
|
feature_category :source_code_management
|
||||||
|
operation_name :update_all
|
||||||
|
|
||||||
|
def perform
|
||||||
|
each_sub_batch { |rel| rel.update_all(last_error: nil) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -42,7 +42,7 @@ module Gitlab
|
||||||
def prohibited_branch_checks
|
def prohibited_branch_checks
|
||||||
return if deletion?
|
return if deletion?
|
||||||
|
|
||||||
if branch_name =~ /\A\h{40}\z/
|
if branch_name =~ %r{\A\h{40}(/|\z)}
|
||||||
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_hex_branch_name]
|
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_hex_branch_name]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -367,12 +367,12 @@ module Gitlab
|
||||||
|
|
||||||
def foreign_key_exists?(source, target = nil, **options)
|
def foreign_key_exists?(source, target = nil, **options)
|
||||||
# This if block is necessary because foreign_key_exists? is called in down migrations that may execute before
|
# This if block is necessary because foreign_key_exists? is called in down migrations that may execute before
|
||||||
# the postgres_foreign_keys view had necessary columns added, or even before the view existed.
|
# the postgres_foreign_keys view had necessary columns added.
|
||||||
# In that case, we revert to the previous behavior of this method.
|
# In that case, we revert to the previous behavior of this method.
|
||||||
# The behavior in the if block has a bug: it always returns false if the fk being checked has multiple columns.
|
# The behavior in the if block has a bug: it always returns false if the fk being checked has multiple columns.
|
||||||
# This can be removed after init_schema.rb passes 20221122210711_add_columns_to_postgres_foreign_keys.rb
|
# This can be removed after init_schema.rb passes 20221122210711_add_columns_to_postgres_foreign_keys.rb
|
||||||
# Tracking issue: https://gitlab.com/gitlab-org/gitlab/-/issues/386796
|
# Tracking issue: https://gitlab.com/gitlab-org/gitlab/-/issues/386796
|
||||||
if ActiveRecord::Migrator.current_version < 20221122210711
|
unless connection.column_exists?('postgres_foreign_keys', 'constrained_table_name')
|
||||||
return foreign_keys(source).any? do |foreign_key|
|
return foreign_keys(source).any? do |foreign_key|
|
||||||
tables_match?(target.to_s, foreign_key.to_table.to_s) &&
|
tables_match?(target.to_s, foreign_key.to_table.to_s) &&
|
||||||
options_match?(foreign_key.options, options)
|
options_match?(foreign_key.options, options)
|
||||||
|
|
|
@ -262,7 +262,11 @@ module Gitlab
|
||||||
|
|
||||||
def archive_metadata(ref, storage_path, project_path, format = "tar.gz", append_sha:, path: nil)
|
def archive_metadata(ref, storage_path, project_path, format = "tar.gz", append_sha:, path: nil)
|
||||||
ref ||= root_ref
|
ref ||= root_ref
|
||||||
commit = Gitlab::Git::Commit.find(self, ref)
|
|
||||||
|
commit_id = extract_commit_id_from_ref(ref)
|
||||||
|
return {} if commit_id.nil?
|
||||||
|
|
||||||
|
commit = Gitlab::Git::Commit.find(self, commit_id)
|
||||||
return {} if commit.nil?
|
return {} if commit.nil?
|
||||||
|
|
||||||
prefix = archive_prefix(ref, commit.id, project_path, append_sha: append_sha, path: path)
|
prefix = archive_prefix(ref, commit.id, project_path, append_sha: append_sha, path: path)
|
||||||
|
@ -1233,6 +1237,26 @@ module Gitlab
|
||||||
def gitaly_delete_refs(*ref_names)
|
def gitaly_delete_refs(*ref_names)
|
||||||
gitaly_ref_client.delete_refs(refs: ref_names) if ref_names.any?
|
gitaly_ref_client.delete_refs(refs: ref_names) if ref_names.any?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# The order is based on git priority to resolve ambiguous references
|
||||||
|
#
|
||||||
|
# `git show <ref>`
|
||||||
|
#
|
||||||
|
# In case of name clashes, it uses this order:
|
||||||
|
# 1. Commit
|
||||||
|
# 2. Tag
|
||||||
|
# 3. Branch
|
||||||
|
def extract_commit_id_from_ref(ref)
|
||||||
|
return ref if Gitlab::Git.commit_id?(ref)
|
||||||
|
|
||||||
|
tag = find_tag(ref)
|
||||||
|
return tag.dereferenced_target.sha if tag
|
||||||
|
|
||||||
|
branch = find_branch(ref)
|
||||||
|
return branch.dereferenced_target.sha if branch
|
||||||
|
|
||||||
|
ref
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -448,6 +448,17 @@ module Gitlab
|
||||||
)
|
)
|
||||||
}mx.freeze
|
}mx.freeze
|
||||||
|
|
||||||
|
# Code blocks:
|
||||||
|
# ```
|
||||||
|
# Anything, including `>>>` blocks which are ignored by this filter
|
||||||
|
# ```
|
||||||
|
MARKDOWN_CODE_BLOCK_REGEX_UNTRUSTED =
|
||||||
|
'(?P<code>' \
|
||||||
|
'^```\n' \
|
||||||
|
'(?:\n|.)*?' \
|
||||||
|
'\n```\ *$' \
|
||||||
|
')'.freeze
|
||||||
|
|
||||||
MARKDOWN_HTML_BLOCK_REGEX = %r{
|
MARKDOWN_HTML_BLOCK_REGEX = %r{
|
||||||
(?<html>
|
(?<html>
|
||||||
# HTML block:
|
# HTML block:
|
||||||
|
@ -461,27 +472,19 @@ module Gitlab
|
||||||
)
|
)
|
||||||
}mx.freeze
|
}mx.freeze
|
||||||
|
|
||||||
MARKDOWN_HTML_COMMENT_LINE_REGEX = %r{
|
# HTML comment line:
|
||||||
(?<html_comment_line>
|
# <!-- some commented text -->
|
||||||
# HTML comment line:
|
MARKDOWN_HTML_COMMENT_LINE_REGEX_UNTRUSTED =
|
||||||
# <!-- some commented text -->
|
'(?P<html_comment_line>' \
|
||||||
|
'^<!--\ .*?\ -->\ *$' \
|
||||||
|
')'.freeze
|
||||||
|
|
||||||
^<!--\ .*\ -->\ *$
|
MARKDOWN_HTML_COMMENT_BLOCK_REGEX_UNTRUSTED =
|
||||||
)
|
'(?P<html_comment_block>' \
|
||||||
}mx.freeze
|
'^<!--.*?\n' \
|
||||||
|
'(?:\n|.)*?' \
|
||||||
MARKDOWN_HTML_COMMENT_BLOCK_REGEX = %r{
|
'\n.*?-->\ *$' \
|
||||||
(?<html_comment_block>
|
')'.freeze
|
||||||
# HTML comment block:
|
|
||||||
# <!-- some commented text
|
|
||||||
# additional text
|
|
||||||
# -->
|
|
||||||
|
|
||||||
^<!--.*\n
|
|
||||||
.+?
|
|
||||||
\n-->\ *$
|
|
||||||
)
|
|
||||||
}mx.freeze
|
|
||||||
|
|
||||||
def markdown_code_or_html_blocks
|
def markdown_code_or_html_blocks
|
||||||
@markdown_code_or_html_blocks ||= %r{
|
@markdown_code_or_html_blocks ||= %r{
|
||||||
|
@ -491,14 +494,13 @@ module Gitlab
|
||||||
}mx.freeze
|
}mx.freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
def markdown_code_or_html_comments
|
def markdown_code_or_html_comments_untrusted
|
||||||
@markdown_code_or_html_comments ||= %r{
|
@markdown_code_or_html_comments_untrusted ||=
|
||||||
#{MARKDOWN_CODE_BLOCK_REGEX}
|
"#{MARKDOWN_CODE_BLOCK_REGEX_UNTRUSTED}" \
|
||||||
|
|
"|" \
|
||||||
#{MARKDOWN_HTML_COMMENT_LINE_REGEX}
|
"#{MARKDOWN_HTML_COMMENT_LINE_REGEX_UNTRUSTED}" \
|
||||||
|
|
"|" \
|
||||||
#{MARKDOWN_HTML_COMMENT_BLOCK_REGEX}
|
"#{MARKDOWN_HTML_COMMENT_BLOCK_REGEX_UNTRUSTED}"
|
||||||
}mx.freeze
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Based on Jira's project key format
|
# Based on Jira's project key format
|
||||||
|
|
|
@ -9,6 +9,12 @@ module Gitlab
|
||||||
# https://idiosyncratic-ruby.com/41-proper-unicoding.html
|
# https://idiosyncratic-ruby.com/41-proper-unicoding.html
|
||||||
BIDI_REGEXP = /\p{Bidi Control}/.freeze
|
BIDI_REGEXP = /\p{Bidi Control}/.freeze
|
||||||
|
|
||||||
|
# Regular expression for identifying space characters
|
||||||
|
#
|
||||||
|
# In web browsers space characters can be confused with simple
|
||||||
|
# spaces which may be misleading
|
||||||
|
SPACE_REGEXP = /\p{Space_Separator}/.freeze
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
# Warning message used to highlight bidi characters in the GUI
|
# Warning message used to highlight bidi characters in the GUI
|
||||||
def bidi_warning
|
def bidi_warning
|
||||||
|
|
|
@ -47,6 +47,17 @@ module Gitlab
|
||||||
RE2.Replace(text, regexp, rewrite)
|
RE2.Replace(text, regexp, rewrite)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# #scan returns an array of the groups captured, rather than MatchData.
|
||||||
|
# Use this to give the capture group name and grab the proper value
|
||||||
|
def extract_named_group(name, match)
|
||||||
|
return unless match
|
||||||
|
|
||||||
|
match_position = regexp.named_capturing_groups[name.to_s]
|
||||||
|
raise RegexpError, "Invalid named capture group: #{name}" unless match_position
|
||||||
|
|
||||||
|
match[match_position - 1]
|
||||||
|
end
|
||||||
|
|
||||||
def ==(other)
|
def ==(other)
|
||||||
self.source == other.source
|
self.source == other.source
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,15 +2,37 @@
|
||||||
|
|
||||||
module Gitlab
|
module Gitlab
|
||||||
class UrlSanitizer
|
class UrlSanitizer
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
ALLOWED_SCHEMES = %w[http https ssh git].freeze
|
ALLOWED_SCHEMES = %w[http https ssh git].freeze
|
||||||
ALLOWED_WEB_SCHEMES = %w[http https].freeze
|
ALLOWED_WEB_SCHEMES = %w[http https].freeze
|
||||||
|
SCHEMIFIED_SCHEME = 'glschemelessuri'
|
||||||
|
SCHEMIFY_PLACEHOLDER = "#{SCHEMIFIED_SCHEME}://".freeze
|
||||||
|
# URI::DEFAULT_PARSER.make_regexp will only match URLs with schemes or
|
||||||
|
# relative URLs. This section will match schemeless URIs with userinfo
|
||||||
|
# e.g. user:pass@gitlab.com but will not match scp-style URIs e.g.
|
||||||
|
# user@server:path/to/file)
|
||||||
|
#
|
||||||
|
# The userinfo part is very loose compared to URI's implementation so we
|
||||||
|
# also match non-escaped userinfo e.g foo:b?r@gitlab.com which should be
|
||||||
|
# encoded as foo:b%3Fr@gitlab.com
|
||||||
|
URI_REGEXP = %r{
|
||||||
|
(?:
|
||||||
|
#{URI::DEFAULT_PARSER.make_regexp(ALLOWED_SCHEMES)}
|
||||||
|
|
|
||||||
|
(?:(?:(?!@)[%#{URI::REGEXP::PATTERN::UNRESERVED}#{URI::REGEXP::PATTERN::RESERVED}])+(?:@))
|
||||||
|
(?# negative lookahead ensures this isn't an SCP-style URL: [host]:[rel_path|abs_path] server:path/to/file)
|
||||||
|
(?!#{URI::REGEXP::PATTERN::HOST}:(?:#{URI::REGEXP::PATTERN::REL_PATH}|#{URI::REGEXP::PATTERN::ABS_PATH}))
|
||||||
|
#{URI::REGEXP::PATTERN::HOSTPORT}
|
||||||
|
)
|
||||||
|
}x
|
||||||
|
|
||||||
def self.sanitize(content)
|
def self.sanitize(content)
|
||||||
regexp = URI::DEFAULT_PARSER.make_regexp(ALLOWED_SCHEMES)
|
content.gsub(URI_REGEXP) do |url|
|
||||||
|
new(url).masked_url
|
||||||
content.gsub(regexp) { |url| new(url).masked_url }
|
rescue Addressable::URI::InvalidURIError
|
||||||
rescue Addressable::URI::InvalidURIError
|
''
|
||||||
content.gsub(regexp, '')
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.valid?(url, allowed_schemes: ALLOWED_SCHEMES)
|
def self.valid?(url, allowed_schemes: ALLOWED_SCHEMES)
|
||||||
|
@ -37,17 +59,6 @@ module Gitlab
|
||||||
@url = parse_url(url)
|
@url = parse_url(url)
|
||||||
end
|
end
|
||||||
|
|
||||||
def sanitized_url
|
|
||||||
@sanitized_url ||= safe_url.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
def masked_url
|
|
||||||
url = @url.dup
|
|
||||||
url.password = "*****" if url.password.present?
|
|
||||||
url.user = "*****" if url.user.present?
|
|
||||||
url.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
def credentials
|
def credentials
|
||||||
@credentials ||= { user: @url.user.presence, password: @url.password.presence }
|
@credentials ||= { user: @url.user.presence, password: @url.password.presence }
|
||||||
end
|
end
|
||||||
|
@ -56,15 +67,37 @@ module Gitlab
|
||||||
credentials[:user]
|
credentials[:user]
|
||||||
end
|
end
|
||||||
|
|
||||||
def full_url
|
def sanitized_url
|
||||||
@full_url ||= generate_full_url.to_s
|
safe_url = @url.dup
|
||||||
|
safe_url.password = nil
|
||||||
|
safe_url.user = nil
|
||||||
|
reverse_schemify(safe_url.to_s)
|
||||||
end
|
end
|
||||||
|
strong_memoize_attr :sanitized_url
|
||||||
|
|
||||||
|
def masked_url
|
||||||
|
url = @url.dup
|
||||||
|
url.password = "*****" if url.password.present?
|
||||||
|
url.user = "*****" if url.user.present?
|
||||||
|
reverse_schemify(url.to_s)
|
||||||
|
end
|
||||||
|
strong_memoize_attr :masked_url
|
||||||
|
|
||||||
|
def full_url
|
||||||
|
return reverse_schemify(@url.to_s) unless valid_credentials?
|
||||||
|
|
||||||
|
url = @url.dup
|
||||||
|
url.password = encode_percent(credentials[:password]) if credentials[:password].present?
|
||||||
|
url.user = encode_percent(credentials[:user]) if credentials[:user].present?
|
||||||
|
reverse_schemify(url.to_s)
|
||||||
|
end
|
||||||
|
strong_memoize_attr :full_url
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def parse_url(url)
|
def parse_url(url)
|
||||||
url = url.to_s.strip
|
url = schemify(url.to_s.strip)
|
||||||
match = url.match(%r{\A(?:git|ssh|http(?:s?))\://(?:(.+)(?:@))?(.+)})
|
match = url.match(%r{\A(?:(?:#{SCHEMIFIED_SCHEME}|git|ssh|http(?:s?)):)?//(?:(.+)(?:@))?(.+)}o)
|
||||||
raw_credentials = match[1] if match
|
raw_credentials = match[1] if match
|
||||||
|
|
||||||
if raw_credentials.present?
|
if raw_credentials.present?
|
||||||
|
@ -83,24 +116,19 @@ module Gitlab
|
||||||
url
|
url
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_full_url
|
def schemify(url)
|
||||||
return @url unless valid_credentials?
|
# Prepend the placeholder scheme unless the URL has a scheme or is relative
|
||||||
|
url.prepend(SCHEMIFY_PLACEHOLDER) unless url.starts_with?(%r{(?:#{URI::REGEXP::PATTERN::SCHEME}:)?//}o)
|
||||||
@url.dup.tap do |generated|
|
url
|
||||||
generated.password = encode_percent(credentials[:password]) if credentials[:password].present?
|
|
||||||
generated.user = encode_percent(credentials[:user]) if credentials[:user].present?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def safe_url
|
def reverse_schemify(url)
|
||||||
safe_url = @url.dup
|
url.slice!(SCHEMIFY_PLACEHOLDER) if url.starts_with?(SCHEMIFY_PLACEHOLDER)
|
||||||
safe_url.password = nil
|
url
|
||||||
safe_url.user = nil
|
|
||||||
safe_url
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_credentials?
|
def valid_credentials?
|
||||||
credentials && credentials.is_a?(Hash) && credentials.any?
|
credentials.is_a?(Hash) && credentials.values.any?
|
||||||
end
|
end
|
||||||
|
|
||||||
def encode_percent(string)
|
def encode_percent(string)
|
||||||
|
|
|
@ -25,7 +25,10 @@ module Rouge
|
||||||
yield %(<span id="LC#{@line_number}" class="line" lang="#{@tag}">)
|
yield %(<span id="LC#{@line_number}" class="line" lang="#{@tag}">)
|
||||||
|
|
||||||
line.each do |token, value|
|
line.each do |token, value|
|
||||||
yield highlight_unicode_control_characters(span(token, value.chomp! || value))
|
value = value.chomp! || value
|
||||||
|
value = replace_space_characters(value)
|
||||||
|
|
||||||
|
yield highlight_unicode_control_characters(span(token, value))
|
||||||
end
|
end
|
||||||
|
|
||||||
yield ellipsis if @ellipsis_indexes.include?(@line_number - 1) && @ellipsis_svg.present?
|
yield ellipsis if @ellipsis_indexes.include?(@line_number - 1) && @ellipsis_svg.present?
|
||||||
|
@ -42,6 +45,10 @@ module Rouge
|
||||||
%(<span class="gl-px-2 gl-rounded-base gl-mx-2 gl-bg-gray-100 gl-cursor-help has-tooltip" title="Content has been trimmed">#{@ellipsis_svg}</span>)
|
%(<span class="gl-px-2 gl-rounded-base gl-mx-2 gl-bg-gray-100 gl-cursor-help has-tooltip" title="Content has been trimmed">#{@ellipsis_svg}</span>)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def replace_space_characters(text)
|
||||||
|
text.gsub(Gitlab::Unicode::SPACE_REGEXP, ' ')
|
||||||
|
end
|
||||||
|
|
||||||
def highlight_unicode_control_characters(text)
|
def highlight_unicode_control_characters(text)
|
||||||
text.gsub(Gitlab::Unicode::BIDI_REGEXP) do |char|
|
text.gsub(Gitlab::Unicode::BIDI_REGEXP) do |char|
|
||||||
%(<span class="unicode-bidi has-tooltip" data-toggle="tooltip" title="#{Gitlab::Unicode.bidi_warning}">#{char}</span>)
|
%(<span class="unicode-bidi has-tooltip" data-toggle="tooltip" title="#{Gitlab::Unicode.bidi_warning}">#{char}</span>)
|
||||||
|
|
|
@ -42698,6 +42698,9 @@ msgstr ""
|
||||||
msgid "The default branch for this project has been changed. Please update your bookmarks."
|
msgid "The default branch for this project has been changed. Please update your bookmarks."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "The default branch of this project clashes with another ref"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "The dependency list details information about the components used within your project."
|
msgid "The dependency list details information about the components used within your project."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -170,8 +170,6 @@ class CreatePipelineFailureIncident
|
||||||
Additionally, a message can be posted in `#backend_maintainers` or `#frontend_maintainers` to get a maintainer take a look at the fix ASAP.
|
Additionally, a message can be posted in `#backend_maintainers` or `#frontend_maintainers` to get a maintainer take a look at the fix ASAP.
|
||||||
- Cherry picking a change that was used to fix a similar master-broken issue.
|
- Cherry picking a change that was used to fix a similar master-broken issue.
|
||||||
|
|
||||||
In both cases, make sure to add the ~"pipeline:expedite" label to speed up the `stable`-fixing pipelines.
|
|
||||||
|
|
||||||
### Resolution
|
### Resolution
|
||||||
|
|
||||||
Add a comment to this issue describing how this incident could have been prevented earlier in the Merge Request pipeline (rather than the merge commit pipeline).
|
Add a comment to this issue describing how this incident could have been prevented earlier in the Merge Request pipeline (rather than the merge commit pipeline).
|
||||||
|
|
|
@ -59,6 +59,7 @@ RSpec.describe Admin::HooksController do
|
||||||
enable_ssl_verification: false,
|
enable_ssl_verification: false,
|
||||||
url_variables: [
|
url_variables: [
|
||||||
{ key: 'token', value: 'some secret value' },
|
{ key: 'token', value: 'some secret value' },
|
||||||
|
{ key: 'baz', value: 'qux' },
|
||||||
{ key: 'foo', value: nil }
|
{ key: 'foo', value: nil }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -71,7 +72,7 @@ RSpec.describe Admin::HooksController do
|
||||||
expect(flash[:notice]).to include('was updated')
|
expect(flash[:notice]).to include('was updated')
|
||||||
expect(hook).to have_attributes(hook_params.except(:url_variables))
|
expect(hook).to have_attributes(hook_params.except(:url_variables))
|
||||||
expect(hook).to have_attributes(
|
expect(hook).to have_attributes(
|
||||||
url_variables: { 'token' => 'some secret value', 'baz' => 'woo' }
|
url_variables: { 'token' => 'some secret value', 'baz' => 'qux' }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe ConfirmEmailWarning do
|
RSpec.describe ConfirmEmailWarning, feature_category: :system_access do
|
||||||
before do
|
before do
|
||||||
stub_feature_flags(soft_email_confirmation: true)
|
stub_feature_flags(soft_email_confirmation: true)
|
||||||
end
|
end
|
||||||
|
@ -82,6 +82,38 @@ RSpec.describe ConfirmEmailWarning do
|
||||||
it { is_expected.to set_confirm_warning_for(user.email) }
|
it { is_expected.to set_confirm_warning_for(user.email) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when user is being impersonated' do
|
||||||
|
let(:impersonator) { create(:admin) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(controller).to receive(:session).and_return({ impersonator_id: impersonator.id })
|
||||||
|
|
||||||
|
get :index
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to set_confirm_warning_for(user.email) }
|
||||||
|
|
||||||
|
context 'when impersonated user email has html in their email' do
|
||||||
|
let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: "malicious@test.com<form><input/title='<script>alert(document.domain)</script>'>") }
|
||||||
|
|
||||||
|
it { is_expected.to set_confirm_warning_for("malicious@test.com<form><input/title='<script>alert(document.domain)</script>'>") }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is not being impersonated' do
|
||||||
|
before do
|
||||||
|
get :index
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to set_confirm_warning_for(user.email) }
|
||||||
|
|
||||||
|
context 'when user email has html in their email' do
|
||||||
|
let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: "malicious@test.com<form><input/title='<script>alert(document.domain)</script>'>") }
|
||||||
|
|
||||||
|
it { is_expected.to set_confirm_warning_for("malicious@test.com<form><input/title='<script>alert(document.domain)</script>'>") }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,15 +2,16 @@
|
||||||
|
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Projects::BlobController do
|
RSpec.describe Projects::BlobController, feature_category: :source_code_management do
|
||||||
include ProjectForksHelper
|
include ProjectForksHelper
|
||||||
|
|
||||||
let(:project) { create(:project, :public, :repository, previous_default_branch: previous_default_branch) }
|
let(:project) { create(:project, :public, :repository, previous_default_branch: previous_default_branch) }
|
||||||
let(:previous_default_branch) { nil }
|
let(:previous_default_branch) { nil }
|
||||||
|
|
||||||
describe "GET show" do
|
describe "GET show" do
|
||||||
def request
|
let(:params) { { namespace_id: project.namespace, project_id: project, id: id } }
|
||||||
get(:show, params: { namespace_id: project.namespace, project_id: project, id: id })
|
let(:request) do
|
||||||
|
get(:show, params: params)
|
||||||
end
|
end
|
||||||
|
|
||||||
render_views
|
render_views
|
||||||
|
@ -18,10 +19,34 @@ RSpec.describe Projects::BlobController do
|
||||||
context 'with file path' do
|
context 'with file path' do
|
||||||
before do
|
before do
|
||||||
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
|
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
|
||||||
|
project.repository.add_tag(project.creator, 'ambiguous_ref', RepoHelpers.sample_commit.id)
|
||||||
|
project.repository.add_branch(project.creator, 'ambiguous_ref', RepoHelpers.another_sample_commit.id)
|
||||||
request
|
request
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the ref is ambiguous' do
|
||||||
|
let(:ref) { 'ambiguous_ref' }
|
||||||
|
let(:path) { 'README.md' }
|
||||||
|
let(:id) { "#{ref}/#{path}" }
|
||||||
|
let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } }
|
||||||
|
|
||||||
|
context 'and explicitly requesting a branch' do
|
||||||
|
let(:ref_type) { 'heads' }
|
||||||
|
|
||||||
|
it 'redirects to blob#show with sha for the branch' do
|
||||||
|
expect(response).to redirect_to(project_blob_path(project, "#{RepoHelpers.another_sample_commit.id}/#{path}"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and explicitly requesting a tag' do
|
||||||
|
let(:ref_type) { 'tags' }
|
||||||
|
|
||||||
|
it 'responds with success' do
|
||||||
|
expect(response).to be_ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "valid branch, valid file" do
|
context "valid branch, valid file" do
|
||||||
let(:id) { 'master/README.md' }
|
let(:id) { 'master/README.md' }
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ RSpec.describe Projects::ClustersController, feature_category: :kubernetes_manag
|
||||||
include GoogleApi::CloudPlatformHelpers
|
include GoogleApi::CloudPlatformHelpers
|
||||||
include KubernetesHelpers
|
include KubernetesHelpers
|
||||||
|
|
||||||
let_it_be(:project) { create(:project) }
|
let_it_be_with_reload(:project) { create(:project) }
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
|
@ -140,6 +140,27 @@ RSpec.describe Projects::ClustersController, feature_category: :kubernetes_manag
|
||||||
expect(response).to redirect_to(new_user_session_path)
|
expect(response).to redirect_to(new_user_session_path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with a public project' do
|
||||||
|
before do
|
||||||
|
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
|
||||||
|
project.project_feature.update!(metrics_dashboard_access_level: ProjectFeature::ENABLED)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with guest user' do
|
||||||
|
let(:prometheus_body) { nil }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_guest(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404' do
|
||||||
|
get :prometheus_proxy, params: prometheus_proxy_params
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Projects::Environments::PrometheusApiController do
|
RSpec.describe Projects::Environments::PrometheusApiController do
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
let_it_be(:project) { create(:project) }
|
let_it_be_with_reload(:project) { create(:project) }
|
||||||
let_it_be(:proxyable) { create(:environment, project: project) }
|
let_it_be(:proxyable) { create(:environment, project: project) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
@ -70,6 +70,27 @@ RSpec.describe Projects::Environments::PrometheusApiController do
|
||||||
expect(response).to redirect_to(new_user_session_path)
|
expect(response).to redirect_to(new_user_session_path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with a public project' do
|
||||||
|
before do
|
||||||
|
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
|
||||||
|
project.project_feature.update!(metrics_dashboard_access_level: ProjectFeature::ENABLED)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with guest user' do
|
||||||
|
let(:prometheus_body) { nil }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_guest(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404' do
|
||||||
|
get :prometheus_proxy, params: prometheus_proxy_params
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,7 +26,7 @@ RSpec.describe Projects::RefsController, feature_category: :source_code_manageme
|
||||||
'tree' | nil | lazy { project_tree_path(project, id) }
|
'tree' | nil | lazy { project_tree_path(project, id) }
|
||||||
'tree' | 'heads' | lazy { project_tree_path(project, id) }
|
'tree' | 'heads' | lazy { project_tree_path(project, id) }
|
||||||
'blob' | nil | lazy { project_blob_path(project, id) }
|
'blob' | nil | lazy { project_blob_path(project, id) }
|
||||||
'blob' | 'heads' | lazy { project_blob_path(project, id) }
|
'blob' | 'heads' | lazy { project_blob_path(project, id, ref_type: 'heads') }
|
||||||
'graph' | nil | lazy { project_network_path(project, id) }
|
'graph' | nil | lazy { project_network_path(project, id) }
|
||||||
'graph' | 'heads' | lazy { project_network_path(project, id, ref_type: 'heads') }
|
'graph' | 'heads' | lazy { project_network_path(project, id, ref_type: 'heads') }
|
||||||
'graphs' | nil | lazy { project_graph_path(project, id) }
|
'graphs' | nil | lazy { project_graph_path(project, id) }
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Projects::TreeController do
|
RSpec.describe Projects::TreeController, feature_category: :source_code_management do
|
||||||
let(:project) { create(:project, :repository, previous_default_branch: previous_default_branch) }
|
let(:project) { create(:project, :repository, previous_default_branch: previous_default_branch) }
|
||||||
let(:previous_default_branch) { nil }
|
let(:previous_default_branch) { nil }
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
@ -15,18 +15,41 @@ RSpec.describe Projects::TreeController do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET show" do
|
describe "GET show" do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
namespace_id: project.namespace.to_param, project_id: project, id: id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
# Make sure any errors accessing the tree in our views bubble up to this spec
|
# Make sure any errors accessing the tree in our views bubble up to this spec
|
||||||
render_views
|
render_views
|
||||||
|
|
||||||
before do
|
before do
|
||||||
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
|
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
|
||||||
|
project.repository.add_tag(project.creator, 'ambiguous_ref', RepoHelpers.sample_commit.id)
|
||||||
|
project.repository.add_branch(project.creator, 'ambiguous_ref', RepoHelpers.another_sample_commit.id)
|
||||||
|
get :show, params: params
|
||||||
|
end
|
||||||
|
|
||||||
get(:show,
|
context 'when the ref is ambiguous' do
|
||||||
params: {
|
let(:id) { 'ambiguous_ref' }
|
||||||
namespace_id: project.namespace.to_param,
|
let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } }
|
||||||
project_id: project,
|
|
||||||
id: id
|
context 'and explicitly requesting a branch' do
|
||||||
})
|
let(:ref_type) { 'heads' }
|
||||||
|
|
||||||
|
it 'redirects to blob#show with sha for the branch' do
|
||||||
|
expect(response).to redirect_to(project_tree_path(project, RepoHelpers.another_sample_commit.id))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and explicitly requesting a tag' do
|
||||||
|
let(:ref_type) { 'tags' }
|
||||||
|
|
||||||
|
it 'responds with success' do
|
||||||
|
expect(response).to be_ok
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "valid branch, no path" do
|
context "valid branch, no path" do
|
||||||
|
|
|
@ -163,6 +163,69 @@ RSpec.describe ProjectsController, feature_category: :projects do
|
||||||
expect(assigns(:notification_setting).level).to eq("watch")
|
expect(assigns(:notification_setting).level).to eq("watch")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when there is a tag with the same name as the default branch' do
|
||||||
|
let_it_be(:tagged_project) { create(:project, :public, :custom_repo, files: ['somefile']) }
|
||||||
|
let(:tree_with_default_branch) do
|
||||||
|
branch = tagged_project.repository.find_branch(tagged_project.default_branch)
|
||||||
|
project_tree_path(tagged_project, branch.target)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
tagged_project.repository.create_file(
|
||||||
|
tagged_project.creator,
|
||||||
|
'file_for_tag',
|
||||||
|
'content for file',
|
||||||
|
message: "Automatically created file",
|
||||||
|
branch_name: 'branch-to-tag'
|
||||||
|
)
|
||||||
|
|
||||||
|
tagged_project.repository.add_tag(
|
||||||
|
tagged_project.creator,
|
||||||
|
tagged_project.default_branch, # tag name
|
||||||
|
'branch-to-tag' # target
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redirects to tree view for the default branch' do
|
||||||
|
get :show, params: { namespace_id: tagged_project.namespace, id: tagged_project }
|
||||||
|
expect(response).to redirect_to(tree_with_default_branch)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the default branch name can resolve to another ref' do
|
||||||
|
let!(:project_with_default_branch) do
|
||||||
|
create(:project, :public, :custom_repo, files: ['somefile']).tap do |p|
|
||||||
|
p.repository.create_branch("refs/heads/refs/heads/#{other_ref}", 'master')
|
||||||
|
p.change_head("refs/heads/#{other_ref}")
|
||||||
|
end.reload
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:other_ref) { 'branch-name' }
|
||||||
|
|
||||||
|
context 'but there is no other ref' do
|
||||||
|
it 'responds with ok' do
|
||||||
|
get :show, params: { namespace_id: project_with_default_branch.namespace, id: project_with_default_branch }
|
||||||
|
expect(response).to be_ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and that other ref exists' do
|
||||||
|
let(:tree_with_default_branch) do
|
||||||
|
branch = project_with_default_branch.repository.find_branch(project_with_default_branch.default_branch)
|
||||||
|
project_tree_path(project_with_default_branch, branch.target)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
project_with_default_branch.repository.create_branch(other_ref, 'master')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redirects to tree view for the default branch' do
|
||||||
|
get :show, params: { namespace_id: project_with_default_branch.namespace, id: project_with_default_branch }
|
||||||
|
expect(response).to redirect_to(tree_with_default_branch)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "when project repository is disabled" do
|
describe "when project repository is disabled" do
|
||||||
|
|
|
@ -7,7 +7,7 @@ FactoryBot.define do
|
||||||
project
|
project
|
||||||
|
|
||||||
trait :url_variables do
|
trait :url_variables do
|
||||||
url_variables { { 'abc' => 'supers3cret' } }
|
url_variables { { 'abc' => 'supers3cret', 'def' => 'foobar' } }
|
||||||
end
|
end
|
||||||
|
|
||||||
trait :token do
|
trait :token do
|
||||||
|
|
|
@ -21,6 +21,7 @@ require_relative 'support/rspec'
|
||||||
require_relative '../lib/gitlab/utils'
|
require_relative '../lib/gitlab/utils'
|
||||||
require_relative '../lib/gitlab/utils/strong_memoize'
|
require_relative '../lib/gitlab/utils/strong_memoize'
|
||||||
require 'active_support/all'
|
require 'active_support/all'
|
||||||
|
require 'pry'
|
||||||
|
|
||||||
require_relative 'simplecov_env'
|
require_relative 'simplecov_env'
|
||||||
SimpleCovEnv.start!
|
SimpleCovEnv.start!
|
||||||
|
|
|
@ -271,6 +271,36 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
|
||||||
icon = first('[data-testid="incognito-icon"]')
|
icon = first('[data-testid="incognito-icon"]')
|
||||||
expect(icon).not_to be nil
|
expect(icon).not_to be nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when viewing the confirm email warning', :js do
|
||||||
|
let_it_be(:another_user) { create(:user, :unconfirmed) }
|
||||||
|
|
||||||
|
let(:warning_alert) { page.find(:css, '[data-testid="alert-warning"]') }
|
||||||
|
let(:expected_styling) { { 'pointer-events' => 'none', 'cursor' => 'default' } }
|
||||||
|
|
||||||
|
context 'with an email that does not contain HTML' do
|
||||||
|
before do
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'displays the warning alert including the email' do
|
||||||
|
expect(warning_alert.text).to include("Please check your email (#{another_user.email}) to verify")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an email that contains HTML' do
|
||||||
|
let(:malicious_email) { "malicious@test.com<form><input/title='<script>alert(document.domain)</script>'>" }
|
||||||
|
let(:another_user) { create(:user, confirmed_at: nil, unconfirmed_email: malicious_email) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'displays the impersonation alert, excludes email, and disables links' do
|
||||||
|
expect(warning_alert.text).to include("check your email (#{another_user.unconfirmed_email}) to verify")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'ending impersonation' do
|
context 'ending impersonation' do
|
||||||
|
|
|
@ -6,6 +6,7 @@ RSpec.describe Environments::EnvironmentNamesFinder do
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
let!(:group) { create(:group) }
|
let!(:group) { create(:group) }
|
||||||
let!(:public_project) { create(:project, :public, namespace: group) }
|
let!(:public_project) { create(:project, :public, namespace: group) }
|
||||||
|
let_it_be_with_reload(:public_project_with_private_environments) { create(:project, :public) }
|
||||||
let!(:private_project) { create(:project, :private, namespace: group) }
|
let!(:private_project) { create(:project, :private, namespace: group) }
|
||||||
let!(:user) { create(:user) }
|
let!(:user) { create(:user) }
|
||||||
|
|
||||||
|
@ -14,6 +15,11 @@ RSpec.describe Environments::EnvironmentNamesFinder do
|
||||||
create(:environment, name: 'gprd', project: public_project)
|
create(:environment, name: 'gprd', project: public_project)
|
||||||
create(:environment, name: 'gprd', project: private_project)
|
create(:environment, name: 'gprd', project: private_project)
|
||||||
create(:environment, name: 'gcny', project: private_project)
|
create(:environment, name: 'gcny', project: private_project)
|
||||||
|
create(:environment, name: 'gprivprd', project: public_project_with_private_environments)
|
||||||
|
create(:environment, name: 'gprivstg', project: public_project_with_private_environments)
|
||||||
|
|
||||||
|
public_project_with_private_environments.update!(namespace: group)
|
||||||
|
public_project_with_private_environments.project_feature.update!(environments_access_level: Featurable::PRIVATE)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'using a group' do
|
context 'using a group' do
|
||||||
|
@ -23,7 +29,7 @@ RSpec.describe Environments::EnvironmentNamesFinder do
|
||||||
|
|
||||||
names = described_class.new(group, user).execute
|
names = described_class.new(group, user).execute
|
||||||
|
|
||||||
expect(names).to eq(%w[gcny gprd gstg])
|
expect(names).to eq(%w[gcny gprd gprivprd gprivstg gstg])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -33,7 +39,7 @@ RSpec.describe Environments::EnvironmentNamesFinder do
|
||||||
|
|
||||||
names = described_class.new(group, user).execute
|
names = described_class.new(group, user).execute
|
||||||
|
|
||||||
expect(names).to eq(%w[gcny gprd gstg])
|
expect(names).to eq(%w[gcny gprd gprivprd gprivstg gstg])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -57,8 +63,18 @@ RSpec.describe Environments::EnvironmentNamesFinder do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with a public project reporter which has private environments' do
|
||||||
|
it 'returns environment names for public projects' do
|
||||||
|
public_project_with_private_environments.add_reporter(user)
|
||||||
|
|
||||||
|
names = described_class.new(group, user).execute
|
||||||
|
|
||||||
|
expect(names).to eq(%w[gprd gprivprd gprivstg gstg])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with a group guest' do
|
context 'with a group guest' do
|
||||||
it 'returns environment names for all public projects' do
|
it 'returns environment names for public projects' do
|
||||||
group.add_guest(user)
|
group.add_guest(user)
|
||||||
|
|
||||||
names = described_class.new(group, user).execute
|
names = described_class.new(group, user).execute
|
||||||
|
@ -68,7 +84,7 @@ RSpec.describe Environments::EnvironmentNamesFinder do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with a non-member' do
|
context 'with a non-member' do
|
||||||
it 'returns environment names for all public projects' do
|
it 'returns environment names for only public projects with public environments' do
|
||||||
names = described_class.new(group, user).execute
|
names = described_class.new(group, user).execute
|
||||||
|
|
||||||
expect(names).to eq(%w[gprd gstg])
|
expect(names).to eq(%w[gprd gstg])
|
||||||
|
@ -76,7 +92,7 @@ RSpec.describe Environments::EnvironmentNamesFinder do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'without a user' do
|
context 'without a user' do
|
||||||
it 'returns environment names for all public projects' do
|
it 'returns environment names for only public projects with public environments' do
|
||||||
names = described_class.new(group).execute
|
names = described_class.new(group).execute
|
||||||
|
|
||||||
expect(names).to eq(%w[gprd gstg])
|
expect(names).to eq(%w[gprd gstg])
|
||||||
|
|
|
@ -106,6 +106,26 @@ RSpec.describe NotesFinder do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'for notes on public issue in public project' do
|
||||||
|
let_it_be(:public_project) { create(:project, :public) }
|
||||||
|
let_it_be(:guest_member) { create(:user) }
|
||||||
|
let_it_be(:reporter_member) { create(:user) }
|
||||||
|
let_it_be(:guest_project_member) { create(:project_member, :guest, user: guest_member, project: public_project) }
|
||||||
|
let_it_be(:reporter_project_member) { create(:project_member, :reporter, user: reporter_member, project: public_project) }
|
||||||
|
let_it_be(:internal_note) { create(:note_on_issue, project: public_project, internal: true) }
|
||||||
|
let_it_be(:public_note) { create(:note_on_issue, project: public_project) }
|
||||||
|
|
||||||
|
it 'shows all notes when the current_user has reporter access' do
|
||||||
|
notes = described_class.new(reporter_member, project: public_project).execute
|
||||||
|
expect(notes).to contain_exactly internal_note, public_note
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows only public notes when the current_user has guest access' do
|
||||||
|
notes = described_class.new(guest_member, project: public_project).execute
|
||||||
|
expect(notes).to contain_exactly public_note
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'for target type' do
|
context 'for target type' do
|
||||||
let(:project) { create(:project, :repository) }
|
let(:project) { create(:project, :repository) }
|
||||||
let!(:note1) { create :note_on_issue, project: project }
|
let!(:note1) { create :note_on_issue, project: project }
|
||||||
|
|
|
@ -16,7 +16,7 @@ describe('Observability iframe renderer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders an observability iframe', () => {
|
it('renders an observability iframe', () => {
|
||||||
document.body.innerHTML = `<div class="js-render-observability" data-frame-url="https://observe.gitlab.com/"></div>`;
|
document.body.innerHTML = `<div class="js-render-observability" data-frame-url="https://observe.gitlab.com/" data-observability-url="https://observe.gitlab.com/" ></div>`;
|
||||||
|
|
||||||
expect(findObservabilityIframes()).toHaveLength(0);
|
expect(findObservabilityIframes()).toHaveLength(0);
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ describe('Observability iframe renderer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders iframe with dark param when GL has dark theme', () => {
|
it('renders iframe with dark param when GL has dark theme', () => {
|
||||||
document.body.innerHTML = `<div class="js-render-observability" data-frame-url="https://observe.gitlab.com/"></div>`;
|
document.body.innerHTML = `<div class="js-render-observability" data-frame-url="https://observe.gitlab.com/" data-observability-url="https://observe.gitlab.com/"></div>`;
|
||||||
jest.spyOn(ColorUtils, 'darkModeEnabled').mockImplementation(() => true);
|
jest.spyOn(ColorUtils, 'darkModeEnabled').mockImplementation(() => true);
|
||||||
|
|
||||||
expect(findObservabilityIframes('dark')).toHaveLength(0);
|
expect(findObservabilityIframes('dark')).toHaveLength(0);
|
||||||
|
@ -35,4 +35,12 @@ describe('Observability iframe renderer', () => {
|
||||||
|
|
||||||
expect(findObservabilityIframes('dark')).toHaveLength(1);
|
expect(findObservabilityIframes('dark')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not render if url is different from observability url', () => {
|
||||||
|
document.body.innerHTML = `<div class="js-render-observability" data-frame-url="https://example.com/" data-observability-url="https://observe.gitlab.com/"></div>`;
|
||||||
|
|
||||||
|
renderEmbeddedObservability();
|
||||||
|
|
||||||
|
expect(findObservabilityIframes()).toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -310,69 +310,58 @@ describe('Description component', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with work_items_mvc feature flag enabled', () => {
|
describe('empty description', () => {
|
||||||
describe('empty description', () => {
|
beforeEach(() => {
|
||||||
beforeEach(() => {
|
createComponent({
|
||||||
createComponent({
|
props: {
|
||||||
props: {
|
descriptionHtml: '',
|
||||||
descriptionHtml: '',
|
},
|
||||||
},
|
|
||||||
provide: {
|
|
||||||
glFeatures: {
|
|
||||||
workItemsMvc: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return nextTick();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders without error', () => {
|
|
||||||
expect(findTaskActionButtons()).toHaveLength(0);
|
|
||||||
});
|
});
|
||||||
|
return nextTick();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('description with checkboxes', () => {
|
it('renders without error', () => {
|
||||||
beforeEach(() => {
|
expect(findTaskActionButtons()).toHaveLength(0);
|
||||||
createComponent({
|
});
|
||||||
props: {
|
});
|
||||||
descriptionHtml: descriptionHtmlWithCheckboxes,
|
|
||||||
},
|
|
||||||
provide: {
|
|
||||||
glFeatures: {
|
|
||||||
workItemsMvc: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return nextTick();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders a list of hidden buttons corresponding to checkboxes in description HTML', () => {
|
describe('description with checkboxes', () => {
|
||||||
expect(findTaskActionButtons()).toHaveLength(3);
|
beforeEach(() => {
|
||||||
});
|
createComponent({
|
||||||
|
props: {
|
||||||
it('does not show a modal by default', () => {
|
descriptionHtml: descriptionHtmlWithCheckboxes,
|
||||||
expect(findModal().exists()).toBe(false);
|
},
|
||||||
});
|
|
||||||
|
|
||||||
it('shows toast after delete success', async () => {
|
|
||||||
const newDesc = 'description';
|
|
||||||
findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc);
|
|
||||||
|
|
||||||
expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]);
|
|
||||||
expect($toast.show).toHaveBeenCalledWith('Task deleted');
|
|
||||||
});
|
});
|
||||||
|
return nextTick();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('task list item actions', () => {
|
it('renders a list of hidden buttons corresponding to checkboxes in description HTML', () => {
|
||||||
describe('converting the task list item to a task', () => {
|
expect(findTaskActionButtons()).toHaveLength(3);
|
||||||
describe('when successful', () => {
|
});
|
||||||
let createWorkItemMutationHandler;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
it('does not show a modal by default', () => {
|
||||||
createWorkItemMutationHandler = jest
|
expect(findModal().exists()).toBe(false);
|
||||||
.fn()
|
});
|
||||||
.mockResolvedValue(createWorkItemMutationResponse);
|
|
||||||
const descriptionText = `Tasks
|
it('shows toast after delete success', async () => {
|
||||||
|
const newDesc = 'description';
|
||||||
|
findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc);
|
||||||
|
|
||||||
|
expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]);
|
||||||
|
expect($toast.show).toHaveBeenCalledWith('Task deleted');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('task list item actions', () => {
|
||||||
|
describe('converting the task list item to a task', () => {
|
||||||
|
describe('when successful', () => {
|
||||||
|
let createWorkItemMutationHandler;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
createWorkItemMutationHandler = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(createWorkItemMutationResponse);
|
||||||
|
const descriptionText = `Tasks
|
||||||
|
|
||||||
1. [ ] item 1
|
1. [ ] item 1
|
||||||
1. [ ] item 2
|
1. [ ] item 2
|
||||||
|
@ -381,218 +370,207 @@ describe('Description component', () => {
|
||||||
|
|
||||||
1. [ ] item 3
|
1. [ ] item 3
|
||||||
1. [ ] item 4;`;
|
1. [ ] item 4;`;
|
||||||
createComponent({
|
createComponent({
|
||||||
props: { descriptionText },
|
props: { descriptionText },
|
||||||
provide: { glFeatures: { workItemsMvc: true } },
|
createWorkItemMutationHandler,
|
||||||
createWorkItemMutationHandler,
|
|
||||||
});
|
|
||||||
await waitForPromises();
|
|
||||||
|
|
||||||
eventHub.$emit('convert-task-list-item', '4:4-8:19');
|
|
||||||
await waitForPromises();
|
|
||||||
});
|
});
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
it('emits an event to update the description with the deleted task list item omitted', () => {
|
eventHub.$emit('convert-task-list-item', '4:4-8:19');
|
||||||
const newDescriptionText = `Tasks
|
await waitForPromises();
|
||||||
|
|
||||||
1. [ ] item 1
|
|
||||||
1. [ ] item 3
|
|
||||||
1. [ ] item 4;`;
|
|
||||||
|
|
||||||
expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls a mutation to create a task', () => {
|
|
||||||
const {
|
|
||||||
confidential,
|
|
||||||
iteration,
|
|
||||||
milestone,
|
|
||||||
} = issueDetailsResponse.data.workspace.issuable;
|
|
||||||
expect(createWorkItemMutationHandler).toHaveBeenCalledWith({
|
|
||||||
input: {
|
|
||||||
confidential,
|
|
||||||
description: '\nparagraph text\n',
|
|
||||||
hierarchyWidget: {
|
|
||||||
parentId: 'gid://gitlab/WorkItem/1',
|
|
||||||
},
|
|
||||||
iterationWidget: {
|
|
||||||
iterationId: IS_EE ? iteration.id : null,
|
|
||||||
},
|
|
||||||
milestoneWidget: {
|
|
||||||
milestoneId: milestone.id,
|
|
||||||
},
|
|
||||||
projectPath: 'gitlab-org/gitlab-test',
|
|
||||||
title: 'item 2',
|
|
||||||
workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows a toast to confirm the creation of the task', () => {
|
|
||||||
expect($toast.show).toHaveBeenCalledWith('Converted to task', expect.any(Object));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when unsuccessful', () => {
|
it('emits an event to update the description with the deleted task list item omitted', () => {
|
||||||
beforeEach(async () => {
|
|
||||||
createComponent({
|
|
||||||
props: { descriptionText: 'description' },
|
|
||||||
provide: { glFeatures: { workItemsMvc: true } },
|
|
||||||
createWorkItemMutationHandler: jest
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValue(createWorkItemMutationErrorResponse),
|
|
||||||
});
|
|
||||||
await waitForPromises();
|
|
||||||
|
|
||||||
eventHub.$emit('convert-task-list-item', '1:1-1:11');
|
|
||||||
await waitForPromises();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows an alert with an error message', () => {
|
|
||||||
expect(createAlert).toHaveBeenCalledWith({
|
|
||||||
message: 'Something went wrong when creating task. Please try again.',
|
|
||||||
error: new Error('an error'),
|
|
||||||
captureError: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('deleting the task list item', () => {
|
|
||||||
it('emits an event to update the description with the deleted task list item', () => {
|
|
||||||
const descriptionText = `Tasks
|
|
||||||
|
|
||||||
1. [ ] item 1
|
|
||||||
1. [ ] item 2
|
|
||||||
1. [ ] item 3
|
|
||||||
1. [ ] item 4;`;
|
|
||||||
const newDescriptionText = `Tasks
|
const newDescriptionText = `Tasks
|
||||||
|
|
||||||
1. [ ] item 1
|
1. [ ] item 1
|
||||||
1. [ ] item 3
|
1. [ ] item 3
|
||||||
1. [ ] item 4;`;
|
1. [ ] item 4;`;
|
||||||
createComponent({
|
|
||||||
props: { descriptionText },
|
|
||||||
provide: { glFeatures: { workItemsMvc: true } },
|
|
||||||
});
|
|
||||||
|
|
||||||
eventHub.$emit('delete-task-list-item', '4:4-5:19');
|
|
||||||
|
|
||||||
expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
|
expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('work items detail', () => {
|
it('calls a mutation to create a task', () => {
|
||||||
describe('when opening and closing', () => {
|
const {
|
||||||
beforeEach(() => {
|
confidential,
|
||||||
|
iteration,
|
||||||
|
milestone,
|
||||||
|
} = issueDetailsResponse.data.workspace.issuable;
|
||||||
|
expect(createWorkItemMutationHandler).toHaveBeenCalledWith({
|
||||||
|
input: {
|
||||||
|
confidential,
|
||||||
|
description: '\nparagraph text\n',
|
||||||
|
hierarchyWidget: {
|
||||||
|
parentId: 'gid://gitlab/WorkItem/1',
|
||||||
|
},
|
||||||
|
iterationWidget: {
|
||||||
|
iterationId: IS_EE ? iteration.id : null,
|
||||||
|
},
|
||||||
|
milestoneWidget: {
|
||||||
|
milestoneId: milestone.id,
|
||||||
|
},
|
||||||
|
projectPath: 'gitlab-org/gitlab-test',
|
||||||
|
title: 'item 2',
|
||||||
|
workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a toast to confirm the creation of the task', () => {
|
||||||
|
expect($toast.show).toHaveBeenCalledWith('Converted to task', expect.any(Object));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when unsuccessful', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
createComponent({
|
createComponent({
|
||||||
props: {
|
props: { descriptionText: 'description' },
|
||||||
descriptionHtml: descriptionHtmlWithTask,
|
createWorkItemMutationHandler: jest
|
||||||
},
|
.fn()
|
||||||
provide: {
|
.mockResolvedValue(createWorkItemMutationErrorResponse),
|
||||||
glFeatures: { workItemsMvc: true },
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
return nextTick();
|
await waitForPromises();
|
||||||
|
|
||||||
|
eventHub.$emit('convert-task-list-item', '1:1-1:11');
|
||||||
|
await waitForPromises();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('opens when task button is clicked', async () => {
|
it('shows an alert with an error message', () => {
|
||||||
await findTaskLink().trigger('click');
|
expect(createAlert).toHaveBeenCalledWith({
|
||||||
|
message: 'Something went wrong when creating task. Please try again.',
|
||||||
expect(showDetailsModal).toHaveBeenCalled();
|
error: new Error('an error'),
|
||||||
expect(updateHistory).toHaveBeenCalledWith({
|
captureError: true,
|
||||||
url: `${TEST_HOST}/?work_item_id=2`,
|
|
||||||
replace: true,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('closes from an open state', async () => {
|
|
||||||
await findTaskLink().trigger('click');
|
|
||||||
|
|
||||||
findWorkItemDetailModal().vm.$emit('close');
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(updateHistory).toHaveBeenLastCalledWith({
|
|
||||||
url: `${TEST_HOST}/`,
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('tracks when opened', async () => {
|
|
||||||
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
|
||||||
|
|
||||||
await findTaskLink().trigger('click');
|
|
||||||
|
|
||||||
expect(trackingSpy).toHaveBeenCalledWith(
|
|
||||||
TRACKING_CATEGORY_SHOW,
|
|
||||||
'viewed_work_item_from_modal',
|
|
||||||
{
|
|
||||||
category: TRACKING_CATEGORY_SHOW,
|
|
||||||
label: 'work_item_view',
|
|
||||||
property: 'type_task',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when url query `work_item_id` exists', () => {
|
|
||||||
it.each`
|
|
||||||
behavior | workItemId | modalOpened
|
|
||||||
${'opens'} | ${'2'} | ${1}
|
|
||||||
${'does not open'} | ${'123'} | ${0}
|
|
||||||
${'does not open'} | ${'123e'} | ${0}
|
|
||||||
${'does not open'} | ${'12e3'} | ${0}
|
|
||||||
${'does not open'} | ${'1e23'} | ${0}
|
|
||||||
${'does not open'} | ${'x'} | ${0}
|
|
||||||
${'does not open'} | ${'undefined'} | ${0}
|
|
||||||
`(
|
|
||||||
'$behavior when url contains `work_item_id=$workItemId`',
|
|
||||||
async ({ workItemId, modalOpened }) => {
|
|
||||||
setWindowLocation(`?work_item_id=${workItemId}`);
|
|
||||||
|
|
||||||
createComponent({
|
|
||||||
props: { descriptionHtml: descriptionHtmlWithTask },
|
|
||||||
provide: { glFeatures: { workItemsMvc: true } },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when hovering task links', () => {
|
describe('deleting the task list item', () => {
|
||||||
|
it('emits an event to update the description with the deleted task list item', () => {
|
||||||
|
const descriptionText = `Tasks
|
||||||
|
|
||||||
|
1. [ ] item 1
|
||||||
|
1. [ ] item 2
|
||||||
|
1. [ ] item 3
|
||||||
|
1. [ ] item 4;`;
|
||||||
|
const newDescriptionText = `Tasks
|
||||||
|
|
||||||
|
1. [ ] item 1
|
||||||
|
1. [ ] item 3
|
||||||
|
1. [ ] item 4;`;
|
||||||
|
createComponent({
|
||||||
|
props: { descriptionText },
|
||||||
|
});
|
||||||
|
|
||||||
|
eventHub.$emit('delete-task-list-item', '4:4-5:19');
|
||||||
|
|
||||||
|
expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('work items detail', () => {
|
||||||
|
describe('when opening and closing', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent({
|
createComponent({
|
||||||
props: {
|
props: {
|
||||||
descriptionHtml: descriptionHtmlWithTask,
|
descriptionHtml: descriptionHtmlWithTask,
|
||||||
},
|
},
|
||||||
provide: {
|
|
||||||
glFeatures: { workItemsMvc: true },
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
return nextTick();
|
return nextTick();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('prefetches work item detail after work item link is hovered for 150ms', async () => {
|
it('opens when task button is clicked', async () => {
|
||||||
await findTaskLink().trigger('mouseover');
|
await findTaskLink().trigger('click');
|
||||||
jest.advanceTimersByTime(150);
|
|
||||||
await waitForPromises();
|
|
||||||
|
|
||||||
expect(queryHandler).toHaveBeenCalledWith({
|
expect(showDetailsModal).toHaveBeenCalled();
|
||||||
id: 'gid://gitlab/WorkItem/2',
|
expect(updateHistory).toHaveBeenCalledWith({
|
||||||
|
url: `${TEST_HOST}/?work_item_id=2`,
|
||||||
|
replace: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not work item detail after work item link is hovered for less than 150ms', async () => {
|
it('closes from an open state', async () => {
|
||||||
await findTaskLink().trigger('mouseover');
|
await findTaskLink().trigger('click');
|
||||||
await findTaskLink().trigger('mouseout');
|
|
||||||
jest.advanceTimersByTime(150);
|
|
||||||
await waitForPromises();
|
|
||||||
|
|
||||||
expect(queryHandler).not.toHaveBeenCalled();
|
findWorkItemDetailModal().vm.$emit('close');
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(updateHistory).toHaveBeenLastCalledWith({
|
||||||
|
url: `${TEST_HOST}/`,
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('tracks when opened', async () => {
|
||||||
|
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||||
|
|
||||||
|
await findTaskLink().trigger('click');
|
||||||
|
|
||||||
|
expect(trackingSpy).toHaveBeenCalledWith(
|
||||||
|
TRACKING_CATEGORY_SHOW,
|
||||||
|
'viewed_work_item_from_modal',
|
||||||
|
{
|
||||||
|
category: TRACKING_CATEGORY_SHOW,
|
||||||
|
label: 'work_item_view',
|
||||||
|
property: 'type_task',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when url query `work_item_id` exists', () => {
|
||||||
|
it.each`
|
||||||
|
behavior | workItemId | modalOpened
|
||||||
|
${'opens'} | ${'2'} | ${1}
|
||||||
|
${'does not open'} | ${'123'} | ${0}
|
||||||
|
${'does not open'} | ${'123e'} | ${0}
|
||||||
|
${'does not open'} | ${'12e3'} | ${0}
|
||||||
|
${'does not open'} | ${'1e23'} | ${0}
|
||||||
|
${'does not open'} | ${'x'} | ${0}
|
||||||
|
${'does not open'} | ${'undefined'} | ${0}
|
||||||
|
`(
|
||||||
|
'$behavior when url contains `work_item_id=$workItemId`',
|
||||||
|
async ({ workItemId, modalOpened }) => {
|
||||||
|
setWindowLocation(`?work_item_id=${workItemId}`);
|
||||||
|
|
||||||
|
createComponent({
|
||||||
|
props: { descriptionHtml: descriptionHtmlWithTask },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when hovering task links', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createComponent({
|
||||||
|
props: {
|
||||||
|
descriptionHtml: descriptionHtmlWithTask,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return nextTick();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefetches work item detail after work item link is hovered for 150ms', async () => {
|
||||||
|
await findTaskLink().trigger('mouseover');
|
||||||
|
jest.advanceTimersByTime(150);
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(queryHandler).toHaveBeenCalledWith({
|
||||||
|
id: 'gid://gitlab/WorkItem/2',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not work item detail after work item link is hovered for less than 150ms', async () => {
|
||||||
|
await findTaskLink().trigger('mouseover');
|
||||||
|
await findTaskLink().trigger('mouseout');
|
||||||
|
jest.advanceTimersByTime(150);
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(queryHandler).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { generateRefDestinationPath } from '~/repository/utils/ref_switcher_utils';
|
import { generateRefDestinationPath } from '~/repository/utils/ref_switcher_utils';
|
||||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||||
|
import { TEST_HOST } from 'spec/test_constants';
|
||||||
import { refWithSpecialCharMock, encodedRefWithSpecialCharMock } from '../mock_data';
|
import { refWithSpecialCharMock, encodedRefWithSpecialCharMock } from '../mock_data';
|
||||||
|
|
||||||
const projectRootPath = 'root/Project1';
|
const projectRootPath = 'root/Project1';
|
||||||
|
@ -16,16 +17,38 @@ describe('generateRefDestinationPath', () => {
|
||||||
${`${projectRootPath}/-/blob/${currentRef}/dir1/test.js`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/test.js`}
|
${`${projectRootPath}/-/blob/${currentRef}/dir1/test.js`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/test.js`}
|
||||||
${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/dir2/test.js`}
|
${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/dir2/test.js`}
|
||||||
${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js#L123`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/dir2/test.js#L123`}
|
${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js#L123`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/dir2/test.js#L123`}
|
||||||
`('generates the correct destination path for $currentPath', ({ currentPath, result }) => {
|
`('generates the correct destination path for $currentPath', ({ currentPath, result }) => {
|
||||||
setWindowLocation(currentPath);
|
setWindowLocation(currentPath);
|
||||||
expect(generateRefDestinationPath(projectRootPath, currentRef, selectedRef)).toBe(result);
|
expect(generateRefDestinationPath(projectRootPath, currentRef, selectedRef)).toBe(
|
||||||
|
`${TEST_HOST}/${result}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when using symbolic ref names', () => {
|
||||||
|
it.each`
|
||||||
|
currentPath | nextRef | result
|
||||||
|
${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js#L123`} | ${'someHash'} | ${`${projectRootPath}/-/blob/someHash/dir1/dir2/test.js#L123`}
|
||||||
|
${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js#L123`} | ${'refs/heads/prefixedByUseSymbolicRefNames'} | ${`${projectRootPath}/-/blob/prefixedByUseSymbolicRefNames/dir1/dir2/test.js?ref_type=heads#L123`}
|
||||||
|
${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js#L123`} | ${'refs/tags/prefixedByUseSymbolicRefNames'} | ${`${projectRootPath}/-/blob/prefixedByUseSymbolicRefNames/dir1/dir2/test.js?ref_type=tags#L123`}
|
||||||
|
${`${projectRootPath}/-/tree/${currentRef}/dir1/dir2/test.js#L123`} | ${'refs/heads/prefixedByUseSymbolicRefNames'} | ${`${projectRootPath}/-/tree/prefixedByUseSymbolicRefNames/dir1/dir2/test.js?ref_type=heads#L123`}
|
||||||
|
${`${projectRootPath}/-/tree/${currentRef}/dir1/dir2/test.js#L123`} | ${'refs/tags/prefixedByUseSymbolicRefNames'} | ${`${projectRootPath}/-/tree/prefixedByUseSymbolicRefNames/dir1/dir2/test.js?ref_type=tags#L123`}
|
||||||
|
${`${projectRootPath}/-/tree/${currentRef}/dir1/dir2/test.js#L123`} | ${'refs/heads/refs/heads/branchNameContainsPrefix'} | ${`${projectRootPath}/-/tree/refs/heads/branchNameContainsPrefix/dir1/dir2/test.js?ref_type=heads#L123`}
|
||||||
|
`(
|
||||||
|
'generates the correct destination path for $currentPath with ref type when it can be extracted',
|
||||||
|
({ currentPath, result, nextRef }) => {
|
||||||
|
setWindowLocation(currentPath);
|
||||||
|
expect(generateRefDestinationPath(projectRootPath, currentRef, nextRef)).toBe(
|
||||||
|
`${TEST_HOST}/${result}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('encodes the selected ref', () => {
|
it('encodes the selected ref', () => {
|
||||||
const result = `${projectRootPath}/-/tree/${encodedRefWithSpecialCharMock}`;
|
const result = `${projectRootPath}/-/tree/${encodedRefWithSpecialCharMock}`;
|
||||||
|
|
||||||
expect(generateRefDestinationPath(projectRootPath, currentRef, refWithSpecialCharMock)).toBe(
|
expect(generateRefDestinationPath(projectRootPath, currentRef, refWithSpecialCharMock)).toBe(
|
||||||
result,
|
`${TEST_HOST}/${result}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -46,10 +46,17 @@ RSpec.describe Mutations::Ci::Runner::Update, feature_category: :runner_fleet do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user can update runner', :enable_admin_mode do
|
context 'when user can update runner' do
|
||||||
let_it_be(:admin_user) { create(:user, :admin) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
let(:current_ctx) { { current_user: admin_user } }
|
let(:original_projects) { [project1, project2] }
|
||||||
|
let(:projects_with_maintainer_access) { original_projects }
|
||||||
|
|
||||||
|
let(:current_ctx) { { current_user: user } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
projects_with_maintainer_access.each { |project| project.add_maintainer(user) }
|
||||||
|
end
|
||||||
|
|
||||||
context 'with valid arguments' do
|
context 'with valid arguments' do
|
||||||
let(:mutation_params) do
|
let(:mutation_params) do
|
||||||
|
@ -82,27 +89,22 @@ RSpec.describe Mutations::Ci::Runner::Update, feature_category: :runner_fleet do
|
||||||
|
|
||||||
context 'with associatedProjects argument' do
|
context 'with associatedProjects argument' do
|
||||||
let_it_be(:project3) { create(:project) }
|
let_it_be(:project3) { create(:project) }
|
||||||
|
let_it_be(:project4) { create(:project) }
|
||||||
|
|
||||||
|
let(:new_projects) { [project3, project4] }
|
||||||
|
let(:mutation_params) do
|
||||||
|
{
|
||||||
|
id: runner.to_global_id,
|
||||||
|
description: 'updated description',
|
||||||
|
associated_projects: new_projects.map { |project| project.to_global_id.to_s }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
context 'with id set to project runner' do
|
context 'with id set to project runner' do
|
||||||
let(:mutation_params) do
|
let(:projects_with_maintainer_access) { original_projects + new_projects }
|
||||||
{
|
|
||||||
id: runner.to_global_id,
|
|
||||||
description: 'updated description',
|
|
||||||
associated_projects: [project3.to_global_id.to_s]
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates runner attributes and project relationships', :aggregate_failures do
|
it 'updates runner attributes and project relationships', :aggregate_failures do
|
||||||
expect_next_instance_of(
|
setup_service_expectations
|
||||||
::Ci::Runners::SetRunnerAssociatedProjectsService,
|
|
||||||
{
|
|
||||||
runner: runner,
|
|
||||||
current_user: admin_user,
|
|
||||||
project_ids: [project3.id]
|
|
||||||
}
|
|
||||||
) do |service|
|
|
||||||
expect(service).to receive(:execute).and_call_original
|
|
||||||
end
|
|
||||||
|
|
||||||
expected_attributes = mutation_params.except(:id, :associated_projects)
|
expected_attributes = mutation_params.except(:id, :associated_projects)
|
||||||
|
|
||||||
|
@ -112,57 +114,32 @@ RSpec.describe Mutations::Ci::Runner::Update, feature_category: :runner_fleet do
|
||||||
expect(response[:runner]).to be_an_instance_of(Ci::Runner)
|
expect(response[:runner]).to be_an_instance_of(Ci::Runner)
|
||||||
expect(response[:runner]).to have_attributes(expected_attributes)
|
expect(response[:runner]).to have_attributes(expected_attributes)
|
||||||
expect(runner.reload).to have_attributes(expected_attributes)
|
expect(runner.reload).to have_attributes(expected_attributes)
|
||||||
expect(runner.projects).to match_array([project1, project3])
|
expect(runner.projects).to match_array([project1] + new_projects)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with user not allowed to assign runner' do
|
context 'with missing permissions on one of the new projects' do
|
||||||
before do
|
let(:projects_with_maintainer_access) { original_projects + [project3] }
|
||||||
allow(admin_user).to receive(:can?).with(:assign_runner, runner).and_return(false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not update runner', :aggregate_failures do
|
it 'does not update runner', :aggregate_failures do
|
||||||
expect_next_instance_of(
|
setup_service_expectations
|
||||||
::Ci::Runners::SetRunnerAssociatedProjectsService,
|
|
||||||
{
|
|
||||||
runner: runner,
|
|
||||||
current_user: admin_user,
|
|
||||||
project_ids: [project3.id]
|
|
||||||
}
|
|
||||||
) do |service|
|
|
||||||
expect(service).to receive(:execute).and_call_original
|
|
||||||
end
|
|
||||||
|
|
||||||
expected_attributes = mutation_params.except(:id, :associated_projects)
|
expected_attributes = mutation_params.except(:id, :associated_projects)
|
||||||
|
|
||||||
response
|
response
|
||||||
|
|
||||||
expect(response[:errors]).to match_array(['user not allowed to assign runner'])
|
expect(response[:errors]).to match_array(['user is not authorized to add runners to project'])
|
||||||
expect(response[:runner]).to be_nil
|
expect(response[:runner]).to be_nil
|
||||||
expect(runner.reload).not_to have_attributes(expected_attributes)
|
expect(runner.reload).not_to have_attributes(expected_attributes)
|
||||||
expect(runner.projects).to match_array([project1, project2])
|
expect(runner.projects).to match_array(original_projects)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with an empty list of projects' do
|
context 'with an empty list of projects' do
|
||||||
let(:mutation_params) do
|
let(:new_projects) { [] }
|
||||||
{
|
|
||||||
id: runner.to_global_id,
|
|
||||||
associated_projects: []
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'removes project relationships', :aggregate_failures do
|
it 'removes project relationships', :aggregate_failures do
|
||||||
expect_next_instance_of(
|
setup_service_expectations
|
||||||
::Ci::Runners::SetRunnerAssociatedProjectsService,
|
|
||||||
{
|
|
||||||
runner: runner,
|
|
||||||
current_user: admin_user,
|
|
||||||
project_ids: []
|
|
||||||
}
|
|
||||||
) do |service|
|
|
||||||
expect(service).to receive(:execute).and_call_original
|
|
||||||
end
|
|
||||||
|
|
||||||
response
|
response
|
||||||
|
|
||||||
|
@ -172,15 +149,9 @@ RSpec.describe Mutations::Ci::Runner::Update, feature_category: :runner_fleet do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with id set to instance runner' do
|
context 'with id set to instance runner', :enable_admin_mode do
|
||||||
let(:instance_runner) { create(:ci_runner, :instance) }
|
let_it_be(:user) { create(:user, :admin) }
|
||||||
let(:mutation_params) do
|
let_it_be(:runner) { create(:ci_runner, :instance) }
|
||||||
{
|
|
||||||
id: instance_runner.to_global_id,
|
|
||||||
description: 'updated description',
|
|
||||||
associated_projects: [project2.to_global_id.to_s]
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'raises error', :aggregate_failures do
|
it 'raises error', :aggregate_failures do
|
||||||
expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do
|
expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do
|
||||||
|
@ -188,6 +159,19 @@ RSpec.describe Mutations::Ci::Runner::Update, feature_category: :runner_fleet do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def setup_service_expectations
|
||||||
|
expect_next_instance_of(
|
||||||
|
::Ci::Runners::SetRunnerAssociatedProjectsService,
|
||||||
|
{
|
||||||
|
runner: runner,
|
||||||
|
current_user: user,
|
||||||
|
project_ids: new_projects.map(&:id)
|
||||||
|
}
|
||||||
|
) do |service|
|
||||||
|
expect(service).to receive(:execute).and_call_original
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with non-existing project ID in associatedProjects argument' do
|
context 'with non-existing project ID in associatedProjects argument' do
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe AvatarsHelper do
|
RSpec.describe AvatarsHelper, feature_category: :source_code_management do
|
||||||
include UploadHelpers
|
include UploadHelpers
|
||||||
|
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
@ -88,7 +88,7 @@ RSpec.describe AvatarsHelper do
|
||||||
describe '#avatar_icon_for' do
|
describe '#avatar_icon_for' do
|
||||||
let!(:user) { create(:user, avatar: File.open(uploaded_image_temp_path), email: 'bar@example.com') }
|
let!(:user) { create(:user, avatar: File.open(uploaded_image_temp_path), email: 'bar@example.com') }
|
||||||
let(:email) { 'foo@example.com' }
|
let(:email) { 'foo@example.com' }
|
||||||
let!(:another_user) { create(:user, avatar: File.open(uploaded_image_temp_path), email: email) }
|
let!(:another_user) { create(:user, :public_email, avatar: File.open(uploaded_image_temp_path), email: email) }
|
||||||
|
|
||||||
it 'prefers the user to retrieve the avatar_url' do
|
it 'prefers the user to retrieve the avatar_url' do
|
||||||
expect(helper.avatar_icon_for(user, email).to_s)
|
expect(helper.avatar_icon_for(user, email).to_s)
|
||||||
|
@ -102,7 +102,7 @@ RSpec.describe AvatarsHelper do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#avatar_icon_for_email', :clean_gitlab_redis_cache do
|
describe '#avatar_icon_for_email', :clean_gitlab_redis_cache do
|
||||||
let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) }
|
let(:user) { create(:user, :public_email, avatar: File.open(uploaded_image_temp_path)) }
|
||||||
|
|
||||||
subject { helper.avatar_icon_for_email(user.email).to_s }
|
subject { helper.avatar_icon_for_email(user.email).to_s }
|
||||||
|
|
||||||
|
@ -114,6 +114,14 @@ RSpec.describe AvatarsHelper do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when a private email is used' do
|
||||||
|
it 'calls gravatar_icon' do
|
||||||
|
expect(helper).to receive(:gravatar_icon).with(user.commit_email, 20, 2)
|
||||||
|
|
||||||
|
helper.avatar_icon_for_email(user.commit_email, 20, 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when no user exists for the email' do
|
context 'when no user exists for the email' do
|
||||||
it 'calls gravatar_icon' do
|
it 'calls gravatar_icon' do
|
||||||
expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2)
|
expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2)
|
||||||
|
@ -136,7 +144,7 @@ RSpec.describe AvatarsHelper do
|
||||||
it_behaves_like "returns avatar for email"
|
it_behaves_like "returns avatar for email"
|
||||||
|
|
||||||
it "caches the request" do
|
it "caches the request" do
|
||||||
expect(User).to receive(:find_by_any_email).once.and_call_original
|
expect(User).to receive(:with_public_email).once.and_call_original
|
||||||
|
|
||||||
expect(helper.avatar_icon_for_email(user.email).to_s).to eq(user.avatar.url)
|
expect(helper.avatar_icon_for_email(user.email).to_s).to eq(user.avatar.url)
|
||||||
expect(helper.avatar_icon_for_email(user.email).to_s).to eq(user.avatar.url)
|
expect(helper.avatar_icon_for_email(user.email).to_s).to eq(user.avatar.url)
|
||||||
|
|
|
@ -26,7 +26,7 @@ RSpec.describe HooksHelper do
|
||||||
it 'returns proper data' do
|
it 'returns proper data' do
|
||||||
expect(subject).to match(
|
expect(subject).to match(
|
||||||
url: project_hook.url,
|
url: project_hook.url,
|
||||||
url_variables: Gitlab::Json.dump([{ key: 'abc' }])
|
url_variables: Gitlab::Json.dump([{ key: 'abc' }, { key: 'def' }])
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -80,6 +80,15 @@ RSpec.describe Banzai::Filter::AssetProxyFilter, feature_category: :team_plannin
|
||||||
expect(doc.at_css('img')['data-canonical-src']).to eq src
|
expect(doc.at_css('img')['data-canonical-src']).to eq src
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'replaces invalid URLs' do
|
||||||
|
src = '///example.com/test.png'
|
||||||
|
new_src = 'https://assets.example.com/3368d2c7b9bed775bdd1e811f36a4b80a0dcd8ab/2f2f2f6578616d706c652e636f6d2f746573742e706e67'
|
||||||
|
doc = filter(image(src), @context)
|
||||||
|
|
||||||
|
expect(doc.at_css('img')['src']).to eq new_src
|
||||||
|
expect(doc.at_css('img')['data-canonical-src']).to eq src
|
||||||
|
end
|
||||||
|
|
||||||
it 'skips internal images' do
|
it 'skips internal images' do
|
||||||
src = "#{Gitlab.config.gitlab.url}/test.png"
|
src = "#{Gitlab.config.gitlab.url}/test.png"
|
||||||
doc = filter(image(src), @context)
|
doc = filter(image(src), @context)
|
||||||
|
|
|
@ -218,7 +218,7 @@ RSpec.describe Banzai::Filter::CommitTrailersFilter, feature_category: :source_c
|
||||||
# any path-only link will automatically be prefixed
|
# any path-only link will automatically be prefixed
|
||||||
# with the path of its repository.
|
# with the path of its repository.
|
||||||
# See: "build_relative_path" in "lib/banzai/filter/relative_link_filter.rb"
|
# See: "build_relative_path" in "lib/banzai/filter/relative_link_filter.rb"
|
||||||
let(:user_with_avatar) { create(:user, :with_avatar, username: 'foobar') }
|
let(:user_with_avatar) { create(:user, :public_email, :with_avatar, username: 'foobar') }
|
||||||
|
|
||||||
it 'returns a full path for avatar urls' do
|
it 'returns a full path for avatar urls' do
|
||||||
_, message_html = build_commit_message(
|
_, message_html = build_commit_message(
|
||||||
|
|
|
@ -34,6 +34,58 @@ RSpec.describe Banzai::Filter::InlineObservabilityFilter do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the document contains an embeddable observability link with redirect' do
|
||||||
|
let(:url) { 'https://observe.gitlab.com@example.com/12345' }
|
||||||
|
|
||||||
|
it 'leaves the original link unchanged' do
|
||||||
|
expect(doc.at_css('a').to_s).to eq(input)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not append an observability charts placeholder' do
|
||||||
|
node = doc.at_css('.js-render-observability')
|
||||||
|
|
||||||
|
expect(node).not_to be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the document contains an embeddable observability link with different port' do
|
||||||
|
let(:url) { 'https://observe.gitlab.com:3000/12345' }
|
||||||
|
let(:observe_url) { 'https://observe.gitlab.com:3001' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_env('OVERRIDE_OBSERVABILITY_URL', observe_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'leaves the original link unchanged' do
|
||||||
|
expect(doc.at_css('a').to_s).to eq(input)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not append an observability charts placeholder' do
|
||||||
|
node = doc.at_css('.js-render-observability')
|
||||||
|
|
||||||
|
expect(node).not_to be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the document contains an embeddable observability link with auth/start' do
|
||||||
|
let(:url) { 'https://observe.gitlab.com/auth/start' }
|
||||||
|
let(:observe_url) { 'https://observe.gitlab.com' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_env('OVERRIDE_OBSERVABILITY_URL', observe_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'leaves the original link unchanged' do
|
||||||
|
expect(doc.at_css('a').to_s).to eq(input)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not append an observability charts placeholder' do
|
||||||
|
node = doc.at_css('.js-render-observability')
|
||||||
|
|
||||||
|
expect(node).not_to be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when feature flag is disabled' do
|
context 'when feature flag is disabled' do
|
||||||
let(:url) { 'https://observe.gitlab.com/12345' }
|
let(:url) { 'https://observe.gitlab.com/12345' }
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Gitlab::BackgroundMigration::NullifyLastErrorFromProjectMirrorData, feature_category: :source_code_management do # rubocop:disable Layout/LineLength
|
||||||
|
it 'nullifies last_error column on all rows' do
|
||||||
|
namespaces = table(:namespaces)
|
||||||
|
projects = table(:projects)
|
||||||
|
project_import_states = table(:project_mirror_data)
|
||||||
|
|
||||||
|
group = namespaces.create!(name: 'gitlab', path: 'gitlab-org')
|
||||||
|
|
||||||
|
project_namespace_1 = namespaces.create!(name: 'gitlab', path: 'gitlab-org')
|
||||||
|
project_namespace_2 = namespaces.create!(name: 'gitlab', path: 'gitlab-org')
|
||||||
|
project_namespace_3 = namespaces.create!(name: 'gitlab', path: 'gitlab-org')
|
||||||
|
|
||||||
|
project_1 = projects.create!(
|
||||||
|
namespace_id: group.id,
|
||||||
|
project_namespace_id: project_namespace_1.id,
|
||||||
|
name: 'test1'
|
||||||
|
)
|
||||||
|
project_2 = projects.create!(
|
||||||
|
namespace_id: group.id,
|
||||||
|
project_namespace_id: project_namespace_2.id,
|
||||||
|
name: 'test2'
|
||||||
|
)
|
||||||
|
project_3 = projects.create!(
|
||||||
|
namespace_id: group.id,
|
||||||
|
project_namespace_id: project_namespace_3.id,
|
||||||
|
name: 'test3'
|
||||||
|
)
|
||||||
|
|
||||||
|
project_import_state_1 = project_import_states.create!(
|
||||||
|
project_id: project_1.id,
|
||||||
|
status: 0,
|
||||||
|
last_update_started_at: 1.hour.ago,
|
||||||
|
last_update_scheduled_at: 1.hour.ago,
|
||||||
|
last_update_at: 1.hour.ago,
|
||||||
|
last_successful_update_at: 2.days.ago,
|
||||||
|
last_error: '13:fetch remote: "fatal: unable to look up user:pass@gitlab.com (port 9418) (nodename nor servname provided, or not known)\n": exit status 128.', # rubocop:disable Layout/LineLength
|
||||||
|
correlation_id_value: SecureRandom.uuid,
|
||||||
|
jid: SecureRandom.uuid
|
||||||
|
)
|
||||||
|
|
||||||
|
project_import_states.create!(
|
||||||
|
project_id: project_2.id,
|
||||||
|
status: 1,
|
||||||
|
last_update_started_at: 1.hour.ago,
|
||||||
|
last_update_scheduled_at: 1.hour.ago,
|
||||||
|
last_update_at: 1.hour.ago,
|
||||||
|
last_successful_update_at: nil,
|
||||||
|
next_execution_timestamp: 1.day.from_now,
|
||||||
|
last_error: '',
|
||||||
|
correlation_id_value: SecureRandom.uuid,
|
||||||
|
jid: SecureRandom.uuid
|
||||||
|
)
|
||||||
|
|
||||||
|
project_import_state_3 = project_import_states.create!(
|
||||||
|
project_id: project_3.id,
|
||||||
|
status: 2,
|
||||||
|
last_update_started_at: 1.hour.ago,
|
||||||
|
last_update_scheduled_at: 1.hour.ago,
|
||||||
|
last_update_at: 1.hour.ago,
|
||||||
|
last_successful_update_at: 1.hour.ago,
|
||||||
|
next_execution_timestamp: 1.day.from_now,
|
||||||
|
last_error: nil,
|
||||||
|
correlation_id_value: SecureRandom.uuid,
|
||||||
|
jid: SecureRandom.uuid
|
||||||
|
)
|
||||||
|
|
||||||
|
migration = described_class.new(
|
||||||
|
start_id: project_import_state_1.id,
|
||||||
|
end_id: project_import_state_3.id,
|
||||||
|
batch_table: :project_mirror_data,
|
||||||
|
batch_column: :id,
|
||||||
|
sub_batch_size: 1,
|
||||||
|
pause_ms: 0,
|
||||||
|
connection: ApplicationRecord.connection
|
||||||
|
)
|
||||||
|
|
||||||
|
w_last_error_count = -> { project_import_states.where.not(last_error: nil).count } # rubocop:disable CodeReuse/ActiveRecord
|
||||||
|
expect { migration.perform }.to change(&w_last_error_count).from(2).to(0)
|
||||||
|
end
|
||||||
|
end
|
|
@ -26,8 +26,14 @@ RSpec.describe Gitlab::Checks::BranchCheck do
|
||||||
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.")
|
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "prohibits 40-character hexadecimal branch names as the start of a path" do
|
||||||
|
allow(subject).to receive(:branch_name).and_return("267208abfe40e546f5e847444276f7d43a39503e/test")
|
||||||
|
|
||||||
|
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.")
|
||||||
|
end
|
||||||
|
|
||||||
it "doesn't prohibit a nested hexadecimal in a branch name" do
|
it "doesn't prohibit a nested hexadecimal in a branch name" do
|
||||||
allow(subject).to receive(:branch_name).and_return("fix-267208abfe40e546f5e847444276f7d43a39503e")
|
allow(subject).to receive(:branch_name).and_return("267208abfe40e546f5e847444276f7d43a39503e-fix")
|
||||||
|
|
||||||
expect { subject.validate! }.not_to raise_error
|
expect { subject.validate! }.not_to raise_error
|
||||||
end
|
end
|
||||||
|
|
|
@ -116,7 +116,8 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
|
||||||
let(:expected_extension) { 'tar.gz' }
|
let(:expected_extension) { 'tar.gz' }
|
||||||
let(:expected_filename) { "#{expected_prefix}.#{expected_extension}" }
|
let(:expected_filename) { "#{expected_prefix}.#{expected_extension}" }
|
||||||
let(:expected_path) { File.join(storage_path, cache_key, "@v2", expected_filename) }
|
let(:expected_path) { File.join(storage_path, cache_key, "@v2", expected_filename) }
|
||||||
let(:expected_prefix) { "gitlab-git-test-#{ref}-#{TestEnv::BRANCH_SHA['master']}" }
|
let(:expected_prefix) { "gitlab-git-test-#{ref.tr('/', '-')}-#{expected_prefix_sha}" }
|
||||||
|
let(:expected_prefix_sha) { TestEnv::BRANCH_SHA['master'] }
|
||||||
|
|
||||||
subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha, path: path) }
|
subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha, path: path) }
|
||||||
|
|
||||||
|
@ -173,6 +174,73 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
|
||||||
it { expect(metadata['ArchivePath']).to eq(expected_path) }
|
it { expect(metadata['ArchivePath']).to eq(expected_path) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when references are ambiguous' do
|
||||||
|
let_it_be(:ambiguous_project) { create(:project, :repository) }
|
||||||
|
let_it_be(:repository) { ambiguous_project.repository.raw }
|
||||||
|
let_it_be(:branch_merged_commit_id) { ambiguous_project.repository.find_branch('branch-merged').dereferenced_target.id }
|
||||||
|
let_it_be(:branch_master_commit_id) { ambiguous_project.repository.find_branch('master').dereferenced_target.id }
|
||||||
|
let_it_be(:tag_1_0_0_commit_id) { ambiguous_project.repository.find_tag('v1.0.0').dereferenced_target.id }
|
||||||
|
|
||||||
|
context 'when tag is ambiguous' do
|
||||||
|
before do
|
||||||
|
ambiguous_project.repository.add_tag(user, ref, 'master', 'foo')
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
ambiguous_project.repository.rm_tag(user, ref)
|
||||||
|
end
|
||||||
|
|
||||||
|
where(:ref, :expected_commit_id, :desc) do
|
||||||
|
'refs/heads/branch-merged' | ref(:branch_master_commit_id) | 'when tag looks like a branch'
|
||||||
|
'branch-merged' | ref(:branch_master_commit_id) | 'when tag has the same name as a branch'
|
||||||
|
ref(:branch_merged_commit_id) | ref(:branch_merged_commit_id) | 'when tag looks like a commit id'
|
||||||
|
'v0.0.0' | ref(:branch_master_commit_id) | 'when tag looks like a normal tag'
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it 'selects the correct commit' do
|
||||||
|
expect(metadata['CommitId']).to eq(expected_commit_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when branch is ambiguous' do
|
||||||
|
before do
|
||||||
|
ambiguous_project.repository.add_branch(user, ref, 'master')
|
||||||
|
end
|
||||||
|
|
||||||
|
where(:ref, :expected_commit_id, :desc) do
|
||||||
|
'refs/tags/v1.0.0' | ref(:branch_master_commit_id) | 'when branch looks like a tag'
|
||||||
|
'v1.0.0' | ref(:tag_1_0_0_commit_id) | 'when branch has the same name as a tag'
|
||||||
|
ref(:branch_merged_commit_id) | ref(:branch_merged_commit_id) | 'when branch looks like a commit id'
|
||||||
|
'just-a-normal-branch' | ref(:branch_master_commit_id) | 'when branch looks like a normal branch'
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it 'selects the correct commit' do
|
||||||
|
expect(metadata['CommitId']).to eq(expected_commit_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when ref is HEAD' do
|
||||||
|
let(:ref) { 'HEAD' }
|
||||||
|
|
||||||
|
it 'selects commit id from HEAD ref' do
|
||||||
|
expect(metadata['CommitId']).to eq(branch_master_commit_id)
|
||||||
|
expect(metadata['ArchivePrefix']).to eq(expected_prefix)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when ref is not found' do
|
||||||
|
let(:ref) { 'unknown-ref-cannot-be-found' }
|
||||||
|
|
||||||
|
it 'returns empty metadata' do
|
||||||
|
expect(metadata).to eq({})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#size' do
|
describe '#size' do
|
||||||
|
|
|
@ -1140,9 +1140,9 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'HTML comment lines' do
|
context 'HTML comment lines' do
|
||||||
subject { described_class::MARKDOWN_HTML_COMMENT_LINE_REGEX }
|
subject { Gitlab::UntrustedRegexp.new(described_class::MARKDOWN_HTML_COMMENT_LINE_REGEX_UNTRUSTED, multiline: true) }
|
||||||
|
|
||||||
let(:expected) { %(<!-- an HTML comment -->) }
|
let(:expected) { [['<!-- an HTML comment -->'], ['<!-- another HTML comment -->']] }
|
||||||
let(:markdown) do
|
let(:markdown) do
|
||||||
<<~MARKDOWN
|
<<~MARKDOWN
|
||||||
Regular text
|
Regular text
|
||||||
|
@ -1150,26 +1150,28 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
|
||||||
<!-- an HTML comment -->
|
<!-- an HTML comment -->
|
||||||
|
|
||||||
more text
|
more text
|
||||||
|
|
||||||
|
<!-- another HTML comment -->
|
||||||
MARKDOWN
|
MARKDOWN
|
||||||
end
|
end
|
||||||
|
|
||||||
it { is_expected.to match(%(<!-- single line comment -->)) }
|
it { is_expected.to match(%(<!-- single line comment -->)) }
|
||||||
it { is_expected.not_to match(%(<!--\nblock comment\n-->)) }
|
it { is_expected.not_to match(%(<!--\nblock comment\n-->)) }
|
||||||
it { is_expected.not_to match(%(must start in first column <!-- comment -->)) }
|
it { is_expected.not_to match(%(must start in first column <!-- comment -->)) }
|
||||||
it { expect(subject.match(markdown)[:html_comment_line]).to eq expected }
|
it { expect(subject.scan(markdown)).to eq expected }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'HTML comment blocks' do
|
context 'HTML comment blocks' do
|
||||||
subject { described_class::MARKDOWN_HTML_COMMENT_BLOCK_REGEX }
|
subject { Gitlab::UntrustedRegexp.new(described_class::MARKDOWN_HTML_COMMENT_BLOCK_REGEX_UNTRUSTED, multiline: true) }
|
||||||
|
|
||||||
let(:expected) { %(<!-- the start of an HTML comment\n- [ ] list item commented out\n-->) }
|
let(:expected) { %(<!-- the start of an HTML comment\n- [ ] list item commented out\nmore text -->) }
|
||||||
let(:markdown) do
|
let(:markdown) do
|
||||||
<<~MARKDOWN
|
<<~MARKDOWN
|
||||||
Regular text
|
Regular text
|
||||||
|
|
||||||
<!-- the start of an HTML comment
|
<!-- the start of an HTML comment
|
||||||
- [ ] list item commented out
|
- [ ] list item commented out
|
||||||
-->
|
more text -->
|
||||||
MARKDOWN
|
MARKDOWN
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,8 @@ RSpec.describe ::Gitlab::Seeders::Ci::Runner::RunnerFleetPipelineSeeder, feature
|
||||||
context 'with job_count specified' do
|
context 'with job_count specified' do
|
||||||
let(:job_count) { 20 }
|
let(:job_count) { 20 }
|
||||||
|
|
||||||
it 'creates expected jobs', :aggregate_failures do
|
it 'creates expected jobs', :aggregate_failures,
|
||||||
|
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/394721' do
|
||||||
expect { seeder.seed }.to change { Ci::Build.count }.by(job_count)
|
expect { seeder.seed }.to change { Ci::Build.count }.by(job_count)
|
||||||
.and change { Ci::Pipeline.count }.by(4)
|
.and change { Ci::Pipeline.count }.by(4)
|
||||||
|
|
||||||
|
|
|
@ -137,6 +137,38 @@ RSpec.describe Gitlab::UntrustedRegexp do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#extract_named_group' do
|
||||||
|
let(:re) { described_class.new('(?P<name>\w+) (?P<age>\d+)|(?P<name_only>\w+)') }
|
||||||
|
let(:text) { 'Bob 40' }
|
||||||
|
|
||||||
|
it 'returns values for both named groups' do
|
||||||
|
matched = re.scan(text).first
|
||||||
|
|
||||||
|
expect(re.extract_named_group(:name, matched)).to eq 'Bob'
|
||||||
|
expect(re.extract_named_group(:age, matched)).to eq '40'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil if there was no match for group' do
|
||||||
|
matched = re.scan('Bob').first
|
||||||
|
|
||||||
|
expect(re.extract_named_group(:name, matched)).to be_nil
|
||||||
|
expect(re.extract_named_group(:age, matched)).to be_nil
|
||||||
|
expect(re.extract_named_group(:name_only, matched)).to eq 'Bob'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil if match is nil' do
|
||||||
|
matched = '(?P<age>\d+)'.scan(text).first
|
||||||
|
|
||||||
|
expect(re.extract_named_group(:age, matched)).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises if name is not a capture group' do
|
||||||
|
matched = re.scan(text).first
|
||||||
|
|
||||||
|
expect { re.extract_named_group(:foo, matched) }.to raise_error('Invalid named capture group: foo')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#match' do
|
describe '#match' do
|
||||||
context 'when there are matches' do
|
context 'when there are matches' do
|
||||||
it 'returns a match object' do
|
it 'returns a match object' do
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue