New upstream version 15.9.7+ds1

This commit is contained in:
Pirate Praveen 2023-05-08 21:46:49 +05:30
parent 081c1a8e73
commit fc1cb7f6a6
126 changed files with 2705 additions and 974 deletions

View file

@ -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"

View 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

View 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

View file

@ -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]

View 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

View file

@ -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)

View file

@ -1 +1 @@
15.9.2 15.9.7

View file

@ -1 +1 @@
15.9.2 15.9.7

View file

@ -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'

View file

@ -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"},

View file

@ -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)

View file

@ -1 +1 @@
15.9.2 15.9.7

View file

@ -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) {

View file

@ -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() {

View file

@ -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);
} }

View file

@ -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) {

View file

@ -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}`;
} }

View file

@ -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

View file

@ -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)

View file

@ -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"

View file

@ -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))

View file

@ -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

View file

@ -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

View file

@ -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')

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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?

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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}"

View file

@ -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 }

View file

@ -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 }

View file

@ -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:

View file

@ -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'

View file

@ -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-.*)/

View file

@ -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.***

View file

@ -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

View file

@ -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
## ##

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
8678040a9fa8da1d455489db89e00084943d1dced6dd01cbf3517afd1a47bac5

View file

@ -0,0 +1 @@
784f8f189eee7b5cf3136f0a859874a1d170d2b148f4c260f968b144816f1322

View file

@ -1353,7 +1353,7 @@ If you have installed GitLab using the Linux package (Omnibus) and have configur
- `15.6.0`-`15.6.3` - `15.6.0`-`15.6.3`
- `15.7.0`-`15.7.1` - `15.7.0`-`15.7.1`
This is due to [a bug introduced in the included version of cURL](https://github.com/curl/curl/issues/10122) shipped with Omnibus GitLab 15.4.6 and later. You are encouraged to upgrade to a later version where this has been [fixed](https://about.gitlab.com/releases/2023/01/09/security-release-gitlab-15-7-2-released/). This is due to [a bug introduced in the included version of cURL](https://github.com/curl/curl/issues/10122) shipped with Omnibus GitLab 15.4.6 and later. You are encouraged to upgrade to a later version where this has been [fixed](https://about.gitlab.com/releases/2023/01/09/security-release-gitlab-15-7-2-released/).
The bug causes all wildcard domains (`.example.com`) to be ignored except for the last on in the `no_proxy` environment variable list. Therefore, if for any reason you cannot upgrade to a newer version, you can work around the issue by moving your wildcard domain to the end of the list: The bug causes all wildcard domains (`.example.com`) to be ignored except for the last on in the `no_proxy` environment variable list. Therefore, if for any reason you cannot upgrade to a newer version, you can work around the issue by moving your wildcard domain to the end of the list:
@ -1363,12 +1363,13 @@ 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:
```shell ```shell
sudo gitlab-ctl reconfigure sudo gitlab-ctl reconfigure
``` ```
You can have only one wildcard domain in the `no_proxy` list. You can have only one wildcard domain in the `no_proxy` list.

View file

@ -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

View file

@ -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

View file

@ -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. |

View file

@ -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

View file

@ -376,10 +376,10 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
- GitLab Runner 15.7.0 introduced a breaking change that impacts CI/CD jobs: [Correctly handle expansion of job file variables](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/3613). - GitLab Runner 15.7.0 introduced a breaking change that impacts CI/CD jobs: [Correctly handle expansion of job file variables](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/3613).
Previously, job-defined variables that referred to Previously, job-defined variables that referred to
[file type variables](../ci/variables/index.md#use-file-type-cicd-variables) [file type variables](../ci/variables/index.md#use-file-type-cicd-variables)
were expanded to the value of the file variable (its content). This behavior did not were expanded to the value of the file variable (its content). This behavior did not
respect the typical rules of shell variable expansion. There was also the potential respect the typical rules of shell variable expansion. There was also the potential
that secrets or sensitive information could leak if the file variable and its that secrets or sensitive information could leak if the file variable and its
contents printed. For example, if they were printed in an echo output. For more information, contents printed. For example, if they were printed in an echo output. For more information,
see [Understanding the file type variable expansion change in GitLab 15.7](https://about.gitlab.com/blog/2023/02/13/impact-of-the-file-type-variable-change-15-7/). see [Understanding the file type variable expansion change in GitLab 15.7](https://about.gitlab.com/blog/2023/02/13/impact-of-the-file-type-variable-change-15-7/).
- Geo: [Container registry push events are rejected](https://gitlab.com/gitlab-org/gitlab/-/issues/386389) by the `/api/v4/container_registry_event/events` endpoint resulting in Geo secondary sites not being aware of updates to container registry images and subsequently not replicating the updates. Secondary sites may contain out of date container images after a failover as a consequence. This impacts versions 15.6.0 - 15.6.6 and 15.7.0 - 15.7.2. If you're using Geo with container repositories, you are advised to upgrade to GitLab 15.6.7, 15.7.3, or 15.8.0 which contain a fix for this issue and avoid potential data loss after a failover. - Geo: [Container registry push events are rejected](https://gitlab.com/gitlab-org/gitlab/-/issues/386389) by the `/api/v4/container_registry_event/events` endpoint resulting in Geo secondary sites not being aware of updates to container registry images and subsequently not replicating the updates. Secondary sites may contain out of date container images after a failover as a consequence. This impacts versions 15.6.0 - 15.6.6 and 15.7.0 - 15.7.2. If you're using Geo with container repositories, you are advised to upgrade to GitLab 15.6.7, 15.7.3, or 15.8.0 which contain a fix for this issue and avoid potential data loss after a failover.
- 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.
@ -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

View file

@ -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),

View file

@ -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.

View file

@ -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:

View file

@ -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:

View file

@ -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">&lt;p&gt;  &amp;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">&lt;p&gt; &amp;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">∲ ≧̸&lt;/p&gt;</span></code></pre> <span id="LC3" class="line" lang="plaintext">∲ ≧̸&lt;/p&gt;</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">&lt;p&gt;&lt;code&gt; b &lt;/code&gt;&lt;/p&gt;</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">&lt;p&gt;&lt;code&gt; b &lt;/code&gt;&lt;/p&gt;</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">&lt;p&gt;&lt;code&gt; &lt;/code&gt;</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">&lt;p&gt;&lt;code&gt; &lt;/code&gt;</span>
<span id="LC2" class="line" lang="plaintext">&lt;code&gt; &lt;/code&gt;&lt;/p&gt;</span></code></pre> <span id="LC2" class="line" lang="plaintext">&lt;code&gt; &lt;/code&gt;&lt;/p&gt;</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">&lt;p&gt;* a *&lt;/p&gt;</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">&lt;p&gt;* a *&lt;/p&gt;</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">

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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>)

View file

@ -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 ""

View file

@ -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).

View file

@ -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

View file

@ -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&lt;form&gt;&lt;input/title=&#39;&lt;script&gt;alert(document.domain)&lt;/script&gt;&#39;&gt;") }
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&lt;form&gt;&lt;input/title=&#39;&lt;script&gt;alert(document.domain)&lt;/script&gt;&#39;&gt;") }
end
end
end end
end end
end end

View file

@ -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' }

View file

@ -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

View file

@ -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

View file

@ -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) }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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!

View file

@ -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

View file

@ -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])

View file

@ -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 }

View file

@ -1,294 +1,294 @@
User-Agent: Microsoft-MacOutlook/10.22.0.200209 User-Agent: Microsoft-MacOutlook/10.22.0.200209
Date: Mon, 17 Feb 2020 22:56:47 +0100 Date: Mon, 17 Feb 2020 22:56:47 +0100
Subject: Re: htmltest | test issue (#1) Subject: Re: htmltest | test issue (#1)
From: "Louzan Martinez, Diego (ext) (SI BP R&D ZG)" From: "Louzan Martinez, Diego (ext) (SI BP R&D ZG)"
<diego.louzan.ext@siemens.com> <diego.louzan.ext@siemens.com>
To: Administrator / htmltest To: Administrator / htmltest
<dlouzan.dummy+c034670b1623e617e15a3df64223d363@gmail.com> <dlouzan.dummy+c034670b1623e617e15a3df64223d363@gmail.com>
Message-ID: <012E37D9-2A3F-4AC8-B79A-871F42914D86@siemens.com> Message-ID: <012E37D9-2A3F-4AC8-B79A-871F42914D86@siemens.com>
Thread-Topic: htmltest | test issue (#1) Thread-Topic: htmltest | test issue (#1)
References: <reply-c034670b1623e617e15a3df64223d363@169.254.169.254> References: <reply-c034670b1623e617e15a3df64223d363@169.254.169.254>
<issue_451@169.254.169.254> <issue_451@169.254.169.254>
<note_1797@169.254.169.254> <note_1797@169.254.169.254>
In-Reply-To: <note_1797@169.254.169.254> In-Reply-To: <note_1797@169.254.169.254>
Content-type: multipart/signed; Content-type: multipart/signed;
protocol="application/pkcs7-signature"; protocol="application/pkcs7-signature";
micalg=sha256; micalg=sha256;
boundary="B_3664825007_1904734766" boundary="B_3664825007_1904734766"
MIME-Version: 1.0 MIME-Version: 1.0
--B_3664825007_1904734766 --B_3664825007_1904734766
Content-type: multipart/mixed; Content-type: multipart/mixed;
boundary="B_3664825007_384940722" boundary="B_3664825007_384940722"
--B_3664825007_384940722 --B_3664825007_384940722
Content-type: multipart/alternative; Content-type: multipart/alternative;
boundary="B_3664825007_1519466360" boundary="B_3664825007_1519466360"
--B_3664825007_1519466360 --B_3664825007_1519466360
Content-type: text/plain; Content-type: text/plain;
charset="UTF-8" charset="UTF-8"
Content-transfer-encoding: quoted-printable Content-transfer-encoding: quoted-printable
Me too, with an attachment Me too, with an attachment
=20 =20
From: Administrator <dlouzan.dummy@gmail.com> From: Administrator <dlouzan.dummy@gmail.com>
Reply to: Administrator / htmltest <dlouzan.dummy+c034670b1623e617e15a3df64= Reply to: Administrator / htmltest <dlouzan.dummy+c034670b1623e617e15a3df64=
223d363@gmail.com> 223d363@gmail.com>
Date: Monday, 17 February 2020 at 22:55 Date: Monday, 17 February 2020 at 22:55
To: "Louzan Martinez, Diego (ext) (SOP IT STG XS)" <diego.louzan.ext@siemen= To: "Louzan Martinez, Diego (ext) (SOP IT STG XS)" <diego.louzan.ext@siemen=
s.com> s.com>
Subject: Re: htmltest | test issue (#1) Subject: Re: htmltest | test issue (#1)
=20 =20
Administrator commented:=20 Administrator commented:=20
I pity the foo !!! I pity the foo !!!
=E2=80=94=20 =E2=80=94=20
Reply to this email directly or view it on GitLab.=20 Reply to this email directly or view it on GitLab.=20
You're receiving this email because of your account on 169.254.169.254. If = You're receiving this email because of your account on 169.254.169.254. If =
you'd like to receive fewer emails, you can unsubscribe from this thread or = you'd like to receive fewer emails, you can unsubscribe from this thread or =
adjust your notification settings.=20 adjust your notification settings.=20
--B_3664825007_1519466360 --B_3664825007_1519466360
Content-type: text/html; Content-type: text/html;
charset="UTF-8" charset="UTF-8"
Content-transfer-encoding: quoted-printable Content-transfer-encoding: quoted-printable
<html xmlns:o=3D"urn:schemas-microsoft-com:office:office" xmlns:w=3D"urn:schema= <html xmlns:o=3D"urn:schemas-microsoft-com:office:office" xmlns:w=3D"urn:schema=
s-microsoft-com:office:word" xmlns:m=3D"http://schemas.microsoft.com/office/20= s-microsoft-com:office:word" xmlns:m=3D"http://schemas.microsoft.com/office/20=
04/12/omml" xmlns=3D"http://www.w3.org/TR/REC-html40"><head><meta http-equiv=3DC= 04/12/omml" xmlns=3D"http://www.w3.org/TR/REC-html40"><head><meta http-equiv=3DC=
ontent-Type content=3D"text/html; charset=3Dutf-8"><meta name=3DGenerator content=3D= ontent-Type content=3D"text/html; charset=3Dutf-8"><meta name=3DGenerator content=3D=
"Microsoft Word 15 (filtered medium)"><title>GitLab</title><style><!-- "Microsoft Word 15 (filtered medium)"><title>GitLab</title><style><!--
/* Font Definitions */ /* Font Definitions */
@font-face @font-face
{font-family:"Cambria Math"; {font-family:"Cambria Math";
panose-1:2 4 5 3 5 4 6 3 2 4;} panose-1:2 4 5 3 5 4 6 3 2 4;}
@font-face @font-face
{font-family:Calibri; {font-family:Calibri;
panose-1:2 15 5 2 2 2 4 3 2 4;} panose-1:2 15 5 2 2 2 4 3 2 4;}
/* Style Definitions */ /* Style Definitions */
p.MsoNormal, li.MsoNormal, div.MsoNormal p.MsoNormal, li.MsoNormal, div.MsoNormal
{margin:0cm; {margin:0cm;
margin-bottom:.0001pt; margin-bottom:.0001pt;
font-size:11.0pt; font-size:11.0pt;
font-family:"Calibri",sans-serif;} font-family:"Calibri",sans-serif;}
a:link, span.MsoHyperlink a:link, span.MsoHyperlink
{mso-style-priority:99; {mso-style-priority:99;
color:blue; color:blue;
text-decoration:underline;} text-decoration:underline;}
span.EmailStyle19 span.EmailStyle19
{mso-style-type:personal-reply; {mso-style-type:personal-reply;
font-family:"Calibri",sans-serif; font-family:"Calibri",sans-serif;
color:windowtext;} color:windowtext;}
.MsoChpDefault .MsoChpDefault
{mso-style-type:export-only; {mso-style-type:export-only;
font-size:10.0pt;} font-size:10.0pt;}
@page WordSection1 @page WordSection1
{size:612.0pt 792.0pt; {size:612.0pt 792.0pt;
margin:72.0pt 72.0pt 72.0pt 72.0pt;} margin:72.0pt 72.0pt 72.0pt 72.0pt;}
div.WordSection1 div.WordSection1
{page:WordSection1;} {page:WordSection1;}
--></style></head><body lang=3Den-ES link=3Dblue vlink=3Dpurple><div class=3DWordSe= --></style></head><body lang=3Den-ES link=3Dblue vlink=3Dpurple><div class=3DWordSe=
ction1><p class=3DMsoNormal><span lang=3DEN-US style=3D'mso-fareast-language:EN-US= ction1><p class=3DMsoNormal><span lang=3DEN-US style=3D'mso-fareast-language:EN-US=
'>Me too, with an attachment<o:p></o:p></span></p><p class=3DMsoNormal><span s= '>Me too, with an attachment<o:p></o:p></span></p><p class=3DMsoNormal><span s=
tyle=3D'mso-fareast-language:EN-US'><o:p>&nbsp;</o:p></span></p><div style=3D'bo= tyle=3D'mso-fareast-language:EN-US'><o:p>&nbsp;</o:p></span></p><div style=3D'bo=
rder:none;border-top:solid #B5C4DF 1.0pt;padding:3.0pt 0cm 0cm 0cm'><p class= rder:none;border-top:solid #B5C4DF 1.0pt;padding:3.0pt 0cm 0cm 0cm'><p class=
=3DMsoNormal><b><span style=3D'font-size:12.0pt;color:black'>From: </span></b><s= =3DMsoNormal><b><span style=3D'font-size:12.0pt;color:black'>From: </span></b><s=
pan style=3D'font-size:12.0pt;color:black'>Administrator &lt;dlouzan.dummy@gma= pan style=3D'font-size:12.0pt;color:black'>Administrator &lt;dlouzan.dummy@gma=
il.com&gt;<br><b>Reply to: </b>Administrator / htmltest &lt;dlouzan.dummy+c0= il.com&gt;<br><b>Reply to: </b>Administrator / htmltest &lt;dlouzan.dummy+c0=
34670b1623e617e15a3df64223d363@gmail.com&gt;<br><b>Date: </b>Monday, 17 Febr= 34670b1623e617e15a3df64223d363@gmail.com&gt;<br><b>Date: </b>Monday, 17 Febr=
uary 2020 at 22:55<br><b>To: </b>&quot;Louzan Martinez, Diego (ext) (SOP IT = uary 2020 at 22:55<br><b>To: </b>&quot;Louzan Martinez, Diego (ext) (SOP IT =
STG XS)&quot; &lt;diego.louzan.ext@siemens.com&gt;<br><b>Subject: </b>Re: ht= STG XS)&quot; &lt;diego.louzan.ext@siemens.com&gt;<br><b>Subject: </b>Re: ht=
mltest | test issue (#1)<o:p></o:p></span></p></div><div><p class=3DMsoNormal>= mltest | test issue (#1)<o:p></o:p></span></p></div><div><p class=3DMsoNormal>=
<o:p>&nbsp;</o:p></p></div><div><p><span style=3D'color:#777777'><a href=3D"http= <o:p>&nbsp;</o:p></p></div><div><p><span style=3D'color:#777777'><a href=3D"http=
://localhost:3000/root">Administrator</a> commented: <o:p></o:p></span></p><= ://localhost:3000/root">Administrator</a> commented: <o:p></o:p></span></p><=
div><p>I pity the foo !!!<o:p></o:p></p></div></div><div style=3D'margin-top:7= div><p>I pity the foo !!!<o:p></o:p></p></div></div><div style=3D'margin-top:7=
.5pt'><p><span style=3D'font-size:12.0pt;color:#777777'>=E2=80=94 <br>Reply to this = .5pt'><p><span style=3D'font-size:12.0pt;color:#777777'>=E2=80=94 <br>Reply to this =
email directly or <a href=3D"http://localhost:3000/root/htmltest/issues/1#note= email directly or <a href=3D"http://localhost:3000/root/htmltest/issues/1#note=
_1797">view it on GitLab</a>. <br>You're receiving this email because of you= _1797">view it on GitLab</a>. <br>You're receiving this email because of you=
r account on 169.254.169.254. If you'd like to receive fewer emails, you can= r account on 169.254.169.254. If you'd like to receive fewer emails, you can=
<a href=3D"http://localhost:3000/sent_notifications/c034670b1623e617e15a3df64= <a href=3D"http://localhost:3000/sent_notifications/c034670b1623e617e15a3df64=
223d363/unsubscribe">unsubscribe</a> from this thread or adjust your notific= 223d363/unsubscribe">unsubscribe</a> from this thread or adjust your notific=
ation settings. <o:p></o:p></span></p></div></div></body></html> ation settings. <o:p></o:p></span></p></div></div></body></html>
--B_3664825007_1519466360-- --B_3664825007_1519466360--
--B_3664825007_384940722 --B_3664825007_384940722
Content-type: image/png; name="gitlab_logo.png"; Content-type: image/png; name="gitlab_logo.png";
x-mac-creator="4F50494D"; x-mac-creator="4F50494D";
x-mac-type="504E4766" x-mac-type="504E4766"
Content-disposition: attachment; Content-disposition: attachment;
filename="gitlab_logo.png" filename="gitlab_logo.png"
Content-transfer-encoding: base64 Content-transfer-encoding: base64
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
--B_3664825007_384940722-- --B_3664825007_384940722--
--B_3664825007_1904734766 --B_3664825007_1904734766
Content-type: application/pkcs7-signature; name="smime.p7s" Content-type: application/pkcs7-signature; name="smime.p7s"
Content-transfer-encoding: base64 Content-transfer-encoding: base64
Content-disposition: attachment; Content-disposition: attachment;
filename="smime.p7s" filename="smime.p7s"
MIIRpwYJKoZIhvcNAQcCoIIRmDCCEZQCAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0B MIIRpwYJKoZIhvcNAQcCoIIRmDCCEZQCAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0B
BwGggg8VMIIHojCCBYqgAwIBAgIEZ5a6PTANBgkqhkiG9w0BAQsFADCBtjELMAkGA1UEBhMC BwGggg8VMIIHojCCBYqgAwIBAgIEZ5a6PTANBgkqhkiG9w0BAQsFADCBtjELMAkGA1UEBhMC
REUxDzANBgNVBAgMBkJheWVybjERMA8GA1UEBwwITXVlbmNoZW4xEDAOBgNVBAoMB1NpZW1l REUxDzANBgNVBAgMBkJheWVybjERMA8GA1UEBwwITXVlbmNoZW4xEDAOBgNVBAoMB1NpZW1l
bnMxETAPBgNVBAUTCFpaWlpaWkE2MR0wGwYDVQQLDBRTaWVtZW5zIFRydXN0IENlbnRlcjE/ bnMxETAPBgNVBAUTCFpaWlpaWkE2MR0wGwYDVQQLDBRTaWVtZW5zIFRydXN0IENlbnRlcjE/
MD0GA1UEAww2U2llbWVucyBJc3N1aW5nIENBIE1lZGl1bSBTdHJlbmd0aCBBdXRoZW50aWNh MD0GA1UEAww2U2llbWVucyBJc3N1aW5nIENBIE1lZGl1bSBTdHJlbmd0aCBBdXRoZW50aWNh
dGlvbiAyMDE2MB4XDTE5MTEyMTE0NDQ0N1oXDTIwMTEyMTE0NDQ0N1owdzERMA8GA1UEBRMI dGlvbiAyMDE2MB4XDTE5MTEyMTE0NDQ0N1oXDTIwMTEyMTE0NDQ0N1owdzERMA8GA1UEBRMI
WjAwM0gwOFQxDjAMBgNVBCoMBURpZWdvMRgwFgYDVQQEDA9Mb3V6YW4gTWFydGluZXoxGDAW WjAwM0gwOFQxDjAMBgNVBCoMBURpZWdvMRgwFgYDVQQEDA9Mb3V6YW4gTWFydGluZXoxGDAW
BgNVBAoMD1NpZW1lbnMtUGFydG5lcjEeMBwGA1UEAwwVTG91emFuIE1hcnRpbmV6IERpZWdv BgNVBAoMD1NpZW1lbnMtUGFydG5lcjEeMBwGA1UEAwwVTG91emFuIE1hcnRpbmV6IERpZWdv
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuInpNaC7NRYD+0pOpHDz2pk9xmPt MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuInpNaC7NRYD+0pOpHDz2pk9xmPt
JGj860SF6Nmn6Eu9EMYKEDfneC6z5QcH+mPS2d0VWgqVVGbRXSPsxJtbi9TCWjQUZdHglEZK JGj860SF6Nmn6Eu9EMYKEDfneC6z5QcH+mPS2d0VWgqVVGbRXSPsxJtbi9TCWjQUZdHglEZK
z9zxoFDh2dvW5/+TOT5Jf78FXyqak0YtY6+oMjQ/i9RUqPL7sIlyXLrBYrILzQ9Afo+7bXZg z9zxoFDh2dvW5/+TOT5Jf78FXyqak0YtY6+oMjQ/i9RUqPL7sIlyXLrBYrILzQ9Afo+7bXZg
v3ypp6xtqAV2ctHzQWFi0onJzxLVYguiVb7fFF9rBEMvSZonuw5tvOwJIhbe5FDFOrDcfbyU v3ypp6xtqAV2ctHzQWFi0onJzxLVYguiVb7fFF9rBEMvSZonuw5tvOwJIhbe5FDFOrDcfbyU
ofZ/wikIZ+A+CE5GryXuuQmGxJaC2QqOkRAWQDzLDx9nG+rKiEs5OvlfEZC7EV1PyjZ93coM ofZ/wikIZ+A+CE5GryXuuQmGxJaC2QqOkRAWQDzLDx9nG+rKiEs5OvlfEZC7EV1PyjZ93coM
faCVdlAgcFZ5fvd37CjyjKl+1QIDAQABo4IC9DCCAvAwggEEBggrBgEFBQcBAQSB9zCB9DAy faCVdlAgcFZ5fvd37CjyjKl+1QIDAQABo4IC9DCCAvAwggEEBggrBgEFBQcBAQSB9zCB9DAy
BggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9aWlpaWlpBNi5jcnQwQQYI BggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9aWlpaWlpBNi5jcnQwQQYI
KwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpaWlpBNixMPVBLST9jQUNl KwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpaWlpBNixMPVBLST9jQUNl
cnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVucy5jb20vQ049WlpaWlpa cnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVucy5jb20vQ049WlpaWlpa
QTYsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUFBzABhiRodHRwOi8vb2Nz QTYsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUFBzABhiRodHRwOi8vb2Nz
cC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wHwYDVR0jBBgwFoAU+BVdRwxsd3tyxAIXkWii cC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wHwYDVR0jBBgwFoAU+BVdRwxsd3tyxAIXkWii
tvdqCUQwDAYDVR0TAQH/BAIwADBFBgNVHSAEPjA8MDoGDSsGAQQBoWkHAgIEAQMwKTAnBggr tvdqCUQwDAYDVR0TAQH/BAIwADBFBgNVHSAEPjA8MDoGDSsGAQQBoWkHAgIEAQMwKTAnBggr
BgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kvMIHKBgNVHR8EgcIwgb8wgbyg BgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kvMIHKBgNVHR8EgcIwgb8wgbyg
gbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTYuY3JshkFsZGFwOi8v gbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTYuY3JshkFsZGFwOi8v
Y2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTYsTD1QS0k/Y2VydGlmaWNhdGVSZXZvY2F0aW9u Y2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTYsTD1QS0k/Y2VydGlmaWNhdGVSZXZvY2F0aW9u
TGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkE2LG89VHJ1c3RjZW50ZXI/ TGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkE2LG89VHJ1c3RjZW50ZXI/
Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH
AwQwDgYDVR0PAQH/BAQDAgeAMFUGA1UdEQROMEygLAYKKwYBBAGCNxQCA6AeDBxkaWVnby5s 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 MAwGA1UEKgwFRGllZ28xGDAWBgNVBAQMD0xvdXphbiBNYXJ0aW5lejEYMBYGA1UECgwPU2ll
bWVucy1QYXJ0bmVyMR4wHAYDVQQDDBVMb3V6YW4gTWFydGluZXogRGllZ28wggEiMA0GCSqG bWVucy1QYXJ0bmVyMR4wHAYDVQQDDBVMb3V6YW4gTWFydGluZXogRGllZ28wggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyby5qKzZIrGYWRqxnaAyMt/a/uc0uMk0F3MjwxvPM SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyby5qKzZIrGYWRqxnaAyMt/a/uc0uMk0F3MjwxvPM
vh5DllUpqx0l8ZDakDjPhlEXTeoL4DHNgmh+CDCs76CppM3cNG/1W1Ajo/L2iwMoXaxYuQ/F vh5DllUpqx0l8ZDakDjPhlEXTeoL4DHNgmh+CDCs76CppM3cNG/1W1Ajo/L2iwMoXaxYuQ/F
q7ED+02KEkWX2DDVVG3fhrUGP20QAq77xPDptmVWZnUnuobZBNYkC49Xfl9HJvkJL8P0+Jqb q7ED+02KEkWX2DDVVG3fhrUGP20QAq77xPDptmVWZnUnuobZBNYkC49Xfl9HJvkJL8P0+Jqb
Eae7p4roiEr7wNkGriwrVXgA3oPNF/W+OuI76JTNTajS/6PAK/GeqIvLjfuBXpdBZTY031nE Eae7p4roiEr7wNkGriwrVXgA3oPNF/W+OuI76JTNTajS/6PAK/GeqIvLjfuBXpdBZTY031nE
Cztca8vI1jUjQzVhS+0dWpvpfhkVumbvOnid8DI9lapYsX8dpZFsa3ya+T3tjUdGSOOKi0kg Cztca8vI1jUjQzVhS+0dWpvpfhkVumbvOnid8DI9lapYsX8dpZFsa3ya+T3tjUdGSOOKi0kg
lWf/XYyyfhmDAgMBAAGjggLVMIIC0TAdBgNVHQ4EFgQUprhTCDwNLfPImpSfWdq+QvPTo9Mw lWf/XYyyfhmDAgMBAAGjggLVMIIC0TAdBgNVHQ4EFgQUprhTCDwNLfPImpSfWdq+QvPTo9Mw
JwYDVR0RBCAwHoEcZGllZ28ubG91emFuLmV4dEBzaWVtZW5zLmNvbTAOBgNVHQ8BAf8EBAMC JwYDVR0RBCAwHoEcZGllZ28ubG91emFuLmV4dEBzaWVtZW5zLmNvbTAOBgNVHQ8BAf8EBAMC
BDAwLAYDVR0lBCUwIwYIKwYBBQUHAwQGCisGAQQBgjcKAwQGCysGAQQBgjcKAwQBMIHKBgNV BDAwLAYDVR0lBCUwIwYIKwYBBQUHAwQGCisGAQQBgjcKAwQGCysGAQQBgjcKAwQBMIHKBgNV
HR8EgcIwgb8wgbyggbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTMu HR8EgcIwgb8wgbyggbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTMu
Y3JshkFsZGFwOi8vY2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTMsTD1QS0k/Y2VydGlmaWNh Y3JshkFsZGFwOi8vY2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTMsTD1QS0k/Y2VydGlmaWNh
dGVSZXZvY2F0aW9uTGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkEzLG89 dGVSZXZvY2F0aW9uTGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkEzLG89
VHJ1c3RjZW50ZXI/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDBFBgNVHSAEPjA8MDoGDSsG VHJ1c3RjZW50ZXI/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDBFBgNVHSAEPjA8MDoGDSsG
AQQBoWkHAgIEAQMwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kv AQQBoWkHAgIEAQMwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kv
MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUoassbqB68NPCTeof8R4hivwMre8wggEEBggr MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUoassbqB68NPCTeof8R4hivwMre8wggEEBggr
BgEFBQcBAQSB9zCB9DAyBggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9a BgEFBQcBAQSB9zCB9DAyBggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9a
WlpaWlpBMy5jcnQwQQYIKwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpa WlpaWlpBMy5jcnQwQQYIKwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpa
WlpBMyxMPVBLST9jQUNlcnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVu WlpBMyxMPVBLST9jQUNlcnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVu
cy5jb20vQ049WlpaWlpaQTMsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUF cy5jb20vQ049WlpaWlpaQTMsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUF
BzABhiRodHRwOi8vb2NzcC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wDQYJKoZIhvcNAQEL BzABhiRodHRwOi8vb2NzcC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wDQYJKoZIhvcNAQEL
BQADggIBAF98ZMNg28LgkwdjOdvOGbC1QitsWjZTyotmQESF0nClDLUhb0O5675vVixntbrf BQADggIBAF98ZMNg28LgkwdjOdvOGbC1QitsWjZTyotmQESF0nClDLUhb0O5675vVixntbrf
eB8xy1+KRiadk40GnAIJ0YzmNl4Tav6hPYv9VBWe5olsWG7C4qB3Q/SwhvW/e+owxv1cBra8 eB8xy1+KRiadk40GnAIJ0YzmNl4Tav6hPYv9VBWe5olsWG7C4qB3Q/SwhvW/e+owxv1cBra8
R3oRudiN81eTZQHyNghRephVqQG/dpPYqydoANfIhEpHa79QlpaCAeYl4896AZOS8HYbkDFs R3oRudiN81eTZQHyNghRephVqQG/dpPYqydoANfIhEpHa79QlpaCAeYl4896AZOS8HYbkDFs
hLdv7sEHtl79YuSWI1wBjbJl70c0Sb4wLRgCPuHyQj2Uw/vQ5xJlEvBDZAIXXe1TP/nqiuY6 hLdv7sEHtl79YuSWI1wBjbJl70c0Sb4wLRgCPuHyQj2Uw/vQ5xJlEvBDZAIXXe1TP/nqiuY6
7nweJbbeqfFE6ZP3kCe+mEIWGSaO0iThZyLGer8fHs1XiEmhhPgvC7P7KodzpXU6+hX+ZzbD 7nweJbbeqfFE6ZP3kCe+mEIWGSaO0iThZyLGer8fHs1XiEmhhPgvC7P7KodzpXU6+hX+ZzbD
DxEjFfetV5sh0aNSXG9xx4hZmS9bpImBGR8MvZ7cgxqItvLtY2xvfUbYW244d4RcWesaCDq3 DxEjFfetV5sh0aNSXG9xx4hZmS9bpImBGR8MvZ7cgxqItvLtY2xvfUbYW244d4RcWesaCDq3
ZEIo6uCIzOzJAwjUdLIac+lLV0rxiHmb7O3cQ19kjpWDB31hmfrus/TKJ55pBKVWBX5m/mFv ZEIo6uCIzOzJAwjUdLIac+lLV0rxiHmb7O3cQ19kjpWDB31hmfrus/TKJ55pBKVWBX5m/mFv
K8Ep5USpGrNS0EzOP7I1kQZv2VsvAhSxk/m5FMLpDy8T0O8YgbLypTXoeJFWCF6RduSjVsaZ K8Ep5USpGrNS0EzOP7I1kQZv2VsvAhSxk/m5FMLpDy8T0O8YgbLypTXoeJFWCF6RduSjVsaZ
lkAtTQYud683pjyOMxJXaQUYGU1PmEYSOonMkVsT9aBcxYkXLp+Ln/+8G0OCYu7dRdwnj+Ut lkAtTQYud683pjyOMxJXaQUYGU1PmEYSOonMkVsT9aBcxYkXLp+Ln/+8G0OCYu7dRdwnj+Ut
7yR/ltxtgDcaFApCb0qBTKbgbqZk1fASmkOp+kbdYmoUMYICVjCCAlICAQEwgb8wgbYxCzAJ 7yR/ltxtgDcaFApCb0qBTKbgbqZk1fASmkOp+kbdYmoUMYICVjCCAlICAQEwgb8wgbYxCzAJ
BgNVBAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcMCE11ZW5jaGVuMRAwDgYDVQQK BgNVBAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcMCE11ZW5jaGVuMRAwDgYDVQQK
DAdTaWVtZW5zMREwDwYDVQQFEwhaWlpaWlpBNjEdMBsGA1UECwwUU2llbWVucyBUcnVzdCBD DAdTaWVtZW5zMREwDwYDVQQFEwhaWlpaWlpBNjEdMBsGA1UECwwUU2llbWVucyBUcnVzdCBD
ZW50ZXIxPzA9BgNVBAMMNlNpZW1lbnMgSXNzdWluZyBDQSBNZWRpdW0gU3RyZW5ndGggQXV0 ZW50ZXIxPzA9BgNVBAMMNlNpZW1lbnMgSXNzdWluZyBDQSBNZWRpdW0gU3RyZW5ndGggQXV0
aGVudGljYXRpb24gMjAxNgIEZ5a6PTANBglghkgBZQMEAgEFAKBpMC8GCSqGSIb3DQEJBDEi aGVudGljYXRpb24gMjAxNgIEZ5a6PTANBglghkgBZQMEAgEFAKBpMC8GCSqGSIb3DQEJBDEi
BCAOR58AbNfSrI+vtMs+dgAQtn3IVZ3RjYC5hz3j9k+6TTAYBgkqhkiG9w0BCQMxCwYJKoZI BCAOR58AbNfSrI+vtMs+dgAQtn3IVZ3RjYC5hz3j9k+6TTAYBgkqhkiG9w0BCQMxCwYJKoZI
hvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMDAyMTcyMTU2NDdaMA0GCSqGSIb3DQEBAQUABIIB hvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMDAyMTcyMTU2NDdaMA0GCSqGSIb3DQEBAQUABIIB
AHLSBcFHhNHPevbwqvA2ecuVb/aKnj45CFF6l8esP1H5DRm1ee5qMKuIS84NFuFC9RUENNhW AHLSBcFHhNHPevbwqvA2ecuVb/aKnj45CFF6l8esP1H5DRm1ee5qMKuIS84NFuFC9RUENNhW
DBzsB+BVGz64o1f8QgIklYVrIJ4JZ0q1abNG7NbkVKWIpS3CQo//YWShUTYg+JpKx4YbahGR DBzsB+BVGz64o1f8QgIklYVrIJ4JZ0q1abNG7NbkVKWIpS3CQo//YWShUTYg+JpKx4YbahGR
sP5zbufbU4eagrrqBChjPTLy+njdjwCNu0XPykBTKOOf6BMjnS33AYjHJyh83JOY7rw3IDLx sP5zbufbU4eagrrqBChjPTLy+njdjwCNu0XPykBTKOOf6BMjnS33AYjHJyh83JOY7rw3IDLx
8POQH4g5EMRpl9354s0rEkIezMt7pfUAsqY3QnQ8hvlE4KTikPQ+tvLMK1l/ffcLAP8BdBNI 8POQH4g5EMRpl9354s0rEkIezMt7pfUAsqY3QnQ8hvlE4KTikPQ+tvLMK1l/ffcLAP8BdBNI
YA3ikb3qCoGNSLKieYzNnBPhNOIJELUtEEaljAFZYMQzMKCbI4JdiDs= YA3ikb3qCoGNSLKieYzNnBPhNOIJELUtEEaljAFZYMQzMKCbI4JdiDs=
--B_3664825007_1904734766-- --B_3664825007_1904734766--

View file

@ -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);
});
}); });

View file

@ -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();
}); });
}); });
}); });

View file

@ -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}`,
); );
}); });
}); });

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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(

View file

@ -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' }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

Some files were not shown because too many files have changed in this diff Show more