Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-11-02 18:12:04 +00:00
parent ef211f6aff
commit eed7260f13
111 changed files with 1198 additions and 1776 deletions

View File

@ -325,9 +325,9 @@ Dangerfile
/ee/app/assets/javascripts/analytics/analytics_dashboards/index.js
/ee/app/assets/javascripts/analytics/analytics_dashboards/router.js
/ee/app/assets/javascripts/analytics/analytics_dashboards/constants.js
/ee/app/assets/javascripts/analytics/analytics_dashboards/graphql/queries/get_all_product_analytics_dashboards.query.graphql
/ee/app/assets/javascripts/analytics/analytics_dashboards/graphql/queries/get_product_analytics_dashboard.query.graphql
/ee/app/assets/javascripts/analytics/analytics_dashboards/graphql/queries/get_all_product_analytics_visualizations.query.graphql
/ee/app/assets/javascripts/analytics/analytics_dashboards/graphql/queries/get_all_customizable_dashboards.query.graphql
/ee/app/assets/javascripts/analytics/analytics_dashboards/graphql/queries/get_customizable_dashboard.query.graphql
/ee/app/assets/javascripts/analytics/analytics_dashboards/graphql/queries/get_all_customizable_visualizations.query.graphql
/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/customizable_dashboard.vue
/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/panels_base.vue
/ee/app/assets/javascripts/product_analytics/

View File

@ -14,7 +14,7 @@ include:
gitlab_auth_token_variable_name: "PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE"
allure_job_name: "${QA_RUN_TYPE}"
- project: gitlab-org/quality/pipeline-common
ref: 7.10.0
ref: 7.10.2
file:
- /ci/base.gitlab-ci.yml
- /ci/knapsack-report.yml

View File

