Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ef211f6aff
commit
eed7260f13
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
query groupsAutocomplete($search: String) {
|
||||
groups(search: $search) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
avatarUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(/^
/, '');
|
||||
},
|
||||
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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
export default {
|
||||
typePolicies: {
|
||||
Project: {
|
||||
fields: {
|
||||
ciCdSettings: {
|
||||
merge: true,
|
||||
},
|
||||
ciJobTokenScope: {
|
||||
merge: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -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') => {
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -279,6 +279,7 @@ class GroupsController < Groups::ApplicationController
|
|||
:avatar,
|
||||
:description,
|
||||
:emails_disabled,
|
||||
:emails_enabled,
|
||||
:show_diff_preview_in_email,
|
||||
:mentions_disabled,
|
||||
:lfs_enabled,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: rugged_commit_is_ancestor
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
milestone:
|
||||
type: development
|
||||
group: group::gitaly
|
||||
default_enabled: false
|
||||
|
|
@ -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
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: rugged_find_commit
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
milestone:
|
||||
type: development
|
||||
group: group::gitaly
|
||||
default_enabled: false
|
||||
|
|
@ -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
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: rugged_tree_entries
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
milestone:
|
||||
type: development
|
||||
group: group::gitaly
|
||||
default_enabled: false
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: rugged_tree_entry
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
milestone:
|
||||
type: development
|
||||
group: group::gitaly
|
||||
default_enabled: false
|
||||
|
|
@ -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 |
|
|
@ -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).
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -230,5 +230,3 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Gitlab::Git::Blob.singleton_class.prepend Gitlab::Git::RuggedImpl::Blob::ClassMethods
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue