Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-07-27 18:10:15 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-07-27 18:10:15 +0300
commit9979d2afd66e12d938ea55372dcf2d8105b5eaca (patch)
tree88df8f6a2f2ffba2fa1318dee83f58d6fe76d40d
parentfdf32113c3924f7faec91101282fc28ec42fc869 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/rspec/missing_feature_category.yml3
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum6
-rw-r--r--Gemfile.lock16
-rw-r--r--app/assets/javascripts/access_tokens/components/access_token_table_app.vue116
-rw-r--r--app/assets/javascripts/access_tokens/components/new_access_token_app.vue25
-rw-r--r--app/assets/javascripts/access_tokens/components/token.vue6
-rw-r--r--app/assets/javascripts/access_tokens/components/tokens_app.vue1
-rw-r--r--app/assets/javascripts/access_tokens/index.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue57
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue6
-rw-r--r--app/assets/stylesheets/framework/new_card.scss2
-rw-r--r--app/assets/stylesheets/pages/settings.scss17
-rw-r--r--app/controllers/repositories/lfs_storage_controller.rb5
-rw-r--r--app/graphql/types/custom_emoji_type.rb8
-rw-r--r--app/graphql/types/permission_types/group.rb2
-rw-r--r--app/models/commit.rb3
-rw-r--r--app/models/commit_range.rb5
-rw-r--r--app/presenters/blob_presenter.rb21
-rw-r--r--app/views/admin/impersonation_tokens/index.html.haml23
-rw-r--r--app/views/groups/settings/access_tokens/index.html.haml41
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml27
-rw-r--r--app/views/projects/settings/access_tokens/index.html.haml30
-rw-r--r--app/views/shared/access_tokens/_form.html.haml4
-rw-r--r--config/routes/project.rb2
-rw-r--r--config/routes/repository.rb2
-rw-r--r--db/docs/batched_background_migrations/backfill_default_branch_protection_namespace_setting.yml6
-rw-r--r--db/migrate/20230707031923_add_emails_to_x509_certificates.rb7
-rw-r--r--db/post_migrate/20230724071541_queue_backfill_default_branch_protection_namespace_setting.rb26
-rw-r--r--db/post_migrate/20230724085146_replace_old_fk_p_ci_builds_metadata_to_builds_v3.rb47
-rw-r--r--db/post_migrate/20230724085149_replace_old_fk_p_ci_runner_machine_builds_to_builds_v3.rb47
-rw-r--r--db/schema_migrations/202307070319231
-rw-r--r--db/schema_migrations/202307240715411
-rw-r--r--db/schema_migrations/202307240851461
-rw-r--r--db/schema_migrations/202307240851491
-rw-r--r--db/structure.sql3
-rw-r--r--doc/administration/audit_event_streaming/index.md22
-rw-r--r--doc/api/graphql/reference/index.md14
-rw-r--r--doc/ci/interactive_web_terminal/index.md2
-rw-r--r--doc/install/openshift_and_gitlab/index.md19
-rw-r--r--doc/integration/jira/connect-app.md5
-rw-r--r--doc/tutorials/infrastructure.md2
-rw-r--r--doc/user/ai_features.md19
-rw-r--r--doc/user/project/integrations/webhook_events.md6
-rw-r--r--lib/gitlab/background_migration/backfill_default_branch_protection_namespace_setting.rb131
-rw-r--r--lib/gitlab/checks/branch_check.rb2
-rw-r--r--lib/gitlab/content_security_policy/config_loader.rb211
-rw-r--r--lib/gitlab/etag_caching/router/graphql.rb2
-rw-r--r--lib/gitlab/git.rb2
-rw-r--r--lib/gitlab/git/commit.rb13
-rw-r--r--lib/gitlab/hook_data/merge_request_builder.rb2
-rw-r--r--lib/gitlab/x509/signature.rb26
-rw-r--r--locale/gitlab.pot26
-rw-r--r--spec/features/admin/admin_users_impersonation_tokens_spec.rb1
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb5
-rw-r--r--spec/features/projects/settings/access_tokens_spec.rb3
-rw-r--r--spec/features/projects/settings/user_searches_in_settings_spec.rb2
-rw-r--r--spec/frontend/access_tokens/components/access_token_table_app_spec.js18
-rw-r--r--spec/frontend/access_tokens/components/new_access_token_app_spec.js11
-rw-r--r--spec/frontend/access_tokens/index_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap20
-rw-r--r--spec/graphql/types/custom_emoji_type_spec.rb13
-rw-r--r--spec/lib/api/validations/validators/git_sha_spec.rb3
-rw-r--r--spec/lib/gitlab/background_migration/backfill_default_branch_protection_namespace_setting_spec.rb65
-rw-r--r--spec/lib/gitlab/checks/branch_check_spec.rb12
-rw-r--r--spec/lib/gitlab/content_security_policy/config_loader_spec.rb245
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb94
-rw-r--r--spec/lib/gitlab/hook_data/merge_request_builder_spec.rb2
-rw-r--r--spec/lib/gitlab/x509/signature_spec.rb44
-rw-r--r--spec/migrations/20230724071541_queue_backfill_default_branch_protection_namespace_setting_spec.rb26
-rw-r--r--spec/models/commit_spec.rb3
-rw-r--r--spec/models/project_team_spec.rb4
-rw-r--r--spec/presenters/blob_presenter_spec.rb81
-rw-r--r--spec/routing/project_routing_spec.rb7
-rw-r--r--spec/support/helpers/filter_spec_helper.rb4
-rw-r--r--spec/support/shared_examples/features/access_tokens_shared_examples.rb2
78 files changed, 1370 insertions, 391 deletions
diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml
index edd083de8d9..04c7e3c4403 100644
--- a/.rubocop_todo/rspec/missing_feature_category.yml
+++ b/.rubocop_todo/rspec/missing_feature_category.yml
@@ -1258,7 +1258,6 @@ RSpec/MissingFeatureCategory:
- 'ee/spec/presenters/ci/minutes/usage_presenter_spec.rb'
- 'ee/spec/presenters/ci/pipeline_presenter_spec.rb'
- 'ee/spec/presenters/dast/site_profile_presenter_spec.rb'
- - 'ee/spec/presenters/ee/blob_presenter_spec.rb'
- 'ee/spec/presenters/ee/clusters/cluster_presenter_spec.rb'
- 'ee/spec/presenters/ee/instance_clusterable_presenter_spec.rb'
- 'ee/spec/presenters/ee/issue_presenter_spec.rb'
@@ -3335,7 +3334,6 @@ RSpec/MissingFeatureCategory:
- 'spec/lib/gitlab/conflict/file_spec.rb'
- 'spec/lib/gitlab/consul/internal_spec.rb'
- 'spec/lib/gitlab/container_repository/tags/cache_spec.rb'
- - 'spec/lib/gitlab/content_security_policy/config_loader_spec.rb'
- 'spec/lib/gitlab/counters/buffered_counter_spec.rb'
- 'spec/lib/gitlab/counters/legacy_counter_spec.rb'
- 'spec/lib/gitlab/cross_project_access/check_collection_spec.rb'
@@ -5122,7 +5120,6 @@ RSpec/MissingFeatureCategory:
- 'spec/policies/work_item_policy_spec.rb'
- 'spec/presenters/alert_management/alert_presenter_spec.rb'
- 'spec/presenters/award_emoji_presenter_spec.rb'
- - 'spec/presenters/blob_presenter_spec.rb'
- 'spec/presenters/blobs/notebook_presenter_spec.rb'
- 'spec/presenters/blobs/unfold_presenter_spec.rb'
- 'spec/presenters/ci/bridge_presenter_spec.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 709793282de..f21e9b1b461 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-6eb139660ed3ebc5921f565672a00c71973fc620
+b3ea2f8bb802758d667f642b1228d31990559343
diff --git a/Gemfile b/Gemfile
index ec48ea59a55..930ababd431 100644
--- a/Gemfile
+++ b/Gemfile
@@ -407,7 +407,7 @@ group :development, :test do
gem 'database_cleaner', '~> 1.7.0'
gem 'factory_bot_rails', '~> 6.2.0'
- gem 'rspec-rails', '~> 6.0.1'
+ gem 'rspec-rails', '~> 6.0.3'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.11.0'
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 9617337b5db..39cf6469b23 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -525,13 +525,13 @@
{"name":"rqrcode-rails3","version":"0.1.7","platform":"ruby","checksum":"6f0582f26485123e5ed6f2a8a2871f00d86d353e0f58c8429a5a13212bcf48c4"},
{"name":"rspec","version":"3.12.0","platform":"ruby","checksum":"ccc41799a43509dc0be84070e3f0410ac95cbd480ae7b6c245543eb64162399c"},
{"name":"rspec-benchmark","version":"0.6.0","platform":"ruby","checksum":"1014adb57ec2599a2455c63884229f367a2fff6a63a77fd68ce5d804c83dd6cf"},
-{"name":"rspec-core","version":"3.12.0","platform":"ruby","checksum":"c466f4137966526e177d2156ca45c249eeecc7ed519b23ae2fb80c4675406bc5"},
-{"name":"rspec-expectations","version":"3.12.2","platform":"ruby","checksum":"8652db70b25ae3378b7274477a906b6ad1833a7b7cfbb001a03f49dd1c1d6a0d"},
+{"name":"rspec-core","version":"3.12.2","platform":"ruby","checksum":"155b54480f28e2b2813185077fe435c2d663031616360ed3b179a9d6a55d2551"},
+{"name":"rspec-expectations","version":"3.12.3","platform":"ruby","checksum":"093d18e2e7e0a2c619ef8f7343d442fc6c0793fb7897d56f16f26c8a9d244416"},
{"name":"rspec-mocks","version":"3.12.6","platform":"ruby","checksum":"de51a4148ba2ce6f1c1646a2a03e9df2f52da9a42b164f2e7467b2cbe37e07bf"},
{"name":"rspec-parameterized","version":"1.0.0","platform":"ruby","checksum":"9c07b043c72afbd23dd9a1dd48c06f46bc2fb1a6d875c6703e254932ba28b386"},
{"name":"rspec-parameterized-core","version":"1.0.0","platform":"ruby","checksum":"287b494985e79821160af63aba4f91db8dbfa9a21cb200db34ba38f40e16ccc1"},
{"name":"rspec-parameterized-table_syntax","version":"1.0.0","platform":"ruby","checksum":"d7df951eff9c5dd367ca7d5f9ae4853bb7ab7941f9d5b35bba361d112704988c"},
-{"name":"rspec-rails","version":"6.0.1","platform":"ruby","checksum":"016c8ebd5b38ce5cbce949de2f5b28f2bde7bb78d4de26940516713597b26e34"},
+{"name":"rspec-rails","version":"6.0.3","platform":"ruby","checksum":"6d1812cfaf18dba5a08d7e30c85149b24a220fae064853a96e451376be6fd820"},
{"name":"rspec-retry","version":"0.6.2","platform":"ruby","checksum":"6101ba23a38809811ae3484acde4ab481c54d846ac66d5037ccb40131a60d858"},
{"name":"rspec-support","version":"3.12.0","platform":"ruby","checksum":"dd4d44b247ff679b95b5607ac5641d197a5f9b1d33f916123cb98fc5f917c58b"},
{"name":"rspec_junit_formatter","version":"0.6.0","platform":"ruby","checksum":"40dde674e6ae4e6cc0ff560da25497677e34fefd2338cc467a8972f602b62b15"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 740779f811c..e5a459b0a9f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1348,9 +1348,9 @@ GEM
benchmark-perf (~> 0.6)
benchmark-trend (~> 0.4)
rspec (>= 3.0)
- rspec-core (3.12.0)
+ rspec-core (3.12.2)
rspec-support (~> 3.12.0)
- rspec-expectations (3.12.2)
+ rspec-expectations (3.12.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.6)
@@ -1367,14 +1367,14 @@ GEM
rspec-parameterized-table_syntax (1.0.0)
binding_of_caller
rspec-parameterized-core (< 2)
- rspec-rails (6.0.1)
+ rspec-rails (6.0.3)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
- rspec-core (~> 3.11)
- rspec-expectations (~> 3.11)
- rspec-mocks (~> 3.11)
- rspec-support (~> 3.11)
+ rspec-core (~> 3.12)
+ rspec-expectations (~> 3.12)
+ rspec-mocks (~> 3.12)
+ rspec-support (~> 3.12)
rspec-retry (0.6.2)
rspec-core (> 3.3)
rspec-support (3.12.0)
@@ -1968,7 +1968,7 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7)
rspec-benchmark (~> 0.6.0)
rspec-parameterized (~> 1.0)
- rspec-rails (~> 6.0.1)
+ rspec-rails (~> 6.0.3)
rspec-retry (~> 0.6.2)
rspec_flaky!
rspec_junit_formatter
diff --git a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
index d15c8e6e703..85b3c994e02 100644
--- a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
+++ b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
@@ -32,7 +32,6 @@ export default {
i18n: {
emptyField: __('Never'),
expired: __('Expired'),
- header: __('Active %{accessTokenTypePlural} (%{totalAccessTokens})'),
modalMessage: __(
'Are you sure you want to revoke this %{accessTokenType}? This action cannot be undone.',
),
@@ -45,7 +44,6 @@ export default {
'initialActiveAccessTokens',
'noActiveTokensMessage',
'showRole',
- 'information',
],
data() {
return {
@@ -74,12 +72,6 @@ export default {
return FIELDS.filter(({ key }) => !ignoredFields.includes(key));
},
- header() {
- return sprintf(this.$options.i18n.header, {
- accessTokenTypePlural: this.accessTokenTypePlural,
- totalAccessTokens: this.activeAccessTokens.length,
- });
- },
modalMessage() {
return sprintf(this.$options.i18n.modalMessage, {
accessTokenType: this.accessTokenType,
@@ -114,65 +106,66 @@ export default {
<template>
<dom-element-listener :selector="$options.FORM_SELECTOR" @[$options.EVENT_SUCCESS]="onSuccess">
- <div class="gl-pt-6">
- <h5>{{ header }}</h5>
-
- <p v-if="information" data-testid="information-section">
- {{ information }}
- </p>
-
- <gl-table
- data-testid="active-tokens"
- :empty-text="noActiveTokensMessage"
- :fields="filteredFields"
- :items="activeAccessTokens"
- :per-page="$options.PAGE_SIZE"
- :current-page="currentPage"
- :sort-compare="sortingChanged"
- show-empty
+ <div>
+ <div
+ class="gl-new-card-body gl-px-0 gl-overflow-hidden gl-bg-gray-10 gl-border-l gl-border-r gl-border-b gl-rounded-bottom-base gl-mb-5 gl-md-mb-0"
>
- <template #cell(createdAt)="{ item: { createdAt } }">
- <user-date :date="createdAt" />
- </template>
+ <gl-table
+ data-testid="active-tokens"
+ :empty-text="noActiveTokensMessage"
+ :fields="filteredFields"
+ :items="activeAccessTokens"
+ :per-page="$options.PAGE_SIZE"
+ :current-page="currentPage"
+ :sort-compare="sortingChanged"
+ show-empty
+ stacked="sm"
+ >
+ <template #cell(createdAt)="{ item: { createdAt } }">
+ <user-date :date="createdAt" />
+ </template>
- <template #head(lastUsedAt)="{ label }">
- <span>{{ label }}</span>
- <gl-link :href="$options.lastUsedHelpLink"
- ><gl-icon name="question-o" /><span class="gl-sr-only">{{
- s__('AccessTokens|The last time a token was used')
- }}</span></gl-link
- >
- </template>
+ <template #head(lastUsedAt)="{ label }">
+ <span>{{ label }}</span>
+ <gl-link :href="$options.lastUsedHelpLink"
+ ><gl-icon name="question-o" /><span class="gl-sr-only">{{
+ s__('AccessTokens|The last time a token was used')
+ }}</span></gl-link
+ >
+ </template>
- <template #cell(lastUsedAt)="{ item: { lastUsedAt } }">
- <time-ago-tooltip v-if="lastUsedAt" :time="lastUsedAt" />
- <template v-else> {{ $options.i18n.emptyField }}</template>
- </template>
+ <template #cell(lastUsedAt)="{ item: { lastUsedAt } }">
+ <time-ago-tooltip v-if="lastUsedAt" :time="lastUsedAt" />
+ <template v-else> {{ $options.i18n.emptyField }}</template>
+ </template>
- <template #cell(expiresAt)="{ item: { expiresAt, expired, expiresSoon } }">
- <template v-if="expiresAt">
- <span v-if="expired" class="text-danger">{{ $options.i18n.expired }}</span>
- <time-ago-tooltip v-else :class="{ 'text-warning': expiresSoon }" :time="expiresAt" />
+ <template #cell(expiresAt)="{ item: { expiresAt, expired, expiresSoon } }">
+ <template v-if="expiresAt">
+ <span v-if="expired" class="text-danger">{{ $options.i18n.expired }}</span>
+ <time-ago-tooltip v-else :class="{ 'text-warning': expiresSoon }" :time="expiresAt" />
+ </template>
+ <span v-else v-gl-tooltip :title="$options.i18n.tokenValidity">{{
+ $options.i18n.emptyField
+ }}</span>
</template>
- <span v-else v-gl-tooltip :title="$options.i18n.tokenValidity">{{
- $options.i18n.emptyField
- }}</span>
- </template>
- <template #cell(action)="{ item: { revokePath } }">
- <gl-button
- v-if="revokePath"
- category="tertiary"
- :aria-label="$options.i18n.revokeButton"
- :data-confirm="modalMessage"
- data-confirm-btn-variant="danger"
- data-qa-selector="revoke_button"
- data-method="put"
- :href="revokePath"
- icon="remove"
- />
- </template>
- </gl-table>
+ <template #cell(action)="{ item: { revokePath } }">
+ <gl-button
+ v-if="revokePath"
+ category="tertiary"
+ :title="$options.i18n.revokeButton"
+ :aria-label="$options.i18n.revokeButton"
+ :data-confirm="modalMessage"
+ data-confirm-btn-variant="danger"
+ data-qa-selector="revoke_button"
+ data-method="put"
+ :href="revokePath"
+ icon="remove"
+ class="has-tooltip"
+ />
+ </template>
+ </gl-table>
+ </div>
<gl-pagination
v-if="showPagination"
v-model="currentPage"
@@ -183,6 +176,7 @@ export default {
:label-next-page="__('Go to next page')"
:label-prev-page="__('Go to previous page')"
align="center"
+ class="gl-mt-5"
/>
</div>
</dom-element-listener>
diff --git a/app/assets/javascripts/access_tokens/components/new_access_token_app.vue b/app/assets/javascripts/access_tokens/components/new_access_token_app.vue
index dfac423b65e..4b51b4333aa 100644
--- a/app/assets/javascripts/access_tokens/components/new_access_token_app.vue
+++ b/app/assets/javascripts/access_tokens/components/new_access_token_app.vue
@@ -93,6 +93,12 @@ export default {
this.form.querySelectorAll('input[type=checkbox]').forEach((el) => {
el.checked = false;
});
+ document.querySelectorAll('.js-token-card').forEach((el) => {
+ el.querySelector('.js-add-new-token-form').style.display = '';
+ el.querySelector('.js-toggle-button').style.display = 'block';
+ el.querySelector('.js-token-count').innerText =
+ parseInt(el.querySelector('.js-token-count').innerText, 10) + 1;
+ });
},
},
};
@@ -105,7 +111,12 @@ export default {
@[$options.EVENT_SUCCESS]="onSuccess"
>
<div ref="container" data-testid="access-token-section" data-qa-selector="access_token_section">
- <template v-if="newToken">
+ <gl-alert
+ v-if="newToken"
+ variant="success"
+ data-testid="success-message"
+ @dismiss="newToken = null"
+ >
<input-copy-toggle-visibility
:copy-button-title="copyButtonTitle"
:label="label"
@@ -113,16 +124,22 @@ export default {
:value="newToken"
:form-input-group-props="formInputGroupProps"
readonly
+ size="lg"
+ class="gl-mb-0"
>
<template #description>
{{ $options.i18n.description }}
</template>
</input-copy-toggle-visibility>
- <hr />
- </template>
+ </gl-alert>
<template v-if="errors">
- <gl-alert :title="alertDangerTitle" variant="danger" @dismiss="errors = null">
+ <gl-alert
+ :title="alertDangerTitle"
+ variant="danger"
+ data-testid="error-message"
+ @dismiss="errors = null"
+ >
<ul class="gl-m-0">
<li v-for="error in errors" :key="error">
{{ error }}
diff --git a/app/assets/javascripts/access_tokens/components/token.vue b/app/assets/javascripts/access_tokens/components/token.vue
index 09263d8e0ea..3b25e331499 100644
--- a/app/assets/javascripts/access_tokens/components/token.vue
+++ b/app/assets/javascripts/access_tokens/components/token.vue
@@ -20,6 +20,11 @@ export default {
type: String,
required: true,
},
+ size: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
formInputGroupProps() {
@@ -40,6 +45,7 @@ export default {
:value="token"
:copy-button-title="copyButtonTitle"
readonly
+ :size="size"
>
<template #description>
<slot name="input-description"></slot>
diff --git a/app/assets/javascripts/access_tokens/components/tokens_app.vue b/app/assets/javascripts/access_tokens/components/tokens_app.vue
index 88119ed8a84..af26bf85941 100644
--- a/app/assets/javascripts/access_tokens/components/tokens_app.vue
+++ b/app/assets/javascripts/access_tokens/components/tokens_app.vue
@@ -88,6 +88,7 @@ export default {
:input-label="$options.i18n[tokenType].label"
:copy-button-title="$options.i18n[tokenType].copyButtonTitle"
:data-testid="$options.htmlAttributes[tokenType].containerTestId"
+ size="md"
>
<template #title>
<div class="settings-sticky-header">
diff --git a/app/assets/javascripts/access_tokens/index.js b/app/assets/javascripts/access_tokens/index.js
index 510f118bbb5..4e0acaa74da 100644
--- a/app/assets/javascripts/access_tokens/index.js
+++ b/app/assets/javascripts/access_tokens/index.js
@@ -20,7 +20,6 @@ export const initAccessTokenTableApp = () => {
const {
accessTokenType,
accessTokenTypePlural,
- information,
initialActiveAccessTokens: initialActiveAccessTokensJson,
noActiveTokensMessage: noTokensMessage,
} = el.dataset;
@@ -39,7 +38,6 @@ export const initAccessTokenTableApp = () => {
provide: {
accessTokenType,
accessTokenTypePlural,
- information,
initialActiveAccessTokens,
noActiveTokensMessage,
showRole,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue
index c38c253564a..68d81e6b60a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue
@@ -74,9 +74,8 @@ export default {
</script>
<template>
- <div class="gl-display-flex gl-align-items-flex-start">
+ <div v-if="tertiaryButtons.length" class="gl-display-flex gl-align-items-flex-start">
<gl-dropdown
- v-if="tertiaryButtons.length"
v-gl-tooltip
:title="__('Options')"
:text="dropdownLabel"
@@ -102,33 +101,31 @@ export default {
{{ btn.text }}
</gl-dropdown-item>
</gl-dropdown>
- <template v-if="tertiaryButtons.length">
- <gl-button
- v-for="(btn, index) in tertiaryButtons"
- :id="btn.id"
- :key="index"
- v-gl-tooltip.hover
- :title="setTooltip(btn)"
- :href="btn.href"
- :target="btn.target"
- :class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]"
- :data-clipboard-text="btn.dataClipboardText"
- :data-qa-selector="actionButtonQaSelector(btn)"
- :data-method="btn.dataMethod"
- :icon="btn.icon"
- :data-testid="btn.testId || 'extension-actions-button'"
- :variant="btn.variant || 'confirm'"
- :loading="btn.loading"
- :disabled="btn.loading"
- category="tertiary"
- size="small"
- class="gl-display-none gl-md-display-block gl-float-left"
- @click="onClickAction(btn)"
- >
- <template v-if="btn.text">
- {{ btn.text }}
- </template>
- </gl-button>
- </template>
+ <gl-button
+ v-for="(btn, index) in tertiaryButtons"
+ :id="btn.id"
+ :key="index"
+ v-gl-tooltip.hover
+ :title="setTooltip(btn)"
+ :href="btn.href"
+ :target="btn.target"
+ :class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]"
+ :data-clipboard-text="btn.dataClipboardText"
+ :data-qa-selector="actionButtonQaSelector(btn)"
+ :data-method="btn.dataMethod"
+ :icon="btn.icon"
+ :data-testid="btn.testId || 'extension-actions-button'"
+ :variant="btn.variant || 'confirm'"
+ :loading="btn.loading"
+ :disabled="btn.loading"
+ category="tertiary"
+ size="small"
+ class="gl-display-none gl-md-display-block gl-float-left"
+ @click="onClickAction(btn)"
+ >
+ <template v-if="btn.text">
+ {{ btn.text }}
+ </template>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
index ec979861283..1a1152446ce 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
@@ -73,10 +73,16 @@ export default {
<template #body>
<div class="gl-w-full gl-display-flex" :class="{ 'gl-flex-direction-column': level === 1 }">
<div class="gl-display-flex gl-flex-grow-1">
- <div class="gl-display-flex gl-flex-grow-1 gl-flex-direction-column">
- <p v-safe-html="generatedText" class="gl-mb-0 gl-mr-1"></p>
- <gl-link v-if="data.link" :href="data.link.href">{{ data.link.text }}</gl-link>
- <p v-if="data.supportingText" v-safe-html="generatedSupportingText" class="gl-mb-0"></p>
+ <div class="gl-display-flex gl-flex-grow-1 gl-align-items-baseline">
+ <div>
+ <p v-safe-html="generatedText" class="gl-mb-0 gl-mr-1"></p>
+ <gl-link v-if="data.link" :href="data.link.href">{{ data.link.text }}</gl-link>
+ <p
+ v-if="data.supportingText"
+ v-safe-html="generatedSupportingText"
+ class="gl-mb-0"
+ ></p>
+ </div>
<gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'">
{{ data.badge.text }}
</gl-badge>
diff --git a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
index c371c1aeff9..eca5803318a 100644
--- a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
+++ b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
@@ -65,6 +65,11 @@ export default {
return {};
},
},
+ size: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
if (!this.readonly && !this.value) {
@@ -151,6 +156,7 @@ export default {
<gl-form-input
ref="input"
:readonly="readonly"
+ :size="size"
class="gl-font-monospace! gl-cursor-default!"
v-bind="formInputGroupProps"
:value="value"
diff --git a/app/assets/stylesheets/framework/new_card.scss b/app/assets/stylesheets/framework/new_card.scss
index 411f5300120..a1d7761bd88 100644
--- a/app/assets/stylesheets/framework/new_card.scss
+++ b/app/assets/stylesheets/framework/new_card.scss
@@ -91,7 +91,9 @@
@include gl-border-gray-100;
@include gl-rounded-base;
}
+}
+.gl-new-card-body {
// Table adjustments
@mixin new-card-table-adjustments {
tbody > tr {
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 6aca2b5646c..ad792a6ee50 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -76,14 +76,10 @@
}
}
-.settings-section {
- @include gl-pt-6;
-
- &::after {
- content: '';
- display: block;
- @include gl-pb-5;
- }
+.settings-section::after {
+ content: '';
+ display: block;
+ @include gl-pb-7;
}
.settings-section,
@@ -91,6 +87,11 @@
@include gl-pt-0;
}
+// Fix for sticky header when there is no search bar.
+.flash-container + .settings-section {
+ @include gl-pt-3;
+}
+
.settings-section ~ .settings-section {
@include gl-pt-6;
}
diff --git a/app/controllers/repositories/lfs_storage_controller.rb b/app/controllers/repositories/lfs_storage_controller.rb
index 22f1a81b95b..80f7153cd7a 100644
--- a/app/controllers/repositories/lfs_storage_controller.rb
+++ b/app/controllers/repositories/lfs_storage_controller.rb
@@ -18,10 +18,7 @@ module Repositories
def download
lfs_object = LfsObject.find_by_oid(oid)
- unless lfs_object && lfs_object.file.exists?
- render_lfs_not_found
- return
- end
+ return render_lfs_not_found unless lfs_object&.file&.exists?
send_upload(lfs_object.file, send_params: { content_type: "application/octet-stream" })
end
diff --git a/app/graphql/types/custom_emoji_type.rb b/app/graphql/types/custom_emoji_type.rb
index 379a0c44d67..b02cd56e6df 100644
--- a/app/graphql/types/custom_emoji_type.rb
+++ b/app/graphql/types/custom_emoji_type.rb
@@ -7,6 +7,10 @@ module Types
authorize :read_custom_emoji
+ connection_type_class(Types::CountableConnectionType)
+
+ expose_permissions Types::PermissionTypes::CustomEmoji
+
field :id, ::Types::GlobalIDType[::CustomEmoji],
null: false,
description: 'ID of the emoji.'
@@ -23,5 +27,9 @@ module Types
field :external, GraphQL::Types::Boolean,
null: false,
description: 'Whether the emoji is an external link.'
+
+ field :created_at, Types::TimeType,
+ null: false,
+ description: 'Timestamp of when the custom emoji was created.'
end
end
diff --git a/app/graphql/types/permission_types/group.rb b/app/graphql/types/permission_types/group.rb
index 6a1031e2532..fbd1140babc 100644
--- a/app/graphql/types/permission_types/group.rb
+++ b/app/graphql/types/permission_types/group.rb
@@ -5,7 +5,7 @@ module Types
class Group < BasePermissionType
graphql_name 'GroupPermissions'
- abilities :read_group, :create_projects
+ abilities :read_group, :create_projects, :create_custom_emoji
end
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index ded4b06a028..d7aa66588d3 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -29,7 +29,8 @@ class Commit
delegate :project, to: :repository, allow_nil: true
MIN_SHA_LENGTH = Gitlab::Git::Commit::MIN_SHA_LENGTH
- COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze
+ MAX_SHA_LENGTH = Gitlab::Git::Commit::MAX_SHA_LENGTH
+ COMMIT_SHA_PATTERN = Gitlab::Git::Commit::SHA_PATTERN.freeze
EXACT_COMMIT_SHA_PATTERN = /\A#{COMMIT_SHA_PATTERN}\z/.freeze
# Used by GFM to match and present link extensions on node texts and hrefs.
LINK_EXTENSION_PATTERN = /(patch)/.freeze
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index c6e507e4b6c..d882a185464 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -31,9 +31,8 @@ class CommitRange
REF_PATTERN = /[0-9a-zA-Z][0-9a-zA-Z_.-]*[0-9a-zA-Z\^]/.freeze
PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/.freeze
- # In text references, the beginning and ending refs can only be SHAs
- # between 7 and 40 hex characters.
- STRICT_PATTERN = /\h{7,40}\.{2,3}\h{7,40}/.freeze
+ # In text references, the beginning and ending refs can only be valid SHAs.
+ STRICT_PATTERN = /#{Gitlab::Git::Commit::RAW_SHA_PATTERN}\.{2,3}#{Gitlab::Git::Commit::RAW_SHA_PATTERN}/.freeze
def self.reference_prefix
'@'
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index bc12d210334..f58ba2b3eaf 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -6,6 +6,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
include DiffHelper
include TreeHelper
include ChecksCollaboration
+ include Gitlab::Utils::StrongMemoize
presents ::Blob, as: :blob
@@ -149,6 +150,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
end
def external_storage_url
+ return lfs_object_url if lfs_blob?
return unless static_objects_external_storage_enabled?
external_storage_url_or_path(url_helpers.project_raw_url(project, ref_qualified_path), project)
@@ -164,6 +166,25 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
private
+ def lfs_object_url
+ return unless lfs_object.present?
+
+ if LfsObjectUploader.proxy_download_enabled? || lfs_object.file.file_storage?
+ "#{project.http_url_to_repo}/gitlab-lfs/objects/#{lfs_object.oid}"
+ else
+ lfs_object.file.url(content_type: "application/octet-stream")
+ end
+ end
+
+ def lfs_object
+ project.lfs_objects.find_by_oid(blob.lfs_oid) if lfs_blob?
+ end
+ strong_memoize_attr :lfs_object
+
+ def lfs_blob?
+ blob.stored_externally? && blob.lfs_pointer?
+ end
+
def path_params
if ref_type.present?
[project, ref_qualified_path, { ref_type: ref_type }]
diff --git a/app/views/admin/impersonation_tokens/index.html.haml b/app/views/admin/impersonation_tokens/index.html.haml
index 0208b8ad836..a844b6846a9 100644
--- a/app/views/admin/impersonation_tokens/index.html.haml
+++ b/app/views/admin/impersonation_tokens/index.html.haml
@@ -6,11 +6,24 @@
= render 'admin/users/head'
-.row.gl-mt-3
- .col-lg-12
- #js-new-access-token-app{ data: { access_token_type: type } }
+#js-new-access-token-app{ data: { access_token_type: type } }
- = render 'shared/access_tokens/form',
+= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-border-b-0 gl-rounded-bottom-left-none gl-rounded-bottom-right-none js-toggle-container js-token-card' }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body' }) do |c|
+ - c.with_header do
+ .gl-new-card-title-wrapper.gl-flex-direction-column
+ %h3.gl-new-card-title
+ = _('Impersonation tokens')
+ .gl-new-card-count
+ = sprite_icon('token', css_class: 'gl-mr-2')
+ %span.js-token-count= @active_impersonation_tokens.size
+ .gl-new-card-description
+ = _("To see all the user's personal access tokens you must impersonate them first.")
+ .gl-new-card-actions
+ = render Pajamas::ButtonComponent.new(size: :small, button_options: { class: 'js-toggle-button js-toggle-content' }) do
+ = _('Add new token')
+ - c.with_body do
+ .gl-new-card-add-form.gl-mt-3.gl-display-none.js-toggle-content.js-add-new-token-form
+ = render 'shared/access_tokens/form',
ajax: true,
type: type,
title: _('Add an impersonation token'),
@@ -20,4 +33,4 @@
scopes: @scopes,
help_path: help_page_path('api/rest/index', anchor: 'impersonation-tokens')
- #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_impersonation_tokens.to_json, information: _("To see all the user's personal access tokens you must impersonate them first.") } }
+#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_impersonation_tokens.to_json } }
diff --git a/app/views/groups/settings/access_tokens/index.html.haml b/app/views/groups/settings/access_tokens/index.html.haml
index ac3be429461..927162d2284 100644
--- a/app/views/groups/settings/access_tokens/index.html.haml
+++ b/app/views/groups/settings/access_tokens/index.html.haml
@@ -25,18 +25,31 @@
#js-new-access-token-app{ data: { access_token_type: type } }
- - if current_user.can?(:create_resource_access_tokens, @group)
- = render 'shared/access_tokens/form',
- ajax: true,
- type: type,
- path: group_settings_access_tokens_path(@group),
- resource: @group,
- token: @resource_access_token,
- scopes: @scopes,
- access_levels: GroupMember.access_level_roles,
- default_access_level: Gitlab::Access::GUEST,
- prefix: :resource_access_token,
- help_path: help_page_path('user/group/settings/group_access_tokens', anchor: 'scopes-for-a-group-access-token')
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-border-b-0 gl-rounded-bottom-left-none gl-rounded-bottom-right-none js-toggle-container js-token-card' }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body' }) do |c|
+ - c.with_header do
+ .gl-new-card-title-wrapper
+ %h3.gl-new-card-title
+ = _('Active group access tokens')
+ .gl-new-card-count
+ = sprite_icon('token', css_class: 'gl-mr-2')
+ %span.js-token-count= @active_access_tokens.size
+ - if current_user.can?(:create_resource_access_tokens, @group)
+ .gl-new-card-actions
+ = render Pajamas::ButtonComponent.new(size: :small, button_options: { class: 'js-toggle-button js-toggle-content' }) do
+ = _('Add new token')
+ - c.with_body do
+ - if current_user.can?(:create_resource_access_tokens, @group)
+ .gl-new-card-add-form.gl-mt-3.gl-display-none.js-toggle-content.js-add-new-token-form
+ = render 'shared/access_tokens/form',
+ ajax: true,
+ type: type,
+ path: group_settings_access_tokens_path(@group),
+ resource: @group,
+ token: @resource_access_token,
+ scopes: @scopes,
+ access_levels: GroupMember.access_level_roles,
+ default_access_level: Gitlab::Access::GUEST,
+ prefix: :resource_access_token,
+ help_path: help_page_path('user/group/settings/group_access_tokens', anchor: 'scopes-for-a-group-access-token')
- #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json, no_active_tokens_message: _('This group has no active access tokens.'), show_role: true
- } }
+ #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json, no_active_tokens_message: _('This group has no active access tokens.'), show_role: true } }
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 5020f6cbb22..461f0165501 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -16,13 +16,26 @@
#js-new-access-token-app{ data: { access_token_type: type } }
- = render 'shared/access_tokens/form',
- ajax: true,
- type: type,
- path: profile_personal_access_tokens_path,
- token: @personal_access_token,
- scopes: @scopes,
- help_path: help_page_path('user/profile/personal_access_tokens.md', anchor: 'personal-access-token-scopes')
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-border-b-0 gl-rounded-bottom-left-none gl-rounded-bottom-right-none js-toggle-container js-token-card' }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body' }) do |c|
+ - c.with_header do
+ .gl-new-card-title-wrapper
+ %h3.gl-new-card-title
+ = _('Active personal access tokens')
+ .gl-new-card-count
+ = sprite_icon('token', css_class: 'gl-mr-2')
+ %span.js-token-count= @active_access_tokens.size
+ .gl-new-card-actions
+ = render Pajamas::ButtonComponent.new(size: :small, button_options: { class: 'js-toggle-button js-toggle-content' }) do
+ = _('Add new token')
+ - c.with_body do
+ .gl-new-card-add-form.gl-mt-3.gl-display-none.js-toggle-content.js-add-new-token-form
+ = render 'shared/access_tokens/form',
+ ajax: true,
+ type: type,
+ path: profile_personal_access_tokens_path,
+ token: @personal_access_token,
+ scopes: @scopes,
+ help_path: help_page_path('user/profile/personal_access_tokens.md', anchor: 'personal-access-token-scopes')
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json } }
diff --git a/app/views/projects/settings/access_tokens/index.html.haml b/app/views/projects/settings/access_tokens/index.html.haml
index e4af6d59cad..53ae5ba26f8 100644
--- a/app/views/projects/settings/access_tokens/index.html.haml
+++ b/app/views/projects/settings/access_tokens/index.html.haml
@@ -4,9 +4,11 @@
- type_plural = _('project access tokens')
- @force_desktop_expanded_sidebar = true
-.gl-mt-5.js-search-settings-section
- %h4.gl-my-0
- = page_title
+.settings-section.js-search-settings-section
+ .settings-sticky-header
+ .settings-sticky-header-inner
+ %h4.gl-my-0
+ = page_title
%p.gl-text-secondary
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/project_access_tokens') }
- if current_user.can?(:create_resource_access_tokens, @project)
@@ -23,9 +25,21 @@
#js-new-access-token-app{ data: { access_token_type: type } }
- - if current_user.can?(:create_resource_access_tokens, @project)
- = render_if_exists 'projects/settings/access_tokens/form',
- type: type
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-border-b-0 gl-rounded-bottom-left-none gl-rounded-bottom-right-none js-toggle-container js-token-card' }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body' }) do |c|
+ - c.with_header do
+ .gl-new-card-title-wrapper
+ %h3.gl-new-card-title
+ = _('Active project access tokens')
+ .gl-new-card-count
+ = sprite_icon('token', css_class: 'gl-mr-2')
+ %span.js-token-count= @active_access_tokens.size
+ - if current_user.can?(:create_resource_access_tokens, @project)
+ .gl-new-card-actions
+ = render Pajamas::ButtonComponent.new(size: :small, button_options: { class: 'js-toggle-button js-toggle-content' }) do
+ = _('Add new token')
+ - c.with_body do
+ - if current_user.can?(:create_resource_access_tokens, @project)
+ .gl-new-card-add-form.gl-mt-3.gl-display-none.js-toggle-content.js-add-new-token-form
+ = render_if_exists 'projects/settings/access_tokens/form', type: type
- #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json, no_active_tokens_message: _('This project has no active access tokens.'), show_role: true
- } }
+ #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json, no_active_tokens_message: _('This project has no active access tokens.'), show_role: true } }
diff --git a/app/views/shared/access_tokens/_form.html.haml b/app/views/shared/access_tokens/_form.html.haml
index a8f781d2272..d397fdf8fc3 100644
--- a/app/views/shared/access_tokens/_form.html.haml
+++ b/app/views/shared/access_tokens/_form.html.haml
@@ -7,7 +7,7 @@
- access_levels = local_assigns.fetch(:access_levels, false)
- default_access_level = local_assigns.fetch(:default_access_level, false)
-%h5.gl-font-lg.gl-mt-0
+%h4.gl-mt-0
= title
= gitlab_ui_form_for token, as: prefix, url: path, method: :post, html: { id: 'js-new-access-token-form', class: 'js-requires-input' }, remote: ajax do |f|
@@ -43,3 +43,5 @@
.gl-mt-3
= f.submit s_('AccessTokens|Create %{type}') % { type: type }, data: { qa_selector: 'create_token_button' }, pajamas_button: true
+ = render Pajamas::ButtonComponent.new(button_options: { type: 'reset', class: 'gl-ml-2 js-toggle-button' }) do
+ = _('Cancel')
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 0665345a24e..d0d7442857b 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -683,7 +683,7 @@ scope path: '(/-/jira)', constraints: ::Constraints::JiraEncodedUrlConstrainer.n
)
}
- get 'commit/:id', constraints: { id: /\h{7,40}/ }, to: redirect { |params, req|
+ get 'commit/:id', constraints: { id: Gitlab::Git::Commit::SHA_PATTERN }, to: redirect { |params, req|
project_full_path = ::Gitlab::Jira::Dvcs.restore_full_path(
namespace: params[:namespace_id],
project: params[:project_id]
diff --git a/config/routes/repository.rb b/config/routes/repository.rb
index 08aa113685a..fc66cb4d6d3 100644
--- a/config/routes/repository.rb
+++ b/config/routes/repository.rb
@@ -92,7 +92,7 @@ scope format: false do
end
end
-resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
+resources :commit, only: [:show], constraints: { id: Gitlab::Git::Commit::SHA_PATTERN } do
member do
get :branches
get :pipelines
diff --git a/db/docs/batched_background_migrations/backfill_default_branch_protection_namespace_setting.yml b/db/docs/batched_background_migrations/backfill_default_branch_protection_namespace_setting.yml
new file mode 100644
index 00000000000..7a9e1cb8f5b
--- /dev/null
+++ b/db/docs/batched_background_migrations/backfill_default_branch_protection_namespace_setting.yml
@@ -0,0 +1,6 @@
+---
+migration_job_name: BackfillDefaultBranchProtectionNamespaceSetting
+description: This migration back fills column default_branch_protection_defaults of namespace settings table
+feature_category: database
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127335/
+milestone: 16.3
diff --git a/db/migrate/20230707031923_add_emails_to_x509_certificates.rb b/db/migrate/20230707031923_add_emails_to_x509_certificates.rb
new file mode 100644
index 00000000000..44ea425fe72
--- /dev/null
+++ b/db/migrate/20230707031923_add_emails_to_x509_certificates.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddEmailsToX509Certificates < Gitlab::Database::Migration[2.1]
+ def change
+ add_column :x509_certificates, :emails, :string, array: true, default: [], null: false
+ end
+end
diff --git a/db/post_migrate/20230724071541_queue_backfill_default_branch_protection_namespace_setting.rb b/db/post_migrate/20230724071541_queue_backfill_default_branch_protection_namespace_setting.rb
new file mode 100644
index 00000000000..57debd9d0a2
--- /dev/null
+++ b/db/post_migrate/20230724071541_queue_backfill_default_branch_protection_namespace_setting.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class QueueBackfillDefaultBranchProtectionNamespaceSetting < Gitlab::Database::Migration[2.1]
+ MIGRATION = "BackfillDefaultBranchProtectionNamespaceSetting"
+ DELAY_INTERVAL = 2.minutes
+ BATCH_SIZE = 10_000
+ SUB_BATCH_SIZE = 100
+
+ disable_ddl_transaction!
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :namespace_settings,
+ :namespace_id,
+ job_interval: DELAY_INTERVAL,
+ batch_size: BATCH_SIZE,
+ sub_batch_size: SUB_BATCH_SIZE
+ )
+ end
+
+ def down
+ delete_batched_background_migration(MIGRATION, :namespace_settings, :namespace_id, [])
+ end
+end
diff --git a/db/post_migrate/20230724085146_replace_old_fk_p_ci_builds_metadata_to_builds_v3.rb b/db/post_migrate/20230724085146_replace_old_fk_p_ci_builds_metadata_to_builds_v3.rb
new file mode 100644
index 00000000000..acfed3032eb
--- /dev/null
+++ b/db/post_migrate/20230724085146_replace_old_fk_p_ci_builds_metadata_to_builds_v3.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+class ReplaceOldFkPCiBuildsMetadataToBuildsV3 < Gitlab::Database::Migration[2.1]
+ include Gitlab::Database::PartitioningMigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ return if new_foreign_key_exists?
+
+ with_lock_retries do
+ remove_foreign_key_if_exists :p_ci_builds_metadata, :ci_builds,
+ name: :fk_e20479742e_p, reverse_lock_order: true
+
+ rename_constraint :p_ci_builds_metadata, :temp_fk_e20479742e_p, :fk_e20479742e_p
+
+ Gitlab::Database::PostgresPartitionedTable.each_partition(:p_ci_builds_metadata) do |partition|
+ rename_constraint partition.identifier, :temp_fk_e20479742e_p, :fk_e20479742e_p
+ end
+ end
+ end
+
+ def down
+ return unless new_foreign_key_exists?
+
+ add_concurrent_partitioned_foreign_key :p_ci_builds_metadata, :ci_builds,
+ name: :temp_fk_e20479742e_p,
+ column: [:partition_id, :build_id],
+ target_column: [:partition_id, :id],
+ on_update: :cascade,
+ on_delete: :cascade,
+ validate: true,
+ reverse_lock_order: true
+
+ switch_constraint_names :p_ci_builds_metadata, :fk_e20479742e_p, :temp_fk_e20479742e_p
+
+ Gitlab::Database::PostgresPartitionedTable.each_partition(:p_ci_builds_metadata) do |partition|
+ switch_constraint_names partition.identifier, :fk_e20479742e_p, :temp_fk_e20479742e_p
+ end
+ end
+
+ private
+
+ def new_foreign_key_exists?
+ foreign_key_exists?(:p_ci_builds_metadata, :p_ci_builds, name: :fk_e20479742e_p)
+ end
+end
diff --git a/db/post_migrate/20230724085149_replace_old_fk_p_ci_runner_machine_builds_to_builds_v3.rb b/db/post_migrate/20230724085149_replace_old_fk_p_ci_runner_machine_builds_to_builds_v3.rb
new file mode 100644
index 00000000000..d3919148240
--- /dev/null
+++ b/db/post_migrate/20230724085149_replace_old_fk_p_ci_runner_machine_builds_to_builds_v3.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+class ReplaceOldFkPCiRunnerMachineBuildsToBuildsV3 < Gitlab::Database::Migration[2.1]
+ include Gitlab::Database::PartitioningMigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ return if new_foreign_key_exists?
+
+ with_lock_retries do
+ remove_foreign_key_if_exists :p_ci_runner_machine_builds, :ci_builds,
+ name: :fk_bb490f12fe_p, reverse_lock_order: true
+
+ rename_constraint :p_ci_runner_machine_builds, :temp_fk_bb490f12fe_p, :fk_bb490f12fe_p
+
+ Gitlab::Database::PostgresPartitionedTable.each_partition(:p_ci_runner_machine_builds) do |partition|
+ rename_constraint partition.identifier, :temp_fk_bb490f12fe_p, :fk_bb490f12fe_p
+ end
+ end
+ end
+
+ def down
+ return unless new_foreign_key_exists?
+
+ add_concurrent_partitioned_foreign_key :p_ci_runner_machine_builds, :ci_builds,
+ name: :temp_fk_bb490f12fe_p,
+ column: [:partition_id, :build_id],
+ target_column: [:partition_id, :id],
+ on_update: :cascade,
+ on_delete: :cascade,
+ validate: true,
+ reverse_lock_order: true
+
+ switch_constraint_names :p_ci_runner_machine_builds, :fk_bb490f12fe_p, :temp_fk_bb490f12fe_p
+
+ Gitlab::Database::PostgresPartitionedTable.each_partition(:p_ci_runner_machine_builds) do |partition|
+ switch_constraint_names partition.identifier, :fk_bb490f12fe_p, :temp_fk_bb490f12fe_p
+ end
+ end
+
+ private
+
+ def new_foreign_key_exists?
+ foreign_key_exists?(:p_ci_runner_machine_builds, :p_ci_builds, name: :fk_bb490f12fe_p)
+ end
+end
diff --git a/db/schema_migrations/20230707031923 b/db/schema_migrations/20230707031923
new file mode 100644
index 00000000000..af20d211b43
--- /dev/null
+++ b/db/schema_migrations/20230707031923
@@ -0,0 +1 @@
+0a9e2fd92a64fbd4f2dc0d9fc866b3c5bb582c10503b0a85f1164db2e42c7cd0 \ No newline at end of file
diff --git a/db/schema_migrations/20230724071541 b/db/schema_migrations/20230724071541
new file mode 100644
index 00000000000..9760046215f
--- /dev/null
+++ b/db/schema_migrations/20230724071541
@@ -0,0 +1 @@
+3ace146e873db96f6dcb556e701339081620aa268f013979c9062b50957d9e13 \ No newline at end of file
diff --git a/db/schema_migrations/20230724085146 b/db/schema_migrations/20230724085146
new file mode 100644
index 00000000000..47b5817d794
--- /dev/null
+++ b/db/schema_migrations/20230724085146
@@ -0,0 +1 @@
+3523475202ec758dc33748af5c2e85392d1ab55af4a5c7f7b76412b6723d1bf6 \ No newline at end of file
diff --git a/db/schema_migrations/20230724085149 b/db/schema_migrations/20230724085149
new file mode 100644
index 00000000000..e5e1a5f2461
--- /dev/null
+++ b/db/schema_migrations/20230724085149
@@ -0,0 +1 @@
+e4236ae465987e86f4a68ee552856afe054dccf5b13c5d71abb72cad186266d1 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 0b8bcde15f8..80261e7b687 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -24966,7 +24966,8 @@ CREATE TABLE x509_certificates (
email character varying(255) NOT NULL,
serial_number bytea NOT NULL,
certificate_status smallint DEFAULT 0 NOT NULL,
- x509_issuer_id bigint NOT NULL
+ x509_issuer_id bigint NOT NULL,
+ emails character varying[] DEFAULT '{}'::character varying[] NOT NULL
);
CREATE SEQUENCE x509_certificates_id_seq
diff --git a/doc/administration/audit_event_streaming/index.md b/doc/administration/audit_event_streaming/index.md
index a8e1f0b015e..9cc70f4b701 100644
--- a/doc/administration/audit_event_streaming/index.md
+++ b/doc/administration/audit_event_streaming/index.md
@@ -385,9 +385,29 @@ To list streaming destinations for an instance and see the verification tokens:
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
1. Select **Admin Area**.
1. On the left sidebar, select **Monitoring > Audit Events**.
-1. On the main area, select the **Streams**.
+1. On the main area, select the **Streams** tab.
1. View the verification token on the right side of each item.
+### Update event filters
+
+> Event type filtering in the UI with a defined list of audit event types [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/415013) in GitLab 16.3.
+
+When this feature is enabled, you can permit users to filter streamed audit events per destination.
+If the feature is enabled with no filters, the destination receives all audit events.
+
+A streaming destination that has an event type filter set has a **filtered** (**{filter}**) label.
+
+To update a streaming destination's event filters:
+
+1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
+1. Select **Admin Area**.
+1. On the left sidebar, select **Monitoring > Audit Events**.
+1. On the main area, select the **Streams** tab.
+1. Select the stream to expand.
+1. Locate the **Filter by audit event type** dropdown list.
+1. Select the dropdown list and select or clear the required event types.
+1. Select **Save** to update the event filters.
+
### Override default content type header
By default, streaming destinations use a `content-type` header of `application/x-www-form-urlencoded`. However, you
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index fdafe9200bf..4ba3a57723e 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -8751,6 +8751,7 @@ The connection type for [`CustomEmoji`](#customemoji).
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="customemojiconnectioncount"></a>`count` | [`Int!`](#int) | Total count of collection. |
| <a id="customemojiconnectionedges"></a>`edges` | [`[CustomEmojiEdge]`](#customemojiedge) | A list of edges. |
| <a id="customemojiconnectionnodes"></a>`nodes` | [`[CustomEmoji]`](#customemoji) | A list of nodes. |
| <a id="customemojiconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
@@ -14336,10 +14337,22 @@ A custom emoji uploaded by user.
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="customemojicreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the custom emoji was created. |
| <a id="customemojiexternal"></a>`external` | [`Boolean!`](#boolean) | Whether the emoji is an external link. |
| <a id="customemojiid"></a>`id` | [`CustomEmojiID!`](#customemojiid) | ID of the emoji. |
| <a id="customemojiname"></a>`name` | [`String!`](#string) | Name of the emoji. |
| <a id="customemojiurl"></a>`url` | [`String!`](#string) | Link to file of the emoji. |
+| <a id="customemojiuserpermissions"></a>`userPermissions` | [`CustomEmojiPermissions!`](#customemojipermissions) | Permissions for the current user on the resource. |
+
+### `CustomEmojiPermissions`
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="customemojipermissionscreatecustomemoji"></a>`createCustomEmoji` | [`Boolean!`](#boolean) | Indicates the user can perform `create_custom_emoji` on this resource. |
+| <a id="customemojipermissionsdeletecustomemoji"></a>`deleteCustomEmoji` | [`Boolean!`](#boolean) | Indicates the user can perform `delete_custom_emoji` on this resource. |
+| <a id="customemojipermissionsreadcustomemoji"></a>`readCustomEmoji` | [`Boolean!`](#boolean) | Indicates the user can perform `read_custom_emoji` on this resource. |
### `CustomerRelationsContact`
@@ -17314,6 +17327,7 @@ Returns [`UserMergeRequestInteraction`](#usermergerequestinteraction).
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="grouppermissionscreatecustomemoji"></a>`createCustomEmoji` | [`Boolean!`](#boolean) | Indicates the user can perform `create_custom_emoji` on this resource. |
| <a id="grouppermissionscreateprojects"></a>`createProjects` | [`Boolean!`](#boolean) | Indicates the user can perform `create_projects` on this resource. |
| <a id="grouppermissionsreadgroup"></a>`readGroup` | [`Boolean!`](#boolean) | Indicates the user can perform `read_group` on this resource. |
diff --git a/doc/ci/interactive_web_terminal/index.md b/doc/ci/interactive_web_terminal/index.md
index a7923cb84a0..b3e5be6c71c 100644
--- a/doc/ci/interactive_web_terminal/index.md
+++ b/doc/ci/interactive_web_terminal/index.md
@@ -61,7 +61,7 @@ improving this behavior.
Sometimes, when a job is running, things don't go as you would expect, and it
would be helpful if one can have a shell to aid debugging. When a job is
running, on the right panel you can see a button `debug` that opens the terminal
-for the current job.
+for the current job. Only the person who started a job can debug it.
![Example of job running with terminal available](img/interactive_web_terminal_running_job.png)
diff --git a/doc/install/openshift_and_gitlab/index.md b/doc/install/openshift_and_gitlab/index.md
index 4f3df3ecff8..c13e586a207 100644
--- a/doc/install/openshift_and_gitlab/index.md
+++ b/doc/install/openshift_and_gitlab/index.md
@@ -18,34 +18,29 @@ Running GitLab within an OpenShift cluster is officially supported using the Git
[setting up GitLab on OpenShift on the GitLab Operator's documentation](https://docs.gitlab.com/charts/installation/operator.html).
Some components (documented on the GitLab Operator doc) are not supported yet.
-## Deploy to and integrate with OpenShift from GitLab
-
-Deploying custom or COTS applications on top of OpenShift from GitLab is supported using [the GitLab agent](../../user/clusters/agent/index.md).
-
## Use OpenShift to run a GitLab Runner Fleet
The GitLab Operator does not include the GitLab Runner. To install and manage a GitLab Runner fleet in an OpenShift cluster, use the
[GitLab Runner Operator](https://gitlab.com/gitlab-org/gl-openshift/gitlab-runner-operator).
-## Unsupported GitLab features
+### Deploy to and integrate with OpenShift from GitLab
-### Secure
+Deploying custom or COTS applications on top of OpenShift from GitLab is supported using [the GitLab agent](../../user/clusters/agent/index.md).
-- [License Compliance via the `License-Scanning.gitlab-ci.yml` CI/CD template](../../user/compliance/license_compliance/index.md). [License scanning of CycloneDX files](../../user/compliance/license_scanning_of_cyclonedx_files/index.md) is supported on OpenShift.
-- [Code Quality scanning](../../ci/testing/code_quality.md)
-- [Operational Container Scanning](../../user/clusters/agent/vulnerabilities.md) (Note: Pipeline [Container Scanning](../../user/application_security/container_scanning/index.md) is supported)
+### Unsupported GitLab features
-### Docker-in-Docker
+#### Docker-in-Docker
When using OpenShift to run a GitLab Runner Fleet, we do not support some GitLab features given OpenShift's security model.
Features requiring Docker-in-Docker might not work.
For Auto DevOps, the following features are not supported yet:
-- Auto Code Quality
-- Auto License Compliance
+- [Auto Code Quality](../../ci/testing/code_quality.md)
+- [Auto License Compliance](../../user/compliance/license_compliance/index.md) ([License scanning of CycloneDX files](../../user/compliance/license_scanning_of_cyclonedx_files/index.md) is supported on OpenShift)
- Auto Browser Performance Testing
- Auto Build
+- [Operational Container Scanning](../../user/clusters/agent/vulnerabilities.md) (Note: Pipeline [Container Scanning](../../user/application_security/container_scanning/index.md) is supported)
For Auto Build, there's a [possible workaround using `kaniko`](../../ci/docker/using_kaniko.md).
You can check the progress of the implementation in this [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/332560).
diff --git a/doc/integration/jira/connect-app.md b/doc/integration/jira/connect-app.md
index 10b1f3ff082..a3025144242 100644
--- a/doc/integration/jira/connect-app.md
+++ b/doc/integration/jira/connect-app.md
@@ -20,8 +20,6 @@ If you use Jira Data Center or Jira Server, use the [Jira DVCS connector](dvcs/i
## Install the GitLab for Jira Cloud app **(FREE SAAS)**
-> **Add namespace** [renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/331432) to **Link groups** in GitLab 16.1.
-
Prerequisites:
- You must have [site administrator](https://support.atlassian.com/user-management/docs/give-users-admin-permissions/#Make-someone-a-site-admin) access to the Jira instance.
@@ -42,13 +40,14 @@ For an overview, see
## Configure the GitLab for Jira Cloud app **(FREE SAAS)**
+> **Add namespace** [renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/331432) to **Link groups** in GitLab 16.1.
+
Prerequisites:
- You must have at least the Maintainer role for the GitLab group.
- You must have [site administrator](https://support.atlassian.com/user-management/docs/give-users-admin-permissions/#Make-someone-a-site-admin) access to the Jira instance.
You can sync data from GitLab to Jira by linking the GitLab for Jira Cloud app to one or more GitLab groups.
-
To configure the GitLab for Jira Cloud app:
1. In Jira, on the top bar, select **Apps > Manage your apps**.
diff --git a/doc/tutorials/infrastructure.md b/doc/tutorials/infrastructure.md
index 688a1a1f7c7..a10e0c68c37 100644
--- a/doc/tutorials/infrastructure.md
+++ b/doc/tutorials/infrastructure.md
@@ -13,3 +13,5 @@ configure the infrastructure for your application.
|-------|-------------|--------------------|
| [Use GitOps with GitLab](https://about.gitlab.com/blog/2022/04/07/the-ultimate-guide-to-gitops-with-gitlab/) | Learn how to provision and manage a base infrastructure, connect to a Kubernetes cluster, deploy third-party applications, and carry out other infrastructure automation tasks. | |
| [Set up Flux for GitOps](../user/clusters/agent/gitops/flux_tutorial.md) | Learn how to set up Flux for GitOps in a sample project. | |
+| [Structure your repository for GitOps](../user/clusters/agent/gitops/example_repository_structure.md) | Learn how to organize a GitLab project for GitOps. | |
+| [Deploy an OCI artifact using Flux](../user/clusters/agent/gitops/flux_oci_tutorial.md) | Learn how to package and deploy your Kubernetes manifests as an OCI artifact. | |
diff --git a/doc/user/ai_features.md b/doc/user/ai_features.md
index 0ae10b897ec..3dea892509e 100644
--- a/doc/user/ai_features.md
+++ b/doc/user/ai_features.md
@@ -230,6 +230,25 @@ Provide feedback on this experimental feature in [issue 407779](https://gitlab.c
**Data usage**: When you use this feature, the text of public comments on the issue are sent to the large
language model referenced above.
+### Show deployment frequency forecast **(ULTIMATE SAAS)**
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10228) in GitLab 16.2 as an [Experiment](../policy/experiment-beta-support.md#experiment).
+
+This feature is an [Experiment](../policy/experiment-beta-support.md) on GitLab.com.
+
+In CI/CD Analytics, you can view a forecast of deployment frequency:
+
+1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
+1. Select **Analyze > CI/CD analytics**.
+1. Select the **Deployment frequency** tab.
+1. Turn on the **Show forecast** toggle.
+1. On the confirmation dialog, select **Accept testing terms**.
+
+The forecast is displayed as a dotted line on the chart. Data is forecasted for a duration that is half of the selected date range.
+For example, if you select a 30-day range, a forecast for the following 15 days is displayed.
+
+Provide feedback on this experimental feature in [issue 416833](https://gitlab.com/gitlab-org/gitlab/-/issues/416833).
+
## Data Usage
GitLab AI features leverage generative AI to help increase velocity and aim to help make you more productive. Each feature operates independently of other features and is not required for other features to function.
diff --git a/doc/user/project/integrations/webhook_events.md b/doc/user/project/integrations/webhook_events.md
index 8d66f3e48dd..2873711a2f4 100644
--- a/doc/user/project/integrations/webhook_events.md
+++ b/doc/user/project/integrations/webhook_events.md
@@ -611,6 +611,7 @@ Payload example:
}
},
"work_in_progress": false,
+ "draft": false,
"assignee": {
"name": "User1",
"username": "user1",
@@ -898,6 +899,7 @@ Payload example:
"state": "opened",
"blocking_discussions_resolved": true,
"work_in_progress": false,
+ "draft": false,
"first_contribution": true,
"merge_status": "unchecked",
"target_project_id": 14,
@@ -983,6 +985,10 @@ Payload example:
"previous": null,
"current": 1
},
+ "draft": {
+ "previous": true,
+ "current": false
+ }
"updated_at": {
"previous": "2017-09-15 16:50:55 UTC",
"current":"2017-09-15 16:52:00 UTC"
diff --git a/lib/gitlab/background_migration/backfill_default_branch_protection_namespace_setting.rb b/lib/gitlab/background_migration/backfill_default_branch_protection_namespace_setting.rb
new file mode 100644
index 00000000000..8da29a61d61
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_default_branch_protection_namespace_setting.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # This class is used to update the default_branch_protection_defaults column
+ # for user namespaces of the namespace_settings table.
+ class BackfillDefaultBranchProtectionNamespaceSetting < BatchedMigrationJob
+ operation_name :set_default_branch_protection_defaults
+ feature_category :database
+
+ # Migration only version of `namespaces` table
+ class Namespace < ::ApplicationRecord
+ self.table_name = 'namespaces'
+ self.inheritance_column = :_type_disabled
+
+ has_one :namespace_setting,
+ class_name: '::Gitlab::BackgroundMigration::BackfillDefaultBranchProtectionNamespaceSetting::NamespaceSetting'
+ end
+
+ # Migration only version of `namespace_settings` table
+ class NamespaceSetting < ::ApplicationRecord
+ self.table_name = 'namespace_settings'
+ belongs_to :namespace,
+ class_name: '::Gitlab::BackgroundMigration::BackfillDefaultBranchProtectionNamespaceSetting::Namespace'
+ end
+
+ # Migration only version of Gitlab::Access:BranchProtection application code.
+ class BranchProtection
+ attr_reader :level
+
+ def initialize(level)
+ @level = level
+ end
+
+ PROTECTION_NONE = 0
+ PROTECTION_DEV_CAN_PUSH = 1
+ PROTECTION_FULL = 2
+ PROTECTION_DEV_CAN_MERGE = 3
+ PROTECTION_DEV_CAN_INITIAL_PUSH = 4
+
+ DEVELOPER = 30
+ MAINTAINER = 40
+
+ def to_hash
+ case level
+ when PROTECTION_NONE
+ self.class.protection_none
+ when PROTECTION_DEV_CAN_PUSH
+ self.class.protection_partial
+ when PROTECTION_FULL
+ self.class.protected_fully
+ when PROTECTION_DEV_CAN_MERGE
+ self.class.protected_against_developer_pushes
+ when PROTECTION_DEV_CAN_INITIAL_PUSH
+ self.class.protected_after_initial_push
+ end
+ end
+
+ class << self
+ def protection_none
+ {
+ allowed_to_push: [{ 'access_level' => DEVELOPER }],
+ allowed_to_merge: [{ 'access_level' => DEVELOPER }],
+ allow_force_push: true
+ }
+ end
+
+ def protection_partial
+ protection_none.merge(allow_force_push: false)
+ end
+
+ def protected_fully
+ {
+ allowed_to_push: [{ 'access_level' => MAINTAINER }],
+ allowed_to_merge: [{ 'access_level' => MAINTAINER }],
+ allow_force_push: false
+ }
+ end
+
+ def protected_against_developer_pushes
+ {
+ allowed_to_push: [{ 'access_level' => MAINTAINER }],
+ allowed_to_merge: [{ 'access_level' => DEVELOPER }],
+ allow_force_push: true
+ }
+ end
+
+ def protected_after_initial_push
+ {
+ allowed_to_push: [{ 'access_level' => MAINTAINER }],
+ allowed_to_merge: [{ 'access_level' => DEVELOPER }],
+ allow_force_push: true,
+ developer_can_initial_push: true
+ }
+ end
+ end
+ end
+
+ def perform
+ each_sub_batch do |sub_batch|
+ update_default_protection_branch_defaults(sub_batch)
+ end
+ end
+
+ private
+
+ def update_default_protection_branch_defaults(batch)
+ namespace_settings = NamespaceSetting.where(namespace_id: batch.pluck(:namespace_id)).includes(:namespace)
+
+ values_list = namespace_settings.map do |namespace_setting|
+ level = namespace_setting.namespace.default_branch_protection.to_i
+ value = BranchProtection.new(level).to_hash.to_json
+ "(#{namespace_setting.namespace_id}, '#{value}'::jsonb)"
+ end.join(", ")
+
+ sql = <<~SQL
+ WITH new_values (namespace_id, default_branch_protection_defaults) AS (
+ VALUES
+ #{values_list}
+ )
+ UPDATE namespace_settings
+ SET default_branch_protection_defaults = new_values.default_branch_protection_defaults
+ FROM new_values
+ WHERE namespace_settings.namespace_id = new_values.namespace_id;
+ SQL
+
+ connection.execute(sql)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/checks/branch_check.rb b/lib/gitlab/checks/branch_check.rb
index 8be1e1716ec..ba7662cd13d 100644
--- a/lib/gitlab/checks/branch_check.rb
+++ b/lib/gitlab/checks/branch_check.rb
@@ -43,7 +43,7 @@ module Gitlab
def prohibited_branch_checks
return if deletion?
- if branch_name =~ %r{\A\h{40}(/|\z)}
+ if branch_name =~ %r{\A#{Gitlab::Git::Commit::RAW_FULL_SHA_PATTERN}(/|\z)}o
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_hex_branch_name]
end
diff --git a/lib/gitlab/content_security_policy/config_loader.rb b/lib/gitlab/content_security_policy/config_loader.rb
index 669c447c09b..8d1fcf4f916 100644
--- a/lib/gitlab/content_security_policy/config_loader.rb
+++ b/lib/gitlab/content_security_policy/config_loader.rb
@@ -3,66 +3,30 @@
module Gitlab
module ContentSecurityPolicy
class ConfigLoader
- DIRECTIVES = %w(base_uri child_src connect_src default_src font_src
- form_action frame_ancestors frame_src img_src manifest_src
- media_src object_src report_uri script_src style_src worker_src).freeze
-
+ DIRECTIVES = %w[
+ base_uri child_src connect_src default_src font_src form_action
+ frame_ancestors frame_src img_src manifest_src media_src object_src
+ report_uri script_src style_src worker_src
+ ].freeze
DEFAULT_FALLBACK_VALUE = '<default_value>'
+ HTTP_PORTS = [80, 443].freeze
def self.default_enabled
Rails.env.development? || Rails.env.test?
end
def self.default_directives
- directives = {
- 'default_src' => "'self'",
- 'base_uri' => "'self'",
- 'connect_src' => ContentSecurityPolicy::Directives.connect_src,
- 'font_src' => "'self'",
- 'form_action' => "'self' https: http:",
- 'frame_ancestors' => "'self'",
- 'frame_src' => ContentSecurityPolicy::Directives.frame_src,
- 'img_src' => "'self' data: blob: http: https:",
- 'manifest_src' => "'self'",
- 'media_src' => "'self' data: blob: http: https:",
- 'script_src' => ContentSecurityPolicy::Directives.script_src,
- 'style_src' => ContentSecurityPolicy::Directives.style_src,
- 'worker_src' => "#{Gitlab::Utils.append_path(Gitlab.config.gitlab.url, 'assets/')} blob: data:",
- 'object_src' => "'none'",
- 'report_uri' => nil
- }
-
- # connect_src with 'self' includes https/wss variations of the origin,
- # however, safari hasn't covered this yet and we need to explicitly add
- # support for websocket origins until Safari catches up with the specs
- if Rails.env.development?
- allow_webpack_dev_server(directives)
- allow_letter_opener(directives)
- allow_snowplow_micro(directives) if Gitlab::Tracking.snowplow_micro_enabled?
- end
+ directives = default_directives_defaults
+ allow_development_tooling(directives)
allow_websocket_connections(directives)
- allow_cdn(directives, Settings.gitlab.cdn_host) if Settings.gitlab.cdn_host.present?
- allow_zuora(directives) if Gitlab.com?
- # Support for Sentry setup via configuration files will be removed in 16.0
- # in favor of Gitlab::CurrentSettings.
- allow_legacy_sentry(directives) if Gitlab.config.sentry&.enabled && Gitlab.config.sentry&.clientside_dsn
- allow_sentry(directives) if Gitlab::CurrentSettings.try(:sentry_enabled) && Gitlab::CurrentSettings.try(:sentry_clientside_dsn)
+ allow_cdn(directives)
+ allow_zuora(directives)
+ allow_sentry(directives)
allow_framed_gitlab_paths(directives)
- allow_customersdot(directives) if ENV['CUSTOMER_PORTAL_URL'].present?
- allow_review_apps(directives) if ENV['REVIEW_APPS_ENABLED']
-
- # The follow section contains workarounds to patch Safari's lack of support for CSP Level 3
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/343579
- # frame-src was deprecated in CSP level 2 in favor of child-src
- # CSP level 3 "undeprecated" frame-src and browsers fall back on child-src if it's missing
- # However Safari seems to read child-src first so we'll just keep both equal
- append_to_directive(directives, 'child_src', directives['frame_src'])
-
- # Safari also doesn't support worker-src and only checks child-src
- # So for compatibility until it catches up to other browsers we need to
- # append worker-src's content to child-src
- append_to_directive(directives, 'child_src', directives['worker_src'])
+ allow_customersdot(directives)
+ allow_review_apps(directives)
+ csp_level_3_backport(directives)
directives
end
@@ -87,41 +51,72 @@ module Gitlab
private
- def arguments_for(directive)
- # In order to disable a directive, the user can explicitly
- # set a falsy value like nil, false or empty string
- arguments = @merged_csp_directives[directive]
- return unless arguments.present? && arguments.is_a?(String)
+ def self.default_directives_defaults
+ {
+ 'default_src' => "'self'",
+ 'base_uri' => "'self'",
+ 'connect_src' => ContentSecurityPolicy::Directives.connect_src,
+ 'font_src' => "'self'",
+ 'form_action' => "'self' https: http:",
+ 'frame_ancestors' => "'self'",
+ 'frame_src' => ContentSecurityPolicy::Directives.frame_src,
+ 'img_src' => "'self' data: blob: http: https:",
+ 'manifest_src' => "'self'",
+ 'media_src' => "'self' data: blob: http: https:",
+ 'script_src' => ContentSecurityPolicy::Directives.script_src,
+ 'style_src' => ContentSecurityPolicy::Directives.style_src,
+ 'worker_src' => "#{Gitlab::Utils.append_path(Gitlab.config.gitlab.url, 'assets/')} blob: data:",
+ 'object_src' => "'none'",
+ 'report_uri' => nil
+ }
+ end
+
+ # connect_src with 'self' includes https/wss variations of the origin,
+ # however, safari hasn't covered this yet and we need to explicitly add
+ # support for websocket origins until Safari catches up with the specs
+ def self.allow_development_tooling(directives)
+ return unless Rails.env.development?
- arguments.strip.split(' ').map(&:strip)
+ allow_webpack_dev_server(directives)
+ allow_letter_opener(directives)
+ allow_snowplow_micro(directives) if Gitlab::Tracking.snowplow_micro_enabled?
+ end
+
+ def self.allow_webpack_dev_server(directives)
+ secure = Settings.webpack.dev_server['https']
+ host_and_port = "#{Settings.webpack.dev_server['host']}:#{Settings.webpack.dev_server['port']}"
+ http_url = "#{secure ? 'https' : 'http'}://#{host_and_port}"
+ ws_url = "#{secure ? 'wss' : 'ws'}://#{host_and_port}"
+
+ append_to_directive(directives, 'connect_src', "#{http_url} #{ws_url}")
+ end
+
+ def self.allow_letter_opener(directives)
+ url = Gitlab::Utils.append_path(Gitlab.config.gitlab.url, '/rails/letter_opener/')
+ append_to_directive(directives, 'frame_src', url)
+ end
+
+ def self.allow_snowplow_micro(directives)
+ url = URI.join(Gitlab::Tracking::Destinations::SnowplowMicro.new.uri, '/').to_s
+ append_to_directive(directives, 'connect_src', url)
end
def self.allow_websocket_connections(directives)
- http_ports = [80, 443]
host = Gitlab.config.gitlab.host
port = Gitlab.config.gitlab.port
secure = Gitlab.config.gitlab.https
protocol = secure ? 'wss' : 'ws'
ws_url = "#{protocol}://#{host}"
-
- unless http_ports.include?(port)
- ws_url = "#{ws_url}:#{port}"
- end
+ ws_url = "#{ws_url}:#{port}" unless HTTP_PORTS.include?(port)
append_to_directive(directives, 'connect_src', ws_url)
end
- def self.allow_webpack_dev_server(directives)
- secure = Settings.webpack.dev_server['https']
- host_and_port = "#{Settings.webpack.dev_server['host']}:#{Settings.webpack.dev_server['port']}"
- http_url = "#{secure ? 'https' : 'http'}://#{host_and_port}"
- ws_url = "#{secure ? 'wss' : 'ws'}://#{host_and_port}"
-
- append_to_directive(directives, 'connect_src', "#{http_url} #{ws_url}")
- end
+ def self.allow_cdn(directives)
+ cdn_host = Settings.gitlab.cdn_host.presence
+ return unless cdn_host
- def self.allow_cdn(directives, cdn_host)
append_to_directive(directives, 'script_src', cdn_host)
append_to_directive(directives, 'style_src', cdn_host)
append_to_directive(directives, 'font_src', cdn_host)
@@ -129,47 +124,35 @@ module Gitlab
append_to_directive(directives, 'frame_src', cdn_host)
end
- def self.zuora_host
- "https://*.zuora.com/apps/PublicHostedPageLite.do"
- end
-
def self.allow_zuora(directives)
+ return unless Gitlab.com?
+
append_to_directive(directives, 'frame_src', zuora_host)
end
- def self.append_to_directive(directives, directive, text)
- directives[directive] = "#{directives[directive]} #{text}".strip
- end
+ def self.allow_sentry(directives)
+ allow_legacy_sentry(directives) if legacy_sentry_configured?
+ return unless sentry_client_side_dsn_enabled?
- def self.allow_customersdot(directives)
- customersdot_host = ENV['CUSTOMER_PORTAL_URL']
+ sentry_uri = URI(Gitlab::CurrentSettings.sentry_clientside_dsn)
- append_to_directive(directives, 'frame_src', customersdot_host)
+ append_to_directive(directives, 'connect_src', "#{sentry_uri.scheme}://#{sentry_uri.host}")
end
def self.allow_legacy_sentry(directives)
# Support for Sentry setup via configuration files will be removed in 16.0
# in favor of Gitlab::CurrentSettings.
- sentry_dsn = Gitlab.config.sentry.clientside_dsn
- sentry_uri = URI(sentry_dsn)
+ sentry_uri = URI(Gitlab.config.sentry.clientside_dsn)
append_to_directive(directives, 'connect_src', "#{sentry_uri.scheme}://#{sentry_uri.host}")
end
- def self.allow_sentry(directives)
- sentry_dsn = Gitlab::CurrentSettings.sentry_clientside_dsn
- sentry_uri = URI(sentry_dsn)
-
- append_to_directive(directives, 'connect_src', "#{sentry_uri.scheme}://#{sentry_uri.host}")
- end
-
- def self.allow_letter_opener(directives)
- append_to_directive(directives, 'frame_src', Gitlab::Utils.append_path(Gitlab.config.gitlab.url, '/rails/letter_opener/'))
+ def self.legacy_sentry_configured?
+ Gitlab.config.sentry&.enabled && Gitlab.config.sentry&.clientside_dsn
end
- def self.allow_snowplow_micro(directives)
- url = URI.join(Gitlab::Tracking::Destinations::SnowplowMicro.new.uri, '/').to_s
- append_to_directive(directives, 'connect_src', url)
+ def self.sentry_client_side_dsn_enabled?
+ Gitlab::CurrentSettings.try(:sentry_enabled) && Gitlab::CurrentSettings.try(:sentry_clientside_dsn)
end
# Using 'self' in the CSP introduces several CSP bypass opportunities
@@ -180,10 +163,50 @@ module Gitlab
end
end
+ def self.allow_customersdot(directives)
+ customersdot_host = ENV['CUSTOMER_PORTAL_URL'].presence
+ return unless customersdot_host
+
+ append_to_directive(directives, 'frame_src', customersdot_host)
+ end
+
def self.allow_review_apps(directives)
+ return unless ENV['REVIEW_APPS_ENABLED'].presence
+
# Allow-listed to allow POSTs to https://gitlab.com/api/v4/projects/278964/merge_requests/:merge_request_iid/visual_review_discussions
append_to_directive(directives, 'connect_src', 'https://gitlab.com/api/v4/projects/278964/merge_requests/')
end
+
+ # The follow contains workarounds to patch Safari's lack of support for CSP Level 3
+ def self.csp_level_3_backport(directives)
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/343579
+ # frame-src was deprecated in CSP level 2 in favor of child-src
+ # CSP level 3 "undeprecated" frame-src and browsers fall back on child-src if it's missing
+ # However Safari seems to read child-src first so we'll just keep both equal
+ append_to_directive(directives, 'child_src', directives['frame_src'])
+
+ # Safari also doesn't support worker-src and only checks child-src
+ # So for compatibility until it catches up to other browsers we need to
+ # append worker-src's content to child-src
+ append_to_directive(directives, 'child_src', directives['worker_src'])
+ end
+
+ def self.append_to_directive(directives, directive, text)
+ directives[directive] = "#{directives[directive]} #{text}".strip
+ end
+
+ def self.zuora_host
+ "https://*.zuora.com/apps/PublicHostedPageLite.do"
+ end
+
+ def arguments_for(directive)
+ # In order to disable a directive, the user can explicitly
+ # set a falsy value like nil, false or empty string
+ arguments = @merged_csp_directives[directive]
+ return unless arguments.is_a?(String)
+
+ arguments.split(' ')
+ end
end
end
end
diff --git a/lib/gitlab/etag_caching/router/graphql.rb b/lib/gitlab/etag_caching/router/graphql.rb
index 92b21a0859d..7946e768e00 100644
--- a/lib/gitlab/etag_caching/router/graphql.rb
+++ b/lib/gitlab/etag_caching/router/graphql.rb
@@ -14,7 +14,7 @@ module Gitlab
'continuous_integration'
],
[
- %r(\Apipelines/sha/\w{7,40}\z),
+ %r(\Apipelines/sha/\w{#{Gitlab::Git::Commit::MIN_SHA_LENGTH},#{Gitlab::Git::Commit::MAX_SHA_LENGTH}}\z)o,
'ci_editor',
'pipeline_composition'
],
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index ef5c242e68a..4e574d6de74 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -8,7 +8,7 @@ module Gitlab
# https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012
EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
BLANK_SHA = ('0' * 40).freeze
- COMMIT_ID = /\A[0-9a-f]{40}\z/.freeze
+ COMMIT_ID = /\A#{Gitlab::Git::Commit::RAW_FULL_SHA_PATTERN}\z/.freeze
TAG_REF_PREFIX = "refs/tags/"
BRANCH_REF_PREFIX = "refs/heads/"
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index b104610038c..571dde6fcfc 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -12,7 +12,20 @@ module Gitlab
attr_accessor :raw_commit, :head
MAX_COMMIT_MESSAGE_DISPLAY_SIZE = 10.megabytes
+
+ SHA1_LENGTH = 40
+ SHA256_LENGTH = 64
+
MIN_SHA_LENGTH = 7
+ MAX_SHA_LENGTH = SHA256_LENGTH
+
+ RAW_SHA_PATTERN = "\\h{#{MIN_SHA_LENGTH},#{MAX_SHA_LENGTH}}".freeze
+ SHA_PATTERN = /#{RAW_SHA_PATTERN}/
+ # Match a full SHA. Note that because this expression is not anchored it will match any SHA that is at
+ # least SHA1_LENGTH long.
+ RAW_FULL_SHA_PATTERN = "\\h{#{SHA1_LENGTH}}(?:\\h{#{SHA256_LENGTH - SHA1_LENGTH}})?".freeze
+ FULL_SHA_PATTERN = /#{RAW_FULL_SHA_PATTERN}/
+
SERIALIZE_KEYS = [
:id, :message, :parent_ids,
:authored_date, :author_name, :author_email,
diff --git a/lib/gitlab/hook_data/merge_request_builder.rb b/lib/gitlab/hook_data/merge_request_builder.rb
index a6ca8323a20..22af06ba09b 100644
--- a/lib/gitlab/hook_data/merge_request_builder.rb
+++ b/lib/gitlab/hook_data/merge_request_builder.rb
@@ -10,6 +10,7 @@ module Gitlab
blocking_discussions_resolved
created_at
description
+ draft
head_pipeline_id
id
iid
@@ -55,6 +56,7 @@ module Gitlab
target: merge_request.target_project.hook_attrs,
last_commit: merge_request.diff_head_commit&.hook_attrs,
work_in_progress: merge_request.draft?,
+ draft: merge_request.draft?,
total_time_spent: merge_request.total_time_spent,
time_change: merge_request.time_change,
human_total_time_spent: merge_request.human_total_time_spent,
diff --git a/lib/gitlab/x509/signature.rb b/lib/gitlab/x509/signature.rb
index d101a6d2522..649e5379927 100644
--- a/lib/gitlab/x509/signature.rb
+++ b/lib/gitlab/x509/signature.rb
@@ -42,11 +42,13 @@ module Gitlab
!verified_signature ||
signed_by_user.nil?
- if signed_by_user.verified_emails.include?(@email.downcase) && certificate_email.casecmp?(@email)
- :verified
- else
- :unverified
+ if signed_by_user.verified_emails.include?(@email.downcase)
+ return :verified if certificate_emails.find do |ce|
+ ce.casecmp?(@email)
+ end
end
+
+ :unverified
end
private
@@ -173,18 +175,13 @@ module Gitlab
end
def certificate_email
- email = nil
+ certificate_emails.first
+ end
- get_certificate_extension('subjectAltName').split(',').each do |item|
- if item.strip.start_with?("email")
- email = item.split('email:')[1]
- break
- end
+ def certificate_emails
+ get_certificate_extension('subjectAltName').split(',').each.with_object([]) do |item, emails|
+ emails << item.split('email:')[1] if item.strip.start_with?("email")
end
-
- return if email.nil?
-
- email
end
def x509_issuer
@@ -206,6 +203,7 @@ module Gitlab
subject_key_identifier: certificate_subject_key_identifier,
subject: certificate_subject,
email: certificate_email,
+ emails: certificate_emails,
serial_number: cert.serial.to_i,
x509_issuer_id: x509_issuer.id
}
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 772d563c6dc..834e89c6ee0 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2638,15 +2638,21 @@ msgstr ""
msgid "Active"
msgstr ""
-msgid "Active %{accessTokenTypePlural} (%{totalAccessTokens})"
-msgstr ""
-
msgid "Active Sessions"
msgstr ""
msgid "Active chat names (%{count})"
msgstr ""
+msgid "Active group access tokens"
+msgstr ""
+
+msgid "Active personal access tokens"
+msgstr ""
+
+msgid "Active project access tokens"
+msgstr ""
+
msgid "Activity"
msgstr ""
@@ -2866,6 +2872,9 @@ msgstr ""
msgid "Add new key"
msgstr ""
+msgid "Add new token"
+msgstr ""
+
msgid "Add new webhook"
msgstr ""
@@ -23585,6 +23594,9 @@ msgstr ""
msgid "Impersonation has been disabled"
msgstr ""
+msgid "Impersonation tokens"
+msgstr ""
+
msgid "Import"
msgstr ""
@@ -37793,6 +37805,9 @@ msgstr ""
msgid "ProtectedEnvironment|Protected Environment (%{protected_environments_count})"
msgstr ""
+msgid "ProtectedEnvironment|Protected Environments"
+msgstr ""
+
msgid "ProtectedEnvironment|Required approvals"
msgstr ""
@@ -37808,12 +37823,15 @@ msgstr ""
msgid "ProtectedEnvironment|Select users"
msgstr ""
-msgid "ProtectedEnvironment|There are currently no protected environments. Protect an environment with this form."
+msgid "ProtectedEnvironment|There are currently no protected environments."
msgstr ""
msgid "ProtectedEnvironment|Unprotect"
msgstr ""
+msgid "ProtectedEnvironment|Unprotect environment"
+msgstr ""
+
msgid "ProtectedEnvironment|Users with at least the Developer role can write to unprotected environments. Are you sure you want to unprotect %{environment_name}?"
msgstr ""
diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
index 0350c8ab066..543dc2cc2a6 100644
--- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb
+++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe 'Admin > Users > Impersonation Tokens', :js, feature_category: :s
name = 'Hello World'
visit admin_user_impersonation_tokens_path(user_id: user.username)
+ click_button 'Add new token'
fill_in "Token name", with: name
# Set date to 1st of next month
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index 65fe1330be2..094855393be 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -18,6 +18,8 @@ RSpec.describe 'Profile > Personal Access Tokens', :js, feature_category: :user_
name = 'My PAT'
visit profile_personal_access_tokens_path
+
+ click_button 'Add new token'
fill_in "Token name", with: name
# Set date to 1st of next month
@@ -43,6 +45,8 @@ RSpec.describe 'Profile > Personal Access Tokens', :js, feature_category: :user_
it "displays an error message" do
number_tokens_before = PersonalAccessToken.count
visit profile_personal_access_tokens_path
+
+ click_button 'Add new token'
fill_in "Token name", with: 'My PAT'
click_on "Create personal access token"
@@ -145,6 +149,7 @@ RSpec.describe 'Profile > Personal Access Tokens', :js, feature_category: :user_
visit profile_personal_access_tokens_path({ name: name, scopes: scopes })
+ click_button 'Add new token'
expect(page).to have_field("Token name", with: name)
expect(find("#personal_access_token_scopes_api")).to be_checked
expect(find("#personal_access_token_scopes_read_user")).to be_checked
diff --git a/spec/features/projects/settings/access_tokens_spec.rb b/spec/features/projects/settings/access_tokens_spec.rb
index 210815f341c..9025bd9052e 100644
--- a/spec/features/projects/settings/access_tokens_spec.rb
+++ b/spec/features/projects/settings/access_tokens_spec.rb
@@ -49,6 +49,7 @@ RSpec.describe 'Project > Settings > Access Tokens', :js, feature_category: :use
it 'shows Owner option' do
visit resource_settings_access_tokens_path
+ click_button 'Add new token'
expect(role_dropdown_options).to include('Owner')
end
end
@@ -63,6 +64,7 @@ RSpec.describe 'Project > Settings > Access Tokens', :js, feature_category: :use
it 'does not show Owner option for a maintainer' do
visit resource_settings_access_tokens_path
+ click_button 'Add new token'
expect(role_dropdown_options).not_to include('Owner')
end
end
@@ -81,6 +83,7 @@ RSpec.describe 'Project > Settings > Access Tokens', :js, feature_category: :use
it 'shows access token creation form and text' do
visit project_settings_access_tokens_path(personal_project)
+ click_button 'Add new token'
expect(page).to have_selector('#js-new-access-token-form')
end
end
diff --git a/spec/features/projects/settings/user_searches_in_settings_spec.rb b/spec/features/projects/settings/user_searches_in_settings_spec.rb
index 978b678c334..1ca4b761788 100644
--- a/spec/features/projects/settings/user_searches_in_settings_spec.rb
+++ b/spec/features/projects/settings/user_searches_in_settings_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe 'User searches project settings', :js, feature_category: :groups_
visit project_settings_access_tokens_path(project)
end
- it_behaves_like 'can highlight results', 'Expiration date'
+ it_behaves_like 'can highlight results', 'Token name'
end
context 'in Repository page' do
diff --git a/spec/frontend/access_tokens/components/access_token_table_app_spec.js b/spec/frontend/access_tokens/components/access_token_table_app_spec.js
index 2fa14810578..5236f38dc35 100644
--- a/spec/frontend/access_tokens/components/access_token_table_app_spec.js
+++ b/spec/frontend/access_tokens/components/access_token_table_app_spec.js
@@ -91,24 +91,6 @@ describe('~/access_tokens/components/access_token_table_app', () => {
expect(cells.at(0).text()).toBe(noTokensMessage);
});
- it('should show a title indicating the amount of tokens', () => {
- createComponent();
-
- expect(wrapper.find('h5').text()).toBe(
- sprintf(__('Active %{accessTokenTypePlural} (%{totalAccessTokens})'), {
- accessTokenTypePlural,
- totalAccessTokens: defaultActiveAccessTokens.length,
- }),
- );
- });
-
- it('should render information section', () => {
- const info = 'This is my information';
- createComponent({ information: info });
-
- expect(wrapper.findByTestId('information-section').text()).toBe(info);
- });
-
describe('table headers', () => {
it('should include `Action` column', () => {
createComponent();
diff --git a/spec/frontend/access_tokens/components/new_access_token_app_spec.js b/spec/frontend/access_tokens/components/new_access_token_app_spec.js
index 70f77932ccf..d51ac638f0e 100644
--- a/spec/frontend/access_tokens/components/new_access_token_app_spec.js
+++ b/spec/frontend/access_tokens/components/new_access_token_app_spec.js
@@ -23,6 +23,8 @@ describe('~/access_tokens/components/new_access_token_app', () => {
};
const findButtonEl = () => document.querySelector('[type=submit]');
+ const findGlAlertError = () => wrapper.findByTestId('error-message');
+ const findGlAlertSuccess = () => wrapper.findByTestId('success-message');
const triggerSuccess = async (newToken = 'new token') => {
wrapper
@@ -57,7 +59,7 @@ describe('~/access_tokens/components/new_access_token_app', () => {
it('should render nothing', () => {
expect(wrapper.findComponent(InputCopyToggleVisibility).exists()).toBe(false);
- expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
+ expect(findGlAlertError().exists()).toBe(false);
});
describe('on success', () => {
@@ -65,7 +67,8 @@ describe('~/access_tokens/components/new_access_token_app', () => {
const newToken = '12345';
await triggerSuccess(newToken);
- expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
+ expect(findGlAlertError().exists()).toBe(false);
+ expect(findGlAlertSuccess().exists()).toBe(true);
const InputCopyToggleVisibilityComponent = wrapper.findComponent(InputCopyToggleVisibility);
expect(InputCopyToggleVisibilityComponent.props('value')).toBe(newToken);
@@ -82,7 +85,7 @@ describe('~/access_tokens/components/new_access_token_app', () => {
const newToken = '12345';
await triggerSuccess(newToken);
- expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
+ expect(findGlAlertError().exists()).toBe(false);
const inputAttributes = wrapper
.findByLabelText(sprintf(__('Your new %{accessTokenType}'), { accessTokenType }))
@@ -135,7 +138,7 @@ describe('~/access_tokens/components/new_access_token_app', () => {
expect(wrapper.findComponent(InputCopyToggleVisibility).exists()).toBe(false);
- let GlAlertComponent = wrapper.findComponent(GlAlert);
+ let GlAlertComponent = findGlAlertError();
expect(GlAlertComponent.props('title')).toBe(__('The form contains the following errors:'));
expect(GlAlertComponent.props('variant')).toBe('danger');
let itemEls = wrapper.findAll('li');
diff --git a/spec/frontend/access_tokens/index_spec.js b/spec/frontend/access_tokens/index_spec.js
index c1158e0d124..7d4d73b00b2 100644
--- a/spec/frontend/access_tokens/index_spec.js
+++ b/spec/frontend/access_tokens/index_spec.js
@@ -50,7 +50,6 @@ describe('access tokens', () => {
initialActiveAccessTokens,
// Default values
- information: undefined,
noActiveTokensMessage: sprintf(__('This user has no active %{accessTokenTypePlural}.'), {
accessTokenTypePlural,
}),
@@ -59,14 +58,12 @@ describe('access tokens', () => {
});
it('mounts the component and provides all values', () => {
- const information = 'Additional information';
const noActiveTokensMessage = 'This group has no active access tokens.';
setHTMLFixture(
`<div id="js-access-token-table-app"
data-access-token-type="${accessTokenType}"
data-access-token-type-plural="${accessTokenTypePlural}"
data-initial-active-access-tokens=${JSON.stringify(initialActiveAccessTokens)}
- data-information="${information}"
data-no-active-tokens-message="${noActiveTokensMessage}"
data-show-role
>
@@ -82,7 +79,6 @@ describe('access tokens', () => {
accessTokenType,
accessTokenTypePlural,
initialActiveAccessTokens,
- information,
noActiveTokensMessage,
showRole: true,
});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
index bb520cd7a47..9c5f9db9762 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
+++ b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
@@ -18,10 +18,12 @@ exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue render
<status-icon-stub level=\\"2\\" name=\\"MyWidget\\" iconname=\\"success\\"></status-icon-stub>
<div class=\\"gl-w-full gl-display-flex\\">
<div class=\\"gl-display-flex gl-flex-grow-1\\">
- <div class=\\"gl-display-flex gl-flex-grow-1 gl-flex-direction-column\\">
- <p class=\\"gl-mb-0 gl-mr-1\\">Main text for the row</p>
- <gl-link-stub href=\\"https://gitlab.com\\">Optional link to display after text</gl-link-stub>
- <!---->
+ <div class=\\"gl-display-flex gl-flex-grow-1 gl-align-items-baseline\\">
+ <div>
+ <p class=\\"gl-mb-0 gl-mr-1\\">Main text for the row</p>
+ <gl-link-stub href=\\"https://gitlab.com\\">Optional link to display after text</gl-link-stub>
+ <!---->
+ </div>
<gl-badge-stub size=\\"md\\" variant=\\"info\\" iconsize=\\"md\\">
Badge is optional. Text to be displayed inside badge
</gl-badge-stub>
@@ -44,10 +46,12 @@ exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue render
<!---->
<div class=\\"gl-w-full gl-display-flex\\">
<div class=\\"gl-display-flex gl-flex-grow-1\\">
- <div class=\\"gl-display-flex gl-flex-grow-1 gl-flex-direction-column\\">
- <p class=\\"gl-mb-0 gl-mr-1\\">This is recursive. It will be listed in level 3.</p>
- <!---->
- <!---->
+ <div class=\\"gl-display-flex gl-flex-grow-1 gl-align-items-baseline\\">
+ <div>
+ <p class=\\"gl-mb-0 gl-mr-1\\">This is recursive. It will be listed in level 3.</p>
+ <!---->
+ <!---->
+ </div>
<!---->
</div>
<actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub>
diff --git a/spec/graphql/types/custom_emoji_type_spec.rb b/spec/graphql/types/custom_emoji_type_spec.rb
index 7f3c99e4b63..17697321602 100644
--- a/spec/graphql/types/custom_emoji_type_spec.rb
+++ b/spec/graphql/types/custom_emoji_type_spec.rb
@@ -3,9 +3,20 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['CustomEmoji'] do
+ expected_fields = %w[
+ id
+ name
+ url
+ external
+ created_at
+ user_permissions
+ ]
+
specify { expect(described_class.graphql_name).to eq('CustomEmoji') }
specify { expect(described_class).to require_graphql_authorizations(:read_custom_emoji) }
- specify { expect(described_class).to have_graphql_fields(:id, :name, :url, :external) }
+ specify { expect(described_class).to have_graphql_fields(*expected_fields) }
+
+ specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::CustomEmoji) }
end
diff --git a/spec/lib/api/validations/validators/git_sha_spec.rb b/spec/lib/api/validations/validators/git_sha_spec.rb
index ae6be52a4c7..2ae3fca7a6e 100644
--- a/spec/lib/api/validations/validators/git_sha_spec.rb
+++ b/spec/lib/api/validations/validators/git_sha_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe API::Validations::Validators::GitSha do
let(:sha) { RepoHelpers.sample_commit.id }
let(:short_sha) { sha[0, Gitlab::Git::Commit::MIN_SHA_LENGTH] }
let(:too_short_sha) { sha[0, Gitlab::Git::Commit::MIN_SHA_LENGTH - 1] }
+ let(:too_long_sha) { "a" * (Gitlab::Git::Commit::MAX_SHA_LENGTH + 1) }
subject do
described_class.new(['test'], {}, false, scope.new)
@@ -29,7 +30,7 @@ RSpec.describe API::Validations::Validators::GitSha do
context 'invalid sha' do
it 'raises a validation error' do
- expect_validation_error('test' => "#{sha}2") # Sha length > 40
+ expect_validation_error('test' => too_long_sha) # too long SHA
expect_validation_error('test' => 'somestring')
expect_validation_error('test' => too_short_sha) # sha length < MIN_SHA_LENGTH (7)
end
diff --git a/spec/lib/gitlab/background_migration/backfill_default_branch_protection_namespace_setting_spec.rb b/spec/lib/gitlab/background_migration/backfill_default_branch_protection_namespace_setting_spec.rb
new file mode 100644
index 00000000000..62c9e240b7a
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_default_branch_protection_namespace_setting_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillDefaultBranchProtectionNamespaceSetting,
+ schema: 20230724071541,
+ feature_category: :database do
+ let(:namespaces_table) { table(:namespaces) }
+ let(:namespace_settings_table) { table(:namespace_settings) }
+
+ subject(:perform_migration) do
+ described_class.new(
+ start_id: 1,
+ end_id: 30,
+ batch_table: :namespace_settings,
+ batch_column: :namespace_id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ ).perform
+ end
+
+ before do
+ namespaces_table.create!(id: 1, name: 'group_namespace', path: 'path-1', type: 'Group',
+ default_branch_protection: 0)
+ namespaces_table.create!(id: 2, name: 'user_namespace', path: 'path-2', type: 'User', default_branch_protection: 1)
+ namespaces_table.create!(id: 3, name: 'user_three_namespace', path: 'path-3', type: 'User',
+ default_branch_protection: 2)
+ namespaces_table.create!(id: 4, name: 'group_four_namespace', path: 'path-4', type: 'Group',
+ default_branch_protection: 3)
+ namespaces_table.create!(id: 5, name: 'group_five_namespace', path: 'path-5', type: 'Group',
+ default_branch_protection: 4)
+
+ namespace_settings_table.create!(namespace_id: 1, default_branch_protection_defaults: {})
+ namespace_settings_table.create!(namespace_id: 2, default_branch_protection_defaults: {})
+ namespace_settings_table.create!(namespace_id: 3, default_branch_protection_defaults: {})
+ namespace_settings_table.create!(namespace_id: 4, default_branch_protection_defaults: {})
+ namespace_settings_table.create!(namespace_id: 5, default_branch_protection_defaults: {})
+ end
+
+ it 'updates default_branch_protection_defaults to a correct value', :aggregate_failures do
+ expect(ActiveRecord::QueryRecorder.new { perform_migration }.count).to eq(16)
+
+ expect(migrated_attribute(1)).to eq({ "allow_force_push" => true,
+ "allowed_to_merge" => [{ "access_level" => 30 }],
+ "allowed_to_push" => [{ "access_level" => 30 }] })
+ expect(migrated_attribute(2)).to eq({ "allow_force_push" => false,
+ "allowed_to_merge" => [{ "access_level" => 30 }],
+ "allowed_to_push" => [{ "access_level" => 30 }] })
+ expect(migrated_attribute(3)).to eq({ "allow_force_push" => false,
+ "allowed_to_merge" => [{ "access_level" => 40 }],
+ "allowed_to_push" => [{ "access_level" => 40 }] })
+ expect(migrated_attribute(4)).to eq({ "allow_force_push" => true,
+ "allowed_to_merge" => [{ "access_level" => 30 }],
+ "allowed_to_push" => [{ "access_level" => 40 }] })
+ expect(migrated_attribute(5)).to eq({ "allow_force_push" => true,
+ "allowed_to_merge" => [{ "access_level" => 30 }],
+ "allowed_to_push" => [{ "access_level" => 40 }],
+ "developer_can_initial_push" => true })
+ end
+
+ def migrated_attribute(namespace_id)
+ namespace_settings_table.find(namespace_id).default_branch_protection_defaults
+ end
+end
diff --git a/spec/lib/gitlab/checks/branch_check_spec.rb b/spec/lib/gitlab/checks/branch_check_spec.rb
index 7ce267c535f..9f6d4c7fd36 100644
--- a/spec/lib/gitlab/checks/branch_check_spec.rb
+++ b/spec/lib/gitlab/checks/branch_check_spec.rb
@@ -32,6 +32,18 @@ RSpec.describe Gitlab::Checks::BranchCheck, feature_category: :source_code_manag
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.")
end
+ it "prohibits 64-character hexadecimal branch names" do
+ allow(subject).to receive(:branch_name).and_return("09b9fd3ea68e9b95a51b693a29568c898e27d1476bbd83c825664f18467fc175")
+
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.")
+ end
+
+ it "prohibits 64-character hexadecimal branch names as the start of a path" do
+ allow(subject).to receive(:branch_name).and_return("09b9fd3ea68e9b95a51b693a29568c898e27d1476bbd83c825664f18467fc175/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
allow(subject).to receive(:branch_name).and_return("267208abfe40e546f5e847444276f7d43a39503e-fix")
diff --git a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
index b40829d72a0..44887a86aff 100644
--- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
+++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
+RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader, feature_category: :shared do
let(:policy) { ActionDispatch::ContentSecurityPolicy.new }
let(:csp_config) do
{
@@ -29,7 +29,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
context 'when in production' do
before do
- allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))
+ stub_rails_env('production')
end
it 'is disabled' do
@@ -40,6 +40,16 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
describe '.default_directives' do
let(:directives) { described_class.default_directives }
+ let(:child_src) { directives['child_src'] }
+ let(:connect_src) { directives['connect_src'] }
+ let(:font_src) { directives['font_src'] }
+ let(:frame_src) { directives['frame_src'] }
+ let(:img_src) { directives['img_src'] }
+ let(:media_src) { directives['media_src'] }
+ let(:report_uri) { directives['report_uri'] }
+ let(:script_src) { directives['script_src'] }
+ let(:style_src) { directives['style_src'] }
+ let(:worker_src) { directives['worker_src'] }
it 'returns default directives' do
directive_names = (described_class::DIRECTIVES - ['report_uri'])
@@ -49,68 +59,167 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
expect(directives.has_key?('report_uri')).to be_truthy
- expect(directives['report_uri']).to be_nil
- expect(directives['child_src']).to eq("#{directives['frame_src']} #{directives['worker_src']}")
+ expect(report_uri).to be_nil
+ expect(child_src).to eq("#{frame_src} #{worker_src}")
end
describe 'the images-src directive' do
it 'can be loaded from anywhere' do
- expect(directives['img_src']).to include('http: https:')
+ expect(img_src).to include('http: https:')
end
end
describe 'the media-src directive' do
it 'can be loaded from anywhere' do
- expect(directives['media_src']).to include('http: https:')
+ expect(media_src).to include('http: https:')
end
end
- context 'adds all websocket origins to support Safari' do
+ describe 'Webpack dev server websocket connections' do
+ let(:webpack_dev_server_host) { 'webpack-dev-server.com' }
+ let(:webpack_dev_server_port) { '9999' }
+ let(:webpack_dev_server_https) { true }
+
+ before do
+ stub_config_setting(
+ webpack: { dev_server: {
+ host: webpack_dev_server_host,
+ webpack_dev_server_port: webpack_dev_server_port,
+ https: webpack_dev_server_https
+ } }
+ )
+ end
+
+ context 'when in production' do
+ before do
+ stub_rails_env('production')
+ end
+
+ context 'with secure domain' do
+ it 'does not include webpack dev server in connect-src' do
+ expect(connect_src).not_to include(webpack_dev_server_host)
+ expect(connect_src).not_to include(webpack_dev_server_port)
+ end
+ end
+
+ context 'with insecure domain' do
+ let(:webpack_dev_server_https) { false }
+
+ it 'does not include webpack dev server in connect-src' do
+ expect(connect_src).not_to include(webpack_dev_server_host)
+ expect(connect_src).not_to include(webpack_dev_server_port)
+ end
+ end
+ end
+
+ context 'when in development' do
+ before do
+ stub_rails_env('development')
+ end
+
+ context 'with secure domain' do
+ before do
+ stub_config_setting(host: webpack_dev_server_host, port: webpack_dev_server_port, https: true)
+ end
+
+ it 'includes secure websocket url for webpack dev server in connect-src' do
+ expect(connect_src).to include("wss://#{webpack_dev_server_host}:#{webpack_dev_server_port}")
+ expect(connect_src).not_to include("ws://#{webpack_dev_server_host}:#{webpack_dev_server_port}")
+ end
+ end
+
+ context 'with insecure domain' do
+ before do
+ stub_config_setting(host: webpack_dev_server_host, port: webpack_dev_server_port, https: false)
+ end
+
+ it 'includes insecure websocket url for webpack dev server in connect-src' do
+ expect(connect_src).not_to include("wss://#{webpack_dev_server_host}:#{webpack_dev_server_port}")
+ expect(connect_src).to include("ws://#{webpack_dev_server_host}:#{webpack_dev_server_port}")
+ end
+ end
+ end
+ end
+
+ describe 'Websocket connections' do
it 'with insecure domain' do
stub_config_setting(host: 'example.com', https: false)
- expect(directives['connect_src']).to eq("'self' ws://example.com")
+ expect(connect_src).to eq("'self' ws://example.com")
end
it 'with secure domain' do
stub_config_setting(host: 'example.com', https: true)
- expect(directives['connect_src']).to eq("'self' wss://example.com")
+ expect(connect_src).to eq("'self' wss://example.com")
end
it 'with custom port' do
stub_config_setting(host: 'example.com', port: '1234')
- expect(directives['connect_src']).to eq("'self' ws://example.com:1234")
+ expect(connect_src).to eq("'self' ws://example.com:1234")
end
it 'with custom port and secure domain' do
stub_config_setting(host: 'example.com', https: true, port: '1234')
- expect(directives['connect_src']).to eq("'self' wss://example.com:1234")
+ expect(connect_src).to eq("'self' wss://example.com:1234")
+ end
+
+ it 'when port is included in HTTP_PORTS' do
+ described_class::HTTP_PORTS.each do |port|
+ stub_config_setting(host: 'example.com', https: true, port: port)
+ expect(connect_src).to eq("'self' wss://example.com")
+ end
end
end
- context 'when CDN host is defined' do
+ describe 'CDN connections' do
before do
- stub_config_setting(cdn_host: 'https://cdn.example.com')
+ allow(described_class).to receive(:allow_letter_opener)
+ allow(described_class).to receive(:allow_zuora)
+ allow(described_class).to receive(:allow_framed_gitlab_paths)
+ allow(described_class).to receive(:allow_customersdot)
+ allow(described_class).to receive(:csp_level_3_backport)
+ end
+
+ context 'when CDN host is defined' do
+ let(:cdn_host) { 'https://cdn.example.com' }
+
+ before do
+ stub_config_setting(cdn_host: cdn_host)
+ end
+
+ it 'adds CDN host to CSP' do
+ expect(script_src).to include(cdn_host)
+ expect(style_src).to include(cdn_host)
+ expect(font_src).to include(cdn_host)
+ expect(worker_src).to include(cdn_host)
+ expect(frame_src).to include(cdn_host)
+ end
end
- it 'adds CDN host to CSP' do
- expect(directives['script_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.script_src + " https://cdn.example.com")
- expect(directives['style_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.style_src + " https://cdn.example.com")
- expect(directives['font_src']).to eq("'self' https://cdn.example.com")
- expect(directives['worker_src']).to eq('http://localhost/assets/ blob: data: https://cdn.example.com')
- expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " https://cdn.example.com http://localhost/admin/ http://localhost/assets/ http://localhost/-/speedscope/index.html http://localhost/-/sandbox/")
+ context 'when CDN host is undefined' do
+ before do
+ stub_config_setting(cdn_host: nil)
+ end
+
+ it 'does not include CDN host in CSP' do
+ expect(script_src).to eq(::Gitlab::ContentSecurityPolicy::Directives.script_src)
+ expect(style_src).to eq(::Gitlab::ContentSecurityPolicy::Directives.style_src)
+ expect(font_src).to eq("'self'")
+ expect(worker_src).to eq("http://localhost/assets/ blob: data:")
+ expect(frame_src).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src)
+ end
end
end
describe 'Zuora directives' do
context 'when on SaaS', :saas do
it 'adds Zuora host to CSP' do
- expect(directives['frame_src']).to include('https://*.zuora.com/apps/PublicHostedPageLite.do')
+ expect(frame_src).to include('https://*.zuora.com/apps/PublicHostedPageLite.do')
end
end
context 'when is not Gitlab.com?' do
it 'does not add Zuora host to CSP' do
- expect(directives['frame_src']).not_to include('https://*.zuora.com/apps/PublicHostedPageLite.do')
+ expect(frame_src).not_to include('https://*.zuora.com/apps/PublicHostedPageLite.do')
end
end
end
@@ -131,7 +240,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
it 'adds legacy sentry path to CSP' do
- expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com dummy://legacy-sentry.example.com")
+ expect(connect_src).to eq("'self' ws://gitlab.example.com dummy://legacy-sentry.example.com")
end
end
@@ -143,7 +252,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
it 'adds new sentry path to CSP' do
- expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com dummy://sentry.example.com")
+ expect(connect_src).to eq("'self' ws://gitlab.example.com dummy://sentry.example.com")
end
end
@@ -159,11 +268,22 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
it 'config is backwards compatible, does not add sentry path to CSP' do
- expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com")
+ expect(connect_src).to eq("'self' ws://gitlab.example.com")
end
end
context 'when legacy sentry and sentry are both configured' do
+ let(:connect_src_expectation) do
+ # rubocop:disable Lint/PercentStringArray
+ %w[
+ 'self'
+ ws://gitlab.example.com
+ dummy://legacy-sentry.example.com
+ dummy://sentry.example.com
+ ].join(' ')
+ # rubocop:enable Lint/PercentStringArray
+ end
+
before do
allow(Gitlab.config.sentry).to receive(:enabled).and_return(true)
allow(Gitlab.config.sentry).to receive(:clientside_dsn).and_return(legacy_dsn)
@@ -173,24 +293,57 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
it 'adds both sentry paths to CSP' do
- expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com dummy://legacy-sentry.example.com dummy://sentry.example.com")
+ expect(connect_src).to eq(connect_src_expectation)
end
end
end
- context 'when CUSTOMER_PORTAL_URL is set' do
- let(:customer_portal_url) { 'https://customers.example.com' }
+ describe 'Customer portal frames' do
+ context 'when CUSTOMER_PORTAL_URL is set' do
+ let(:customer_portal_url) { 'https://customers.example.com' }
+ let(:frame_src_expectation) do
+ [
+ ::Gitlab::ContentSecurityPolicy::Directives.frame_src,
+ 'http://localhost/admin/',
+ 'http://localhost/assets/',
+ 'http://localhost/-/speedscope/index.html',
+ 'http://localhost/-/sandbox/',
+ customer_portal_url
+ ].join(' ')
+ end
- before do
- stub_env('CUSTOMER_PORTAL_URL', customer_portal_url)
+ before do
+ stub_env('CUSTOMER_PORTAL_URL', customer_portal_url)
+ end
+
+ it 'adds CUSTOMER_PORTAL_URL to CSP' do
+ expect(frame_src).to eq(frame_src_expectation)
+ end
end
- it 'adds CUSTOMER_PORTAL_URL to CSP' do
- expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " http://localhost/admin/ http://localhost/assets/ http://localhost/-/speedscope/index.html http://localhost/-/sandbox/ #{customer_portal_url}")
+ context 'when CUSTOMER_PORTAL_URL is blank' do
+ let(:customer_portal_url) { '' }
+ let(:frame_src_expectation) do
+ [
+ ::Gitlab::ContentSecurityPolicy::Directives.frame_src,
+ 'http://localhost/admin/',
+ 'http://localhost/assets/',
+ 'http://localhost/-/speedscope/index.html',
+ 'http://localhost/-/sandbox/'
+ ].join(' ')
+ end
+
+ before do
+ stub_env('CUSTOMER_PORTAL_URL', customer_portal_url)
+ end
+
+ it 'adds CUSTOMER_PORTAL_URL to CSP' do
+ expect(frame_src).to eq(frame_src_expectation)
+ end
end
end
- context 'letter_opener application URL' do
+ describe 'letter_opener application URL' do
let(:gitlab_url) { 'http://gitlab.example.com' }
let(:letter_opener_url) { "#{gitlab_url}/rails/letter_opener/" }
@@ -200,21 +353,21 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
context 'when in production' do
before do
- allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))
+ stub_rails_env('production')
end
it 'does not add letter_opener to CSP' do
- expect(directives['frame_src']).not_to include(letter_opener_url)
+ expect(frame_src).not_to include(letter_opener_url)
end
end
context 'when in development' do
before do
- allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development'))
+ stub_rails_env('development')
end
it 'adds letter_opener to CSP' do
- expect(directives['frame_src']).to include(letter_opener_url)
+ expect(frame_src).to include(letter_opener_url)
end
end
end
@@ -234,7 +387,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
it 'does not add Snowplow Micro URL to connect-src' do
- expect(directives['connect_src']).not_to include(snowplow_micro_url)
+ expect(connect_src).not_to include(snowplow_micro_url)
end
end
@@ -244,7 +397,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
it 'adds Snowplow Micro URL with trailing slash to connect-src' do
- expect(directives['connect_src']).to match(Regexp.new(snowplow_micro_url))
+ expect(connect_src).to match(Regexp.new(snowplow_micro_url))
end
context 'when not enabled using config' do
@@ -253,7 +406,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
it 'does not add Snowplow Micro URL to connect-src' do
- expect(directives['connect_src']).not_to include(snowplow_micro_url)
+ expect(connect_src).not_to include(snowplow_micro_url)
end
end
@@ -262,8 +415,18 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
stub_env('REVIEW_APPS_ENABLED', 'true')
end
- it 'adds gitlab-org/gitlab merge requests API endpoint to CSP' do
- expect(directives['connect_src']).to include('https://gitlab.com/api/v4/projects/278964/merge_requests/')
+ it "includes review app's merge requests API endpoint in the CSP" do
+ expect(connect_src).to include('https://gitlab.com/api/v4/projects/278964/merge_requests/')
+ end
+ end
+
+ context 'when REVIEW_APPS_ENABLED is blank' do
+ before do
+ stub_env('REVIEW_APPS_ENABLED', '')
+ end
+
+ it "does not include review app's merge requests API endpoint in the CSP" do
+ expect(connect_src).not_to include('https://gitlab.com/api/v4/projects/278964/merge_requests/')
end
end
end
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 3b1dbf7d602..5c4be1003c3 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -691,6 +691,100 @@ RSpec.describe Gitlab::Git::Commit, feature_category: :source_code_management do
end
end
+ describe 'SHA patterns' do
+ shared_examples 'a SHA-matching pattern' do
+ let(:expected_match) { sha }
+
+ shared_examples 'a match' do
+ it 'matches the pattern' do
+ expect(value).to match(pattern)
+ expect(pattern.match(value).to_a).to eq([expected_match])
+ end
+ end
+
+ shared_examples 'no match' do
+ it 'does not match the pattern' do
+ expect(value).not_to match(pattern)
+ end
+ end
+
+ shared_examples 'a SHA pattern' do
+ context "with too short value" do
+ let(:value) { sha[0, described_class::MIN_SHA_LENGTH - 1] }
+
+ it_behaves_like 'no match'
+ end
+
+ context "with full length" do
+ let(:value) { sha }
+
+ it_behaves_like 'a match'
+ end
+
+ context "with exceeeding length" do
+ let(:value) { sha + sha }
+
+ # This case is not exactly pretty for SHA1 as we would still match the full SHA256 length. It's arguable what
+ # the correct behaviour would be, but without starting to distinguish SHA1 and SHA256 hashes this is the best
+ # we can do.
+ let(:expected_match) { (sha + sha)[0, described_class::MAX_SHA_LENGTH] }
+
+ it_behaves_like 'a match'
+ end
+
+ context "with embedded SHA" do
+ let(:value) { "xxx#{sha}xxx" }
+
+ it_behaves_like 'a match'
+ end
+ end
+
+ context 'abbreviated SHA pattern' do
+ let(:pattern) { described_class::SHA_PATTERN }
+
+ context "with minimum length" do
+ let(:value) { sha[0, described_class::MIN_SHA_LENGTH] }
+ let(:expected_match) { value }
+
+ it_behaves_like 'a match'
+ end
+
+ context "with medium length" do
+ let(:value) { sha[0, described_class::MIN_SHA_LENGTH + 20] }
+ let(:expected_match) { value }
+
+ it_behaves_like 'a match'
+ end
+
+ it_behaves_like 'a SHA pattern'
+ end
+
+ context 'full SHA pattern' do
+ let(:pattern) { described_class::FULL_SHA_PATTERN }
+
+ context 'with abbreviated length' do
+ let(:value) { sha[0, described_class::SHA1_LENGTH - 1] }
+
+ it_behaves_like 'no match'
+ end
+
+ it_behaves_like 'a SHA pattern'
+ end
+ end
+
+ context 'SHA1' do
+ let(:sha) { "5716ca5987cbf97d6bb54920bea6adde242d87e6" }
+
+ it_behaves_like 'a SHA-matching pattern'
+ end
+
+ context 'SHA256' do
+ let(:sha) { "a52e146ac2ab2d0efbb768ab8ebd1e98a6055764c81fe424fbae4522f5b4cb92" }
+
+ it_behaves_like 'a SHA-matching pattern'
+ end
+ end
+
def sample_commit_hash
{
author_email: "dmitriy.zaporozhets@gmail.com",
diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
index f9a6c25b786..1818693974e 100644
--- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
@@ -39,6 +39,7 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do
title
updated_at
updated_by_id
+ draft
].freeze
expect(safe_attribute_keys).to match_array(expected_safe_attribute_keys)
@@ -66,6 +67,7 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do
url
last_commit
work_in_progress
+ draft
total_time_spent
time_change
human_total_time_spent
diff --git a/spec/lib/gitlab/x509/signature_spec.rb b/spec/lib/gitlab/x509/signature_spec.rb
index d119a4e2b9d..e0823aa8153 100644
--- a/spec/lib/gitlab/x509/signature_spec.rb
+++ b/spec/lib/gitlab/x509/signature_spec.rb
@@ -36,6 +36,7 @@ RSpec.describe Gitlab::X509::Signature do
it 'returns a verified signature if email does match' do
expect(signature.x509_certificate).to have_attributes(certificate_attributes)
+
expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
expect(signature.verified_signature).to be_truthy
expect(signature.verification_status).to eq(:verified)
@@ -55,6 +56,27 @@ RSpec.describe Gitlab::X509::Signature do
expect(signature.verification_status).to eq(:verified)
end
+ context 'when the certificate contains multiple emails' do
+ before do
+ allow_any_instance_of(described_class).to receive(:get_certificate_extension).and_call_original
+
+ allow_any_instance_of(described_class).to receive(:get_certificate_extension)
+ .with('subjectAltName')
+ .and_return("email:gitlab2@example.com, othername:<unsupported>, email:#{X509Helpers::User1.certificate_email}")
+ end
+
+ context 'and the email matches one of them' do
+ it 'returns a verified signature' do
+ expect(signature.x509_certificate).to have_attributes(certificate_attributes.except(:email, :emails))
+ expect(signature.x509_certificate.email).to eq('gitlab2@example.com')
+ expect(signature.x509_certificate.emails).to contain_exactly('gitlab2@example.com', X509Helpers::User1.certificate_email)
+ expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
+ expect(signature.verified_signature).to be_truthy
+ expect(signature.verification_status).to eq(:verified)
+ end
+ end
+ end
+
context "if the email matches but isn't confirmed" do
let!(:user) { create(:user, :unconfirmed, email: X509Helpers::User1.certificate_email) }
@@ -106,6 +128,7 @@ RSpec.describe Gitlab::X509::Signature do
subject_key_identifier: X509Helpers::User1.certificate_subject_key_identifier,
subject: X509Helpers::User1.certificate_subject,
email: X509Helpers::User1.certificate_email,
+ emails: [X509Helpers::User1.certificate_email],
serial_number: X509Helpers::User1.certificate_serial
}
end
@@ -248,15 +271,31 @@ RSpec.describe Gitlab::X509::Signature do
.and_return("email:gitlab@example.com, othername:<unsupported>")
end
- it 'extracts email' do
- signature = described_class.new(
+ let(:signature) do
+ described_class.new(
X509Helpers::User1.signed_commit_signature,
X509Helpers::User1.signed_commit_base_data,
'gitlab@example.com',
X509Helpers::User1.signed_commit_time
)
+ end
+ it 'extracts email' do
expect(signature.x509_certificate.email).to eq("gitlab@example.com")
+ expect(signature.x509_certificate.emails).to contain_exactly("gitlab@example.com")
+ end
+
+ context 'when there are multiple emails' do
+ before do
+ allow_any_instance_of(described_class).to receive(:get_certificate_extension)
+ .with('subjectAltName')
+ .and_return("email:gitlab@example.com, othername:<unsupported>, email:gitlab2@example.com")
+ end
+
+ it 'extracts all the emails' do
+ expect(signature.x509_certificate.email).to eq("gitlab@example.com")
+ expect(signature.x509_certificate.emails).to contain_exactly("gitlab@example.com", "gitlab2@example.com")
+ end
end
end
@@ -311,6 +350,7 @@ RSpec.describe Gitlab::X509::Signature do
subject_key_identifier: X509Helpers::User1.tag_certificate_subject_key_identifier,
subject: X509Helpers::User1.certificate_subject,
email: X509Helpers::User1.certificate_email,
+ emails: [X509Helpers::User1.certificate_email],
serial_number: X509Helpers::User1.tag_certificate_serial
}
end
diff --git a/spec/migrations/20230724071541_queue_backfill_default_branch_protection_namespace_setting_spec.rb b/spec/migrations/20230724071541_queue_backfill_default_branch_protection_namespace_setting_spec.rb
new file mode 100644
index 00000000000..5ba8c6b853c
--- /dev/null
+++ b/spec/migrations/20230724071541_queue_backfill_default_branch_protection_namespace_setting_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueBackfillDefaultBranchProtectionNamespaceSetting, feature_category: :database do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :namespace_settings,
+ column_name: :namespace_id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ }
+ end
+ end
+end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index dd3d4f1865c..7ab43611108 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -831,7 +831,8 @@ eos
expect(described_class.valid_hash?('a' * 6)).to be false
expect(described_class.valid_hash?('a' * 7)).to be true
expect(described_class.valid_hash?('a' * 40)).to be true
- expect(described_class.valid_hash?('a' * 41)).to be false
+ expect(described_class.valid_hash?('a' * 64)).to be true
+ expect(described_class.valid_hash?('a' * 65)).to be false
end
end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 7df49fbe548..a6766035c1e 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -180,7 +180,9 @@ RSpec.describe ProjectTeam, feature_category: :groups_and_projects do
subject(:import) { target_project.team.import(source_project, current_user) }
- it { is_expected.to match(imported_members) }
+ it 'matches the imported members', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/419394' do
+ is_expected.to match(imported_members)
+ end
it 'target project includes source member with the same access' do
import
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
index 150c7bd5f3e..00a2c218466 100644
--- a/spec/presenters/blob_presenter_spec.rb
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe BlobPresenter do
- let_it_be(:project) { create(:project, :repository) }
+RSpec.describe BlobPresenter, feature_category: :source_code_management do
+ let_it_be(:project) { create(:project, :repository, lfs: true) }
let_it_be(:user) { project.first_owner }
let(:repository) { project.repository }
@@ -228,6 +228,83 @@ RSpec.describe BlobPresenter do
it { expect(presenter.code_owners).to match_array([]) }
end
+ describe '#external_storage_url' do
+ let(:static_objects_external_storage_url) { nil }
+ let(:raw_path) { Rails.application.routes.url_helpers.project_raw_path(project, File.join(ref, path)) }
+ let(:token) { '' }
+ let(:cdn_fronted_url) do
+ "#{static_objects_external_storage_url}#{raw_path}#{token}"
+ end
+
+ subject { presenter.external_storage_url }
+
+ before do
+ stub_application_setting(static_objects_external_storage_url: static_objects_external_storage_url)
+ end
+
+ context 'when blob is an lfs pointer' do
+ let_it_be(:blob) { project.repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') }
+ let_it_be(:lfs_object) { project.lfs_objects.find_by_oid(blob.lfs_oid) }
+
+ let(:lfs_object_url) { lfs_object.file.url(content_type: "application/octet-stream") }
+ let(:lfs_object_proxy_url) { "#{project.http_url_to_repo}/gitlab-lfs/objects/#{lfs_object.oid}" }
+ let(:proxy_download) { true }
+ let(:lfs_config) do
+ Gitlab.config.lfs.deep_merge(
+ 'enabled' => true,
+ 'object_store' => {
+ 'remote_directory' => 'lfs-objects',
+ 'enabled' => true,
+ 'proxy_download' => proxy_download,
+ 'connection' => {
+ 'endpoint' => 'http:127.0.0.1:9000',
+ 'path_style' => true
+ }
+ }
+ )
+ end
+
+ before do
+ stub_lfs_setting(lfs_config)
+ stub_lfs_object_storage(proxy_download: proxy_download)
+ end
+
+ context 'when direct download is enabled' do
+ let(:proxy_download) { false }
+
+ it { is_expected.to eq(lfs_object_url) }
+ end
+
+ context 'when proxy download is enabled' do
+ it { is_expected.to eq(lfs_object_proxy_url) }
+ end
+ end
+
+ context 'when static_objects_external_storage_enabled?' do
+ let(:static_objects_external_storage_url) { 'https://cdn.gitlab.com' }
+
+ context 'and project is private' do
+ let(:token) { "?token=#{user.static_object_token}" }
+
+ it { is_expected.to eq(cdn_fronted_url) }
+ end
+
+ context 'and project is public' do
+ let(:token) { '' }
+
+ before do
+ allow(project).to receive(:public?).and_return(true)
+ end
+
+ it { is_expected.to eq(cdn_fronted_url) }
+ end
+ end
+
+ context 'when not static_objects_external_storage_enabled?' do
+ it { is_expected.to be_nil }
+ end
+ end
+
describe '#ide_edit_path' do
it { expect(presenter.ide_edit_path).to eq("/-/ide/project/#{project.full_path}/edit/HEAD/-/files/ruby/regex.rb") }
end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index c78adc2dcef..abc42d11c63 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -380,13 +380,14 @@ RSpec.describe 'project routing' do
it_behaves_like 'redirecting a legacy path', '/gitlab/gitlabhq/hooks/hook_logs/1', '/gitlab/gitlabhq/-/hooks/hook_logs/1'
end
- # project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /\h{7,40}/, project_id: /[^\/]+/}
+ # project_commit GET /:project_id/commit/:id(.:format) commit#show {id: Gitlab::Git::Commit::SHA_PATTERN, project_id: /[^\/]+/}
describe Projects::CommitController, 'routing' do
it 'to #show' do
expect(get('/gitlab/gitlabhq/-/commit/4246fbd')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd')
expect(get('/gitlab/gitlabhq/-/commit/4246fbd.diff')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'diff')
expect(get('/gitlab/gitlabhq/-/commit/4246fbd.patch')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'patch')
expect(get('/gitlab/gitlabhq/-/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5')
+ expect(get('/gitlab/gitlabhq/-/commit/6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321')
end
it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/commit/4246fbd", "/gitlab/gitlabhq/-/commit/4246fbd"
@@ -652,6 +653,10 @@ RSpec.describe 'project routing' do
it 'to #show' do
expect(get('/gitlab/gitlabhq/-/compare/master...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'master', to: 'stable')
expect(get('/gitlab/gitlabhq/-/compare/issue/1234...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'issue/1234', to: 'stable')
+ expect(get('/gitlab/gitlabhq/-/compare/257cc5642cb1a054f08cc83f2d943e56fd3ebe99...5716ca5987cbf97d6bb54920bea6adde242d87e6'))
+ .to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', to: '5716ca5987cbf97d6bb54920bea6adde242d87e6')
+ expect(get('/gitlab/gitlabhq/-/compare/47d6aca82756ff2e61e53520bfdf1faa6c86d933be4854eb34840c57d12e0c85...a52e146ac2ab2d0efbb768ab8ebd1e98a6055764c81fe424fbae4522f5b4cb92'))
+ .to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: '47d6aca82756ff2e61e53520bfdf1faa6c86d933be4854eb34840c57d12e0c85', to: 'a52e146ac2ab2d0efbb768ab8ebd1e98a6055764c81fe424fbae4522f5b4cb92')
end
it_behaves_like 'redirecting a legacy path', '/gitlab/gitlabhq/compare', '/gitlab/gitlabhq/-/compare'
diff --git a/spec/support/helpers/filter_spec_helper.rb b/spec/support/helpers/filter_spec_helper.rb
index 7beed9c7755..dc282bf0a68 100644
--- a/spec/support/helpers/filter_spec_helper.rb
+++ b/spec/support/helpers/filter_spec_helper.rb
@@ -94,9 +94,9 @@ module FilterSpecHelper
when /\A(.+)?[^\d]\d+\z/
# Integer-based reference with optional project prefix
reference.gsub(/\d+\z/) { |i| i.to_i + 10_000 }
- when /\A(.+@)?(\h{7,40}\z)/
+ when /\A(.+@)?(#{Gitlab::Git::Commit::RAW_SHA_PATTERN}\z)/o
# SHA-based reference with optional prefix
- reference.gsub(/\h{7,40}\z/) { |v| v.reverse }
+ reference.gsub(/#{Gitlab::Git::Commit::RAW_SHA_PATTERN}\z/o) { |v| v.reverse }
else
reference.gsub(/\w+\z/) { |v| v.reverse }
end
diff --git a/spec/support/shared_examples/features/access_tokens_shared_examples.rb b/spec/support/shared_examples/features/access_tokens_shared_examples.rb
index 3c78869ffaa..34e3ba95b0d 100644
--- a/spec/support/shared_examples/features/access_tokens_shared_examples.rb
+++ b/spec/support/shared_examples/features/access_tokens_shared_examples.rb
@@ -15,6 +15,8 @@ RSpec.shared_examples 'resource access tokens creation' do |resource_type|
name = 'My access token'
visit resource_settings_access_tokens_path
+
+ click_button 'Add new token'
fill_in 'Token name', with: name
# Set date to 1st of next month