@ -32,13 +32,13 @@ _What [personas](https://about.gitlab.com/handbook/product/personas/#list-of-use
### Success
_How will you measure whether this experiment is a success?_
**UX maturity requirements** _[Experiment to Beta](https://internal-handbook.gitlab.io/handbook/product/ai-strategy/ai-integration-effort/ux_maturity/#criteria-and-requirements)_
**UX maturity requirements** _[Experiment to Beta](https://about.gitlab.com/handbook/product/ai/ux-maturity/#criteria-and-requirements)_
| Criteria | Minimum Requirement | Assessment for Beta |
| -------- | ------------------- | ------------------- |
| [Problem validation](https://internal-handbook.gitlab.io/handbook/product/ai-strategy/ai-integration-effort/ux_maturity/#validation-problem-validation)<br>How well do we understand the problem? | [Mix of evidence and assumptions](https://internal-handbook.gitlab.io/handbook/product/ai-strategy/ai-integration-effort/ux_maturity/#questions-to-ask) | <!-- Acceptable answers: Yes, Somewhat or Somewhat, Somewhat --> |
| [Solution validation](https://internal-handbook.gitlab.io/handbook/product/ai-strategy/ai-integration-effort/ux_maturity/#validation-solution-validation)<br>How usable is the solution? | [Heuristic Eval](https://about.gitlab.com/handbook/product/ux/ux-scorecards/#option-a-conduct-a-heuristic-evaluation), Grade C | <!-- Acceptable grade: C --> |
| [Improve](https://internal-handbook.gitlab.io/handbook/product/ai-strategy/ai-integration-effort/ux_maturity/#build-improve)<br>How successful is the solution? | [Success metrics defined by the team.](https://about.gitlab.com/handbook/product/ux/ux-research/usability-testing/#usability-at-gitlab) | <!-- Acceptable answers: :white_check_mark: All success metrics defined for this phase have been met. --> |
| [Design standards](https://internal-handbook.gitlab.io/handbook/product/ai-strategy/ai-integration-effort/ux_maturity/#design-standards) adherence<br>How compliant is the solution with our design standards? | Should adhere to ([PJs](https://design.gitlab.com/), [Design](https://docs.gitlab.com/ee/development/contributing/design.html#checklist)) | <!-- Acceptable: Mostly adheres to PJs and design standards --> |
| [Problem validation](https://about.gitlab.com/handbook/product/ai/ux-maturity/#validation-problem-validation)<br>How well do we understand the problem? | [Mix of evidence and assumptions](https://about.gitlab.com/handbook/product/ai/ux-maturity/#questions-to-ask) | <!-- Acceptable answers: Yes, Somewhat or Somewhat, Somewhat --> |
| [Solution validation](https://about.gitlab.com/handbook/product/ai/ux-maturity/#validation-solution-validation)<br>How usable is the solution? | [Usability testing](https://about.gitlab.com/handbook/product/ux/ux-scorecards/#option-b-perform-a-formative-evaluation), Grade C | <!-- Acceptable: >80% and grade C --> |
| [Improve](https://about.gitlab.com/handbook/product/ai/ux-maturity/#build-improve)<br>How successful is the solution? | Quality goals set by the team are reached. | <!-- Acceptable answers: :white_check_mark: Reached all quality goals for this phase. --> |
| [Design standards](https://about.gitlab.com/handbook/product/ai/ux-maturity/#design-standards) adherence<br>How compliant is the solution with our design standards? | Should adhere to ([Pajamas](https://design.gitlab.com/), [checklist](https://docs.gitlab.com/ee/development/contributing/design.html#checklist)) | <!-- Acceptable: Mostly adheres to design standards --> |
# [Beta](https://docs.gitlab.com/ee/policy/alpha-beta-support.html#beta)
_This section should be completed prior to beginning work on the Beta experience._
@ -61,7 +61,7 @@ _What assumptions are you making about this problem and the solution?_
### Problem validation
_What validation exists that customers have this problem?_
<!-- Refer to https://about.gitlab.com/handbook/product/ux/ux-research/research-in-the-AI-space/#guideline-1-problem-validation --- to help identify and understand user needs -->
<!-- Refer to https://about.gitlab.com/handbook/product/ux/ux-research/research-in-the-AI-space/#guideline-1-problem-validation---identify-and-understand-user-needs --- to help identify and understand user needs -->
### Business objective
_What business objective will be achieved with this proposal?_
@ -80,13 +80,13 @@ _What tasks or actions should the user be capable of performing with this featur
_How will you measure whether this Beta is a success?_
<!-- Consider how successful the solution is by looking beyond feature usage as the success metric. Instead consider how useful, efficient, effective, satisfying, and learnable was the feature. The Product Development Flow recommends outcomes and potential activities to create a combined and ongoing quantitative and qualitative feedback loop to evaluate feature success. -->
**UX maturity requirements** _[Beta to GA](https://internal-handbook.gitlab.io/handbook/product/ai-strategy/ai-integration-effort/ux_maturity/#criteria-and-requirements)_
**UX maturity requirements** _[Beta to GA](https://about.gitlab.com/handbook/product/ai/ux-maturity/#criteria-and-requirements)_
| Criteria | Minimum Requirement | Assessment for GA |
| -------- | ------------------- | ------------------- |
| [Problem validation](https://internal-handbook.gitlab.io/handbook/product/ai-strategy/ai-integration-effort/ux_maturity/#validation-problem-validation)<br>How well do we understand the problem? | [Mix of evidence and assumptions](https://internal-handbook.gitlab.io/handbook/product/ai-strategy/ai-integration-effort/ux_maturity/#questions-to-ask) | <!-- Acceptable answers: Yes, Yes --> |
| [Solution validation](https://internal-handbook.gitlab.io/handbook/product/ai-strategy/ai-integration-effort/ux_maturity/#validation-solution-validation)<br>How usable is the solution? | [Usability Testing](https://about.gitlab.com/handbook/product/ux/ux-scorecards/#option-b-perform-a-formative-evaluation) and/or [Heuristic Eval](https://about.gitlab.com/handbook/product/ux/ux-scorecards/#option-a-conduct-a-heuristic-evaluation), Avg. task pass rate >80%, Grade B | <!-- Acceptable: >80% and B --> |
| [Improve](https://internal-handbook.gitlab.io/handbook/product/ai-strategy/ai-integration-effort/ux_maturity/#build-improve)<br>How successful is the solution? | [Success metrics defined by the team.](https://about.gitlab.com/handbook/product/ux/ux-research/usability-testing/#usability-at-gitlab) | <!-- Acceptable answers: :white_check_mark: All success metrics defined for this phase have been met. --> |
| [Design standards](https://internal-handbook.gitlab.io/handbook/product/ai-strategy/ai-integration-effort/ux_maturity/#design-standards) adherence<br>How compliant is the solution with our design standards? | Should adhere to ([PJs](https://design.gitlab.com/), [Design](https://docs.gitlab.com/ee/development/contributing/design.html#checklist)) | <!-- Acceptable: Completely adheres to PJs and design standards --> |
| [Problem validation](https://about.gitlab.com/handbook/product/ai/ux-maturity/#validation-problem-validation)<br>How well do we understand the problem? | [Mix of evidence and assumptions](https://about.gitlab.com/handbook/product/ai/ux-maturity/#questions-to-ask) | <!-- Acceptable answers: Yes, Yes --> |
| [Solution validation](https://about.gitlab.com/handbook/product/ai/ux-maturity/#validation-solution-validation)<br>How usable is the solution? | [Usability testing](https://about.gitlab.com/handbook/product/ux/ux-scorecards/#option-b-perform-a-formative-evaluation) and [Heuristic evaluation](https://about.gitlab.com/handbook/product/ux/ux-scorecards/#option-a-conduct-a-heuristic-evaluation), Avg. task pass rate >80%, Grade B | <!-- Acceptable: >80% and grade B --> |
| [Improve](https://about.gitlab.com/handbook/product/ai/ux-maturity/#build-improve)<br>How successful is the solution? | Quality goals set by the team are reached. | <!-- Acceptable answers: :white_check_mark: Reached all quality goals for this phase. --> |
| [Design standards](https://about.gitlab.com/handbook/product/ai/ux-maturity/#design-standards) adherence<br>How compliant is the solution with our design standards? | Should adhere to ([Pajamas](https://design.gitlab.com/), [checklist](https://docs.gitlab.com/ee/development/contributing/design.html#checklist)) | <!-- Acceptable: Completely adheres to design standards --> |
# [Generally Available](https://docs.gitlab.com/ee/policy/alpha-beta-support.html#generally-available-ga)
<!-- DO NOT REMOVE THIS SECTION
@ -202,7 +202,7 @@ _What tasks or actions should the user be capable of performing with this featur
- Read about our [AI Integration strategy](https://internal-handbook.gitlab.io/handbook/product/ai-strategy/ai-integration-effort/)
- [AI-human interaction guidelines](https://design.gitlab.com/usability/ai-human-interaction)
- [Highlighting feature versions guidelines](https://design.gitlab.com/usability/feature-management#highlighting-feature-versions)
- [UX maturity requirements](https://internal-handbook.gitlab.io/handbook/product/ai-strategy/ai-integration-effort/ux_maturity/)
- [UX maturity requirements](https://about.gitlab.com/handbook/product/ai/ux-maturity/)
- **Slack channels**
- `#wg_ai_integration` - Slack channel for the working group and the high-level alignment on getting AI ready for Production (Development, Product, UX, Legal, etc.) But from the other channels feel free to reach out and post progress here
- `#ai_integration_dev_lobby` - Channel for all implementation-related topics and discussions of actual AI features (e.g. explain the code)

View File

@ -27,7 +27,6 @@ Gitlab/DocUrl:
- 'lib/gitlab/audit/auditor.rb'
- 'lib/gitlab/ci/config/entry/processable.rb'
- 'lib/gitlab/config_checker/external_database_checker.rb'
- 'lib/gitlab/config_checker/puma_rugged_checker.rb'
- 'lib/gitlab/database_warnings.rb'
- 'lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables.rb'
- 'lib/gitlab/database/migration_helpers/v2.rb'

View File

@ -195,7 +195,6 @@ Layout/FirstHashElementIndentation:
- 'spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb'
- 'spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb'
- 'spec/lib/gitlab/ci/yaml_processor_spec.rb'
- 'spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb'
- 'spec/lib/gitlab/data_builder/build_spec.rb'
- 'spec/lib/gitlab/data_builder/issuable_spec.rb'
- 'spec/lib/gitlab/data_builder/pipeline_spec.rb'

View File

@ -109,7 +109,6 @@ Layout/LineContinuationSpacing:
- 'lib/gitlab/background_migration/populate_operation_visibility_permissions_from_operations.rb'
- 'lib/gitlab/checks/tag_check.rb'
- 'lib/gitlab/ci/parsers/security/validators/schema_validator.rb'
- 'lib/gitlab/config_checker/puma_rugged_checker.rb'
- 'lib/gitlab/database/background_migration/batched_migration_runner.rb'
- 'lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb'
- 'lib/gitlab/database/migration_helpers.rb'
@ -144,7 +143,6 @@ Layout/LineContinuationSpacing:
- 'spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb'
- 'spec/lib/gitlab/ci/trace/stream_spec.rb'
- 'spec/lib/gitlab/closing_issue_extractor_spec.rb'
- 'spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb'
- 'spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb'
- 'spec/lib/gitlab/git_access_spec.rb'
- 'spec/lib/gitlab/github_import/markdown_text_spec.rb'

View File

@ -164,7 +164,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'lib/gitlab/ci/parsers/security/validators/schema_validator.rb'
- 'lib/gitlab/ci/pipeline/chain/populate.rb'
- 'lib/gitlab/ci/pipeline/seed/build.rb'
- 'lib/gitlab/config_checker/puma_rugged_checker.rb'
- 'lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb'
- 'lib/gitlab/database/migration_helpers.rb'
- 'lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb'

View File

@ -2640,7 +2640,6 @@ Layout/LineLength:
- 'lib/gitlab/git/conflict/resolver.rb'
- 'lib/gitlab/git/remote_mirror.rb'
- 'lib/gitlab/git/repository.rb'
- 'lib/gitlab/git/rugged_impl/repository.rb'
- 'lib/gitlab/git/user.rb'
- 'lib/gitlab/git_access.rb'
- 'lib/gitlab/git_access_project.rb'
@ -3675,7 +3674,6 @@ Layout/LineLength:
- 'spec/lib/gitlab/code_navigation_path_spec.rb'
- 'spec/lib/gitlab/composer/cache_spec.rb'
- 'spec/lib/gitlab/composer/version_index_spec.rb'
- 'spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb'
- 'spec/lib/gitlab/conflict/file_spec.rb'
- 'spec/lib/gitlab/consul/internal_spec.rb'
- 'spec/lib/gitlab/content_security_policy/config_loader_spec.rb'

View File

@ -102,7 +102,6 @@ Performance/MapCompact:
- 'lib/gitlab/database/load_balancing/service_discovery.rb'
- 'lib/gitlab/git/commit.rb'
- 'lib/gitlab/git/conflict/file.rb'
- 'lib/gitlab/git/rugged_impl/commit.rb'
- 'lib/gitlab/sql/pattern.rb'
- 'lib/gitlab/url_blocker.rb'
- 'qa/qa/page/component/issuable/sidebar.rb'

View File

@ -1670,7 +1670,6 @@ RSpec/ContextWording:
- 'spec/lib/gitlab/composer/cache_spec.rb'
- 'spec/lib/gitlab/config/entry/composable_array_spec.rb'
- 'spec/lib/gitlab/config/entry/composable_hash_spec.rb'
- 'spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb'
- 'spec/lib/gitlab/consul/internal_spec.rb'
- 'spec/lib/gitlab/content_security_policy/config_loader_spec.rb'
- 'spec/lib/gitlab/cycle_analytics/permissions_spec.rb'
@ -1771,7 +1770,6 @@ RSpec/ContextWording:
- 'spec/lib/gitlab/git/pre_receive_error_spec.rb'
- 'spec/lib/gitlab/git/raw_diff_change_spec.rb'
- 'spec/lib/gitlab/git/repository_spec.rb'
- 'spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb'
- 'spec/lib/gitlab/git/tag_spec.rb'
- 'spec/lib/gitlab/git/wiki_page_version_spec.rb'
- 'spec/lib/gitlab/git_access_snippet_spec.rb'

View File

@ -3221,7 +3221,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/config/entry/validator_spec.rb'
- 'spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb'
- 'spec/lib/gitlab/config_checker/external_database_checker_spec.rb'
- 'spec/lib/gitlab/config_checker/puma_rugged_checker_spec.rb'
- 'spec/lib/gitlab/conflict/file_collection_spec.rb'
- 'spec/lib/gitlab/conflict/file_spec.rb'
- 'spec/lib/gitlab/consul/internal_spec.rb'

View File

@ -504,7 +504,6 @@ RSpec/VerifiedDoubles:
- 'spec/lib/gitlab/git/blob_spec.rb'
- 'spec/lib/gitlab/git/commit_spec.rb'
- 'spec/lib/gitlab/git/repository_spec.rb'
- 'spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb'
- 'spec/lib/gitlab/git/tag_spec.rb'
- 'spec/lib/gitlab/git_access_snippet_spec.rb'
- 'spec/lib/gitlab/gitaly_client/commit_service_spec.rb'

View File

@ -243,7 +243,6 @@ Style/FormatString:
- 'lib/gitlab/ci/parsers/sbom/cyclonedx.rb'
- 'lib/gitlab/ci/status/build/waiting_for_approval.rb'
- 'lib/gitlab/config_checker/external_database_checker.rb'
- 'lib/gitlab/config_checker/puma_rugged_checker.rb'
- 'lib/gitlab/console.rb'
- 'lib/gitlab/database/async_indexes/index_creator.rb'
- 'lib/gitlab/database/background_migration/batched_migration.rb'

View File

@ -817,7 +817,6 @@ Style/IfUnlessModifier:
- 'lib/gitlab/git/merge_base.rb'
- 'lib/gitlab/git/push.rb'
- 'lib/gitlab/git/repository.rb'
- 'lib/gitlab/git/rugged_impl/tree.rb'
- 'lib/gitlab/git_access.rb'
- 'lib/gitlab/git_access_project.rb'
- 'lib/gitlab/git_access_snippet.rb'

View File

@ -32,7 +32,7 @@ export default {
<template>
<div>
<ci-icon
class="gl-mb-3"
class="gl-mb-2"
:status="pipelineStatus"
show-status-text
@ciStatusBadgeClick="trackClick"

View File

@ -3,22 +3,20 @@ import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
import micromatch from 'micromatch';
import { debounce } from 'lodash';
import { getModifierKey } from '~/constants';
import { s__, sprintf } from '~/locale';
import { RecycleScroller } from 'vendor/vue-virtual-scroller';
import { contentTop } from '~/lib/utils/common_utils';
import DiffFileRow from './diff_file_row.vue';
import TreeListHeight from './tree_list_height.vue';
const MODIFIER_KEY = getModifierKey();
const MAX_ITEMS_ON_NARROW_SCREEN = 8;
const BOTTOM_MARGIN = 16;
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
TreeListHeight,
GlIcon,
DiffFileRow,
RecycleScroller,
@ -32,17 +30,10 @@ export default {
data() {
return {
search: '',
scrollerHeight: 0,
rowHeight: 0,
debouncedHeightCalc: null,
reviewBarHeight: 0,
largeBreakpointSize: 0,
};
},
computed: {
...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId', 'viewedDiffFileIds']),
...mapState('batchComments', ['reviewBarRendered']),
...mapGetters('batchComments', ['draftsCount']),
...mapGetters('diffs', ['allBlobs']),
filteredTreeList() {
let search = this.search.toLowerCase().trim();
@ -95,76 +86,21 @@ export default {
return result;
},
reviewBarEnabled() {
return this.draftsCount > 0;
},
},
watch: {
reviewBarEnabled() {
this.debouncedHeightCalc();
},
calculateReviewBarHeight() {
this.debouncedHeightCalc();
},
},
created() {
this.debouncedHeightCalc = debounce(this.calculateScrollerHeight, 50);
},
mounted() {
const heightProp = getComputedStyle(this.$refs.wrapper).getPropertyValue('--file-row-height');
const breakpointProp = getComputedStyle(window.document.body).getPropertyValue(
'--breakpoint-lg',
);
this.largeBreakpointSize = parseInt(breakpointProp, 10);
this.rowHeight = parseInt(heightProp, 10);
this.calculateScrollerHeight();
let stop;
// eslint-disable-next-line prefer-const
stop = this.$watch(
() => this.reviewBarRendered,
(enabled) => {
if (!enabled) return;
this.calculateReviewBarHeight();
stop();
},
{ immediate: true },
);
window.addEventListener('resize', this.debouncedHeightCalc, { passive: true });
},
beforeDestroy() {
window.removeEventListener('resize', this.debouncedHeightCalc, { passive: true });
},
methods: {
...mapActions('diffs', ['toggleTreeOpen', 'goToFile']),
clearSearch() {
this.search = '';
},
calculateScrollerHeight() {
if (window.matchMedia(`(max-width: ${this.largeBreakpointSize - 1}px)`).matches) {
this.calculateMobileScrollerHeight();
} else {
let clipping = BOTTOM_MARGIN;
if (this.reviewBarEnabled) clipping += this.reviewBarHeight;
this.scrollerHeight = this.$refs.scrollRoot.clientHeight - clipping;
}
},
calculateMobileScrollerHeight() {
const maxItems = Math.min(MAX_ITEMS_ON_NARROW_SCREEN, this.flatFilteredTreeList.length);
this.scrollerHeight = Math.min(maxItems * this.rowHeight, window.innerHeight - contentTop());
},
calculateReviewBarHeight() {
this.reviewBarHeight = document.querySelector('.js-review-bar')?.offsetHeight || 0;
},
},
searchPlaceholder: sprintf(s__('MergeRequest|Search (e.g. *.vue) (%{MODIFIER_KEY}P)'), {
MODIFIER_KEY,
}),
DiffFileRow,
};
</script>
<template>
<div ref="wrapper" class="tree-list-holder d-flex flex-column" data-testid="file-tree-container">
<div class="tree-list-holder d-flex flex-column" data-testid="file-tree-container">
<div class="gl-pb-3 position-relative tree-list-search d-flex">
<div class="flex-fill d-flex">
<gl-icon name="search" class="gl-absolute gl-top-3 gl-left-3 tree-list-icon" />
@ -189,41 +125,41 @@ export default {
</button>
</div>
</div>
<div
ref="scrollRoot"
:class="{ 'tree-list-blobs': !renderTreeList || search }"
class="gl-flex-grow-1 mr-tree-list"
>
<recycle-scroller
v-if="flatFilteredTreeList.length"
:style="{ height: `${scrollerHeight}px` }"
:items="flatFilteredTreeList"
:item-size="rowHeight"
:buffer="100"
key-field="key"
>
<template #default="{ item }">
<diff-file-row
:file="item"
:level="item.level"
:viewed-files="viewedDiffFileIds"
:hide-file-stats="hideFileStats"
:current-diff-file-id="currentDiffFileId"
:style="{ '--level': item.level }"
:class="{ 'tree-list-parent': item.level > 0 }"
class="gl-relative"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="(path) => goToFile({ path })"
/>
</template>
<template #after>
<div class="tree-list-gutter"></div>
</template>
</recycle-scroller>
<p v-else class="prepend-top-20 append-bottom-20 text-center">
{{ s__('MergeRequest|No files found') }}
</p>
</div>
<tree-list-height class="gl-flex-grow-1 gl-min-h-0" :items-count="flatFilteredTreeList.length">
<template #default="{ scrollerHeight, rowHeight }">
<div :class="{ 'tree-list-blobs': !renderTreeList || search }" class="mr-tree-list">
<recycle-scroller
v-if="flatFilteredTreeList.length"
:style="{ height: `${scrollerHeight}px` }"
:items="flatFilteredTreeList"
:item-size="rowHeight"
:buffer="100"
key-field="key"
>
<template #default="{ item }">
<diff-file-row
:file="item"
:level="item.level"
:viewed-files="viewedDiffFileIds"
:hide-file-stats="hideFileStats"
:current-diff-file-id="currentDiffFileId"
:style="{ '--level': item.level }"
:class="{ 'tree-list-parent': item.level > 0 }"
class="gl-relative"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="(path) => goToFile({ path })"
/>
</template>
<template #after>
<div class="tree-list-gutter"></div>
</template>
</recycle-scroller>
<p v-else class="prepend-top-20 append-bottom-20 text-center">
{{ s__('MergeRequest|No files found') }}
</p>
</div>
</template>
</tree-list-height>
</div>
</template>

View File

@ -0,0 +1,108 @@
<script>
import { debounce } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapGetters } from 'vuex';
import { contentTop } from '~/lib/utils/common_utils';
const MAX_ITEMS_ON_NARROW_SCREEN = 8;
// Should be enough for the very long titles (10+ lines) on the max smallest screen
const MAX_SCROLL_Y = 600;
const BOTTOM_OFFSET = 16;
export default {
name: 'TreeListHeight',
props: {
itemsCount: {
type: Number,
required: true,
},
},
data() {
return {
scrollerHeight: 0,
rowHeight: 0,
reviewBarHeight: 0,
scrollY: 0,
isNarrowScreen: false,
mediaQueryMatch: null,
};
},
computed: {
...mapState('batchComments', ['reviewBarRendered']),
...mapGetters('batchComments', ['draftsCount']),
reviewBarEnabled() {
return this.draftsCount > 0;
},
debouncedHeightCalc() {
return debounce(this.calculateScrollerHeight, 100);
},
debouncedRecordScroll() {
return debounce(this.recordScroll, 50);
},
},
watch: {
reviewBarRendered: {
handler(rendered) {
if (!rendered || this.reviewBarHeight) return;
this.reviewBarHeight = document.querySelector('.js-review-bar').offsetHeight;
this.debouncedHeightCalc();
},
immediate: true,
},
reviewBarEnabled: 'debouncedHeightCalc',
scrollY: 'debouncedHeightCalc',
isNarrowScreen: 'recordScroll',
},
mounted() {
const computedStyles = getComputedStyle(this.$refs.scrollRoot);
this.rowHeight = parseInt(computedStyles.getPropertyValue('--file-row-height'), 10);
const largeBreakpointSize = parseInt(computedStyles.getPropertyValue('--breakpoint-lg'), 10);
this.mediaQueryMatch = window.matchMedia(`(max-width: ${largeBreakpointSize - 1}px)`);
this.isNarrowScreen = this.mediaQueryMatch.matches;
this.mediaQueryMatch.addEventListener('change', this.handleMediaMatch);
window.addEventListener('resize', this.debouncedHeightCalc, { passive: true });
window.addEventListener('scroll', this.debouncedRecordScroll, { passive: true });
this.calculateScrollerHeight();
},
beforeDestroy() {
this.mediaQueryMatch.removeEventListener('change', this.handleMediaMatch);
this.mediaQueryMatch = null;
window.removeEventListener('resize', this.debouncedHeightCalc, { passive: true });
window.removeEventListener('scroll', this.debouncedRecordScroll, { passive: true });
},
methods: {
recordScroll() {
const { scrollY } = window;
if (scrollY > MAX_SCROLL_Y || this.isNarrowScreen) {
this.scrollY = MAX_SCROLL_Y;
} else {
this.scrollY = window.scrollY;
}
},
handleMediaMatch({ matches }) {
this.isNarrowScreen = matches;
},
calculateScrollerHeight() {
if (this.isNarrowScreen) {
const maxItems = Math.min(MAX_ITEMS_ON_NARROW_SCREEN, this.itemsCount);
const maxHeight = maxItems * this.rowHeight;
this.scrollerHeight = Math.min(maxHeight, window.innerHeight - contentTop());
} else {
const { y } = this.$refs.scrollRoot.getBoundingClientRect();
const reviewBarOffset = this.reviewBarEnabled ? this.reviewBarHeight : 0;
// distance from element's top vertical position in the viewport to the bottom of the viewport minus offsets
this.scrollerHeight = window.innerHeight - y - reviewBarOffset - BOTTOM_OFFSET;
}
},
},
};
</script>
<template>
<div ref="scrollRoot">
<slot :scroller-height="scrollerHeight" :row-height="rowHeight"></slot>
</div>
</template>

View File

@ -0,0 +1,10 @@
query groupsAutocomplete($search: String) {
groups(search: $search) {
nodes {
id
name
fullName
avatarUrl
}
}
}

View File

@ -1,6 +1,6 @@
<script>
import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import { __, sprintf } from '~/locale';
import SafeHtml from '~/vue_shared/directives/safe_html';
import defaultAvatarUrl from 'images/no_avatar.png';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@ -35,6 +35,9 @@ export default {
// Strip the newline at the beginning
return this.commit?.descriptionHtml?.replace(/^&#x000A;/, '');
},
avatarLinkAltText() {
return sprintf(__(`%{username}'s avatar`), { username: this.commit.authorName });
},
},
methods: {
toggleShowDescription() {
@ -58,6 +61,7 @@ export default {
v-if="commit.author"
:link-href="commit.author.webPath"
:img-src="commit.author.avatarUrl"
:img-alt="avatarLinkAltText"
:img-size="32"
class="gl-my-2 gl-mr-4"
/>

View File

@ -0,0 +1,14 @@
export default {
typePolicies: {
Project: {
fields: {
ciCdSettings: {
merge: true,
},
ciJobTokenScope: {
merge: true,
},
},
},
},
};

View File

@ -2,11 +2,12 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import TokenAccessApp from './components/token_access_app.vue';
import cacheConfig from './graphql/cache_config';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
defaultClient: createDefaultClient({}, { cacheConfig }),
});
export const initTokenAccess = (containerId = 'js-ci-token-access-app') => {

View File

@ -1,7 +1,6 @@
import { __ } from '~/locale';
// Note, we can extend this config in future to make the component work in other contexts
// https://gitlab.com/gitlab-org/gitlab/-/issues/428865
export const CONFIG = {
users: { title: __('Users'), icon: 'user', filterKey: 'username' },
groups: { title: __('Groups'), icon: 'group', filterKey: 'name' },
};

View File

@ -0,0 +1,55 @@
<script>
import { GlAvatar, GlButton } from '@gitlab/ui';
import { sprintf, __ } from '~/locale';
export default {
name: 'GroupItem',
components: {
GlAvatar,
GlButton,
},
props: {
data: {
type: Object,
required: true,
},
canDelete: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
deleteButtonLabel() {
return sprintf(__('Delete %{name}'), { name: this.name });
},
fullName() {
return this.data.fullName;
},
name() {
return this.data.name;
},
avatarUrl() {
return this.data.avatarUrl;
},
},
};
</script>
<template>
<span class="gl-display-flex gl-align-items-center gl-gap-3" @click="$emit('select', name)">
<gl-avatar :alt="fullName" :size="32" :src="avatarUrl" />
<span class="gl-display-flex gl-flex-direction-column gl-flex-grow-1">
<span class="gl-font-weight-bold">{{ fullName }}</span>
<span class="gl-text-gray-600">@{{ name }}</span>
</span>
<gl-button
v-if="canDelete"
icon="remove"
:aria-label="deleteButtonLabel"
category="tertiary"
@click="$emit('delete', name)"
/>
</span>
</template>

View File

@ -1,7 +1,9 @@
<script>
import { GlCard, GlIcon, GlCollapsibleListbox, GlSearchBoxByType } from '@gitlab/ui';
import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql';
import User from './user.vue';
import groupsAutocompleteQuery from '~/graphql_shared/queries/groups_autocomplete.query.graphql';
import UserItem from './user_item.vue';
import GroupItem from './group_item.vue';
import { CONFIG } from './constants';
export default {
@ -11,7 +13,6 @@ export default {
GlIcon,
GlSearchBoxByType,
GlCollapsibleListbox,
User,
},
props: {
title: {
@ -46,24 +47,23 @@ export default {
return CONFIG[this.type];
},
searchItems() {
return (
this.items?.map((item) => ({
value: item.username,
text: item.name,
...item,
})) || []
);
return this.items;
},
isUserVariant() {
return this.type === 'users';
},
component() {
// Note, we can extend this for the component to support other contexts
// https://gitlab.com/gitlab-org/gitlab/-/issues/428865
return User;
return this.isUserVariant ? UserItem : GroupItem;
},
},
methods: {
async handleSearchInput(search) {
this.$refs.results.open();
this.items = await this.fetchUsersBySearchTerm(search);
if (this.isUserVariant) {
this.items = await this.fetchUsersBySearchTerm(search);
} else {
this.items = await this.fetchGroupsBySearchTerm(search);
}
},
fetchUsersBySearchTerm(search) {
const namespace = this.isProject ? 'project' : 'group';
@ -72,7 +72,27 @@ export default {
query: usersAutocompleteQuery,
variables: { fullPath: this.projectPath, search, isProject: this.isProject },
})
.then(({ data }) => data[namespace]?.autocompleteUsers);
.then(({ data }) =>
data[namespace]?.autocompleteUsers.map((user) => ({
text: user.name,
value: user.username,
...user,
})),
);
},
fetchGroupsBySearchTerm(search) {
return this.$apollo
.query({
query: groupsAutocompleteQuery,
variables: { search },
})
.then(({ data }) =>
data?.groups.nodes.map((group) => ({
text: group.fullName,
value: group.name,
...group,
})),
);
},
getItemByKey(key) {
return this.searchItems.find((item) => item[this.config.filterKey] === key);
@ -90,9 +110,9 @@ export default {
<template>
<gl-card header-class="gl-new-card-header gl-border-none" body-class="gl-card-footer">
<template #header
><strong
><strong data-testid="list-selector-title"
>{{ title }}
<span class="gl-text-gray-500"
<span class="gl-text-gray-700 gl-ml-3"
><gl-icon :name="config.icon" /> {{ selectedItems.length }}</span
></strong
></template

View File

@ -487,7 +487,7 @@ $performance-bar-height: 2.5rem;
$system-header-height: 16px;
$system-footer-height: $system-header-height;
$mr-sticky-header-height: 72px;
$mr-review-bar-height: calc(2rem + 13px);
$mr-review-bar-height: calc(2rem + 16px);
$flash-height: 52px;
$context-header-height: 60px;
$top-bar-height: 48px;
@ -717,10 +717,10 @@ $blame-blue: #254e77;
*/
$builds-log-bg: #111;
$job-log-highlight-height: 18px;
$job-log-line-padding: 55px;
$job-log-line-padding: 63px;
$job-line-number-width: 50px;
$job-line-number-margin: 43px;
$job-arrow-margin: 55px;
$job-line-number-margin: 51px;
$job-arrow-margin: 63px;
/*
* Calendar

View File

@ -258,15 +258,15 @@ $tabs-holder-z-index: 250;
position: sticky;
top: calc(#{$calc-application-header-height} + #{$mr-tabs-height} + #{$diff-file-header-top});
// height calc is fully delegated to the tree_list_height.vue component
height: 0;
min-height: 300px;
height: calc(#{$calc-application-viewport-height} - (#{$mr-tabs-height} + #{$diff-file-header-top}));
.drag-handle {
bottom: 16px;
}
&.is-sidebar-moved {
height: calc(#{$calc-application-viewport-height} - (#{$mr-sticky-header-height} + #{$diff-file-header-top}));
top: calc(#{$calc-application-header-height} + #{$mr-sticky-header-height} + #{$diff-file-header-top});
}
}
@ -1108,7 +1108,7 @@ $tabs-holder-z-index: 250;
display: flex;
align-items: center;
width: 100%;
height: $toggle-sidebar-height;
height: var(--mr-review-bar-height);
padding-left: $contextual-sidebar-width;
padding-right: $right-sidebar-collapsed-width;
background: var(--white, $white);

View File

@ -14,10 +14,6 @@
// - app/assets/javascripts/commit/pipelines/pipelines_bundle.js
.pipelines {
.badge {
margin-bottom: 3px;
}
.pipeline-actions {
min-width: 170px; //Guarantees buttons don't break in several lines.

View File

@ -13,8 +13,7 @@ class Admin::DashboardController < Admin::ApplicationController
@projects = Project.order_id_desc.without_deleted.with_route.limit(10)
@users = User.order_id_desc.limit(10)
@groups = Group.order_id_desc.with_route.limit(10)
@notices = Gitlab::ConfigChecker::PumaRuggedChecker.check
@notices += Gitlab::ConfigChecker::ExternalDatabaseChecker.check
@notices = Gitlab::ConfigChecker::ExternalDatabaseChecker.check
@redis_versions = Gitlab::Redis::ALL_CLASSES.map(&:version).uniq
end

View File

@ -279,6 +279,7 @@ class GroupsController < Groups::ApplicationController
:avatar,
:description,
:emails_disabled,
:emails_enabled,
:show_diff_preview_in_email,
:mentions_disabled,
:lfs_enabled,

View File

@ -11,11 +11,16 @@ class UserGroupNotificationSettingsFinder
@loaded_groups_with_ancestors = groups_with_ancestors.index_by(&:id)
@loaded_notification_settings = user.notification_settings_for_groups(groups_with_ancestors).preload_source_route.index_by(&:source_id)
preload_emails_disabled
preload_emails_enabled
groups.map do |group|
group_notifications = groups.map do |group|
find_notification_setting_for(group)
end
group_sources = group_notifications.map(&:source)
ActiveRecord::Associations::Preloader.new(records: group_sources, associations: :namespace_settings).call
group_notifications
end
private
@ -45,18 +50,18 @@ class UserGroupNotificationSettingsFinder
parent_setting.level != NotificationSetting.levels[:global] || parent_setting.notification_email.present?
end
# This method preloads the `emails_disabled` strong memoized method for the given groups.
# This method preloads the `emails_enabled` strong memoized method for the given groups.
#
# For each group, look up the ancestor hierarchy and look for any group where emails_disabled is true.
# For each group, look up the ancestor hierarchy and look for any group where emails_enabled is false.
# The lookup is implemented with an EXISTS subquery, so we can look up the ancestor chain for each group individually.
# The query will return groups where at least one ancestor has the `emails_disabled` set to true.
#
# After the query, we set the instance variable.
def preload_emails_disabled
def preload_emails_enabled
group_ids_with_disabled_email = Group.ids_with_disabled_email(groups.to_a)
groups.each do |group|
group.emails_disabled_memoized = group_ids_with_disabled_email.include?(group.id) if group.parent_id
group.emails_enabled_memoized = group_ids_with_disabled_email.exclude?(group.id) if group.parent_id
end
end
end

View File

@ -372,9 +372,7 @@ class Commit
strong_memoize(:raw_signature_type) do
next unless @raw.instance_of?(Gitlab::Git::Commit)
if raw_commit_from_rugged? && gpg_commit.signature_text.present?
:PGP
elsif defined? @raw.raw_commit.signature_type
if defined? @raw.raw_commit.signature_type
@raw.raw_commit.signature_type
end
end
@ -397,10 +395,6 @@ class Commit
end
end
def raw_commit_from_rugged?
@raw.raw_commit.is_a?(Rugged::Commit)
end
def gpg_commit
@gpg_commit ||= Gitlab::Gpg::Commit.new(self)
end

View File

@ -300,14 +300,15 @@ class Group < Namespace
groups.drop(1).each { |group| group.root_ancestor = root }
end
# Returns the ids of the passed group models where the `emails_disabled`
# column is set to true anywhere in the ancestor hierarchy.
# Returns the ids of the passed group models where the `emails_enabled`
# column is set to false anywhere in the ancestor hierarchy.
def ids_with_disabled_email(groups)
inner_groups = Group.where('id = namespaces_with_emails_disabled.id')
inner_query = inner_groups
.self_and_ancestors
.where(emails_disabled: true)
.joins(:namespace_settings)
.where(namespace_settings: { emails_enabled: false })
.select('1')
.limit(1)

View File

@ -138,6 +138,8 @@ class Namespace < ApplicationRecord
to: :namespace_settings
delegate :runner_registration_enabled, :runner_registration_enabled?, :runner_registration_enabled=,
to: :namespace_settings
delegate :emails_enabled, :emails_enabled=,
to: :namespace_settings, allow_nil: true
delegate :allow_runner_registration_token,
:allow_runner_registration_token=,
to: :namespace_settings
@ -204,7 +206,7 @@ class Namespace < ApplicationRecord
# Make sure that the name is same as strong_memoize name in root_ancestor
# method
attr_writer :root_ancestor, :emails_disabled_memoized
attr_writer :root_ancestor, :emails_enabled_memoized
class << self
def sti_class_for(type_name)
@ -382,17 +384,16 @@ class Namespace < ApplicationRecord
# any ancestor can disable emails for all descendants
def emails_disabled?
strong_memoize(:emails_disabled_memoized) do
if parent_id
self_and_ancestors.where(emails_disabled: true).exists?
else
!!emails_disabled
end
end
!emails_enabled?
end
def emails_enabled?
!emails_disabled?
# If no namespace_settings, we can assume it has not changed from enabled
return true unless namespace_settings
strong_memoize(:emails_enabled_memoized) do
namespace_settings.emails_enabled?
end
end
def lfs_enabled?

View File

@ -63,6 +63,12 @@ class NamespaceSetting < ApplicationRecord
namespace.root_ancestor.prevent_sharing_groups_outside_hierarchy
end
def emails_enabled?
return emails_enabled unless namespace.has_parent?
all_ancestors_have_emails_enabled?
end
def show_diff_preview_in_email?
return show_diff_preview_in_email unless namespace.has_parent?
@ -89,6 +95,10 @@ class NamespaceSetting < ApplicationRecord
private
def all_ancestors_have_emails_enabled?
self.class.where(namespace_id: namespace.self_and_ancestors, emails_enabled: false).none?
end
def all_ancestors_allow_diff_preview_in_email?
!self.class.where(namespace_id: namespace.self_and_ancestors, show_diff_preview_in_email: false).exists?
end

View File

@ -1,8 +0,0 @@
---
name: rugged_commit_is_ancestor
introduced_by_url:
rollout_issue_url:
milestone:
type: development
group: group::gitaly
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: rugged_commit_tree_entry
introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/25896
rollout_issue_url:
milestone: '11.9'
type: development
group: group::gitaly
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: rugged_find_commit
introduced_by_url:
rollout_issue_url:
milestone:
type: development
group: group::gitaly
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: rugged_list_commits_by_oid
introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/27441
rollout_issue_url:
milestone: '11.10'
type: development
group: group::gitaly
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: rugged_tree_entries
introduced_by_url:
rollout_issue_url:
milestone:
type: development
group: group::gitaly
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: rugged_tree_entry
introduced_by_url:
rollout_issue_url:
milestone:
type: development
group: group::gitaly
default_enabled: false

View File

@ -59,6 +59,7 @@ GET /groups
"auto_devops_enabled": null,
"subgroup_creation_level": "owner",
"emails_disabled": null,
"emails_enabled": null,
"mentions_disabled": null,
"lfs_enabled": true,
"default_branch_protection": 2,
@ -97,6 +98,7 @@ GET /groups?statistics=true
"auto_devops_enabled": null,
"subgroup_creation_level": "owner",
"emails_disabled": null,
"emails_enabled": null,
"mentions_disabled": null,
"lfs_enabled": true,
"default_branch_protection": 2,
@ -181,6 +183,7 @@ GET /groups/:id/subgroups
"auto_devops_enabled": null,
"subgroup_creation_level": "owner",
"emails_disabled": null,
"emails_enabled": null,
"mentions_disabled": null,
"lfs_enabled": true,
"default_branch_protection": 2,
@ -242,6 +245,7 @@ GET /groups/:id/descendant_groups
"auto_devops_enabled": null,
"subgroup_creation_level": "owner",
"emails_disabled": null,
"emails_enabled": null,
"mentions_disabled": null,
"lfs_enabled": true,
"default_branch_protection": 2,
@ -267,6 +271,7 @@ GET /groups/:id/descendant_groups
"auto_devops_enabled": null,
"subgroup_creation_level": "owner",
"emails_disabled": null,
"emails_enabled": null,
"mentions_disabled": null,
"lfs_enabled": true,
"default_branch_protection": 2,
@ -467,6 +472,7 @@ Example response:
"pages_access_level":"enabled",
"security_and_compliance_access_level":"enabled",
"emails_disabled":null,
"emails_enabled": null,
"shared_runners_enabled":true,
"lfs_enabled":true,
"creator_id":1,
@ -818,7 +824,8 @@ Parameters:
| `avatar` | mixed | no | Image file for avatar of the group. [Introduced in GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/36681) |
| `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). Default to the global level default branch protection setting. |
| `description` | string | no | The group's description. |
| `emails_disabled` | boolean | no | Disable email notifications. |
| `emails_disabled` | boolean | no | _([Deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127899) in GitLab 16.5.)_ Disable email notifications. Use `emails_enabled` instead. |
| `emails_enabled` | boolean | no | Enable email notifications. |
| `lfs_enabled` | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
| `mentions_disabled` | boolean | no | Disable the capability of a group from getting mentioned. |
| `parent_id` | integer | no | The parent group ID for creating nested group. |
@ -975,7 +982,8 @@ PUT /groups/:id
| `avatar` | mixed | no | Image file for avatar of the group. [Introduced in GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/36681) |
| `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). |
| `description` | string | no | The description of the group. |
| `emails_disabled` | boolean | no | Disable email notifications. |
| `emails_disabled` | boolean | no | _([Deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127899) in GitLab 16.5.)_ Disable email notifications. Use `emails_enabled` instead. |
| `emails_enabled` | boolean | no | Enable email notifications. |
| `lfs_enabled` | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
| `mentions_disabled` | boolean | no | Disable the capability of a group from getting mentioned. |
| `prevent_sharing_groups_outside_hierarchy` | boolean | no | See [Prevent group sharing outside the group hierarchy](../user/group/access_and_permissions.md#prevent-group-sharing-outside-the-group-hierarchy). This attribute is only available on top-level groups. [Introduced in GitLab 14.1](https://gitlab.com/gitlab-org/gitlab/-/issues/333721) |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@ -105,6 +105,7 @@ identifying abstract concepts and are subject to changes as we refine the design
allows components to be pinned to a specific revision.
- **Step** is a type of component that contains a collection of instructions for job execution.
- **Template** is a type of component that contains a snippet of CI/CD configuration that can be [included](../../../ci/yaml/includes.md) in a project's pipeline configuration.
- **Publishing** is the act of listing a version of the resource (for example, a project release) on the Catalog.
## Definition of pipeline component
@ -524,17 +525,26 @@ spec:
The CI Catalog is an index of resources that users can leverage in CI/CD. It initially
contains a list of components repositories that users can discover and use in their pipelines.
The user sees only resources based on their permissions and project visibility level.
Unauthenticated users will only see public resources.
Project admins are responsible for setting the project private or public.
The CI Catalog should not provide security functionalities like prevent projects from appearing in the Community Catalog.
If the project is public it's visible to the world anyway.
The Catalog page can provide different filters to refine the user search including
predefined filters such as resources from groups the user is member of.
In the future, the Catalog could contain also other types of resources (for example:
integrations, project templates, etc.).
integrations, project templates, container images, etc.).
To list a components repository in the Catalog we need to mark the project as being a
catalog resource. We do that initially with an API endpoint, similar to changing a project setting.
catalog resource. We do that initially with a project setting.
Once a project is marked as a "catalog resource" it can be displayed in the Catalog.
Once a project is marked as a "catalog resource" it can eventually be displayed in the Catalog.
We could create a database record when the API endpoint is used and remove the record when
the same is disabled/removed.
We could create a database record when the setting is enabled and modify the record's state when
the same is disabled.
## Catalog resource
@ -552,9 +562,6 @@ Other properties of a catalog resource:
- indicators of popularity (stars, forks).
- categorization: user should select a category and or define search tags
As soon as a components repository is marked as being a "catalog resource"
we should be seeing the resource listed in the Catalog.
Initially for the resource, the project may not have any released tags.
Users would be able to use the components repository by specifying a branch name or
commit SHA for the version. However, these types of version qualifiers should not
@ -564,10 +571,14 @@ be listed in the catalog resource's page for various reasons:
- Branches and tags may not be meaningful for the end-user.
- Branches and tags don't communicate versioning thoroughly.
To list a catalog resource in the Catalog we first need to create a release for
the project.
## Releasing new resource versions to the Catalog
The versions that should be displayed for the resource should be the project [releases](../../../user/project/releases/index.md).
Creating project releases is an official act of versioning a resource.
The versions that will be published for the resource should be the project
[releases](../../../user/project/releases/index.md). Creating project releases is an official
act of versioning a resource.
A resource page would have:
@ -599,29 +610,6 @@ For example: index the content of `spec:` section for CI components.
See an [example of development workflow](dev_workflow.md) for a components repository.
## Availability of CI catalog as a feature
We plan to introduce 2 features of CI catalog as separate views:
1. **Namespace Catalog (GitLab Ultimate):** allows organizations to share and discover catalog resources
created inside the top-level namespace.
Users will be able to access the Namespace Catalog from a project or subgroup inside the top-level
namespace.
1. **Community Catalog (GitLab free):** allows anyone in a GitLab instance to share and discover catalog
resources. The Community Catalog presents only resources/projects that are public.
If a resource in a Namespace Catalog is made public (changing the project's visibility) the resource is
available in both Namespace Catalog (because it comes from there) as well as the Community Catalog
(because it's public).
![Namespace and Community Catalogs](img/catalogs.png)
There is only 1 CI catalog. The Namespace and Community Catalogs are different views of the CI catalog.
Project admins are responsible for setting the project private or public.
The CI Catalog should not provide security functionalities like prevent projects from appearing in the Community Catalog.
If the project is public it's visible to the world anyway.
## Note about future resource types
In the future, to support multiple types of resources in the Catalog we could
@ -673,6 +661,8 @@ metadata:
## Iterations
The first plan of iterations constisted in:
1. Experimentation phase
- Build an MVC behind a feature flag with `namespace` actor.
- Enable the feature flag only for `gitlab-com` and `gitlab-org` namespaces to initiate the dogfooding.
@ -691,6 +681,9 @@ metadata:
components from GitLab.com or from repository exports.
- Iterate on feedback.
In October 2023, after releasing the namespace-view (previously called private catalog view) as Experiment we changed
focus moving away from 2 separate views (namespace view and global view) and combining the UX in a single global view.
## Limits
Any MVC that exposes a feature should be added with limitations from the beginning.

View File

@ -14,10 +14,20 @@ type: index, concepts, howto
Use GitLab ChatOps to interact with CI/CD jobs through chat services
like Slack.
Many organizations use chat services to collaborate, troubleshoot, and plan work. With ChatOps,
Many organizations use Slack or Mattermost to collaborate, troubleshoot, and plan work. With ChatOps,
you can discuss work with your team, run CI/CD jobs, and view job output, all from the same
application.
## Slash command integrations
You can trigger ChatOps with the [`run` slash command](../../user/project/integrations/gitlab_slack_application.md#slash-commands).
The following integrations are available:
- [GitLab for Slack app](../../user/project/integrations/gitlab_slack_application.md) (recommended for Slack)
- [Slack slash commands](../../user/project/integrations/slack_slash_commands.md)
- [Mattermost slash commands](../../user/project/integrations/mattermost_slash_commands.md)
## ChatOps workflow and CI/CD configuration
ChatOps looks for the specified job in the
@ -37,7 +47,7 @@ run as part of the standard CI/CD pipeline.
ChatOps passes the following [CI/CD variables](../variables/index.md#predefined-cicd-variables)
to the job:
- `CHAT_INPUT` - The arguments passed to `/project-name run`.
- `CHAT_INPUT` - The arguments passed to the `run` slash command.
- `CHAT_CHANNEL` - The name of the chat channel the job is run from.
- `CHAT_USER_ID` - The chat service ID of the user who runs the job.
@ -47,30 +57,13 @@ When the job runs:
- If the job completes in more than 30 minutes, you must use a method like the
[Slack API](https://api.slack.com/) to send data to the channel.
## Run a CI/CD job
Prerequisite:
- You must have at least the Developer role for the project.
You can run a CI/CD job on the default branch from chat. To run a CI/CD job:
- In the chat client, enter `/<project-name> run <job name> <arguments>` where:
- `<project-name>` is the name of the project.
- `<job name>` is the name of the CI/CD job to run.
- `<arguments>` is the arguments to pass to the CI/CD job.
ChatOps schedules a pipeline that contains only the specified job.
Other [slash commands](../../user/project/integrations/gitlab_slack_application.md#slash-commands) are also available.
### Exclude a job from ChatOps
To prevent a job from being run from chat:
- In `.gitlab-ci.yml`, set the job to `except: [chat]`.
## Customize the ChatOps reply
### Customize the ChatOps reply
ChatOps sends the output for a job with a single command to the
channel as a reply. For example, when the following job runs,
@ -108,8 +101,34 @@ ls:
- echo -e "section_start:$( date +%s ):chat_reply\r\033[0K\n$( ls -la )\nsection_end:$( date +%s ):chat_reply\r\033[0K"
```
## Trigger a CI/CD job using ChatOps
Prerequisite:
- You must have at least the Developer role for the project.
- The project is configured to use a slash command integration.
You can run a CI/CD job on the default branch from Slack or Mattermost.
The slash command to trigger a CI/CD job depends on which slash command integration
is configured for the project.
- For the GitLab for Slack app, use `/gitlab <project-name> run <job name> <arguments>`.
- For Slack or Mattermost slash commands, use `/<trigger-name> run <job name> <arguments>`.
Where:
- `<job name>` is the name of the CI/CD job to run.
- `<arguments>` are the arguments to pass to the CI/CD job.
- `<trigger-name>` is the trigger name configured for the Slack or Mattermost integration.
ChatOps schedules a pipeline that contains only the specified job.
## Related topics
- [The official GitLab ChatOps icon](img/gitlab-chatops-icon.png)
- [A repository of common ChatOps scripts](https://gitlab.com/gitlab-com/chatops)
that GitLab uses to interact with GitLab.com
- [GitLab for Slack app](../../user/project/integrations/gitlab_slack_application.md)
- [Slack slash commands](../../user/project/integrations/slack_slash_commands.md)
- [Mattermost slash commands](../../user/project/integrations/mattermost_slash_commands.md)
- [The official GitLab ChatOps icon](img/gitlab-chatops-icon.png)

View File

@ -2794,226 +2794,6 @@ The `linux:rspec` job runs as soon as the `linux:build: [aws, app1]` job finishe
script: echo "Running rspec on linux..."
```
### `only` / `except`
NOTE:
`only` and `except` are not being actively developed. To control when to add jobs to pipelines,
use [`rules`](#rules) instead.
You can use `only` and `except` to control when to add jobs to pipelines.
- Use `only` to define when a job runs.
- Use `except` to define when a job **does not** run.
See [specify when jobs run with `only` and `except`](../jobs/job_control.md#specify-when-jobs-run-with-only-and-except)
for more details and examples.
#### `only:refs` / `except:refs`
NOTE:
`only:refs` and `except:refs` are not being actively developed. To use refs, regular expressions,
or variables to control when to add jobs to pipelines, use [`rules:if`](#rulesif) instead.
Use the `only:refs` and `except:refs` keywords to control when to add jobs to a
pipeline based on branch names or pipeline types.
**Keyword type**: Job keyword. You can use it only as part of a job.
**Possible inputs**: An array including any number of:
- Branch names, for example `main` or `my-feature-branch`.
- [Regular expressions](../jobs/job_control.md#only--except-regex-syntax)
that match against branch names, for example `/^feature-.*/`.
- The following keywords:
| **Value** | **Description** |
| -------------------------|-----------------|
| `api` | For pipelines triggered by the [pipelines API](../../api/pipelines.md#create-a-new-pipeline). |
| `branches` | When the Git reference for a pipeline is a branch. |
| `chat` | For pipelines created by using a [GitLab ChatOps](../chatops/index.md) command. |
| `external` | When you use CI services other than GitLab. |
| `external_pull_requests` | When an external pull request on GitHub is created or updated (See [Pipelines for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests)). |
| `merge_requests` | For pipelines created when a merge request is created or updated. Enables [merge request pipelines](../pipelines/merge_request_pipelines.md), [merged results pipelines](../pipelines/merged_results_pipelines.md), and [merge trains](../pipelines/merge_trains.md). |
| `pipelines` | For [multi-project pipelines](../pipelines/downstream_pipelines.md#multi-project-pipelines) created by [using the API with `CI_JOB_TOKEN`](../pipelines/downstream_pipelines.md#trigger-a-multi-project-pipeline-by-using-the-api), or the [`trigger`](#trigger) keyword. |
| `pushes` | For pipelines triggered by a `git push` event, including for branches and tags. |
| `schedules` | For [scheduled pipelines](../pipelines/schedules.md). |
| `tags` | When the Git reference for a pipeline is a tag. |
| `triggers` | For pipelines created by using a [trigger token](../triggers/index.md#configure-cicd-jobs-to-run-in-triggered-pipelines). |
| `web` | For pipelines created by selecting **Run pipeline** in the GitLab UI, from the project's **Build > Pipelines** section. |
**Example of `only:refs` and `except:refs`**:
```yaml
job1:
script: echo
only:
- main
- /^issue-.*$/
- merge_requests
job2:
script: echo
except:
- main
- /^stable-branch.*$/
- schedules
```
**Additional details**:
- Scheduled pipelines run on specific branches, so jobs configured with `only: branches`
run on scheduled pipelines too. Add `except: schedules` to prevent jobs with `only: branches`
from running on scheduled pipelines.
- `only` or `except` used without any other keywords are equivalent to `only: refs`
or `except: refs`. For example, the following two jobs configurations have the same
behavior:
```yaml
job1:
script: echo
only:
- branches
job2:
script: echo
only:
refs:
- branches
```
- If a job does not use `only`, `except`, or [`rules`](#rules), then `only` is set to `branches`
and `tags` by default.
For example, `job1` and `job2` are equivalent:
```yaml
job1:
script: echo "test"
job2:
script: echo "test"
only:
- branches
- tags
```
#### `only:variables` / `except:variables`
NOTE:
`only:variables` and `except:variables` are not being actively developed. To use refs,
regular expressions, or variables to control when to add jobs to pipelines, use [`rules:if`](#rulesif) instead.
Use the `only:variables` or `except:variables` keywords to control when to add jobs
to a pipeline, based on the status of [CI/CD variables](../variables/index.md).
**Keyword type**: Job keyword. You can use it only as part of a job.
**Possible inputs**:
- An array of [CI/CD variable expressions](../jobs/job_control.md#cicd-variable-expressions).
**Example of `only:variables`**:
```yaml
deploy:
script: cap staging deploy
only:
variables:
- $RELEASE == "staging"
- $STAGING
```
**Related topics**:
- [`only:variables` and `except:variables` examples](../jobs/job_control.md#only-variables--except-variables-examples).
#### `only:changes` / `except:changes`
NOTE:
`only:changes` and `except:changes` are not being actively developed. To use changed files
to control when to add a job to a pipeline, use [`rules:changes`](#ruleschanges) instead.
Use the `changes` keyword with `only` to run a job, or with `except` to skip a job,
when a Git push event modifies a file.
Use `changes` in pipelines with the following refs:
- `branches`
- `external_pull_requests`
- `merge_requests` (see additional details about [using `only:changes` with merge request pipelines](../jobs/job_control.md#use-onlychanges-with-merge-request-pipelines))
**Keyword type**: Job keyword. You can use it only as part of a job.
**Possible inputs**: An array including any number of:
- Paths to files.
- Wildcard paths for single directories, for example `path/to/directory/*`, or a directory
and all its subdirectories, for example `path/to/directory/**/*`.
- Wildcard [glob](https://en.wikipedia.org/wiki/Glob_(programming)) paths for all
files with the same extension or multiple extensions, for example `*.md` or `path/to/directory/*.{rb,py,sh}`.
See the [Ruby `fnmatch` documentation](https://docs.ruby-lang.org/en/master/File.html#method-c-fnmatch)
for the supported syntax list.
- Wildcard paths to files in the root directory, or all directories, wrapped in double quotes.
For example `"*.json"` or `"**/*.json"`.
**Example of `only:changes`**:
```yaml
docker build:
script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
only:
refs:
- branches
changes:
- Dockerfile
- docker/scripts/*
- dockerfiles/**/*
- more_scripts/*.{rb,py,sh}
- "**/*.json"
```
**Additional details**:
- `changes` resolves to `true` if any of the matching files are changed (an `OR` operation).
- If you use refs other than `branches`, `external_pull_requests`, or `merge_requests`,
`changes` can't determine if a given file is new or old and always returns `true`.
- If you use `only: changes` with other refs, jobs ignore the changes and always run.
- If you use `except: changes` with other refs, jobs ignore the changes and never run.
**Related topics**:
- [`only: changes` and `except: changes` examples](../jobs/job_control.md#onlychanges--exceptchanges-examples).
- If you use `changes` with [only allow merge requests to be merged if the pipeline succeeds](../../user/project/merge_requests/merge_when_pipeline_succeeds.md#require-a-successful-pipeline-for-merge),
you should [also use `only:merge_requests`](../jobs/job_control.md#use-onlychanges-with-merge-request-pipelines).
- [Jobs or pipelines can run unexpectedly when using `only: changes`](../jobs/job_control.md#jobs-or-pipelines-run-unexpectedly-when-using-changes).
#### `only:kubernetes` / `except:kubernetes`
NOTE:
`only:refs` and `except:refs` are not being actively developed. To control if jobs are added
to the pipeline when the Kubernetes service is active in the project, use [`rules:if`](#rulesif)
with the [`CI_KUBERNETES_ACTIVE`](../variables/predefined_variables.md) predefined CI/CD variable instead.
Use `only:kubernetes` or `except:kubernetes` to control if jobs are added to the pipeline
when the Kubernetes service is active in the project.
**Keyword type**: Job-specific. You can use it only as part of a job.
**Possible inputs**:
- The `kubernetes` strategy accepts only the `active` keyword.
**Example of `only:kubernetes`**:
```yaml
deploy:
only:
kubernetes: active
```
In this example, the `deploy` job runs only when the Kubernetes service is active
in the project.
### `pages`
Use `pages` to define a [GitLab Pages](../../user/project/pages/index.md) job that
@ -4930,9 +4710,9 @@ The following keywords are deprecated.
### Globally-defined `image`, `services`, `cache`, `before_script`, `after_script`
Defining `image`, `services`, `cache`, `before_script`, and
`after_script` globally is deprecated. Support could be removed
from a future release.
Defining `image`, `services`, `cache`, `before_script`, and `after_script` globally is deprecated.
Using these keywords at the top level is still possible to ensure backwards compatibility,
but could be scheduled for removal in a future milestone.
Use [`default`](#default) instead. For example:
@ -4950,14 +4730,233 @@ default:
- rm -rf tmp/
```
<!-- ## Troubleshooting
### `only` / `except`
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
one might have when setting this up, or when something is changed, or on upgrading, it's
important to describe those, too. Think of things that may go wrong and include them here.
This is important to minimize requests for support, and to avoid doc comments with
questions that you know someone might ask.
NOTE:
`only` and `except` are deprecated and not being actively developed. These keywords
are still usable to ensure backwards compatibility, but could be scheduled for removal
in a future milestone. To control when to add jobs to pipelines, use [`rules`](#rules) instead.
Each scenario can be a third-level heading, for example, `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
You can use `only` and `except` to control when to add jobs to pipelines.
- Use `only` to define when a job runs.
- Use `except` to define when a job **does not** run.
See [specify when jobs run with `only` and `except`](../jobs/job_control.md#specify-when-jobs-run-with-only-and-except)
for more details and examples.
#### `only:refs` / `except:refs`
NOTE:
`only:refs` and `except:refs` are deprecated and not being actively developed. These keywords
are still usable to ensure backwards compatibility, but could be scheduled for removal
in a future milestone. To use refs, regular expressions, or variables to control
when to add jobs to pipelines, use [`rules:if`](#rulesif) instead.
You can use the `only:refs` and `except:refs` keywords to control when to add jobs to a
pipeline based on branch names or pipeline types.
**Keyword type**: Job keyword. You can use it only as part of a job.
**Possible inputs**: An array including any number of:
- Branch names, for example `main` or `my-feature-branch`.
- [Regular expressions](../jobs/job_control.md#only--except-regex-syntax)
that match against branch names, for example `/^feature-.*/`.
- The following keywords:
| **Value** | **Description** |
| -------------------------|-----------------|
| `api` | For pipelines triggered by the [pipelines API](../../api/pipelines.md#create-a-new-pipeline). |
| `branches` | When the Git reference for a pipeline is a branch. |
| `chat` | For pipelines created by using a [GitLab ChatOps](../chatops/index.md) command. |
| `external` | When you use CI services other than GitLab. |
| `external_pull_requests` | When an external pull request on GitHub is created or updated (See [Pipelines for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests)). |
| `merge_requests` | For pipelines created when a merge request is created or updated. Enables [merge request pipelines](../pipelines/merge_request_pipelines.md), [merged results pipelines](../pipelines/merged_results_pipelines.md), and [merge trains](../pipelines/merge_trains.md). |
| `pipelines` | For [multi-project pipelines](../pipelines/downstream_pipelines.md#multi-project-pipelines) created by [using the API with `CI_JOB_TOKEN`](../pipelines/downstream_pipelines.md#trigger-a-multi-project-pipeline-by-using-the-api), or the [`trigger`](#trigger) keyword. |
| `pushes` | For pipelines triggered by a `git push` event, including for branches and tags. |
| `schedules` | For [scheduled pipelines](../pipelines/schedules.md). |
| `tags` | When the Git reference for a pipeline is a tag. |
| `triggers` | For pipelines created by using a [trigger token](../triggers/index.md#configure-cicd-jobs-to-run-in-triggered-pipelines). |
| `web` | For pipelines created by selecting **Run pipeline** in the GitLab UI, from the project's **Build > Pipelines** section. |
**Example of `only:refs` and `except:refs`**:
```yaml
job1:
script: echo
only:
- main
- /^issue-.*$/
- merge_requests
job2:
script: echo
except:
- main
- /^stable-branch.*$/
- schedules
```
**Additional details**:
- Scheduled pipelines run on specific branches, so jobs configured with `only: branches`
run on scheduled pipelines too. Add `except: schedules` to prevent jobs with `only: branches`
from running on scheduled pipelines.
- `only` or `except` used without any other keywords are equivalent to `only: refs`
or `except: refs`. For example, the following two jobs configurations have the same
behavior:
```yaml
job1:
script: echo
only:
- branches
job2:
script: echo
only:
refs:
- branches
```
- If a job does not use `only`, `except`, or [`rules`](#rules), then `only` is set to `branches`
and `tags` by default.
For example, `job1` and `job2` are equivalent:
```yaml
job1:
script: echo "test"
job2:
script: echo "test"
only:
- branches
- tags
```
#### `only:variables` / `except:variables`
NOTE:
`only:variables` and `except:variables` are deprecated and not being actively developed.
These keywords are still usable to ensure backwards compatibility, but could be scheduled
for removal in a future milestone. To use refs, regular expressions, or variables
to control when to add jobs to pipelines, use [`rules:if`](#rulesif) instead.
You can use the `only:variables` or `except:variables` keywords to control when to add jobs
to a pipeline, based on the status of [CI/CD variables](../variables/index.md).
**Keyword type**: Job keyword. You can use it only as part of a job.
**Possible inputs**:
- An array of [CI/CD variable expressions](../jobs/job_control.md#cicd-variable-expressions).
**Example of `only:variables`**:
```yaml
deploy:
script: cap staging deploy
only:
variables:
- $RELEASE == "staging"
- $STAGING
```
**Related topics**:
- [`only:variables` and `except:variables` examples](../jobs/job_control.md#only-variables--except-variables-examples).
#### `only:changes` / `except:changes`
`only:variables` and `except:variables`
NOTE:
`only:changes` and `except:changes` are deprecated and not being actively developed.
These keywords are still usable to ensure backwards compatibility, but could be scheduled
for removal in a future milestone. To use changed files to control when to add a job to a pipeline,
use [`rules:changes`](#ruleschanges) instead.
Use the `changes` keyword with `only` to run a job, or with `except` to skip a job,
when a Git push event modifies a file.
Use `changes` in pipelines with the following refs:
- `branches`
- `external_pull_requests`
- `merge_requests` (see additional details about [using `only:changes` with merge request pipelines](../jobs/job_control.md#use-onlychanges-with-merge-request-pipelines))
**Keyword type**: Job keyword. You can use it only as part of a job.
**Possible inputs**: An array including any number of:
- Paths to files.
- Wildcard paths for single directories, for example `path/to/directory/*`, or a directory
and all its subdirectories, for example `path/to/directory/**/*`.
- Wildcard [glob](https://en.wikipedia.org/wiki/Glob_(programming)) paths for all
files with the same extension or multiple extensions, for example `*.md` or `path/to/directory/*.{rb,py,sh}`.
See the [Ruby `fnmatch` documentation](https://docs.ruby-lang.org/en/master/File.html#method-c-fnmatch)
for the supported syntax list.
- Wildcard paths to files in the root directory, or all directories, wrapped in double quotes.
For example `"*.json"` or `"**/*.json"`.
**Example of `only:changes`**:
```yaml
docker build:
script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
only:
refs:
- branches
changes:
- Dockerfile
- docker/scripts/*
- dockerfiles/**/*
- more_scripts/*.{rb,py,sh}
- "**/*.json"
```
**Additional details**:
- `changes` resolves to `true` if any of the matching files are changed (an `OR` operation).
- If you use refs other than `branches`, `external_pull_requests`, or `merge_requests`,
`changes` can't determine if a given file is new or old and always returns `true`.
- If you use `only: changes` with other refs, jobs ignore the changes and always run.
- If you use `except: changes` with other refs, jobs ignore the changes and never run.
**Related topics**:
- [`only: changes` and `except: changes` examples](../jobs/job_control.md#onlychanges--exceptchanges-examples).
- If you use `changes` with [only allow merge requests to be merged if the pipeline succeeds](../../user/project/merge_requests/merge_when_pipeline_succeeds.md#require-a-successful-pipeline-for-merge),
you should [also use `only:merge_requests`](../jobs/job_control.md#use-onlychanges-with-merge-request-pipelines).
- [Jobs or pipelines can run unexpectedly when using `only: changes`](../jobs/job_control.md#jobs-or-pipelines-run-unexpectedly-when-using-changes).
#### `only:kubernetes` / `except:kubernetes`
NOTE:
`only:kubernetes` and `except:kubernetes` are deprecated and not being actively developed.
These keywords are still usable to ensure backwards compatibility, but could be scheduled
for removal in a future milestone. To control if jobs are added to the pipeline when
the Kubernetes service is active in the project, use [`rules:if`](#rulesif) with the
[`CI_KUBERNETES_ACTIVE`](../variables/predefined_variables.md) predefined CI/CD variable instead.
Use `only:kubernetes` or `except:kubernetes` to control if jobs are added to the pipeline
when the Kubernetes service is active in the project.
**Keyword type**: Job-specific. You can use it only as part of a job.
**Possible inputs**:
- The `kubernetes` strategy accepts only the `active` keyword.
**Example of `only:kubernetes`**:
```yaml
deploy:
only:
kubernetes: active
```
In this example, the `deploy` job runs only when the Kubernetes service is active
in the project.

View File

@ -49,6 +49,8 @@ The usage of schema enforces the base class to be used:
### Guidelines on choosing between `gitlab_main_cell` and `gitlab_main_clusterwide` schema
Depending on the use case, your feature may be [cell-local or clusterwide](../../architecture/blueprints/cells/index.md#how-do-i-decide-whether-to-move-my-feature-to-the-cluster-cell-or-organization-level) and hence the tables used for the feature should also use the appropriate schema.
When you choose the appropriate schema for tables, consider the following guidelines as part of the [Cells](../../architecture/blueprints/cells/index.md) architecture:
- Default to `gitlab_main_cell`: We expect most tables to be assigned to the `gitlab_main_cell` schema by default. Choose this schema if the data in the table is related to `projects` or `namespaces`.

View File

@ -24,8 +24,7 @@ To contribute to the development of the GitLab product, view
## How it works
GitLab uses [Unleash](https://github.com/Unleash/unleash), a feature
toggle service.
GitLab offers an [Unleash](https://github.com/Unleash/unleash)-compatible API for feature flags.
By enabling or disabling a flag in GitLab, your application
can determine which features to enable or disable.
@ -76,10 +75,9 @@ is 200. For GitLab SaaS, the maximum number is determined by [tier](https://abou
You can apply a feature flag strategy across multiple environments, without defining
the strategy multiple times.
GitLab feature flags use [Unleash](https://docs.getunleash.io/) as the feature flag
engine. In Unleash, there are [strategies](https://docs.getunleash.io/reference/activation-strategies)
for granular feature flag controls. GitLab feature flags can have multiple strategies,
and the supported strategies are:
GitLab feature flags are based on [Unleash](https://docs.getunleash.io/). In Unleash, there are
[strategies](https://docs.getunleash.io/reference/activation-strategies) for granular feature
flag controls. GitLab feature flags can have multiple strategies, and the supported strategies are:
- [All users](#all-users)
- [Percent of Users](#percent-of-users)

View File

@ -151,15 +151,66 @@ Prerequisite:
- You have an agent configured with the `user_access` entry.
To grant Kubernetes API access:
### Configure local access with the GitLab CLI (recommended)
You can use the [GitLab CLI `glab`](../../../editor_extensions/gitlab_cli/index.md) to create or update
a Kubernetes configuration file to access the agent Kubernetes API.
Use `glab cluster agent` commands to manage cluster connections:
1. View a list of all the agents associated with your project:
```shell
glab cluster agent list --repo '<group>/<project>'
# If your current working directory is the Git repository of the project with the agent, you can omit the --repo option:
glab cluster agent list
```
1. Use the numerical agent ID presented in the first column of the output to update your `kubeconfig`:
```shell
glab cluster agent update-kubeconfig --repo '<group>/<project>' --agent '<agent-id>' --use-context
```
1. Verify the update with `kubectl` or your preferred Kubernetes tooling:
```shell
kubectl get nodes
```
The `update-kubeconfig` command sets `glab cluster agent get-token` as a
[credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins)
for Kubernetes tools to retrieve a token. The `get-token` command creates and
returns a personal access token that is valid until the end of the current day.
Kubernetes tools cache the token until it expires, the API returns an authorization error, or the process exits. Expect all subsequent calls to your Kubernetes tooling to create a new token.
The `glab cluster agent update-kubeconfig` command supports a number of command line flags. You can view all supported flags with `glab cluster agent update-kubeconfig --help`.
Some examples:
```shell
# When the current working directory is the Git repository where the agent is registered the --repo / -R flag can be omitted
glab cluster agent update-kubeconfig --agent '<agent-id>'
# When the --use-context option is specified the `current-context` of the kubeconfig file is changed to the agent context
glab cluster agent update-kubeconfig --agent '<agent-id>' --use-context
# The --kubeconfig flag can be used to specify an alternative kubeconfig path
glab cluster agent update-kubeconfig --agent '<agent-id>' --kubeconfig ~/gitlab.kubeconfig
```
### Configure local access manually using a personal access token
You can configure access to a Kubernetes cluster using a long-lived personal access token following these steps:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Operate > Kubernetes clusters** and retrieve the numerical ID of the agent you want to access. You need the ID to construct the full API token.
1. Create a [personal access token](../../profile/personal_access_tokens.md) with the `k8s_proxy` scope. You need the access token to construct the full API token.
1. Construct `kube config` entries to access the cluster:
1. Make sure that the proper `kube config` is selected.
1. Construct `kubeconfig` entries to access the cluster:
1. Make sure that the proper `kubeconfig` is selected.
For example, you can set the `KUBECONFIG` environment variable.
1. Add the GitLab KAS proxy cluster to the `kube config`:
1. Add the GitLab KAS proxy cluster to the `kubeconfig`:
```shell
kubectl config set-cluster <cluster_name> --server "https://kas.gitlab.com/k8s-proxy"

View File

@ -54,7 +54,8 @@ To set up SSO with Azure as your identity provider:
1. You should set the following attributes:
- **Unique User Identifier (Name identifier)** to `user.objectID`.
- **nameid-format** to `persistent`. For more information, see how to [manage user SAML identity](#manage-user-saml-identity).
- **Additional claims** to [supported attributes](#user-attributes).
- **email** to `user.mail` or similar.
- **Additional claims** to [supported attributes](#configure-assertions).
1. Make sure the identity provider is set to have provider-initiated calls
to link existing GitLab accounts.
@ -98,7 +99,7 @@ To set up Google Workspace as your identity provider:
- For **Last name**: `last_name`.
- For **Name ID format**: `EMAIL`.
- For **NameID**: `Basic Information > Primary email`.
For more information, see [manage user SAML identity](#manage-user-saml-identity).
For more information, see [supported attributes](#configure-assertions).
1. Make sure the identity provider is set to have provider-initiated calls
to link existing GitLab accounts.
@ -134,6 +135,8 @@ To set up SSO with Okta as your identity provider:
1. Set these values:
- For **Application username (NameID)**: **Custom** `user.getInternalProperty("id")`.
- For **Name ID Format**: `Persistent`. For more information, see [manage user SAML identity](#manage-user-saml-identity).
- For **email**: `user.email` or similar.
- For additional **Attribute Statements**, see [supported attributes](#configure-assertions).
1. Make sure the identity provider is set to have provider-initiated calls
to link existing GitLab accounts.
@ -170,10 +173,28 @@ To set up OneLogin as your identity provider:
| **Identity provider single sign-on URL** | **SAML 2.0 Endpoint** |
1. For **NameID**, use `OneLogin ID`. For more information, see [manage user SAML identity](#manage-user-saml-identity).
1. Configure [required and supported attributes](#configure-assertions).
1. Make sure the identity provider is set to have provider-initiated calls
to link existing GitLab accounts.
### Configure assertions
At minimum, you must configure the following assertions:
1. [NameID](#manage-user-saml-identity).
1. Email.
Optionally, you can pass user information to GitLab as attributes in the SAML assertion.
- The user's email address can be an **email** or **mail** attribute.
- The username can be either a **username** or **nickname** attribute. You should specify only
one of these.
For more information, see the [attributes available for self-managed GitLab instances](../../../integration/saml.md#configure-assertions).
NOTE:
Attribute names starting with phrases such as `http://schemas.microsoft.com/ws/2008/06/identity/claims/` are not supported. For more information on configuring required attribute names in the SAML identity provider's settings, see [example group SAML and SCIM configurations](../../../user/group/saml_sso/example_saml_config.md).
### Use metadata
To configure some identity providers, you need a GitLab metadata URL.
@ -253,19 +274,6 @@ When a user tries to sign in with Group SSO, GitLab attempts to find or create a
- Create a new account with another email address.
- Sign-in to their existing account to link the SAML identity.
### User attributes
You can pass user information to GitLab as attributes in the SAML assertion.
- The user's email address can be an **email** or **mail** attribute.
- The username can be either a **username** or **nickname** attribute. You should specify only
one of these.
For more information, see the [attributes available for self-managed GitLab instances](../../../integration/saml.md#configure-assertions).
NOTE:
Attribute names starting with phrases such as `http://schemas.microsoft.com/ws/2008/06/identity/claims/` are not supported. For more information on configuring required attribute names in the SAML identity provider's settings, see [example group SAML and SCIM configurations](../../../user/group/saml_sso/example_saml_config.md).
### Link SAML to your existing GitLab.com account
> **Remember me** checkbox [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/121569) in GitLab 15.7.

View File

@ -33,7 +33,7 @@ Prerequisites:
- You must [authenticate with the API](../../../api/rest/index.md#authentication).
If authenticating with a deploy token, it must be configured with the `write_package_registry`
scope. If authenticating with a personal access token or project access token, it must be
configured with the `api` scope.
configured with the `api` scope. Project access tokens must have at least the Developer role.
- You must call this API endpoint serially when attempting to upload multiple files under the
same package name and version. Attempts to concurrently upload multiple files into
a new package name and version may face partial failures with
@ -142,7 +142,9 @@ If multiple packages have the same name, version, and filename, then the most re
Prerequisites:
- You need to [authenticate with the API](../../../api/rest/index.md#authentication). If authenticating with a deploy token, it must be configured with the `read_package_registry` and/or `write_package_registry` scope.
- You need to [authenticate with the API](../../../api/rest/index.md#authentication).
- If authenticating with a deploy token, it must be configured with the `read_package_registry` and/or `write_package_registry` scope.
- Project access tokens require the `read_api` scope and at least the `Reporter` role.
```plaintext
GET /projects/:id/packages/generic/:package_name/:package_version/:file_name

View File

@ -74,7 +74,7 @@ You can use slash commands to run common GitLab operations. Replace `<project>`
- You must authorize your Slack user on GitLab.com when you run your first slash command.
- You can [create a shorter project alias](#create-a-project-alias-for-slash-commands) for slash commands.
**For [Slack slash commands](slack_slash_commands.md) on self-managed GitLab, [Mattermost slash commands](mattermost_slash_commands.md), and [ChatOps](../../../ci/chatops/index.md)**, replace `/gitlab` with the slash command trigger name configured for your integration.
**For [Slack slash commands](slack_slash_commands.md) on self-managed GitLab and [Mattermost slash commands](mattermost_slash_commands.md), replace `/gitlab` with the slash command trigger name configured for your integration.
The following slash commands are available:

View File

@ -6,27 +6,24 @@ module API
expose :signature_type, documentation: { type: 'string', example: 'PGP' }
expose :signature, merge: true do |commit, options|
if commit.signature.is_a?(::CommitSignatures::GpgSignature) || commit.raw_commit_from_rugged?
case commit.signature
when ::CommitSignatures::GpgSignature
::API::Entities::GpgCommitSignature.represent commit_signature(commit), options
elsif commit.signature.is_a?(::CommitSignatures::X509CommitSignature)
when ::CommitSignatures::X509CommitSignature
::API::Entities::X509Signature.represent commit.signature, options
elsif commit.signature.is_a?(::CommitSignatures::SshSignature)
when ::CommitSignatures::SshSignature
::API::Entities::SshSignature.represent(commit.signature, options)
end
end
expose :commit_source, documentation: { type: 'string', example: 'gitaly' } do |commit, _|
commit.raw_commit_from_rugged? ? "rugged" : "gitaly"
expose :commit_source, documentation: { type: 'string', example: 'gitaly' } do |_commit, _|
"gitaly"
end
private
def commit_signature(commit)
if commit.raw_commit_from_rugged?
commit.gpg_commit.signature
else
commit.signature
end
commit.signature
end
end
end

View File

@ -10,7 +10,8 @@ module API
expose :project_creation_level_str, as: :project_creation_level
expose :auto_devops_enabled
expose :subgroup_creation_level_str, as: :subgroup_creation_level
expose :emails_disabled
expose(:emails_disabled, documentation: { type: 'boolean' }) { |group, options| group.emails_disabled? }
expose :emails_enabled, documentation: { type: 'boolean' }
expose :mentions_disabled
expose :lfs_enabled?, as: :lfs_enabled
expose :default_branch_protection

View File

@ -18,7 +18,8 @@ module API
optional :project_creation_level, type: String, values: ::Gitlab::Access.project_creation_string_values, desc: 'Determine if developers can create projects in the group', as: :project_creation_level_str
optional :auto_devops_enabled, type: Boolean, desc: 'Default to Auto DevOps pipeline for all projects within this group'
optional :subgroup_creation_level, type: String, values: ::Gitlab::Access.subgroup_creation_string_values, desc: 'Allowed to create subgroups', as: :subgroup_creation_level_str
optional :emails_disabled, type: Boolean, desc: 'Disable email notifications'
optional :emails_disabled, type: Boolean, desc: '_(Deprecated)_ Disable email notifications. Use: emails_enabled'
optional :emails_enabled, type: Boolean, desc: 'Enable email notifications'
optional :mentions_disabled, type: Boolean, desc: 'Disable a group from getting mentioned'
optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group'
optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'

View File

@ -1,28 +0,0 @@
# frozen_string_literal: true
module Gitlab
module ConfigChecker
module PumaRuggedChecker
extend self
extend Gitlab::Git::RuggedImpl::UseRugged
def check
notices = []
if running_puma_with_multiple_threads? && rugged_enabled_through_feature_flag?
link_start = '<a href="https://docs.gitlab.com/ee/administration/operations/puma.html#performance-caveat-when-using-puma-with-rugged">'
link_end = '</a>'
notices << {
type: 'warning',
message: _('Puma is running with a thread count above 1 and the Rugged '\
'service is enabled. This may decrease performance in some environments. '\
'See our %{link_start}documentation%{link_end} '\
'for details of this issue.') % { link_start: link_start, link_end: link_end }
}
end
notices
end
end
end
end

View File

@ -230,5 +230,3 @@ module Gitlab
end
end
end
Gitlab::Git::Blob.singleton_class.prepend Gitlab::Git::RuggedImpl::Blob::ClassMethods

View File

@ -5,7 +5,6 @@ module Gitlab
module Git
class Commit
include Gitlab::EncodingHelper
prepend Gitlab::Git::RuggedImpl::Commit
extend Gitlab::Git::WrapsGitalyErrors
include Gitlab::Utils::StrongMemoize
@ -502,5 +501,3 @@ module Gitlab
end
end
end
Gitlab::Git::Commit.singleton_class.prepend Gitlab::Git::RuggedImpl::Commit::ClassMethods

View File

@ -4,7 +4,6 @@ module Gitlab
module Git
class Ref
include Gitlab::EncodingHelper
include Gitlab::Git::RuggedImpl::Ref
# Branch or tag name
# without "refs/tags|heads" prefix

View File

@ -11,7 +11,6 @@ module Gitlab
include Gitlab::Git::WrapsGitalyErrors
include Gitlab::EncodingHelper
include Gitlab::Utils::StrongMemoize
prepend Gitlab::Git::RuggedImpl::Repository
SEARCH_CONTEXT_LINES = 3
REV_LIST_COMMIT_LIMIT = 2_000

View File

@ -1,107 +0,0 @@
# frozen_string_literal: true
# NOTE: This code is legacy. Do not add/modify code here unless you have
# discussed with the Gitaly team. See
# https://docs.gitlab.com/ee/development/gitaly.html#legacy-rugged-code
# for more details.
module Gitlab
module Git
module RuggedImpl
module Blob
module ClassMethods
extend ::Gitlab::Utils::Override
include Gitlab::Git::RuggedImpl::UseRugged
override :tree_entry
def tree_entry(repository, sha, path, limit)
if use_rugged?(repository, :rugged_tree_entry)
execute_rugged_call(:rugged_tree_entry, repository, sha, path, limit)
else
super
end
end
private
def rugged_tree_entry(repository, sha, path, limit)
return unless path
# Strip any leading / characters from the path
path = path.sub(%r{\A/*}, '')
rugged_commit = repository.lookup(sha)
root_tree = rugged_commit.tree
blob_entry = find_entry_by_path(repository, root_tree.oid, *path.split('/'))
return unless blob_entry
if blob_entry[:type] == :commit
submodule_blob(blob_entry, path, sha)
else
blob = repository.lookup(blob_entry[:oid])
if blob
new(
id: blob.oid,
name: blob_entry[:name],
size: blob.size,
# Rugged::Blob#content is expensive; don't call it if we don't have to.
data: limit == 0 ? '' : blob.content(limit),
mode: blob_entry[:filemode].to_s(8),
path: path,
commit_id: sha,
binary: blob.binary?
)
end
end
rescue Rugged::ReferenceError
nil
end
# Recursive search of blob id by path
#
# Ex.
# blog/ # oid: 1a
# app/ # oid: 2a
# models/ # oid: 3a
# file.rb # oid: 4a
#
#
# Blob.find_entry_by_path(repo, '1a', 'blog', 'app', 'file.rb') # => '4a'
#
def find_entry_by_path(repository, root_id, *path_parts)
root_tree = repository.lookup(root_id)
entry = root_tree.find do |entry|
entry[:name] == path_parts[0]
end
return unless entry
if path_parts.size > 1
return unless entry[:type] == :tree
path_parts.shift
find_entry_by_path(repository, entry[:oid], *path_parts)
else
[:blob, :commit].include?(entry[:type]) ? entry : nil
end
end
def submodule_blob(blob_entry, path, sha)
new(
id: blob_entry[:oid],
name: blob_entry[:name],
size: 0,
data: '',
path: path,
commit_id: sha
)
end
end
end
end
end
end

View File

@ -1,115 +0,0 @@
# frozen_string_literal: true
# NOTE: This code is legacy. Do not add/modify code here unless you have
# discussed with the Gitaly team. See
# https://docs.gitlab.com/ee/development/gitaly.html#legacy-rugged-code
# for more details.
# rubocop:disable Gitlab/ModuleWithInstanceVariables
module Gitlab
module Git
module RuggedImpl
module Commit
module ClassMethods
extend ::Gitlab::Utils::Override
include Gitlab::Git::RuggedImpl::UseRugged
def rugged_find(repo, commit_id)
obj = repo.rev_parse_target(commit_id)
obj.is_a?(::Rugged::Commit) ? obj : nil
rescue ::Rugged::Error
nil
end
# This needs to return an array of Gitlab::Git:Commit objects
# instead of Rugged::Commit objects to ensure upstream models
# operate on a consistent interface. Unlike
# Gitlab::Git::Commit.find, Gitlab::Git::Commit.batch_by_oid
# doesn't attempt to decorate the result.
def rugged_batch_by_oid(repo, oids)
oids.map { |oid| rugged_find(repo, oid) }
.compact
.map { |commit| decorate(repo, commit) }
# Match Gitaly's list_commits_by_oid behavior
rescue ::Gitlab::Git::Repository::NoRepository
[]
end
override :find_commit
def find_commit(repo, commit_id)
if use_rugged?(repo, :rugged_find_commit)
execute_rugged_call(:rugged_find, repo, commit_id)
else
super
end
end
override :batch_by_oid
def batch_by_oid(repo, oids)
if use_rugged?(repo, :rugged_list_commits_by_oid)
execute_rugged_call(:rugged_batch_by_oid, repo, oids)
else
super
end
end
end
extend ::Gitlab::Utils::Override
include Gitlab::Git::RuggedImpl::UseRugged
override :init_commit
def init_commit(raw_commit)
case raw_commit
when ::Rugged::Commit
init_from_rugged(raw_commit)
else
super
end
end
override :commit_tree_entry
def commit_tree_entry(path)
if use_rugged?(@repository, :rugged_commit_tree_entry)
execute_rugged_call(:rugged_tree_entry, path)
else
super
end
end
# Is this the same as Blob.find_entry_by_path ?
def rugged_tree_entry(path)
rugged_commit.tree.path(path)
rescue Rugged::TreeError
nil
end
def rugged_commit
@rugged_commit ||= if raw_commit.is_a?(Rugged::Commit)
raw_commit
else
@repository.rev_parse_target(id)
end
end
def init_from_rugged(commit)
author = commit.author
committer = commit.committer
@raw_commit = commit
@id = commit.oid
@message = commit.message
@authored_date = author[:time]
@committed_date = committer[:time]
@author_name = author[:name]
@author_email = author[:email]
@committer_name = committer[:name]
@committer_email = committer[:email]
@parent_ids = commit.parents.map(&:oid)
@trailers = Hash[commit.trailers]
end
end
end
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables

View File

@ -1,20 +0,0 @@
# frozen_string_literal: true
# NOTE: This code is legacy. Do not add/modify code here unless you have
# discussed with the Gitaly team. See
# https://docs.gitlab.com/ee/development/gitaly.html#legacy-rugged-code
# for more details.
module Gitlab
module Git
module RuggedImpl
module Ref
def self.dereference_object(object)
object = object.target while object.is_a?(::Rugged::Tag::Annotation)
object
end
end
end
end
end

View File

@ -1,79 +0,0 @@
# frozen_string_literal: true
# NOTE: This code is legacy. Do not add/modify code here unless you have
# discussed with the Gitaly team. See
# https://docs.gitlab.com/ee/development/gitaly.html#legacy-rugged-code
# for more details.
# rubocop:disable Gitlab/ModuleWithInstanceVariables
module Gitlab
module Git
module RuggedImpl
module Repository
extend ::Gitlab::Utils::Override
include Gitlab::Git::RuggedImpl::UseRugged
FEATURE_FLAGS = %i[rugged_find_commit rugged_tree_entries rugged_tree_entry rugged_commit_is_ancestor rugged_commit_tree_entry rugged_list_commits_by_oid].freeze
def alternate_object_directories
relative_object_directories.map { |d| File.join(path, d) }
end
ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES = %w[
GIT_OBJECT_DIRECTORY_RELATIVE
GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE
].freeze
def relative_object_directories
Gitlab::Git::HookEnv.all(gl_repository).values_at(*ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES).flatten.compact
end
def rugged
@rugged ||= ::Rugged::Repository.new(path, alternates: alternate_object_directories)
rescue ::Rugged::RepositoryError, ::Rugged::OSError
raise ::Gitlab::Git::Repository::NoRepository, 'no repository for such path'
end
def cleanup
@rugged&.close
end
# Return the object that +revspec+ points to. If +revspec+ is an
# annotated tag, then return the tag's target instead.
def rev_parse_target(revspec)
obj = rugged.rev_parse(revspec)
Ref.dereference_object(obj)
end
override :ancestor?
def ancestor?(from, to)
if use_rugged?(self, :rugged_commit_is_ancestor)
execute_rugged_call(:rugged_is_ancestor?, from, to)
else
super
end
end
def rugged_is_ancestor?(ancestor_id, descendant_id)
return false if ancestor_id.nil? || descendant_id.nil?
rugged_merge_base(ancestor_id, descendant_id) == ancestor_id
rescue Rugged::OdbError
false
end
def rugged_merge_base(from, to)
rugged.merge_base(from, to)
rescue Rugged::ReferenceError
nil
end
# Lookup for rugged object by oid or ref name
def lookup(oid_or_ref_name)
rev_parse_target(oid_or_ref_name)
end
end
end
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables

View File

@ -1,147 +0,0 @@
# frozen_string_literal: true
# NOTE: This code is legacy. Do not add/modify code here unless you have
# discussed with the Gitaly team. See
# https://docs.gitlab.com/ee/development/gitaly.html#legacy-rugged-code
# for more details.
module Gitlab
module Git
module RuggedImpl
module Tree
module ClassMethods
extend ::Gitlab::Utils::Override
include Gitlab::Git::RuggedImpl::UseRugged
TREE_SORT_ORDER = { tree: 0, blob: 1, commit: 2 }.freeze
override :tree_entries
def tree_entries(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, pagination_params = nil)
if use_rugged?(repository, :rugged_tree_entries)
entries = execute_rugged_call(
:tree_entries_with_flat_path_from_rugged, repository, sha, path, recursive, skip_flat_paths)
if pagination_params
paginated_response(entries, pagination_params[:limit], pagination_params[:page_token].to_s)
else
[entries, nil]
end
else
super
end
end
# Rugged version of TreePagination in Go: https://gitlab.com/gitlab-org/gitaly/-/merge_requests/3611
def paginated_response(entries, limit, token)
total_entries = entries.count
return [[], nil] if limit == 0 || limit.blank?
entries = Gitlab::Utils.stable_sort_by(entries) { |x| TREE_SORT_ORDER[x.type] }
if token.blank?
index = 0
else
index = entries.index { |entry| entry.id == token }
raise Gitlab::Git::CommandError, "could not find starting OID: #{token}" if index.nil?
index += 1
end
return [entries[index..], nil] if limit < 0
last_index = index + limit
result = entries[index...last_index]
if last_index < total_entries
cursor = Gitaly::PaginationCursor.new(next_cursor: result.last.id)
end
[result, cursor]
end
def tree_entries_with_flat_path_from_rugged(repository, sha, path, recursive, skip_flat_paths)
tree_entries_from_rugged(repository, sha, path, recursive).tap do |entries|
# This was an optimization to reduce N+1 queries for Gitaly
# (https://gitlab.com/gitlab-org/gitaly/issues/530).
rugged_populate_flat_path(repository, sha, path, entries) unless skip_flat_paths
end
end
def tree_entries_from_rugged(repository, sha, path, recursive)
current_path_entries = get_tree_entries_from_rugged(repository, sha, path)
ordered_entries = []
current_path_entries.each do |entry|
ordered_entries << entry
if recursive && entry.dir?
ordered_entries.concat(tree_entries_from_rugged(repository, sha, entry.path, true))
end
end
ordered_entries
end
def rugged_populate_flat_path(repository, sha, path, entries)
entries.each do |entry|
entry.flat_path = entry.path
next unless entry.dir?
entry.flat_path =
if path
File.join(path, rugged_flatten_tree(repository, sha, entry, path))
else
rugged_flatten_tree(repository, sha, entry, path)
end
end
end
# Returns the relative path of the first subdir that doesn't have only one directory descendant
def rugged_flatten_tree(repository, sha, tree, root_path)
subtree = tree_entries_from_rugged(repository, sha, tree.path, false)
if subtree.count == 1 && subtree.first.dir?
File.join(tree.name, rugged_flatten_tree(repository, sha, subtree.first, root_path))
else
tree.name
end
end
def get_tree_entries_from_rugged(repository, sha, path)
commit = repository.lookup(sha)
root_tree = commit.tree
tree = if path
id = find_id_by_path(repository, root_tree.oid, path)
if id
repository.lookup(id)
else
[]
end
else
root_tree
end
tree.map do |entry|
current_path = path ? File.join(path, entry[:name]) : entry[:name]
new(
id: entry[:oid],
name: entry[:name],
type: entry[:type],
mode: entry[:filemode].to_s(8),
path: current_path,
commit_id: sha
)
end
rescue Rugged::ReferenceError
[]
end
end
end
end
end
end

View File

@ -1,50 +0,0 @@
# frozen_string_literal: true
module Gitlab
module Git
module RuggedImpl
module UseRugged
def use_rugged?(_, _)
false
end
def execute_rugged_call(method_name, *args)
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
start = Gitlab::Metrics::System.monotonic_time
result = send(method_name, *args) # rubocop:disable GitlabSecurity/PublicSend
duration = Gitlab::Metrics::System.monotonic_time - start
if Gitlab::RuggedInstrumentation.active?
Gitlab::RuggedInstrumentation.increment_query_count
Gitlab::RuggedInstrumentation.add_query_time(duration)
Gitlab::RuggedInstrumentation.add_call_details(
feature: method_name,
args: args,
duration: duration,
backtrace: Gitlab::BacktraceCleaner.clean_backtrace(caller))
end
result
end
end
def running_puma_with_multiple_threads?
return false unless Gitlab::Runtime.puma?
::Puma.respond_to?(:cli_config) && ::Puma.cli_config.options[:max_threads] > 1
end
def rugged_feature_keys
Gitlab::Git::RuggedImpl::Repository::FEATURE_FLAGS
end
def rugged_enabled_through_feature_flag?
false
end
end
end
end
end

View File

@ -12,9 +12,6 @@ module Gitlab
class << self
# Get list of tree objects
# for repository based on commit sha and path
# Uses rugged for raw objects
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/320
def where(
repository, sha, path = nil, recursive = false, skip_flat_paths = true, rescue_not_found = true,
pagination_params = nil)
@ -110,5 +107,3 @@ module Gitlab
end
end
end
Gitlab::Git::Tree.singleton_class.prepend Gitlab::Git::RuggedImpl::Tree::ClassMethods

View File

@ -31,19 +31,11 @@ module Gitlab
end
def self.disk_access_denied?
return false if rugged_enabled?
!temporarily_allowed?(ALLOW_KEY)
rescue StandardError
false # Err on the side of caution, don't break gitlab for people
end
def self.rugged_enabled?
Gitlab::Git::RuggedImpl::Repository::FEATURE_FLAGS.any? do |flag|
Feature.enabled?(flag)
end
end
def initialize(storage)
raise InvalidConfigurationError, "expected a Hash, got a #{storage.class.name}" unless storage.is_a?(Hash)
raise InvalidConfigurationError, INVALID_STORAGE_MESSAGE unless storage.has_key?('path')

View File

@ -1,34 +0,0 @@
# frozen_string_literal: true
namespace :gitlab do
namespace :features do
desc 'GitLab | Features | Enable direct Git access via Rugged for NFS'
task enable_rugged: :environment do
set_rugged_feature_flags(true)
puts 'All Rugged feature flags were enabled.'
end
task disable_rugged: :environment do
set_rugged_feature_flags(false)
puts 'All Rugged feature flags were disabled.'
end
task unset_rugged: :environment do
set_rugged_feature_flags(nil)
puts 'All Rugged feature flags were unset.'
end
end
def set_rugged_feature_flags(status)
Gitlab::Git::RuggedImpl::Repository::FEATURE_FLAGS.each do |flag|
case status
when nil
Feature.remove(flag)
when true
Feature.enable(flag)
when false
Feature.disable(flag)
end
end
end
end

View File

@ -38905,9 +38905,6 @@ msgstr ""
msgid "Pull requests from fork are not supported"
msgstr ""
msgid "Puma is running with a thread count above 1 and the Rugged service is enabled. This may decrease performance in some environments. See our %{link_start}documentation%{link_end} for details of this issue."
msgstr ""
msgid "PumbleIntegration|Send notifications about project events to Pumble."
msgstr ""

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Profiles::NotificationsController do
RSpec.describe Profiles::NotificationsController, feature_category: :team_planning do
let(:user) do
create(:user) do |user|
user.emails.create!(email: 'original@example.com', confirmed_at: Time.current)

View File

@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Multiple Issue Boards', :js, feature_category: :team_planning do
let_it_be(:user) { create(:user, :no_super_sidebar) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:planning) { create(:label, project: project, name: 'Planning') }
let_it_be(:board) { create(:board, name: 'board1', project: project) }

View File

@ -329,7 +329,7 @@ RSpec.describe 'Edit group settings', feature_category: :groups_and_projects do
end
def updated_emails_disabled?
group.reload.clear_memoization(:emails_disabled_memoized)
group.reload.clear_memoization(:emails_enabled_memoized)
group.emails_disabled?
end
end

View File

@ -276,7 +276,7 @@ RSpec.describe 'Group show page', feature_category: :groups_and_projects do
end
it 'is disabled if emails are disabled' do
group.update!(emails_disabled: true)
group.update!(emails_enabled: false)
visit path

View File

@ -15,48 +15,56 @@ RSpec.describe 'Merge request > User sees merge request file tree sidebar', :js,
sign_in(user)
visit diffs_project_merge_request_path(project, merge_request)
wait_for_requests
scroll_into_view
end
it 'sees file tree sidebar' do
expect(page).to have_selector('.file-row[role=button]')
end
# TODO: fix this test
# For some reason the browser in CI doesn't update the file tree sidebar when review bar is shown
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118378#note_1403906356
#
# it 'has last entry visible with discussions enabled' do
# add_diff_line_draft_comment('foo', find('.line_holder', match: :first))
# scroll_into_view
# scroll_to_end
# button = find_all('.file-row[role=button]').last
# expect(button.obscured?).to be_falsy
# end
shared_examples 'shows last visible file in sidebar' do
it 'shows last file' do
scroll_to_end
shared_examples 'last entry clickable' do
specify do
sidebar_scroller.execute_script('this.scrollBy(0,99999)')
button = find_all('.file-row[role=button]').last
title = button.find('[data-testid=file-row-name-container]')[:title]
expect(button.obscured?).to be_falsy
button.click
expect(page).to have_selector(".file-title-name[title*=\"#{title}\"]")
end
end
it_behaves_like 'shows last visible file in sidebar'
it_behaves_like 'last entry clickable'
context 'when has started a review' do
before do
add_diff_line_draft_comment('foo', find('.line_holder', match: :first))
# wait for review bar to appear
find_by_testid('review_bar_component')
# wait for sidebar to adjust
sleep(1)
end
it_behaves_like 'last entry clickable'
context 'when scrolled into full view' do
before do
sidebar.execute_script("this.scrollIntoView({ block: 'end' })")
end
it_behaves_like 'last entry clickable'
end
end
context 'when viewing using file-by-file mode' do
let(:user) { create(:user, view_diffs_file_by_file: true) }
it_behaves_like 'shows last visible file in sidebar'
end
it_behaves_like 'last entry clickable'
def scroll_into_view
sidebar.execute_script("this.scrollIntoView({ block: 'end' })")
end
context 'when navigating to the next file' do
before do
click_link 'Next'
end
def scroll_to_end
sidebar_scroller.execute_script('this.scrollBy(0,99999)')
it_behaves_like 'last entry clickable'
end
end
end

View File

@ -1,17 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'User visits the profile account page', feature_category: :user_profile do
let(:user) { create(:user, :no_super_sidebar) }
before do
sign_in(user)
visit(profile_account_path)
end
it 'shows correct menu item' do
expect(page).to have_active_navigation('Account')
end
end

View File

@ -3,19 +3,7 @@
require 'spec_helper'
RSpec.describe 'User visits the authentication log', feature_category: :user_profile do
let(:user) { create(:user, :no_super_sidebar) }
context 'when user signed in' do
before do
sign_in(user)
end
it 'shows correct menu item' do
visit(audit_log_profile_path)
expect(page).to have_active_navigation('Authentication Log')
end
end
let(:user) { create(:user) }
context 'when user has activity' do
before do

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'User visits the profile preferences page', :js, feature_category: :user_profile do
include ListboxHelpers
let(:user) { create(:user, :no_super_sidebar) }
let(:user) { create(:user) }
before do
sign_in(user)
@ -13,10 +13,6 @@ RSpec.describe 'User visits the profile preferences page', :js, feature_category
visit(profile_preferences_path)
end
it 'shows correct menu item' do
expect(page).to have_active_navigation('Preferences')
end
describe 'User changes their syntax highlighting theme', :js do
it 'updates their preference' do
choose 'user_color_scheme_id_5'
@ -44,7 +40,7 @@ RSpec.describe 'User visits the profile preferences page', :js, feature_category
wait_for_requests
find('#logo').click
find('[data-track-label="gitlab_logo_link"]').click
expect(page).to have_content("You don't have starred projects yet")
expect(page).to have_current_path starred_dashboard_projects_path, ignore_query: true

View File

@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User visits their profile', feature_category: :user_profile do
let_it_be_with_refind(:user) { create(:user, :no_super_sidebar) }
let_it_be_with_refind(:user) { create(:user) }
before do
stub_feature_flags(profile_tabs_vue: false)
@ -11,12 +11,6 @@ RSpec.describe 'User visits their profile', feature_category: :user_profile do
sign_in(user)
end
it 'shows correct menu item' do
visit(profile_path)
expect(page).to have_active_navigation('Profile')
end
it 'shows profile info' do
visit(profile_path)
@ -59,7 +53,7 @@ RSpec.describe 'User visits their profile', feature_category: :user_profile do
expect(page).to have_content user.username
end
page.within ".content" do
within_testid('super-sidebar') do
click_link link
end

View File

@ -1,17 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'User visits the profile SSH keys page', feature_category: :user_profile do
let(:user) { create(:user, :no_super_sidebar) }
before do
sign_in(user)
visit(profile_keys_path)
end
it 'shows correct menu item' do
expect(page).to have_active_navigation('SSH Keys')
end
end

View File

@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_projects do
let_it_be(:user) { create(:user, :no_super_sidebar) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, namespace: user.namespace) }
before do
@ -21,14 +21,14 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('o')
expect(page).to have_active_navigation(project.name)
expect(page).to have_active_sub_navigation(project.name)
end
it 'redirects to the activity page' do
find('body').native.send_key('g')
find('body').native.send_key('v')
expect(page).to have_active_navigation('Project')
expect(page).to have_active_navigation('Manage')
expect(page).to have_active_sub_navigation('Activity')
end
end
@ -38,31 +38,39 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('f')
expect(page).to have_active_navigation('Repository')
expect(page).to have_active_sub_navigation('Files')
expect(page).to have_active_navigation('Code')
expect(page).to have_active_sub_navigation('Repository')
end
it 'redirects to the repository commits page' do
find('body').native.send_key('g')
find('body').native.send_key('c')
context 'when hitting the commits controller' do
# Hitting the commits controller with the super sidebar enabled seems to trigger more SQL
# queries, exceeding the 100 limit. We need to increase the limit a bit for these tests to pass.
before do
allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(110)
end
expect(page).to have_active_navigation('Repository')
expect(page).to have_active_sub_navigation('Commits')
it 'redirects to the repository commits page' do
find('body').native.send_key('g')
find('body').native.send_key('c')
expect(page).to have_active_navigation('Code')
expect(page).to have_active_sub_navigation('Commits')
end
end
it 'redirects to the repository graph page' do
find('body').native.send_key('g')
find('body').native.send_key('n')
expect(page).to have_active_navigation('Repository')
expect(page).to have_active_sub_navigation('Graph')
expect(page).to have_active_navigation('Code')
expect(page).to have_active_sub_navigation('Repository graph')
end
it 'redirects to the repository charts page' do
find('body').native.send_key('g')
find('body').native.send_key('d')
expect(page).to have_active_navigation(_('Analytics'))
expect(page).to have_active_navigation(_('Analyze'))
expect(page).to have_active_sub_navigation(_('Repository'))
end
end
@ -72,16 +80,16 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('i')
expect(page).to have_active_navigation('Issues')
expect(page).to have_active_sub_navigation('List')
expect(page).to have_active_navigation('Pinned')
expect(page).to have_active_sub_navigation('Issues')
end
it 'redirects to the issue board page' do
find('body').native.send_key('g')
find('body').native.send_key('b')
expect(page).to have_active_navigation('Issues')
expect(page).to have_active_sub_navigation('Board')
expect(page).to have_active_navigation('Plan')
expect(page).to have_active_sub_navigation('Issue boards')
end
it 'redirects to the new issue page' do
@ -97,7 +105,8 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('m')
expect(page).to have_active_navigation('Merge requests')
expect(page).to have_active_navigation('Pinned')
expect(page).to have_active_sub_navigation('Merge requests')
end
end
@ -106,7 +115,7 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('p')
expect(page).to have_active_navigation('CI/CD')
expect(page).to have_active_navigation('Build')
expect(page).to have_active_sub_navigation('Pipelines')
end
@ -114,7 +123,7 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('j')
expect(page).to have_active_navigation('CI/CD')
expect(page).to have_active_navigation('Build')
expect(page).to have_active_sub_navigation('Jobs')
end
end
@ -124,7 +133,7 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('e')
expect(page).to have_active_navigation('Deployments')
expect(page).to have_active_navigation('Operate')
expect(page).to have_active_sub_navigation('Environments')
end
end
@ -134,7 +143,7 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('k')
expect(page).to have_active_navigation('Infrastructure')
expect(page).to have_active_navigation('Operate')
expect(page).to have_active_sub_navigation('Kubernetes')
end
end
@ -144,7 +153,8 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('s')
expect(page).to have_active_navigation('Snippets')
expect(page).to have_active_navigation('Code')
expect(page).to have_active_sub_navigation('Snippets')
end
end
@ -153,7 +163,8 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :groups_and_project
find('body').native.send_key('g')
find('body').native.send_key('w')
expect(page).to have_active_navigation('Wiki')
expect(page).to have_active_navigation('Plan')
expect(page).to have_active_sub_navigation('Wiki')
end
end
end

View File

@ -0,0 +1,63 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'User sees correct active nav items in the super sidebar', :js, feature_category: :value_stream_management do
let_it_be(:current_user) { create(:user) }
before do
sign_in(current_user)
end
describe 'profile pages' do
context 'when visiting profile page' do
before do
visit profile_path
end
it 'renders the side navigation with the correct submenu set as active' do
expect(page).to have_active_sub_navigation('Profile')
end
end
context 'when visiting preferences page' do
before do
visit profile_preferences_path
end
it 'renders the side navigation with the correct submenu set as active' do
expect(page).to have_active_sub_navigation('Preferences')
end
end
context 'when visiting authentication logs' do
before do
visit audit_log_profile_path
end
it 'renders the side navigation with the correct submenu set as active' do
expect(page).to have_active_sub_navigation('Authentication Log')
end
end
context 'when visiting SSH keys page' do
before do
visit profile_keys_path
end
it 'renders the side navigation with the correct submenu set as active' do
expect(page).to have_active_sub_navigation('SSH Keys')
end
end
context 'when visiting account page' do
before do
visit profile_account_path
end
it 'renders the side navigation with the correct submenu set as active' do
expect(page).to have_active_sub_navigation('Account')
end
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe UserGroupNotificationSettingsFinder do
RSpec.describe UserGroupNotificationSettingsFinder, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
subject { described_class.new(user, Group.where(id: groups.map(&:id))).execute }
@ -127,38 +127,38 @@ RSpec.describe UserGroupNotificationSettingsFinder do
expect(result.count).to eq(3)
end
end
end
context 'preloading `emails_disabled`' do
let_it_be(:root_group) { create(:group) }
let_it_be(:sub_group) { create(:group, parent: root_group) }
let_it_be(:sub_sub_group) { create(:group, parent: sub_group) }
context 'preloading `emails_enabled`' do
let_it_be(:root_group) { create(:group) }
let_it_be(:sub_group) { create(:group, parent: root_group) }
let_it_be(:sub_sub_group) { create(:group, parent: sub_group) }
let_it_be(:another_root_group) { create(:group) }
let_it_be(:sub_group_with_emails_disabled) { create(:group, emails_disabled: true, parent: another_root_group) }
let_it_be(:another_sub_sub_group) { create(:group, parent: sub_group_with_emails_disabled) }
let_it_be(:another_root_group) { create(:group) }
let_it_be(:sub_group_with_emails_disabled) { create(:group, emails_enabled: false, parent: another_root_group) }
let_it_be(:another_sub_sub_group) { create(:group, parent: sub_group_with_emails_disabled) }
let_it_be(:root_group_with_emails_disabled) { create(:group, emails_disabled: true) }
let_it_be(:group) { create(:group, parent: root_group_with_emails_disabled) }
let_it_be(:root_group_with_emails_disabled) { create(:group, emails_enabled: false) }
let_it_be(:group) { create(:group, parent: root_group_with_emails_disabled) }
let(:groups) { Group.where(id: [sub_sub_group, another_sub_sub_group, group]) }
let(:groups) { Group.where(id: [sub_sub_group, another_sub_sub_group, group]) }
before do
described_class.new(user, groups).execute
end
before do
described_class.new(user, groups).execute
end
it 'preloads the `group.emails_disabled` method' do
recorder = ActiveRecord::QueryRecorder.new do
groups.each(&:emails_disabled?)
end
it 'preloads the `group.emails_enabled` method' do
recorder = ActiveRecord::QueryRecorder.new do
groups.each(&:emails_enabled?)
end
expect(recorder.count).to eq(0)
end
expect(recorder.count).to eq(0)
end
it 'preloads the `group.emails_disabled` method correctly' do
groups.each do |group|
expect(group.emails_disabled?).to eq(Group.find(group.id).emails_disabled?) # compare the memoized and the freshly loaded value
it 'preloads the `group.emails_enabled` method correctly' do
groups.each do |group|
expect(group.emails_enabled?).to eq(Group.find(group.id).emails_enabled?) # compare the memoized and the freshly loaded value
end
end
end
end
end

View File

@ -35,6 +35,7 @@ describe('Repository last commit component', () => {
expect(findUserLink().exists()).toBe(true);
expect(findUserAvatarLink().exists()).toBe(true);
expect(findUserAvatarLink().props('imgAlt')).toBe("Test authorName's avatar");
});
it('hides author component when author does not exist', () => {

View File

@ -71,7 +71,7 @@ describe('EntitySelect', () => {
fetchItemsMock = jest.fn().mockImplementation(() => ({ items: [itemMock], totalPages: 1 }));
});
describe('GlCollapsableListbox props', () => {
describe('GlCollapsibleListbox props', () => {
beforeEach(() => {
createComponent();
});
@ -81,7 +81,7 @@ describe('EntitySelect', () => {
${'block'} | ${block}
${'toggleClass'} | ${toggleClass}
${'headerText'} | ${headerText}
`('passes the $prop prop to GlCollapsableListbox', ({ prop, expectedValue }) => {
`('passes the $prop prop to GlCollapsibleListbox', ({ prop, expectedValue }) => {
expect(findListbox().props(prop)).toBe(expectedValue);
});
});

View File

@ -0,0 +1,55 @@
import { GlAvatar } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import GroupItem from '~/vue_shared/components/list_selector/group_item.vue';
describe('GroupItem spec', () => {
let wrapper;
const MOCK_GROUP = { fullName: 'Group 1', name: 'group1', avatarUrl: 'some/avatar.jpg' };
const createComponent = (props) => {
wrapper = mountExtended(GroupItem, {
propsData: {
data: MOCK_GROUP,
...props,
},
});
};
const findAvatar = () => wrapper.findComponent(GlAvatar);
const findDeleteButton = () => wrapper.findByRole('button', { fullName: 'Delete Group 1' });
beforeEach(() => createComponent());
it('renders an Avatar component', () => {
expect(findAvatar().props('size')).toBe(32);
expect(findAvatar().attributes()).toMatchObject({
src: MOCK_GROUP.avatarUrl,
alt: MOCK_GROUP.fullName,
});
});
it('renders a fullName and name', () => {
expect(wrapper.text()).toContain('Group 1');
expect(wrapper.text()).toContain('@group1');
});
it('does not render a delete button by default', () => {
expect(findDeleteButton().exists()).toBe(false);
});
describe('Delete button', () => {
beforeEach(() => createComponent({ canDelete: true }));
it('renders a delete button', () => {
expect(findDeleteButton().exists()).toBe(true);
expect(findDeleteButton().props('icon')).toBe('remove');
});
it('emits a delete event if the delete button is clicked', () => {
findDeleteButton().trigger('click');
expect(wrapper.emitted('delete')).toEqual([[MOCK_GROUP.name]]);
});
});
});

View File

@ -3,11 +3,13 @@ import VueApollo from 'vue-apollo';
import { GlCard, GlIcon, GlCollapsibleListbox, GlSearchBoxByType } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ListSelector from '~/vue_shared/components/list_selector/index.vue';
import User from '~/vue_shared/components/list_selector/user.vue';
import UserItem from '~/vue_shared/components/list_selector/user_item.vue';
import GroupItem from '~/vue_shared/components/list_selector/group_item.vue';
import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql';
import groupsAutocompleteQuery from '~/graphql_shared/queries/groups_autocomplete.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { USERS_RESPONSE_MOCK } from './mock_data';
import { USERS_RESPONSE_MOCK, GROUPS_RESPONSE_MOCK } from './mock_data';
Vue.use(VueApollo);
@ -15,21 +17,31 @@ describe('List Selector spec', () => {
let wrapper;
let fakeApollo;
const MOCK_PROPS = {
const USERS_MOCK_PROPS = {
title: 'Users',
projectPath: 'some/project/path',
type: 'users',
};
const usersAutocompleteQuerySuccess = jest.fn().mockResolvedValue(USERS_RESPONSE_MOCK);
const GROUPS_MOCK_PROPS = {
title: 'Groups',
projectPath: 'some/project/path',
type: 'groups',
};
const createComponent = async (props) => {
fakeApollo = createMockApollo([[usersAutocompleteQuery, usersAutocompleteQuerySuccess]]);
const usersAutocompleteQuerySuccess = jest.fn().mockResolvedValue(USERS_RESPONSE_MOCK);
const groupsAutocompleteQuerySuccess = jest.fn().mockResolvedValue(GROUPS_RESPONSE_MOCK);
const createComponent = async (
props,
query = usersAutocompleteQuery,
queryResponse = usersAutocompleteQuerySuccess,
) => {
fakeApollo = createMockApollo([[query, queryResponse]]);
wrapper = mountExtended(ListSelector, {
apolloProvider: fakeApollo,
propsData: {
...MOCK_PROPS,
...props,
},
});
@ -38,21 +50,23 @@ describe('List Selector spec', () => {
};
const findCard = () => wrapper.findComponent(GlCard);
const findTitle = () => wrapper.findByText(MOCK_PROPS.title);
const findTitle = () => findCard().find('[data-testid="list-selector-title"]');
const findIcon = () => wrapper.findComponent(GlIcon);
const findListBox = () => wrapper.findComponent(GlCollapsibleListbox);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const findAllUserComponents = () => wrapper.findAllComponents(User);
const findAllUserComponents = () => wrapper.findAllComponents(UserItem);
const findAllGroupComponents = () => wrapper.findAllComponents(GroupItem);
describe('Users type', () => {
beforeEach(() => createComponent({ type: 'users' }));
beforeEach(() => createComponent(USERS_MOCK_PROPS));
it('renders a Card component', () => {
expect(findCard().exists()).toBe(true);
});
it('renders a title', () => {
it('renders a correct title', () => {
expect(findTitle().exists()).toBe(true);
expect(findTitle().text()).toContain('Users');
});
it('renders the correct icon', () => {
@ -81,7 +95,7 @@ describe('List Selector spec', () => {
it('calls query with correct variables when Search box receives an input', () => {
expect(usersAutocompleteQuerySuccess).toHaveBeenCalledWith({
fullPath: MOCK_PROPS.projectPath,
fullPath: USERS_MOCK_PROPS.projectPath,
isProject: true,
search,
});
@ -108,11 +122,11 @@ describe('List Selector spec', () => {
describe('selected items', () => {
const selectedUser = { username: 'root' };
const selectedItems = [selectedUser];
beforeEach(() => createComponent({ selectedItems }));
beforeEach(() => createComponent({ ...USERS_MOCK_PROPS, selectedItems }));
it('renders a heading with the total selected items', () => {
expect(findCard().text()).toContain('Users');
expect(findCard().text()).toContain('1');
expect(findTitle().text()).toContain('Users');
expect(findTitle().text()).toContain('1');
});
it('renders a user component for each selected item', () => {
@ -131,4 +145,85 @@ describe('List Selector spec', () => {
});
});
});
describe('Groups type', () => {
beforeEach(() =>
createComponent(GROUPS_MOCK_PROPS, groupsAutocompleteQuery, groupsAutocompleteQuerySuccess),
);
it('renders a correct title', () => {
expect(findTitle().exists()).toBe(true);
expect(findTitle().text()).toContain('Groups');
});
it('renders the correct icon', () => {
expect(findIcon().props('name')).toBe('group');
});
it('does not call query when search box has not received an input', () => {
expect(groupsAutocompleteQuerySuccess).not.toHaveBeenCalled();
expect(findAllGroupComponents().length).toBe(0);
});
describe('searching', () => {
const searchResponse = GROUPS_RESPONSE_MOCK.data.groups.nodes;
const search = 'foo';
const emitSearchInput = async () => {
findSearchBox().vm.$emit('input', search);
await waitForPromises();
};
beforeEach(() => emitSearchInput());
it('calls query with correct variables when Search box receives an input', () => {
expect(groupsAutocompleteQuerySuccess).toHaveBeenCalledWith({
search,
});
});
it('renders a List box component with the correct props', () => {
expect(findListBox().props()).toMatchObject({ multiple: true, items: searchResponse });
});
it('renders a group component for each search result', () => {
expect(findAllGroupComponents().length).toBe(searchResponse.length);
});
it('emits an event when a search result is selected', () => {
const firstSearchResult = searchResponse[0];
findAllGroupComponents().at(0).vm.$emit('select', firstSearchResult.name);
expect(wrapper.emitted('select')).toEqual([
[{ ...firstSearchResult, text: 'Flightjs', value: 'Flightjs' }],
]);
});
});
describe('selected items', () => {
const selectedGroup = { name: 'Flightjs' };
const selectedItems = [selectedGroup];
beforeEach(() => createComponent({ ...GROUPS_MOCK_PROPS, selectedItems }));
it('renders a heading with the total selected items', () => {
expect(findTitle().text()).toContain('Groups');
expect(findTitle().text()).toContain('1');
});
it('renders a group component for each selected item', () => {
expect(findAllGroupComponents().length).toBe(selectedItems.length);
expect(findAllGroupComponents().at(0).props()).toMatchObject({
data: selectedGroup,
canDelete: true,
});
});
it('emits a delete event when a delete event is emitted from the group component', () => {
const name = 'Flightjs';
findAllGroupComponents().at(0).vm.$emit('delete', name);
expect(wrapper.emitted('delete')).toEqual([[name]]);
});
});
});
});

View File

@ -23,3 +23,27 @@ export const USERS_RESPONSE_MOCK = {
},
},
};
export const GROUPS_RESPONSE_MOCK = {
data: {
groups: {
nodes: [
{
id: 'gid://gitlab/Group/33',
name: 'Flightjs',
fullName: 'Flightjs',
avatarUrl: null,
__typename: 'Group',
},
{
id: 'gid://gitlab/Group/34',
name: 'Flight 2',
fullName: 'Flight2',
avatarUrl: null,
__typename: 'Group',
},
],
__typename: 'GroupConnection',
},
},
};

View File

@ -1,14 +1,14 @@
import { GlAvatar } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import User from '~/vue_shared/components/list_selector/user.vue';
import UserItem from '~/vue_shared/components/list_selector/user_item.vue';
describe('User spec', () => {
describe('UserItem spec', () => {
let wrapper;
const MOCK_USER = { name: 'Admin', username: 'root', avatarUrl: 'some/avatar.jpg' };
const createComponent = (props) => {
wrapper = mountExtended(User, {
wrapper = mountExtended(UserItem, {
propsData: {
data: MOCK_USER,
...props,

View File

@ -288,13 +288,13 @@ RSpec.describe GroupsHelper, feature_category: :groups_and_projects do
end
it 'returns false if parent group is disabling emails' do
allow(group).to receive(:emails_disabled?).and_return(true)
allow(group).to receive(:emails_enabled?).and_return(false)
expect(helper.can_disable_group_emails?(subgroup)).to be_falsey
end
it 'returns true if parent group is not disabling emails' do
allow(group).to receive(:emails_disabled?).and_return(false)
allow(group).to receive(:emails_enabled?).and_return(true)
expect(helper.can_disable_group_emails?(subgroup)).to be_truthy
end

View File

@ -1,65 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::ConfigChecker::PumaRuggedChecker do
describe '#check' do
subject { described_class.check }
context 'application is not puma' do
before do
allow(Gitlab::Runtime).to receive(:puma?).and_return(false)
end
it { is_expected.to be_empty }
end
context 'application is puma' do
let(:notice_multi_threaded_puma_with_rugged) do
{
type: 'warning',
message: 'Puma is running with a thread count above 1 and the Rugged '\
'service is enabled. This may decrease performance in some environments. '\
'See our <a href="https://docs.gitlab.com/ee/administration/operations/puma.html#performance-caveat-when-using-puma-with-rugged">documentation</a> '\
'for details of this issue.'
}
end
before do
allow(Gitlab::Runtime).to receive(:puma?).and_return(true)
allow(described_class).to receive(:running_puma_with_multiple_threads?).and_return(multithreaded_puma)
allow(described_class).to receive(:rugged_enabled_through_feature_flag?).and_return(rugged_enabled)
end
context 'not multithreaded_puma and rugged API enabled' do
let(:multithreaded_puma) { false }
let(:rugged_enabled) { true }
it { is_expected.to be_empty }
end
context 'not multithreaded_puma and rugged API is not enabled' do
let(:multithreaded_puma) { false }
let(:rugged_enabled) { false }
it { is_expected.to be_empty }
end
context 'multithreaded_puma and rugged API is not enabled' do
let(:multithreaded_puma) { true }
let(:rugged_enabled) { false }
it { is_expected.to be_empty }
end
context 'multithreaded_puma and rugged API is enabled' do
let(:multithreaded_puma) { true }
let(:rugged_enabled) { true }
it 'report multi_threaded_puma_with_rugged notices' do
is_expected.to contain_exactly(notice_multi_threaded_puma_with_rugged)
end
end
end
end
end

View File

@ -154,18 +154,6 @@ RSpec.describe Gitlab::Git::Blob do
it_behaves_like '.find'
end
describe '.find with Rugged enabled', :enable_rugged do
it 'calls out to the Rugged implementation' do
allow_next_instance_of(Rugged) do |instance|
allow(instance).to receive(:rev_parse).with(TestEnv::BRANCH_SHA['master']).and_call_original
end
described_class.find(repository, TestEnv::BRANCH_SHA['master'], 'files/images/6049019_460s.jpg')
end
it_behaves_like '.find'
end
describe '.raw' do
let(:raw_blob) { described_class.raw(repository, SeedRepo::RubyBlob::ID) }
let(:bad_blob) { described_class.raw(repository, SeedRepo::BigCommit::ID) }

View File

@ -160,18 +160,6 @@ RSpec.describe Gitlab::Git::Commit, feature_category: :source_code_management do
it_behaves_like '.find'
end
describe '.find with Rugged enabled', :enable_rugged do
it 'calls out to the Rugged implementation' do
allow_next_instance_of(Rugged) do |instance|
allow(instance).to receive(:rev_parse).with(SeedRepo::Commit::ID).and_call_original
end
described_class.find(repository, SeedRepo::Commit::ID)
end
it_behaves_like '.find'
end
describe '.last_for_path' do
context 'no path' do
subject { described_class.last_for_path(repository, 'master') }
@ -459,18 +447,6 @@ RSpec.describe Gitlab::Git::Commit, feature_category: :source_code_management do
end
end
describe '.batch_by_oid with Rugged enabled', :enable_rugged do
it_behaves_like '.batch_by_oid'
it 'calls out to the Rugged implementation' do
allow_next_instance_of(Rugged) do |instance|
allow(instance).to receive(:rev_parse).with(SeedRepo::Commit::ID).and_call_original
end
described_class.batch_by_oid(repository, [SeedRepo::Commit::ID])
end
end
describe '.extract_signature_lazily' do
subject { described_class.extract_signature_lazily(repository, commit_id).itself }

View File

@ -1,116 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, feature_category: :gitaly do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:feature_flag_name) { wrapper.rugged_feature_keys.first }
subject(:wrapper) do
klazz = Class.new do
include Gitlab::Git::RuggedImpl::UseRugged
def rugged_test(ref, test_number); end
end
klazz.new
end
describe '#execute_rugged_call', :request_store do
let(:args) { ['refs/heads/master', 1] }
before do
allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true)
end
it 'instruments Rugged call' do
expect(subject).to receive(:rugged_test).with(args)
subject.execute_rugged_call(:rugged_test, args)
expect(Gitlab::RuggedInstrumentation.query_count).to eq(1)
expect(Gitlab::RuggedInstrumentation.list_call_details.count).to eq(1)
end
end
describe '#use_rugged?' do
it 'returns false' do
expect(subject.use_rugged?(repository, feature_flag_name)).to be false
end
end
describe '#running_puma_with_multiple_threads?' do
context 'when using Puma' do
before do
stub_const('::Puma', double('puma constant'))
allow(Gitlab::Runtime).to receive(:puma?).and_return(true)
end
it "returns false when Puma doesn't support the cli_config method" do
allow(::Puma).to receive(:respond_to?).with(:cli_config).and_return(false)
expect(subject.running_puma_with_multiple_threads?).to be_falsey
end
it 'returns false for single thread Puma' do
allow(::Puma).to receive_message_chain(:cli_config, :options).and_return(max_threads: 1)
expect(subject.running_puma_with_multiple_threads?).to be false
end
it 'returns true for multi-threaded Puma' do
allow(::Puma).to receive_message_chain(:cli_config, :options).and_return(max_threads: 2)
expect(subject.running_puma_with_multiple_threads?).to be true
end
end
context 'when not using Puma' do
before do
allow(Gitlab::Runtime).to receive(:puma?).and_return(false)
end
it 'returns false' do
expect(subject.running_puma_with_multiple_threads?).to be false
end
end
end
describe '#rugged_enabled_through_feature_flag?' do
subject { wrapper.send(:rugged_enabled_through_feature_flag?) }
before do
allow(Feature).to receive(:enabled?).with(:feature_key_1).and_return(true)
allow(Feature).to receive(:enabled?).with(:feature_key_2).and_return(true)
allow(Feature).to receive(:enabled?).with(:feature_key_3).and_return(false)
allow(Feature).to receive(:enabled?).with(:feature_key_4).and_return(false)
stub_const('Gitlab::Git::RuggedImpl::Repository::FEATURE_FLAGS', feature_keys)
end
context 'no feature keys given' do
let(:feature_keys) { [] }
it { is_expected.to be_falsey }
end
context 'all features are enabled' do
let(:feature_keys) { [:feature_key_1, :feature_key_2] }
it { is_expected.to be_falsey }
end
context 'all features are not enabled' do
let(:feature_keys) { [:feature_key_3, :feature_key_4] }
it { is_expected.to be_falsey }
end
context 'some feature is enabled' do
let(:feature_keys) { [:feature_key_4, :feature_key_2] }
it { is_expected.to be_falsey }
end
end
end

View File

@ -192,122 +192,4 @@ RSpec.describe Gitlab::Git::Tree, feature_category: :source_code_management do
end
end
end
describe '.where with Rugged enabled', :enable_rugged do
it 'does not call to the Rugged implementation' do
allow_next_instance_of(Rugged) do |instance|
allow(instance).not_to receive(:lookup)
end
described_class.where(repository, SeedRepo::Commit::ID, 'files', false, false)
end
it_behaves_like 'repo' do
describe 'Pagination' do
context 'with restrictive limit' do
let(:pagination_params) { { limit: 3, page_token: nil } }
it 'returns limited paginated list of tree objects' do
expect(entries.count).to eq(3)
expect(cursor.next_cursor).to be_present
end
end
context 'when limit is equal to number of entries' do
let(:entries_count) { entries.count }
it 'returns all entries with a cursor' do
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: entries_count, page_token: nil })
expect(cursor).to eq(Gitaly::PaginationCursor.new)
expect(result.entries.count).to eq(entries_count)
end
end
context 'when limit is 0' do
let(:pagination_params) { { limit: 0, page_token: nil } }
it 'returns empty result' do
expect(entries).to eq([])
expect(cursor).to be_nil
end
end
context 'when limit is missing' do
let(:pagination_params) { { limit: nil, page_token: nil } }
it 'returns all entries' do
expect(entries.count).to be < 20
expect(cursor).to eq(Gitaly::PaginationCursor.new)
end
end
context 'when limit is negative' do
let(:entries_count) { entries.count }
it 'returns all entries' do
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: -1, page_token: nil })
expect(result.count).to eq(entries_count)
expect(cursor).to eq(Gitaly::PaginationCursor.new)
end
context 'when token is provided' do
let(:pagination_params) { { limit: 1000, page_token: nil } }
let(:token) { entries.second.id }
it 'returns all entries after token' do
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: -1, page_token: token })
expect(result.count).to eq(entries.count - 2)
expect(cursor).to eq(Gitaly::PaginationCursor.new)
end
end
end
context 'when token does not exist' do
let(:pagination_params) { { limit: 5, page_token: 'aabbccdd' } }
it 'raises a command error' do
expect { entries }.to raise_error(Gitlab::Git::CommandError, /could not find starting OID: aabbccdd/)
end
end
context 'when limit is bigger than number of entries' do
let(:pagination_params) { { limit: 1000, page_token: nil } }
it 'returns only available entries' do
expect(entries.count).to be < 20
expect(cursor).to eq(Gitaly::PaginationCursor.new)
end
end
it 'returns all tree entries in specific order during cursor pagination' do
collected_entries = []
token = nil
expected_entries = entries
loop do
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, rescue_not_found, { limit: 5, page_token: token })
collected_entries += result.entries
token = cursor&.next_cursor
break if token.blank?
end
expect(collected_entries.map(&:path)).to match_array(expected_entries.map(&:path))
expected_order = [
collected_entries.select(&:dir?).map(&:path),
collected_entries.select(&:file?).map(&:path),
collected_entries.select(&:submodule?).map(&:path)
].flatten
expect(collected_entries.map(&:path)).to eq(expected_order)
end
end
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::GitalyClient::StorageSettings do
RSpec.describe Gitlab::GitalyClient::StorageSettings, feature_category: :gitaly do
describe "#initialize" do
context 'when the storage contains no path' do
it 'raises an error' do
@ -62,16 +62,16 @@ RSpec.describe Gitlab::GitalyClient::StorageSettings do
end
describe '.disk_access_denied?' do
context 'when Rugged is enabled', :enable_rugged do
it 'returns false' do
expect(described_class.disk_access_denied?).to be_falsey
end
end
subject { described_class.disk_access_denied? }
context 'when Rugged is disabled' do
it 'returns true' do
expect(described_class.disk_access_denied?).to be_truthy
it { is_expected.to be_truthy }
context 'in case of an exception' do
before do
allow(described_class).to receive(:temporarily_allowed?).and_raise('boom')
end
it { is_expected.to be_falsey }
end
end
end

View File

@ -707,16 +707,6 @@ eos
it_behaves_like "#uri_type"
end
describe '#uri_type with Rugged enabled', :enable_rugged do
it 'calls out to the Rugged implementation' do
allow_any_instance_of(Rugged::Tree).to receive(:path).with('files/html').and_call_original
commit.uri_type('files/html')
end
it_behaves_like '#uri_type'
end
describe '.diff_max_files' do
subject(:diff_max_files) { described_class.diff_max_files }

View File

@ -2930,18 +2930,21 @@ RSpec.describe Group, feature_category: :groups_and_projects do
end
describe '.ids_with_disabled_email' do
let_it_be(:parent_1) { create(:group, emails_disabled: true) }
let_it_be(:parent_1) { create(:group) }
let_it_be(:child_1) { create(:group, parent: parent_1) }
let_it_be(:parent_2) { create(:group, emails_disabled: false) }
let_it_be(:parent_2) { create(:group) }
let_it_be(:child_2) { create(:group, parent: parent_2) }
let_it_be(:other_group) { create(:group, emails_disabled: false) }
let_it_be(:other_group) { create(:group) }
shared_examples 'returns namespaces with disabled email' do
subject(:group_ids_where_email_is_disabled) { described_class.ids_with_disabled_email([child_1, child_2, other_group]) }
it { is_expected.to eq(Set.new([child_1.id])) }
it do
parent_1.update_attribute(:emails_enabled, false)
is_expected.to eq(Set.new([child_1.id]))
end
end
it_behaves_like 'returns namespaces with disabled email'

View File

@ -216,36 +216,6 @@ RSpec.describe NamespaceSetting, feature_category: :groups_and_projects, type: :
end
end
describe '#emails_enabled?' do
context 'when a group has no parent'
let(:settings) { create(:namespace_settings, emails_enabled: true) }
let(:grandparent) { create(:group) }
let(:parent) { create(:group, parent: grandparent) }
let(:group) { create(:group, parent: parent, namespace_settings: settings) }
context 'when the groups setting is changed' do
it 'returns false when the attribute is false' do
group.update_attribute(:emails_disabled, true)
expect(group.emails_enabled?).to be_falsey
end
end
context 'when a group has a parent' do
it 'returns true when no parent has disabled emails' do
expect(group.emails_enabled?).to be_truthy
end
context 'when ancestor emails are disabled' do
it 'returns false' do
grandparent.update_attribute(:emails_disabled, true)
expect(group.emails_enabled?).to be_falsey
end
end
end
end
context 'when a group has parent groups' do
let(:grandparent) { create(:group, namespace_settings: settings) }
let(:parent) { create(:group, parent: grandparent) }
@ -269,6 +239,58 @@ RSpec.describe NamespaceSetting, feature_category: :groups_and_projects, type: :
end
end
describe '#emails_enabled?' do
context 'when a group has no parent'
let(:settings) { create(:namespace_settings, emails_enabled: true) }
let(:grandparent) { create(:group) }
let(:parent) { create(:group, parent: grandparent) }
let(:group) { create(:group, parent: parent, namespace_settings: settings) }
context 'when the groups setting is changed' do
it 'returns false when the attribute is false' do
group.update_attribute(:emails_enabled, false)
expect(group.emails_enabled?).to be_falsey
end
end
context 'when a group has a parent' do
it 'returns true when no parent has disabled emails' do
expect(group.emails_enabled?).to be_truthy
end
context 'when ancestor emails are disabled' do
it 'returns false' do
grandparent.update_attribute(:emails_enabled, false)
expect(group.emails_enabled?).to be_falsey
end
end
end
context 'when a group has parent groups' do
let(:grandparent) { create(:group, namespace_settings: settings) }
let(:parent) { create(:group, parent: grandparent) }
let!(:group) { create(:group, parent: parent) }
context "when a parent group has emails disabled" do
let(:settings) { create(:namespace_settings, emails_enabled: false) }
it 'returns false' do
expect(group.emails_enabled?).to be_falsey
end
end
context 'when all parent groups have emails enabled' do
let(:settings) { create(:namespace_settings, emails_enabled: true) }
it 'returns true' do
expect(group.emails_enabled?).to be_truthy
end
end
end
end
context 'runner registration settings' do
shared_context 'with runner registration settings changing in hierarchy' do
context 'when there are no parents' do

View File

@ -1863,20 +1863,22 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
describe '#emails_disabled?' do
context 'when not a subgroup' do
let(:group) { create(:group) }
it 'returns false' do
group = create(:group, emails_disabled: false)
group.update_attribute(:emails_enabled, true)
expect(group.emails_disabled?).to be_falsey
end
it 'returns true' do
group = create(:group, emails_disabled: true)
group.update_attribute(:emails_enabled, false)
expect(group.emails_disabled?).to be_truthy
end
it 'does not query the db when there is no parent group' do
group = create(:group, emails_disabled: true)
group.update_attribute(:emails_enabled, false)
expect { group.emails_disabled? }.not_to exceed_query_limit(0)
end
@ -1903,7 +1905,8 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
describe '#emails_enabled?' do
context 'without a persisted namespace_setting object' do
let(:group) { build(:group, emails_disabled: false) }
let(:group_settings) { create(:namespace_settings) }
let(:group) { build(:group, emails_disabled: false, namespace_settings: group_settings) }
it "is the opposite of emails_disabled" do
expect(group.emails_enabled?).to be_truthy

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