Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ae9f43a2c4
commit
d944f09d32
|
|
@ -109,7 +109,6 @@ linters:
|
|||
# These cops should eventually get enabled
|
||||
- Cop/LineBreakAfterGuardClauses
|
||||
- Cop/ProjectPathHelper
|
||||
- Gitlab/DocUrl
|
||||
- Gitlab/FeatureAvailableUsage
|
||||
- Gitlab/Json
|
||||
- GitlabSecurity/PublicSend
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
54a1400cccb31b1869a7a9b735bad1cfb047d3bb
|
||||
492447cb10b95301aea0e6410b09b11b5ea61a6b
|
||||
|
|
|
|||
10
Gemfile
10
Gemfile
|
|
@ -162,8 +162,16 @@ gem 'fog-aliyun', '~> 0.4'
|
|||
gem 'gitlab-fog-azure-rm', '~> 1.4.0', require: 'fog/azurerm'
|
||||
|
||||
# for Google storage
|
||||
gem 'google-api-client', '~> 0.33'
|
||||
gem 'google-cloud-storage', '~> 1.44.0'
|
||||
gem 'google-apis-core', '~> 0.10.0'
|
||||
gem 'google-apis-compute_v1', '~> 0.57.0'
|
||||
gem 'google-apis-container_v1', '~> 0.43.0'
|
||||
gem 'google-apis-container_v1beta1', '~> 0.43.0'
|
||||
gem 'google-apis-cloudbilling_v1', '~> 0.21.0'
|
||||
gem 'google-apis-cloudresourcemanager_v1', '~> 0.31.0'
|
||||
gem 'google-apis-iam_v1', '~> 0.36.0'
|
||||
gem 'google-apis-serviceusage_v1', '~> 0.28.0'
|
||||
gem 'google-apis-sqladmin_v1beta4', '~> 0.41.0'
|
||||
|
||||
# for aws storage
|
||||
gem 'unf', '~> 0.1.4'
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@
|
|||
{"name":"binding_ninja","version":"0.2.3","platform":"java","checksum":"bbcf70b211d6e397493bf57c249bbec6aaf28fa7dafeb78e447b1b2f0610484f"},
|
||||
{"name":"binding_ninja","version":"0.2.3","platform":"ruby","checksum":"4a85550a0066ee4721506b4e150857486808e50c9ddfeed04bdc896bb61eca9d"},
|
||||
{"name":"bootsnap","version":"1.16.0","platform":"ruby","checksum":"f87410c00f69cd84a6e72a6c4bdba733f800d80d934f4315849d18ca9f288fed"},
|
||||
{"name":"bootstrap_form","version":"4.2.0","platform":"ruby","checksum":"f578b3c900d2cf15fab641064d357318b29e285bd5fdf090f903727912889710"},
|
||||
{"name":"browser","version":"5.3.1","platform":"ruby","checksum":"62745301701ff2c6c5d32d077bb12532b20be261929dcb52c6781ed0d5658b3c"},
|
||||
{"name":"builder","version":"3.2.4","platform":"ruby","checksum":"99caf08af60c8d7f3a6b004029c4c3c0bdaebced6c949165fe98f1db27fbbc10"},
|
||||
{"name":"bullet","version":"7.0.2","platform":"ruby","checksum":"4b7986b366f694bb05d5c1b4ea8ba949a99224d4511bf02f0c3944112f719c81"},
|
||||
|
|
@ -100,7 +99,6 @@
|
|||
{"name":"dead_end","version":"3.1.1","platform":"ruby","checksum":"1011df7f7c0149be004e11cbbc37747760227c55305cd902fd3c06e1394b2f5b"},
|
||||
{"name":"deckar01-task_list","version":"2.3.2","platform":"ruby","checksum":"5a19092548d24309d8b2c2704d64cdc08a4a615823c9a722f4142edec1de8805"},
|
||||
{"name":"declarative","version":"0.0.20","platform":"ruby","checksum":"8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9"},
|
||||
{"name":"declarative-option","version":"0.1.0","platform":"ruby","checksum":"17508349f51c5631e5ad4158c29f78a4b2de618abffa066d76c11953705f91bc"},
|
||||
{"name":"declarative_policy","version":"1.1.0","platform":"ruby","checksum":"9af4cf299ade03f2bbf63908f2ce6a117d132fc714c39a128596667fb13331cb"},
|
||||
{"name":"default_value_for","version":"3.4.0","platform":"ruby","checksum":"35d2dc51675a6bedfa875778628d44b823e0d7336da9432519477174ebb0f40f"},
|
||||
{"name":"deprecation_toolkit","version":"1.5.1","platform":"ruby","checksum":"a8a1ab1a19ae40ea12560b65010e099f3459ebde390b76621ef0c21c516a04ba"},
|
||||
|
|
@ -194,7 +192,6 @@
|
|||
{"name":"fuubar","version":"2.2.0","platform":"ruby","checksum":"9b0263c4074f39c68b37f1e4e69a7d3cfc7523c41bea43601235daa723179b4a"},
|
||||
{"name":"fuzzyurl","version":"0.9.0","platform":"ruby","checksum":"542efa80f2bcaadbdc402c2f0b572f2e335a1d53e375aecad68bbb3d86860c0f"},
|
||||
{"name":"gemoji","version":"3.0.1","platform":"ruby","checksum":"80553f2f4932a7a95fb1b3c7c63f7dd937e7c8c610164bbdea28fd06eba5f36d"},
|
||||
{"name":"gems","version":"1.2.0","platform":"ruby","checksum":"343d74bd54d906f38193f3ccd983f9d08c4b54cd01ee7e5fe8467ab41a9946f0"},
|
||||
{"name":"get_process_mem","version":"0.2.7","platform":"ruby","checksum":"4afd3c3641dd6a817c09806c7d6d509d8a9984512ac38dea8b917426bbf77eba"},
|
||||
{"name":"gettext","version":"3.3.6","platform":"ruby","checksum":"ee6bbd1b2f833ee52d7797fa68acbfecc4726aec6b6280fd7eab92aa0190b413"},
|
||||
{"name":"gettext_i18n_rails","version":"1.8.0","platform":"ruby","checksum":"95e5cf8440b1e08705b27f2bccb56143272c5a7a0dabcf54ea1bd701140a496f"},
|
||||
|
|
@ -217,16 +214,19 @@
|
|||
{"name":"gitlab_omniauth-ldap","version":"2.2.0","platform":"ruby","checksum":"bb4d20acb3b123ed654a8f6a47d3fac673ece7ed0b6992edb92dca14bad2838c"},
|
||||
{"name":"globalid","version":"1.0.0","platform":"ruby","checksum":"1253641b1dc3392721c964351773755d75135d3d3c5cc65d88b0a3880a60bed8"},
|
||||
{"name":"gon","version":"6.4.0","platform":"ruby","checksum":"e3a618d659392890f1aa7db420f17c75fd7d35aeb5f8fe003697d02c4b88d2f0"},
|
||||
{"name":"google-api-client","version":"0.53.0","platform":"ruby","checksum":"41006ef21fe02a70cff39a10aebf84fa7fb5f24c63566ab12b149ff1f1d9d7ff"},
|
||||
{"name":"google-apis-compute_v1","version":"0.53.0","platform":"ruby","checksum":"629537cf9efc1aeda0bb00d78c2a6ffa8488de833a8b19bdb150ce0a6a105f4b"},
|
||||
{"name":"google-apis-core","version":"0.9.1","platform":"ruby","checksum":"c012a364891a4602b4b1aa8468400dd3fa50b00e694edb4411af6b85aa3eb034"},
|
||||
{"name":"google-apis-discovery_v1","version":"0.12.0","platform":"ruby","checksum":"2e5accfe126884e5ebd8540b3a17a878a3a050d0dfdf0ece6b231846fc485a15"},
|
||||
{"name":"google-apis-cloudbilling_v1","version":"0.21.0","platform":"ruby","checksum":"ea2d847b4409e2ccd7f8a11a58cfcfdcbfb44ffd81c05768389f67341e291e02"},
|
||||
{"name":"google-apis-cloudresourcemanager_v1","version":"0.31.0","platform":"ruby","checksum":"f0a472a228c0b9b592741380ce79ead2458ea0066a4b5a78635818b9b62efbbf"},
|
||||
{"name":"google-apis-compute_v1","version":"0.57.0","platform":"ruby","checksum":"404514548abc3a44f5e96393d6a6d588d287548ecb6f5a886ad76e1beea78068"},
|
||||
{"name":"google-apis-container_v1","version":"0.43.0","platform":"ruby","checksum":"781d2514cb27268be9cfbae57cbc4203966afb2cf8f2c636326f5bc603862424"},
|
||||
{"name":"google-apis-container_v1beta1","version":"0.43.0","platform":"ruby","checksum":"68c48fcf88db926ceab16f56890c85890269e6366b272fcde958a9b5550313d0"},
|
||||
{"name":"google-apis-core","version":"0.10.0","platform":"ruby","checksum":"68caa26b6e51e7a89ce01e0342ba9801a93ba460b1a84b4d1d6de0024ab2d809"},
|
||||
{"name":"google-apis-dns_v1","version":"0.28.0","platform":"ruby","checksum":"f523631ea2737b67096e21eff25e426edb51ffefa9979a42f798936a950df34c"},
|
||||
{"name":"google-apis-generator","version":"0.11.0","platform":"ruby","checksum":"4656febed121b21e9071118c79ab67cbec9e40a39b6a38acc05d07fafa321279"},
|
||||
{"name":"google-apis-iam_v1","version":"0.36.0","platform":"ruby","checksum":"0db7e2876b5d0d636e8326baa6b9cf1cddd58b607151e5db1fe8fd00899a1f66"},
|
||||
{"name":"google-apis-iamcredentials_v1","version":"0.15.0","platform":"ruby","checksum":"e9a256a6d80fbfc77d44bd7e65bc94b9e1e9863a00e6d413edc0102d6cb5551b"},
|
||||
{"name":"google-apis-monitoring_v3","version":"0.37.0","platform":"ruby","checksum":"2d9262ae8dfa83ac7db895b03c7deeaae9f13107e94c8781a432202fbc20736a"},
|
||||
{"name":"google-apis-pubsub_v1","version":"0.30.0","platform":"ruby","checksum":"b8905915388041bf54f9b7e988c8cc64fe00c2132475d5c753d10479415ee13d"},
|
||||
{"name":"google-apis-sqladmin_v1beta4","version":"0.38.0","platform":"ruby","checksum":"d00279cdcc5548bf4f4e40cc29cbd942b79708011e59c75a18726b6826be1665"},
|
||||
{"name":"google-apis-serviceusage_v1","version":"0.28.0","platform":"ruby","checksum":"5f0b7e023647e7da07f6bce6ada0a6b1aafdb545a1ae985dbac921b76d11b062"},
|
||||
{"name":"google-apis-sqladmin_v1beta4","version":"0.41.0","platform":"ruby","checksum":"551553b6481879f1cd39fb83cc2a2c2ea9334afc4bf261b96900dd559f96749d"},
|
||||
{"name":"google-apis-storage_v1","version":"0.19.0","platform":"ruby","checksum":"522b6172722c7b18ad7a440d1949efbafdf984422f99f002df5f086725ab0a9f"},
|
||||
{"name":"google-cloud-core","version":"1.6.0","platform":"ruby","checksum":"ea1744cd5a3085d3072de3fab9106afc769cd198609ebb5c6eeb5f13da46b72a"},
|
||||
{"name":"google-cloud-env","version":"1.6.0","platform":"ruby","checksum":"6179acb946975892c7908748df5722a4ebadfc8cf5bb7b0d8d933ca67183fa15"},
|
||||
|
|
@ -411,7 +411,7 @@
|
|||
{"name":"optimist","version":"3.0.1","platform":"ruby","checksum":"336b753676d6117cad9301fac7e91dab4228f747d4e7179891ad3a163c64e2ed"},
|
||||
{"name":"org-ruby","version":"0.9.12","platform":"ruby","checksum":"93cbec3a4470cb9dca6a4a98dc276a6434ea9d9e7bc2d42ea33c3aedd5d1c974"},
|
||||
{"name":"orm_adapter","version":"0.5.0","platform":"ruby","checksum":"aa5d0be5d540cbb46d3a93e88061f4ece6a25f6e97d6a47122beb84fe595e9b9"},
|
||||
{"name":"os","version":"1.1.1","platform":"ruby","checksum":"3db1fbc14ab8ea99b69ed8e353c894613e1b35e665fffb90414996cf8989d489"},
|
||||
{"name":"os","version":"1.1.4","platform":"ruby","checksum":"57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f"},
|
||||
{"name":"pact","version":"1.63.0","platform":"ruby","checksum":"cc2991ed242bf182c6a4abadfd492b2923d09a9b3ed24578126cc056921cb151"},
|
||||
{"name":"pact-mock_service","version":"3.10.0","platform":"ruby","checksum":"898ec3b8d96f1934d15941c701ca7d5fef5ccff32022d9a196fb82073cd95e27"},
|
||||
{"name":"pact-support","version":"1.18.1","platform":"ruby","checksum":"4a25961c8b1c4132e433a8eaa838b1e6914c6d3aae48eee705b9860a5e8b0476"},
|
||||
|
|
@ -480,7 +480,7 @@
|
|||
{"name":"regexp_parser","version":"2.6.0","platform":"ruby","checksum":"f163ba463a45ca2f2730e0902f2475bb0eefcd536dfc2f900a86d1e5a7d7a556"},
|
||||
{"name":"regexp_property_values","version":"1.0.0","platform":"java","checksum":"5e26782b01241616855c4ee7bb8a62fce9387e484f2d3eaf04f2a0633708222e"},
|
||||
{"name":"regexp_property_values","version":"1.0.0","platform":"ruby","checksum":"162499dc0bba1e66d334273a059f207a61981cc8cc69d2ca743594e7886d080f"},
|
||||
{"name":"representable","version":"3.0.4","platform":"ruby","checksum":"07d43917dea4712ecebd19c1909e769deed863ad444d23ceb6461519e2cba962"},
|
||||
{"name":"representable","version":"3.2.0","platform":"ruby","checksum":"cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace"},
|
||||
{"name":"request_store","version":"1.5.1","platform":"ruby","checksum":"07a204d161590789f2b1d27f9f0eadcdecd6d868cb2f03240250e1bc747df78e"},
|
||||
{"name":"responders","version":"3.0.0","platform":"ruby","checksum":"a267b281582802d04cf0968dbc7d60df0d48384915934b3bf9018f811dc3f7dc"},
|
||||
{"name":"rest-client","version":"2.1.0","platform":"ruby","checksum":"35a6400bdb14fae28596618e312776c158f7ebbb0ccad752ff4fa142bf2747e3"},
|
||||
|
|
@ -610,6 +610,7 @@
|
|||
{"name":"toml-rb","version":"2.2.0","platform":"ruby","checksum":"a1e2c54ac3cc9d49861004f75f0648b3622ac03a76abe105358c31553227d9a6"},
|
||||
{"name":"tomlrb","version":"1.3.0","platform":"ruby","checksum":"68666bf53fa70ba686a48a7435ce7e086f5227c58c4c993bd9792f4760f2a503"},
|
||||
{"name":"tpm-key_attestation","version":"0.9.0","platform":"ruby","checksum":"e469ad9111a68dab4d04596e1c0621d7c877c2e3e247f765af3c04f1adf2b8cd"},
|
||||
{"name":"trailblazer-option","version":"0.1.2","platform":"ruby","checksum":"20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3"},
|
||||
{"name":"train-core","version":"3.4.9","platform":"ruby","checksum":"d7ad8fa9a379c43a30baaaf1141af1cb28349d386c054f7fc81d169a625d6edd"},
|
||||
{"name":"truncato","version":"0.7.12","platform":"ruby","checksum":"fed9e8a04fa35fd1a64506cd2089761bae4adfe47e756c3ce98a5c43856c9c4c"},
|
||||
{"name":"tty-color","version":"0.6.0","platform":"ruby","checksum":"6f9c37ca3a4e2367fb2e6d09722762647d6f455c111f05b59f35730eeb24332a"},
|
||||
|
|
|
|||
50
Gemfile.lock
50
Gemfile.lock
|
|
@ -334,7 +334,6 @@ GEM
|
|||
deckar01-task_list (2.3.2)
|
||||
html-pipeline
|
||||
declarative (0.0.20)
|
||||
declarative-option (0.1.0)
|
||||
declarative_policy (1.1.0)
|
||||
default_value_for (3.4.0)
|
||||
activerecord (>= 3.2.0, < 7.0)
|
||||
|
|
@ -550,7 +549,6 @@ GEM
|
|||
ruby-progressbar (~> 1.4)
|
||||
fuzzyurl (0.9.0)
|
||||
gemoji (3.0.1)
|
||||
gems (1.2.0)
|
||||
get_process_mem (0.2.7)
|
||||
ffi (~> 1.0)
|
||||
gettext (3.3.6)
|
||||
|
|
@ -622,12 +620,17 @@ GEM
|
|||
i18n (>= 0.7)
|
||||
multi_json
|
||||
request_store (>= 1.0)
|
||||
google-api-client (0.53.0)
|
||||
google-apis-core (~> 0.1)
|
||||
google-apis-generator (~> 0.1)
|
||||
google-apis-compute_v1 (0.53.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
google-apis-core (0.9.1)
|
||||
google-apis-cloudbilling_v1 (0.21.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-cloudresourcemanager_v1 (0.31.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-compute_v1 (0.57.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-container_v1 (0.43.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-container_v1beta1 (0.43.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-core (0.10.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
|
|
@ -636,24 +639,20 @@ GEM
|
|||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-discovery_v1 (0.12.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
google-apis-dns_v1 (0.28.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
google-apis-generator (0.11.0)
|
||||
activesupport (>= 5.0)
|
||||
gems (~> 1.2)
|
||||
google-apis-iam_v1 (0.36.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-discovery_v1 (~> 0.5)
|
||||
thor (>= 0.20, < 2.a)
|
||||
google-apis-iamcredentials_v1 (0.15.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
google-apis-monitoring_v3 (0.37.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-pubsub_v1 (0.30.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-sqladmin_v1beta4 (0.38.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
google-apis-serviceusage_v1 (0.28.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-sqladmin_v1beta4 (0.41.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-storage_v1 (0.19.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
|
|
@ -1052,7 +1051,7 @@ GEM
|
|||
org-ruby (0.9.12)
|
||||
rubypants (~> 0.2)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.1)
|
||||
os (1.1.4)
|
||||
pact (1.63.0)
|
||||
pact-mock_service (~> 3.0, >= 3.3.1)
|
||||
pact-support (~> 1.16, >= 1.16.9)
|
||||
|
|
@ -1208,9 +1207,9 @@ GEM
|
|||
redis (>= 4, < 5)
|
||||
regexp_parser (2.6.0)
|
||||
regexp_property_values (1.0.0)
|
||||
representable (3.0.4)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
declarative-option (< 0.2.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
request_store (1.5.1)
|
||||
rack (>= 1.4)
|
||||
|
|
@ -1479,6 +1478,7 @@ GEM
|
|||
tpm-key_attestation (0.9.0)
|
||||
bindata (~> 2.4)
|
||||
openssl-signature_algorithm (~> 0.4.0)
|
||||
trailblazer-option (0.1.2)
|
||||
train-core (3.4.9)
|
||||
addressable (~> 2.5)
|
||||
ffi (!= 1.13.0)
|
||||
|
|
@ -1692,7 +1692,15 @@ DEPENDENCIES
|
|||
gitlab_chronic_duration (~> 0.10.6.2)
|
||||
gitlab_omniauth-ldap (~> 2.2.0)
|
||||
gon (~> 6.4.0)
|
||||
google-api-client (~> 0.33)
|
||||
google-apis-cloudbilling_v1 (~> 0.21.0)
|
||||
google-apis-cloudresourcemanager_v1 (~> 0.31.0)
|
||||
google-apis-compute_v1 (~> 0.57.0)
|
||||
google-apis-container_v1 (~> 0.43.0)
|
||||
google-apis-container_v1beta1 (~> 0.43.0)
|
||||
google-apis-core (~> 0.10.0)
|
||||
google-apis-iam_v1 (~> 0.36.0)
|
||||
google-apis-serviceusage_v1 (~> 0.28.0)
|
||||
google-apis-sqladmin_v1beta4 (~> 0.41.0)
|
||||
google-cloud-storage (~> 1.44.0)
|
||||
google-protobuf (~> 3.21, >= 3.21.12)
|
||||
gpgme (~> 2.0.22)
|
||||
|
|
|
|||
|
|
@ -323,16 +323,25 @@ export default {
|
|||
data-testid="widget-extension-top-level"
|
||||
>
|
||||
<div
|
||||
class="gl-flex-grow-1 gl-display-flex gl-align-items-center"
|
||||
class="gl-flex-grow-1 gl-display-flex gl-align-items-center gl-flex-wrap"
|
||||
data-testid="widget-extension-top-level-summary"
|
||||
>
|
||||
<template v-if="isLoadingSummary">{{ widgetLoadingText }}</template>
|
||||
<template v-else-if="hasFetchError">{{ widgetErrorText }}</template>
|
||||
<div v-if="isLoadingSummary" class="gl-w-full gl-line-height-normal">
|
||||
{{ widgetLoadingText }}
|
||||
</div>
|
||||
<div v-else-if="hasFetchError" class="gl-w-full gl-line-height-normal">
|
||||
{{ widgetErrorText }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<span v-safe-html="hydratedSummary.subject"></span>
|
||||
<div
|
||||
v-safe-html="hydratedSummary.subject"
|
||||
class="gl-w-full gl-line-height-normal"
|
||||
></div>
|
||||
<template v-if="hydratedSummary.meta">
|
||||
<br />
|
||||
<span v-safe-html="hydratedSummary.meta" class="gl-font-sm"></span>
|
||||
<div
|
||||
v-safe-html="hydratedSummary.meta"
|
||||
class="gl-w-full gl-font-sm gl-line-height-normal"
|
||||
></div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Exportable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def readable_records(association, current_user: nil)
|
||||
association_records = try(association)
|
||||
return unless association_records.present?
|
||||
|
||||
if has_many_association?(association)
|
||||
DeclarativePolicy.user_scope do
|
||||
association_records.select { |record| readable_record?(record, current_user) }
|
||||
end
|
||||
else
|
||||
readable_record?(association_records, current_user) ? association_records : nil
|
||||
end
|
||||
end
|
||||
|
||||
def exportable_association?(association, current_user: nil)
|
||||
return false unless respond_to?(association)
|
||||
return true if has_many_association?(association)
|
||||
|
||||
readable = try(association)
|
||||
return true if readable.nil?
|
||||
|
||||
readable_record?(readable, current_user)
|
||||
end
|
||||
|
||||
def restricted_associations(keys)
|
||||
exportable_restricted_associations & keys
|
||||
end
|
||||
|
||||
def has_many_association?(association_name)
|
||||
self.class.reflect_on_association(association_name)&.macro == :has_many
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def exportable_restricted_associations
|
||||
[]
|
||||
end
|
||||
|
||||
def readable_record?(record, user)
|
||||
if record.respond_to?(:exportable_record?)
|
||||
record.exportable_record?(user)
|
||||
else
|
||||
record.readable_by?(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -25,6 +25,7 @@ class Issue < ApplicationRecord
|
|||
include FromUnion
|
||||
include EachBatch
|
||||
include PgFullTextSearchable
|
||||
include Exportable
|
||||
|
||||
extend ::Gitlab::Utils::Override
|
||||
|
||||
|
|
|
|||
|
|
@ -710,6 +710,12 @@ class Note < ApplicationRecord
|
|||
confidential? ? :read_internal_note : :read_note
|
||||
end
|
||||
|
||||
def exportable_record?(user)
|
||||
return true unless system?
|
||||
|
||||
readable_by?(user)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def system_note_viewable_by?(user)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
%p= _('Transfer group to another parent group.')
|
||||
= form_for group, url: transfer_group_path(group), method: :put, html: { id: form_id, class: 'js-group-transfer-form' } do |f|
|
||||
%ul
|
||||
- learn_more_link_start = '<a href="https://docs.gitlab.com/ee/user/project/repository/index.html#what-happens-when-a-repository-path-changes" target="_blank" rel="noopener noreferrer">'.html_safe
|
||||
- learn_more_link = help_page_url('user/project/repository/index', anchor: 'what-happens-when-a-repository-path-changes')
|
||||
- learn_more_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: learn_more_link }
|
||||
- warning_text = s_("GroupSettings|Be careful. Changing a group's parent can have unintended side effects. %{learn_more_link_start}Learn more.%{learn_more_link_end}") % { learn_more_link_start: learn_more_link_start, learn_more_link_end: '</a>'.html_safe }
|
||||
%li= warning_text.html_safe
|
||||
%li= s_('GroupSettings|You can only transfer the group to a group you manage.')
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@
|
|||
- link_style = "color: #1b69b6; text-decoration:none;"
|
||||
- pipeline_link = link_to("\##{@pipeline.iid}", pipeline_url(@pipeline), style: link_style).html_safe
|
||||
- project_link = link_to(@project.name, project_url(@project), style: link_style).html_safe
|
||||
- supported_langs_link = link_to(s_('Notify|currently supported languages'), 'https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages', style: link_style).html_safe
|
||||
- supported_langs_link = link_to(s_('Notify|currently supported languages'), help_page_url('topics/autodevops/stages', anchor: 'currently-supported-languages'), style: link_style).html_safe
|
||||
- settings_link = link_to(s_('Notify|CI/CD project settings'), project_settings_ci_cd_url(@project), style: link_style).html_safe
|
||||
= s_('Notify|The Auto DevOps pipeline failed for pipeline %{pipeline_link} and has been disabled for %{project_link}. In order to use the Auto DevOps pipeline with your project, please review the %{supported_langs_link}, adjust your project accordingly, and turn on the Auto DevOps pipeline within your %{settings_link}.').html_safe % { pipeline_link: pipeline_link, project_link: project_link, supported_langs_link: supported_langs_link, settings_link: settings_link }
|
||||
|
||||
%tr.pre-section
|
||||
%td{ style: 'text-align: center;border-bottom:1px solid #ededed' }
|
||||
%a{ href: 'https://docs.gitlab.com/ee/topics/autodevops/', style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
|
||||
%a{ href: help_page_url('topics/autodevops/index'), style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
|
||||
%button{ type: 'button', style: 'border-color: #dfdfdf; border-style: solid; border-width: 1px; border-radius: 4px; font-size: 14px; padding: 8px 16px; background-color:#fff; margin: 8px 0; cursor: pointer;' }
|
||||
= s_('Notify|Learn more about Auto DevOps')
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
%tbody
|
||||
%tr{ style: 'width:100%;' }
|
||||
%td{ style: "#{default_style}text-align:center;" }
|
||||
- password_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: 'https://docs.gitlab.com/ee/user/profile/user_passwords.html#change-your-password' }
|
||||
- password_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_url('user/profile/user_passwords', anchor: 'change-your-password') }
|
||||
= _('If you recently tried to sign in, but mistakenly entered a wrong two-factor authentication code, you may ignore this email.')
|
||||
|
||||
- if password_authentication_enabled_for_web?
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@
|
|||
= _('We detected an attempt to sign in to your %{host} account using a wrong two-factor authentication code, from the following IP address: %{ip}, at %{time}') % { host: Gitlab.config.gitlab.host, ip: @ip, time: @time }
|
||||
|
||||
= _('If you recently tried to sign in, but mistakenly entered a wrong two-factor authentication code, you may ignore this email.')
|
||||
= _('If you did not recently try to sign in, you should immediately change your password: %{password_link}.') % { password_link: 'https://docs.gitlab.com/ee/user/profile/user_passwords.html#change-your-password' }
|
||||
= _('If you did not recently try to sign in, you should immediately change your password: %{password_link}.') % { password_link: help_page_url('user/profile/user_passwords', anchor: 'change-your-password') }
|
||||
= _('Make sure you choose a strong, unique password.')
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
%tbody
|
||||
%tr{ style: 'width:100%;' }
|
||||
%td{ style: "#{default_style}text-align:center;" }
|
||||
- password_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: 'https://docs.gitlab.com/ee/user/profile/user_passwords.html#change-your-password' }
|
||||
- password_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_url('user/profile/user_passwords', anchor: 'change-your-password') }
|
||||
= _('If you recently signed in and recognize the IP address, you may disregard this email.')
|
||||
|
||||
- if password_authentication_enabled_for_web?
|
||||
|
|
@ -52,5 +52,6 @@
|
|||
|
||||
- unless @user.two_factor_enabled?
|
||||
%p
|
||||
- mfa_link_start = '<a href="https://docs.gitlab.com/ee/user/profile/account/two_factor_authentication.html" target="_blank">'.html_safe
|
||||
- mfa_url = help_page_url('user/profile/account/two_factor_authentication')
|
||||
- mfa_link_start = '<a href="%{url}" target="_blank">'.html_safe % { url: mfa_url }
|
||||
= _('To further protect your account, consider configuring a %{mfa_link_start}two-factor authentication%{mfa_link_end} method.').html_safe % { mfa_link_start: mfa_link_start, mfa_link_end: '</a>'.html_safe }
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
= _('A sign-in to your account has been made from the following IP address: %{ip}') % { ip: @ip }
|
||||
|
||||
= _('If you recently signed in and recognize the IP address, you may disregard this email.')
|
||||
= _('If you did not recently sign in, you should immediately change your password: %{password_link}.') % { password_link: 'https://docs.gitlab.com/ee/user/profile/user_passwords.html#change-your-password' }
|
||||
= _('If you did not recently sign in, you should immediately change your password: %{password_link}.') % { password_link: help_page_url('user/profile/user_passwords', anchor: 'change-your-password') }
|
||||
= _('Passwords should be unique and not used for any other sites or services.')
|
||||
|
||||
- unless @user.two_factor_enabled?
|
||||
= _('To further protect your account, consider configuring a two-factor authentication method: %{mfa_link}.') % { mfa_link: 'https://docs.gitlab.com/ee/user/profile/account/two_factor_authentication.html' }
|
||||
= _('To further protect your account, consider configuring a two-factor authentication method: %{mfa_link}.') % { mfa_link: help_page_url('user/profile/account/two_factor_authentication') }
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@
|
|||
%ul.list-unstyled.indent-list
|
||||
%li
|
||||
1.
|
||||
= link_to 'https://docs.gitlab.com/ee/user/project/integrations/mattermost_slash_commands.html#enable-custom-slash-commands', target: '_blank', rel: 'noopener noreferrer nofollow' do
|
||||
= link_to help_page_url('user/project/integrations/mattermost_slash_commands', anchor: 'enable-custom-slash-commands-in-mattermost'), target: '_blank', rel: 'noopener noreferrer nofollow' do
|
||||
Enable custom slash commands
|
||||
= sprite_icon('external-link')
|
||||
on your Mattermost installation.
|
||||
%li
|
||||
2.
|
||||
= link_to 'https://docs.gitlab.com/ee/user/project/integrations/mattermost_slash_commands.html#create-a-slash-command', target: '_blank', rel: 'noopener noreferrer nofollow' do
|
||||
= link_to help_page_url('user/project/integrations/mattermost_slash_commands', anchor: 'create-a-slash-command-in-mattermost'), target: '_blank', rel: 'noopener noreferrer nofollow' do
|
||||
Add a slash command
|
||||
= sprite_icon('external-link')
|
||||
in your Mattermost team with the options listed below.
|
||||
|
|
|
|||
|
|
@ -11,13 +11,7 @@ require 'signet/errors'
|
|||
# that may hit timeouts will mainly benefit from this.
|
||||
Google::Apis::RequestOptions.default.retries = 3 if Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_GOOGLE_API_RETRIES', true))
|
||||
|
||||
# By default, httpclient will set a send timeout of 120 seconds (https://github.com/nahi/httpclient/blob/82929c4baae14c2319c3f9aba49488c6f6def875/lib/httpclient/session.rb#L147),
|
||||
# which causes any request to be interrupted every 2 minutes (https://github.com/nahi/httpclient/blob/82929c4baae14c2319c3f9aba49488c6f6def875/lib/httpclient/session.rb#L515).
|
||||
#
|
||||
# The Google API client uses resumable uploads so that if a transfer
|
||||
# request is interrupted, it can retry where it left off. The client
|
||||
# will retry at most N + 1 times, which means transfers can only last as
|
||||
# long as this (N + 1) * send timeout. We raise this timeout to an hour
|
||||
# since otherwise transfers can only last 8 minutes (4 * 2 min) before
|
||||
# being interrupted.
|
||||
Google::Apis::ClientOptions.default.send_timeout_sec = 3600
|
||||
# The default chunk size of 100MB provides no performance benefits
|
||||
# while using more memory at peak, see discussion in
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108456#note_1259008557
|
||||
Google::Apis::RequestOptions.default.upload_chunk_size = 10.megabytes
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'google/apis/core/http_command'
|
||||
require 'google/apis/version'
|
||||
|
||||
raise 'This patch is only tested with google-api-client-ruby v0.53.0' unless Google::Apis::VERSION == "0.53.0"
|
||||
|
||||
# The google-api-ruby-client does not have a way to increase or disable
|
||||
# the maximum allowed time for a request to be retried. By default, it
|
||||
# is using the Retriable gem's 15-minute timeout, which appears to be
|
||||
# too low for uploads over 10 GB. This patches the gem with the upstream
|
||||
# changes:
|
||||
# https://github.com/googleapis/google-api-ruby-client/pull/8106
|
||||
module Google
|
||||
module Apis
|
||||
module Core
|
||||
# Command for HTTP request/response.
|
||||
class HttpCommand
|
||||
MAX_ELAPSED_TIME = 3600
|
||||
|
||||
# Execute the command, retrying as necessary
|
||||
#
|
||||
# @param [HTTPClient] client
|
||||
# HTTP client
|
||||
# @yield [result, err] Result or error if block supplied
|
||||
# @return [Object]
|
||||
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
|
||||
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
|
||||
# @raise [Google::Apis::AuthorizationError] Authorization is required
|
||||
def execute(client)
|
||||
prepare!
|
||||
opencensus_begin_span
|
||||
begin
|
||||
Retriable.retriable tries: options.retries + 1,
|
||||
max_elapsed_time: MAX_ELAPSED_TIME,
|
||||
base_interval: 1,
|
||||
multiplier: 2,
|
||||
on: RETRIABLE_ERRORS do |try|
|
||||
# This 2nd level retriable only catches auth errors, and supports 1 retry, which allows
|
||||
# auth to be re-attempted without having to retry all sorts of other failures like
|
||||
# NotFound, etc
|
||||
auth_tries = (try == 1 && authorization_refreshable? ? 2 : 1)
|
||||
Retriable.retriable tries: auth_tries,
|
||||
on: [Google::Apis::AuthorizationError, Signet::AuthorizationError, Signet::RemoteServerError, Signet::UnexpectedStatusError],
|
||||
on_retry: proc { |*| refresh_authorization } do
|
||||
execute_once(client).tap do |result|
|
||||
if block_given?
|
||||
yield result, nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
rescue => e # rubocop:disable Style/RescueStandardError
|
||||
if block_given?
|
||||
yield nil, e
|
||||
else
|
||||
raise e
|
||||
end
|
||||
end
|
||||
ensure
|
||||
opencensus_end_span
|
||||
@http_res = nil
|
||||
release!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -10,6 +10,7 @@ value_type: number
|
|||
status: active
|
||||
time_frame: all
|
||||
data_source: database
|
||||
instrumentation_class: CountCiInternalPipelinesMetric
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ value_type: number
|
|||
status: active
|
||||
time_frame: all
|
||||
data_source: database
|
||||
instrumentation_class: CountIssuesCreatedManuallyFromAlertsMetric
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ value_type: number
|
|||
status: active
|
||||
time_frame: all
|
||||
data_source: database
|
||||
instrumentation_class: CountIssuesCreatedManuallyFromAlertsMetric
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
|||
|
|
@ -157,6 +157,17 @@ You can sort projects by:
|
|||
|
||||
You can also choose to hide or show archived projects.
|
||||
|
||||
### Filter projects by language
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/385465) in GitLab 15.9 [with a flag](../../administration/feature_flags.md) named `project_language_search`. Enabled by default.
|
||||
|
||||
You can filter projects by the programming language they use. To do this:
|
||||
|
||||
1. On the top bar, select **Main menu > Projects > View all projects**.
|
||||
1. From the **Language** dropdown list, select the language you want to filter projects by.
|
||||
|
||||
A list of projects that use the selected language is displayed.
|
||||
|
||||
## Change the visibility of individual features in a project
|
||||
|
||||
You can change the visibility of individual features in a project.
|
||||
|
|
|
|||
|
|
@ -85,17 +85,30 @@ module Gitlab
|
|||
end
|
||||
|
||||
def exportable_json_record(record, options, key)
|
||||
associations = relations_schema[:include_if_exportable]&.dig(key)
|
||||
return Raw.new(record.to_json(options)) unless associations && options[:include]
|
||||
return Raw.new(record.to_json(options)) unless options[:include].any?
|
||||
|
||||
conditional_associations = relations_schema[:include_if_exportable]&.dig(key)
|
||||
|
||||
filtered_options =
|
||||
if conditional_associations.present?
|
||||
filter_conditional_include(record, options, conditional_associations)
|
||||
else
|
||||
options
|
||||
end
|
||||
|
||||
Raw.new(authorized_record_json(record, filtered_options))
|
||||
end
|
||||
|
||||
def filter_conditional_include(record, options, conditional_associations)
|
||||
filtered_options = options.deep_dup
|
||||
associations.each do |association|
|
||||
|
||||
conditional_associations.each do |association|
|
||||
filtered_options[:include].delete_if do |option|
|
||||
!exportable_json_association?(option, record, association.to_sym)
|
||||
end
|
||||
end
|
||||
|
||||
Raw.new(record.to_json(filtered_options))
|
||||
filtered_options
|
||||
end
|
||||
|
||||
def exportable_json_association?(option, record, association)
|
||||
|
|
@ -105,6 +118,34 @@ module Gitlab
|
|||
record.exportable_association?(association, current_user: current_user)
|
||||
end
|
||||
|
||||
def authorized_record_json(record, options)
|
||||
include_keys = options[:include].flat_map(&:keys)
|
||||
keys_to_authorize = record.try(:restricted_associations, include_keys)
|
||||
return record.to_json(options) if keys_to_authorize.blank?
|
||||
|
||||
record_hash = record.as_json(options).with_indifferent_access
|
||||
filtered_record_hash(record, keys_to_authorize, record_hash).to_json(options)
|
||||
end
|
||||
|
||||
def filtered_record_hash(record, keys_to_authorize, record_hash)
|
||||
keys_to_authorize.each do |key|
|
||||
next unless record_hash[key].present?
|
||||
|
||||
readable = record.try(:readable_records, key, current_user: current_user)
|
||||
if record.has_many_association?(key)
|
||||
readable_ids = readable.pluck(:id)
|
||||
|
||||
record_hash[key].keep_if do |association_record|
|
||||
readable_ids.include?(association_record[:id])
|
||||
end
|
||||
else
|
||||
record_hash[key] = nil unless readable.present?
|
||||
end
|
||||
end
|
||||
|
||||
record_hash
|
||||
end
|
||||
|
||||
def batch(relation, key)
|
||||
opts = { of: BATCH_SIZE }
|
||||
order_by = reorders(relation, key)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Usage
|
||||
module Metrics
|
||||
module Instrumentations
|
||||
class CountCiInternalPipelinesMetric < DatabaseMetric
|
||||
operation :count
|
||||
|
||||
relation do
|
||||
::Ci::Pipeline.internal
|
||||
end
|
||||
|
||||
def value
|
||||
return FALLBACK if Gitlab.com?
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Usage
|
||||
module Metrics
|
||||
module Instrumentations
|
||||
class CountIssuesCreatedManuallyFromAlertsMetric < DatabaseMetric
|
||||
operation :count
|
||||
|
||||
start { Issue.minimum(:id) }
|
||||
finish { Issue.maximum(:id) }
|
||||
|
||||
cache_start_and_finish_as :issue
|
||||
|
||||
relation do
|
||||
Issue.with_alert_management_alerts.not_authored_by(::User.alert_bot)
|
||||
end
|
||||
|
||||
def value
|
||||
return FALLBACK if Gitlab.com?
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -65,17 +65,10 @@ module Gitlab
|
|||
# rubocop: disable Metrics/AbcSize
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def system_usage_data
|
||||
issues_created_manually_from_alerts = if Gitlab.com?
|
||||
FALLBACK
|
||||
else
|
||||
count(Issue.with_alert_management_alerts.not_authored_by(::User.alert_bot), start: minimum_id(Issue), finish: maximum_id(Issue))
|
||||
end
|
||||
|
||||
{
|
||||
counts: {
|
||||
assignee_lists: count(List.assignee),
|
||||
ci_builds: count(::Ci::Build),
|
||||
ci_internal_pipelines: Gitlab.com? ? FALLBACK : count(::Ci::Pipeline.internal),
|
||||
ci_external_pipelines: count(::Ci::Pipeline.external),
|
||||
ci_pipeline_config_auto_devops: count(::Ci::Pipeline.auto_devops_source),
|
||||
ci_pipeline_config_repository: count(::Ci::Pipeline.repository_source),
|
||||
|
|
@ -116,8 +109,6 @@ module Gitlab
|
|||
issues_using_zoom_quick_actions: distinct_count(ZoomMeeting, :issue_id),
|
||||
issues_with_embedded_grafana_charts_approx: grafana_embed_usage_data,
|
||||
issues_created_from_alerts: total_alert_issues,
|
||||
issues_created_gitlab_alerts: issues_created_manually_from_alerts,
|
||||
issues_created_manually_from_alerts: issues_created_manually_from_alerts,
|
||||
incident_issues: count(::Issue.incident, start: minimum_id(Issue), finish: maximum_id(Issue)),
|
||||
alert_bot_incident_issues: count(::Issue.authored(::User.alert_bot), start: minimum_id(Issue), finish: maximum_id(Issue)),
|
||||
keys: count(Key),
|
||||
|
|
|
|||
|
|
@ -91,6 +91,14 @@ module Gitlab
|
|||
count(Users::InProductMarketingEmail.where(track: track, series: series).where.not(cta_clicked_at: nil))
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def stage_manage_events(time_period)
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
# rubocop: disable UsageData/LargeTable
|
||||
estimate_batch_distinct_count(::Event.where(time_period), :author_id)
|
||||
# rubocop: enable UsageData/LargeTable
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,8 +26,9 @@ RSpec.describe Google::Apis::Core::HttpCommand do # rubocop:disable RSpec/FilePa
|
|||
it 'retries with max elapsed_time and retries' do
|
||||
expect(Retriable).to receive(:retriable).with(
|
||||
tries: Google::Apis::RequestOptions.default.retries + 1,
|
||||
max_elapsed_time: 3600,
|
||||
max_elapsed_time: 900,
|
||||
base_interval: 1,
|
||||
max_interval: 60,
|
||||
multiplier: 2,
|
||||
on: described_class::RETRIABLE_ERRORS).and_call_original
|
||||
allow(Retriable).to receive(:retriable).and_call_original
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do
|
||||
RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer, feature_category: :importers do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:release) { create(:release) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
|
@ -213,59 +213,143 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'conditional export of included associations' do
|
||||
describe 'with inaccessible associations' do
|
||||
let_it_be(:milestone) { create(:milestone, project: exportable) }
|
||||
let_it_be(:issue) { create(:issue, assignees: [user], project: exportable, milestone: milestone) }
|
||||
let_it_be(:label1) { create(:label, project: exportable) }
|
||||
let_it_be(:label2) { create(:label, project: exportable) }
|
||||
let_it_be(:link1) { create(:label_link, label: label1, target: issue) }
|
||||
let_it_be(:link2) { create(:label_link, label: label2, target: issue) }
|
||||
|
||||
let(:options) { { include: [{ label_links: { include: [:label] } }, { milestone: { include: [] } }] } }
|
||||
|
||||
let(:include) do
|
||||
[{ issues: { include: [{ label_links: { include: [:label] } }] } }]
|
||||
[{ issues: options }]
|
||||
end
|
||||
|
||||
let(:include_if_exportable) do
|
||||
{ issues: [:label_links] }
|
||||
end
|
||||
|
||||
let_it_be(:label) { create(:label, project: exportable) }
|
||||
let_it_be(:link) { create(:label_link, label: label, target: issue) }
|
||||
|
||||
context 'when association is exportable' do
|
||||
before do
|
||||
allow_next_found_instance_of(Issue) do |issue|
|
||||
allow(issue).to receive(:exportable_association?).with(:label_links, current_user: user).and_return(true)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'record with exportable associations' do
|
||||
it 'includes exportable association' do
|
||||
expected_issue = issue.to_json(include: [{ label_links: { include: [:label] } }])
|
||||
|
||||
expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(expected_issue))
|
||||
|
||||
subject.execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'when association is not exportable' do
|
||||
before do
|
||||
allow_next_found_instance_of(Issue) do |issue|
|
||||
allow(issue).to receive(:exportable_association?).with(:label_links, current_user: user).and_return(false)
|
||||
context 'conditional export of included associations' do
|
||||
let(:include_if_exportable) do
|
||||
{ issues: [:label_links, :milestone] }
|
||||
end
|
||||
|
||||
context 'when association is exportable' do
|
||||
before do
|
||||
allow_next_found_instance_of(Issue) do |issue|
|
||||
allow(issue).to receive(:exportable_association?).with(:label_links, current_user: user).and_return(true)
|
||||
allow(issue).to receive(:exportable_association?).with(:milestone, current_user: user).and_return(true)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'record with exportable associations' do
|
||||
let(:expected_issue) { issue.to_json(options) }
|
||||
end
|
||||
end
|
||||
|
||||
it 'filters out not exportable association' do
|
||||
expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(issue.to_json))
|
||||
context 'when an association is not exportable' do
|
||||
before do
|
||||
allow_next_found_instance_of(Issue) do |issue|
|
||||
allow(issue).to receive(:exportable_association?).with(:label_links, current_user: user).and_return(true)
|
||||
allow(issue).to receive(:exportable_association?).with(:milestone, current_user: user).and_return(false)
|
||||
end
|
||||
end
|
||||
|
||||
subject.execute
|
||||
it_behaves_like 'record with exportable associations' do
|
||||
let(:expected_issue) { issue.to_json(include: [{ label_links: { include: [:label] } }]) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when association does not respond to exportable_association?' do
|
||||
before do
|
||||
allow_next_found_instance_of(Issue) do |issue|
|
||||
allow(issue).to receive(:respond_to?).and_call_original
|
||||
allow(issue).to receive(:respond_to?).with(:exportable_association?).and_return(false)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'record with exportable associations' do
|
||||
let(:expected_issue) { issue.to_json }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when association does not respond to exportable_association?' do
|
||||
before do
|
||||
allow_next_found_instance_of(Issue) do |issue|
|
||||
allow(issue).to receive(:respond_to?).with(:exportable_association?).and_return(false)
|
||||
context 'export of included restricted associations' do
|
||||
let(:many_relation) { :label_links }
|
||||
let(:single_relation) { :milestone }
|
||||
let(:issue_hash) { issue.as_json(options).with_indifferent_access }
|
||||
let(:expected_issue) { issue.to_json(options) }
|
||||
|
||||
context 'when the association is restricted' do
|
||||
context 'when some association records are exportable' do
|
||||
before do
|
||||
allow_next_found_instance_of(Issue) do |issue|
|
||||
allow(issue).to receive(:restricted_associations).with([many_relation, single_relation]).and_return([many_relation])
|
||||
allow(issue).to receive(:readable_records).with(many_relation, current_user: user).and_return([link1])
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'record with exportable associations' do
|
||||
let(:expected_issue) do
|
||||
issue_hash[many_relation].delete_at(1)
|
||||
issue_hash.to_json(options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when all association records are exportable' do
|
||||
before do
|
||||
allow_next_found_instance_of(Issue) do |issue|
|
||||
allow(issue).to receive(:restricted_associations).with([many_relation, single_relation]).and_return([many_relation])
|
||||
allow(issue).to receive(:readable_records).with(many_relation, current_user: user).and_return([link1, link2])
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'record with exportable associations'
|
||||
end
|
||||
|
||||
context 'when the single association record is exportable' do
|
||||
before do
|
||||
allow_next_found_instance_of(Issue) do |issue|
|
||||
allow(issue).to receive(:restricted_associations).with([many_relation, single_relation]).and_return([single_relation])
|
||||
allow(issue).to receive(:readable_records).with(single_relation, current_user: user).and_return(milestone)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'record with exportable associations'
|
||||
end
|
||||
|
||||
context 'when the single association record is not exportable' do
|
||||
before do
|
||||
allow_next_found_instance_of(Issue) do |issue|
|
||||
allow(issue).to receive(:restricted_associations).with([many_relation, single_relation]).and_return([single_relation])
|
||||
allow(issue).to receive(:readable_records).with(single_relation, current_user: user).and_return(nil)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'record with exportable associations' do
|
||||
let(:expected_issue) do
|
||||
issue_hash[single_relation] = nil
|
||||
issue_hash.to_json(options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'filters out not exportable association' do
|
||||
expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(issue.to_json))
|
||||
context 'when the associations are not restricted' do
|
||||
before do
|
||||
allow_next_found_instance_of(Issue) do |issue|
|
||||
allow(issue).to receive(:restricted_associations).with([many_relation, single_relation]).and_return([])
|
||||
end
|
||||
end
|
||||
|
||||
subject.execute
|
||||
it_behaves_like 'record with exportable associations'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountCiInternalPipelinesMetric,
|
||||
feature_category: :service_ping do
|
||||
let_it_be(:ci_pipeline_1) { create(:ci_pipeline, source: :external) }
|
||||
let_it_be(:ci_pipeline_2) { create(:ci_pipeline, source: :push) }
|
||||
|
||||
let(:expected_value) { 1 }
|
||||
let(:expected_query) do
|
||||
'SELECT COUNT("ci_pipelines"."id") FROM "ci_pipelines" ' \
|
||||
'WHERE ("ci_pipelines"."source" IN (1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15) ' \
|
||||
'OR "ci_pipelines"."source" IS NULL)'
|
||||
end
|
||||
|
||||
it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
|
||||
|
||||
context 'on Gitlab.com' do
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(true)
|
||||
end
|
||||
|
||||
let(:expected_value) { -1 }
|
||||
|
||||
it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountIssuesCreatedManuallyFromAlertsMetric,
|
||||
feature_category: :service_ping do
|
||||
let_it_be(:issue) { create(:issue) }
|
||||
let_it_be(:issue_with_alert) { create(:issue, :with_alert) }
|
||||
|
||||
let(:expected_value) { 1 }
|
||||
let(:expected_query) do
|
||||
'SELECT COUNT("issues"."id") FROM "issues" ' \
|
||||
'INNER JOIN "alert_management_alerts" ON "alert_management_alerts"."issue_id" = "issues"."id" ' \
|
||||
'WHERE "issues"."author_id" != 99'
|
||||
end
|
||||
|
||||
it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
|
||||
|
||||
context 'on Gitlab.com' do
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(true)
|
||||
end
|
||||
|
||||
let(:expected_value) { -1 }
|
||||
|
||||
it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
|
||||
end
|
||||
end
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do
|
||||
RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator, feature_category: :service_ping do
|
||||
include UsageDataHelpers
|
||||
|
||||
before do
|
||||
|
|
@ -43,9 +43,9 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do
|
|||
context 'joined relations' do
|
||||
context 'counted attribute comes from source relation' do
|
||||
it_behaves_like 'name suggestion' do
|
||||
# corresponding metric is collected with count(Issue.with_alert_management_alerts.not_authored_by(::User.alert_bot), start: issue_minimum_id, finish: issue_maximum_id)
|
||||
let(:key_path) { 'counts.issues_created_manually_from_alerts' }
|
||||
let(:name_suggestion) { /count_<adjective describing: '\(issues\.author_id != \d+\)'>_issues_<with>_alert_management_alerts/ }
|
||||
# corresponding metric is collected with distinct_count(Release.with_milestones, :author_id)
|
||||
let(:key_path) { 'usage_activity_by_stage.release.releases_with_milestones' }
|
||||
let(:name_suggestion) { /count_distinct_author_id_from_releases_<with>_milestone_releases/ }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -171,14 +171,25 @@ RSpec.describe Gitlab::Usage::ServicePingReport, :use_clean_rails_memory_store_c
|
|||
let(:metric_definitions) { ::Gitlab::Usage::MetricDefinition.definitions }
|
||||
|
||||
it 'generates queries that match collected data', :aggregate_failures do
|
||||
message = "Expected %{query} result to match %{value} for %{key_path} metric"
|
||||
message = "Expected %{query} result to match %{value} for %{key_path} metric (got %{payload_value} instead)"
|
||||
|
||||
metrics_queries_with_values.each do |key_path, query, value|
|
||||
value = type_cast_to_defined_type(value, metric_definitions[key_path.join('.')])
|
||||
metric_definition = metric_definitions[key_path.join('.')]
|
||||
|
||||
# Skip broken metrics since they are usually overriden to return -1
|
||||
next if metric_definition&.attributes&.fetch(:status) == 'broken'
|
||||
|
||||
value = type_cast_to_defined_type(value, metric_definition)
|
||||
payload_value = service_ping_payload.dig(*key_path)
|
||||
|
||||
expect(value).to(
|
||||
eq(service_ping_payload.dig(*key_path)),
|
||||
message % { query: query, value: (value || 'NULL'), key_path: key_path.join('.') }
|
||||
eq(payload_value),
|
||||
message % {
|
||||
query: query,
|
||||
value: (value || 'NULL'),
|
||||
payload_value: payload_value,
|
||||
key_path: key_path.join('.')
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -557,9 +557,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
|
|||
expect(count_data[:issues_using_zoom_quick_actions]).to eq(3)
|
||||
expect(count_data[:issues_with_embedded_grafana_charts_approx]).to eq(2)
|
||||
expect(count_data[:incident_issues]).to eq(4)
|
||||
expect(count_data[:issues_created_gitlab_alerts]).to eq(1)
|
||||
expect(count_data[:issues_created_from_alerts]).to eq(3)
|
||||
expect(count_data[:issues_created_manually_from_alerts]).to eq(1)
|
||||
expect(count_data[:alert_bot_incident_issues]).to eq(4)
|
||||
expect(count_data[:clusters_enabled]).to eq(6)
|
||||
expect(count_data[:project_clusters_enabled]).to eq(4)
|
||||
|
|
@ -1137,20 +1135,4 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
|
|||
expect(result).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'on Gitlab.com' do
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(true)
|
||||
end
|
||||
|
||||
describe '.system_usage_data' do
|
||||
subject { described_class.system_usage_data }
|
||||
|
||||
it 'returns fallback value for disabled metrics' do
|
||||
expect(subject[:counts][:ci_internal_pipelines]).to eq(Gitlab::Utils::UsageData::FALLBACK)
|
||||
expect(subject[:counts][:issues_created_gitlab_alerts]).to eq(Gitlab::Utils::UsageData::FALLBACK)
|
||||
expect(subject[:counts][:issues_created_manually_from_alerts]).to eq(Gitlab::Utils::UsageData::FALLBACK)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -438,7 +438,7 @@ RSpec.describe Emails::Profile do
|
|||
end
|
||||
|
||||
it 'includes a link to the change password documentation' do
|
||||
is_expected.to have_body_text 'https://docs.gitlab.com/ee/user/profile/user_passwords.html#change-your-password'
|
||||
is_expected.to have_body_text help_page_url('user/profile/user_passwords', anchor: 'change-your-password')
|
||||
end
|
||||
|
||||
it 'mentions two factor authentication when two factor is not enabled' do
|
||||
|
|
@ -446,7 +446,7 @@ RSpec.describe Emails::Profile do
|
|||
end
|
||||
|
||||
it 'includes a link to two-factor authentication documentation' do
|
||||
is_expected.to have_body_text 'https://docs.gitlab.com/ee/user/profile/account/two_factor_authentication.html'
|
||||
is_expected.to have_body_text help_page_url('user/profile/account/two_factor_authentication')
|
||||
end
|
||||
|
||||
context 'when two factor authentication is enabled' do
|
||||
|
|
@ -488,7 +488,7 @@ RSpec.describe Emails::Profile do
|
|||
end
|
||||
|
||||
it 'includes a link to the change password documentation' do
|
||||
is_expected.to have_body_text 'https://docs.gitlab.com/ee/user/profile/user_passwords.html#change-your-password'
|
||||
is_expected.to have_body_text help_page_url('user/profile/user_passwords', anchor: 'change-your-password')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,236 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Exportable, feature_category: :importers do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:milestone) { create(:milestone, project: project) }
|
||||
let_it_be(:issue) { create(:issue, project: project, milestone: milestone) }
|
||||
let_it_be(:note1) { create(:system_note, project: project, noteable: issue) }
|
||||
let_it_be(:note2) { create(:system_note, project: project, noteable: issue) }
|
||||
|
||||
let_it_be(:model_klass) do
|
||||
Class.new(ApplicationRecord) do
|
||||
include Exportable
|
||||
|
||||
belongs_to :project
|
||||
has_one :milestone
|
||||
has_many :notes
|
||||
|
||||
self.table_name = 'issues'
|
||||
|
||||
def self.name
|
||||
'Issue'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
subject { model_klass.new }
|
||||
|
||||
describe '.readable_records' do
|
||||
let_it_be(:model_record) { model_klass.new }
|
||||
|
||||
context 'when model does not respond to association name' do
|
||||
it 'returns nil' do
|
||||
expect(subject.readable_records(:foo, current_user: user)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when model does respond to association name' do
|
||||
context 'when there are no records' do
|
||||
it 'returns nil' do
|
||||
expect(model_record.readable_records(:notes, current_user: user)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when association has #exportable_record? defined' do
|
||||
before do
|
||||
allow(model_record).to receive(:try).with(:notes).and_return(issue.notes)
|
||||
end
|
||||
|
||||
context 'when user can read all records' do
|
||||
before do
|
||||
allow_next_found_instance_of(Note) do |note|
|
||||
allow(note).to receive(:respond_to?).with(:exportable_record?).and_return(true)
|
||||
allow(note).to receive(:exportable_record?).with(user).and_return(true)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns collection of readable records' do
|
||||
expect(model_record.readable_records(:notes, current_user: user)).to contain_exactly(note1, note2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user can not read records' do
|
||||
before do
|
||||
allow_next_instance_of(Note) do |note|
|
||||
allow(note).to receive(:respond_to?).with(:exportable_record?).and_return(true)
|
||||
allow(note).to receive(:exportable_record?).with(user).and_return(false)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns collection of readable records' do
|
||||
expect(model_record.readable_records(:notes, current_user: user)).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when association does not have #exportable_record? defined' do
|
||||
before do
|
||||
allow(model_record).to receive(:try).with(:notes).and_return([note1])
|
||||
|
||||
allow(note1).to receive(:respond_to?).and_call_original
|
||||
allow(note1).to receive(:respond_to?).with(:exportable_record?).and_return(false)
|
||||
end
|
||||
|
||||
it 'calls #readable_by?' do
|
||||
expect(note1).to receive(:readable_by?).with(user)
|
||||
|
||||
model_record.readable_records(:notes, current_user: user)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with single relation' do
|
||||
before do
|
||||
allow(model_record).to receive(:try).with(:milestone).and_return(issue.milestone)
|
||||
end
|
||||
|
||||
context 'when user can read the record' do
|
||||
before do
|
||||
allow(milestone).to receive(:readable_by?).with(user).and_return(true)
|
||||
end
|
||||
|
||||
it 'returns collection of readable records' do
|
||||
expect(model_record.readable_records(:milestone, current_user: user)).to eq(milestone)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user can not read the record' do
|
||||
before do
|
||||
allow(milestone).to receive(:readable_by?).with(user).and_return(false)
|
||||
end
|
||||
|
||||
it 'returns collection of readable records' do
|
||||
expect(model_record.readable_records(:milestone, current_user: user)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.exportable_association?' do
|
||||
context 'when model does not respond to association name' do
|
||||
it 'returns false' do
|
||||
expect(subject.exportable_association?(:tests)).to eq(false)
|
||||
|
||||
allow(issue).to receive(:respond_to?).with(:tests).and_return(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when model responds to association name' do
|
||||
let_it_be(:model_record) { model_klass.new }
|
||||
|
||||
context 'when association contains records' do
|
||||
before do
|
||||
allow(model_record).to receive(:try).with(:milestone).and_return(milestone)
|
||||
end
|
||||
|
||||
context 'when current_user is not present' do
|
||||
it 'returns false' do
|
||||
expect(model_record.exportable_association?(:milestone)).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when current_user can read association' do
|
||||
before do
|
||||
allow(milestone).to receive(:readable_by?).with(user).and_return(true)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(model_record.exportable_association?(:milestone, current_user: user)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when current_user can not read association' do
|
||||
before do
|
||||
allow(milestone).to receive(:readable_by?).with(user).and_return(false)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(model_record.exportable_association?(:milestone, current_user: user)).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when association is empty' do
|
||||
before do
|
||||
allow(model_record).to receive(:try).with(:milestone).and_return(nil)
|
||||
allow(milestone).to receive(:readable_by?).with(user).and_return(true)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(model_record.exportable_association?(:milestone, current_user: user)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when association type is has_many' do
|
||||
it 'returns true' do
|
||||
expect(subject.exportable_association?(:notes)).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.restricted_associations' do
|
||||
let(:model_associations) { [:notes, :labels] }
|
||||
|
||||
context 'when `exportable_restricted_associations` is not defined in inheriting class' do
|
||||
it 'returns empty array' do
|
||||
expect(subject.restricted_associations(model_associations)).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when `exportable_restricted_associations` is defined in inheriting class' do
|
||||
before do
|
||||
stub_const('DummyModel', model_klass)
|
||||
|
||||
DummyModel.class_eval do
|
||||
def exportable_restricted_associations
|
||||
super + [:notes]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns empty array if provided key are not restricted' do
|
||||
expect(subject.restricted_associations([:labels])).to eq([])
|
||||
end
|
||||
|
||||
it 'returns array with restricted keys' do
|
||||
expect(subject.restricted_associations(model_associations)).to contain_exactly(:notes)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.has_many_association?' do
|
||||
let(:model_associations) { [:notes, :labels] }
|
||||
|
||||
context 'when association type is `has_many`' do
|
||||
it 'returns true' do
|
||||
expect(subject.has_many_association?(:notes)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when association type is `has_one`' do
|
||||
it 'returns true' do
|
||||
expect(subject.has_many_association?(:milestone)).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when association type is `belongs_to`' do
|
||||
it 'returns true' do
|
||||
expect(subject.has_many_association?(:project)).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1878,4 +1878,34 @@ RSpec.describe Note do
|
|||
it { is_expected.to eq :read_internal_note }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#exportable_record?' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project, :private) }
|
||||
let_it_be(:noteable) { create(:issue, project: project) }
|
||||
|
||||
subject { note.exportable_record?(user) }
|
||||
|
||||
context 'when not a system note' do
|
||||
let(:note) { build(:note, noteable: noteable) }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'with system note' do
|
||||
let(:note) { build(:system_note, project: project, noteable: noteable) }
|
||||
|
||||
it 'returns `false` when the user cannot read the note' do
|
||||
is_expected.to be_falsey
|
||||
end
|
||||
|
||||
context 'when user can read the note' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ module UsageDataHelpers
|
|||
COUNTS_KEYS = %i(
|
||||
assignee_lists
|
||||
ci_builds
|
||||
ci_internal_pipelines
|
||||
ci_external_pipelines
|
||||
ci_pipeline_config_auto_devops
|
||||
ci_pipeline_config_repository
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ RSpec.shared_examples "chat integration" do |integration_name|
|
|||
|
||||
describe "#execute" do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be_with_reload(:project) { create(:project, :repository) }
|
||||
let_it_be_with_refind(:project) { create(:project, :repository) }
|
||||
|
||||
let(:webhook_url) { "https://example.gitlab.com/" }
|
||||
let(:webhook_url_regex) { /\A#{webhook_url}.*/ }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'resource with exportable associations' do
|
||||
before do
|
||||
stub_licensed_features(stubbed_features) if stubbed_features.any?
|
||||
end
|
||||
|
||||
describe '#exportable_association?' do
|
||||
let(:association) { single_association }
|
||||
|
||||
subject { resource.exportable_association?(association, current_user: user) }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
|
||||
context 'when user can read resource' do
|
||||
before do
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
|
||||
context "when user can read resource's association" do
|
||||
before do
|
||||
other_group.add_developer(user)
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
||||
context 'for an unknown association' do
|
||||
let(:association) { :foo }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'for an unauthenticated user' do
|
||||
let(:user) { nil }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#readable_records' do
|
||||
subject { resource.readable_records(association, current_user: user) }
|
||||
|
||||
before do
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
||||
context 'when association not supported' do
|
||||
let(:association) { :foo }
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'when association is `:notes`' do
|
||||
let(:association) { :notes }
|
||||
|
||||
it { is_expected.to match_array([readable_note]) }
|
||||
|
||||
context 'when user have access' do
|
||||
before do
|
||||
other_group.add_developer(user)
|
||||
end
|
||||
|
||||
it 'returns all records' do
|
||||
is_expected.to match_array([readable_note, restricted_note])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -2,52 +2,6 @@
|
|||
|
||||
require 'knapsack'
|
||||
|
||||
module KnapsackRefinements
|
||||
# Refine https://github.com/KnapsackPro/knapsack/blob/v1.21.1/lib/knapsack/distributors/base_distributor.rb
|
||||
# to take in account the additional filtering we do for predictive jobs.
|
||||
refine ::Knapsack::Distributors::BaseDistributor do
|
||||
attr_reader :filter_tests
|
||||
|
||||
def initialize(args = {})
|
||||
super
|
||||
|
||||
@filter_tests = args[:filter_tests]
|
||||
end
|
||||
|
||||
def all_tests
|
||||
@all_tests ||= begin
|
||||
pattern_tests = Dir.glob(test_file_pattern).uniq
|
||||
|
||||
if filter_tests.empty?
|
||||
Knapsack.logger.info 'Running all node tests without filter'
|
||||
pattern_tests
|
||||
else
|
||||
pattern_tests & filter_tests
|
||||
end
|
||||
end.sort
|
||||
end
|
||||
end
|
||||
|
||||
# Refine https://github.com/KnapsackPro/knapsack/blob/v1.21.1/lib/knapsack/allocator_builder.rb
|
||||
# to take in account the additional filtering we do for predictive jobs.
|
||||
refine ::Knapsack::AllocatorBuilder do
|
||||
attr_accessor :filter_tests
|
||||
|
||||
def allocator
|
||||
Knapsack::Allocator.new({
|
||||
report: Knapsack.report.open,
|
||||
test_file_pattern: test_file_pattern,
|
||||
ci_node_total: Knapsack::Config::Env.ci_node_total,
|
||||
ci_node_index: Knapsack::Config::Env.ci_node_index,
|
||||
# Additional argument
|
||||
filter_tests: filter_tests
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
using KnapsackRefinements
|
||||
|
||||
# A custom parallel rspec runner based on Knapsack runner
|
||||
# which takes in additional option for a file containing
|
||||
# list of test files.
|
||||
|
|
@ -59,7 +13,7 @@ using KnapsackRefinements
|
|||
# would be executed in the CI node.
|
||||
#
|
||||
# Reference:
|
||||
# https://github.com/ArturT/knapsack/blob/v1.21.1/lib/knapsack/runners/rspec_runner.rb
|
||||
# https://github.com/ArturT/knapsack/blob/v1.20.0/lib/knapsack/runners/rspec_runner.rb
|
||||
module Tooling
|
||||
class ParallelRSpecRunner
|
||||
def self.run(rspec_args: nil, filter_tests_file: nil)
|
||||
|
|
@ -73,15 +27,18 @@ module Tooling
|
|||
end
|
||||
|
||||
def run
|
||||
Knapsack.logger.info
|
||||
Knapsack.logger.info 'Knapsack node specs:'
|
||||
Knapsack.logger.info node_tests
|
||||
Knapsack.logger.info
|
||||
Knapsack.logger.info 'Filter specs:'
|
||||
Knapsack.logger.info filter_tests
|
||||
Knapsack.logger.info
|
||||
Knapsack.logger.info 'Running specs:'
|
||||
Knapsack.logger.info node_tests
|
||||
Knapsack.logger.info tests_to_run
|
||||
Knapsack.logger.info
|
||||
|
||||
if node_tests.empty?
|
||||
if tests_to_run.empty?
|
||||
Knapsack.logger.info 'No tests to run on this node, exiting.'
|
||||
return
|
||||
end
|
||||
|
|
@ -100,10 +57,19 @@ module Tooling
|
|||
cmd.push(*rspec_args)
|
||||
cmd.push('--default-path', allocator.test_dir)
|
||||
cmd.push('--')
|
||||
cmd.push(*node_tests)
|
||||
cmd.push(*tests_to_run)
|
||||
end
|
||||
end
|
||||
|
||||
def tests_to_run
|
||||
if filter_tests.empty?
|
||||
Knapsack.logger.info 'Running all node tests without filter'
|
||||
return node_tests
|
||||
end
|
||||
|
||||
@tests_to_run ||= node_tests & filter_tests
|
||||
end
|
||||
|
||||
def node_tests
|
||||
allocator.node_tests
|
||||
end
|
||||
|
|
@ -120,9 +86,7 @@ module Tooling
|
|||
end
|
||||
|
||||
def knapsack_allocator
|
||||
Knapsack::AllocatorBuilder.new(Knapsack::Adapters::RSpecAdapter).tap do |builder|
|
||||
builder.filter_tests = filter_tests
|
||||
end.allocator
|
||||
Knapsack::AllocatorBuilder.new(Knapsack::Adapters::RSpecAdapter).allocator
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue