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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/editor/source_editor.js29
-rw-r--r--app/assets/javascripts/editor/utils.js25
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue33
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/dependency_row.vue17
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql19
-rw-r--r--app/assets/javascripts/terraform/components/init_command_modal.vue86
-rw-r--r--app/assets/javascripts/terraform/components/states_table_actions.vue28
-rw-r--r--app/assets/javascripts/terraform/index.js7
-rw-r--r--app/assets/stylesheets/themes/_dark.scss15
-rw-r--r--app/finders/group_members_finder.rb9
-rw-r--r--app/helpers/projects/terraform_helper.rb5
-rw-r--r--app/models/ci/build.rb2
-rw-r--r--app/models/integration.rb2
-rw-r--r--app/models/namespaces/traversal/linear.rb4
-rw-r--r--app/models/project.rb4
-rw-r--r--app/views/projects/packages/packages/show.html.haml2
-rw-r--r--config/feature_flags/development/ci_pending_builds_maintain_ci_minutes_data.yml2
-rw-r--r--config/feature_flags/development/ci_pending_builds_maintain_shared_runners_data.yml4
-rw-r--r--config/feature_flags/development/ci_queueing_denormalize_shared_runners_information.yml4
-rw-r--r--config/feature_flags/development/instance_level_integration_overrides.yml2
-rw-r--r--config/feature_flags/development/jira_issue_details_edit_labels.yml2
-rw-r--r--config/feature_flags/development/jira_issue_details_edit_status.yml2
-rw-r--r--config/feature_flags/development/package_details_apollo.yml2
-rw-r--r--config/feature_flags/development/report_on_long_redis_durations.yml8
-rw-r--r--config/feature_flags/development/web_hooks_disable_failed.yml2
-rw-r--r--config/feature_flags/ops/api_kaminari_count_with_limit.yml2
-rw-r--r--doc/user/infrastructure/img/terraform_list_view_actions_v13_8.pngbin36949 -> 0 bytes
-rw-r--r--doc/user/infrastructure/terraform_state.md16
-rw-r--r--lib/api/entities/ci/job_request/dependency.rb2
-rw-r--r--lib/gitlab/checks/changes_access.rb20
-rw-r--r--lib/gitlab/instrumentation/redis_interceptor.rb20
-rw-r--r--locale/gitlab.pot11
-rw-r--r--spec/features/groups/packages_spec.rb2
-rw-r--r--spec/features/projects/packages_spec.rb2
-rw-r--r--spec/features/projects/terraform_spec.rb23
-rw-r--r--spec/finders/group_members_finder_spec.rb6
-rw-r--r--spec/frontend/editor/utils_spec.js84
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/dependency_row_spec.js.snap8
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js49
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js31
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js38
-rw-r--r--spec/frontend/terraform/components/init_command_modal_spec.js79
-rw-r--r--spec/frontend/terraform/components/states_table_actions_spec.js26
-rw-r--r--spec/helpers/projects/terraform_helper_spec.rb12
-rw-r--r--spec/lib/gitlab/checks/changes_access_spec.rb30
-rw-r--r--spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb31
-rw-r--r--spec/models/ci/build_spec.rb36
-rw-r--r--spec/models/project_spec.rb54
-rw-r--r--spec/serializers/build_details_entity_spec.rb1
-rw-r--r--spec/services/ci/register_job_service_spec.rb30
-rw-r--r--spec/support/shared_examples/features/packages_shared_examples.rb6
52 files changed, 761 insertions, 179 deletions
diff --git a/app/assets/javascripts/editor/source_editor.js b/app/assets/javascripts/editor/source_editor.js
index ee97714824e..81ddf8d77fa 100644
--- a/app/assets/javascripts/editor/source_editor.js
+++ b/app/assets/javascripts/editor/source_editor.js
@@ -1,7 +1,6 @@
-import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor';
+import { editor as monacoEditor, Uri } from 'monaco-editor';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
import languages from '~/ide/lib/languages';
-import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
import { registerLanguages } from '~/ide/utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { uuids } from '~/lib/utils/uuids';
@@ -11,7 +10,7 @@ import {
EDITOR_READY_EVENT,
EDITOR_TYPE_DIFF,
} from './constants';
-import { clearDomElement } from './utils';
+import { clearDomElement, setupEditorTheme, getBlobLanguage } from './utils';
export default class SourceEditor {
constructor(options = {}) {
@@ -22,26 +21,11 @@ export default class SourceEditor {
...options,
};
- SourceEditor.setupMonacoTheme();
+ setupEditorTheme();
registerLanguages(...languages);
}
- static setupMonacoTheme() {
- const themeName = window.gon?.user_color_scheme || DEFAULT_THEME;
- const theme = themes.find((t) => t.name === themeName);
- if (theme) monacoEditor.defineTheme(themeName, theme.data);
- monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME);
- }
-
- static getModelLanguage(path) {
- const ext = `.${path.split('.').pop()}`;
- const language = monacoLanguages
- .getLanguages()
- .find((lang) => lang.extensions.indexOf(ext) !== -1);
- return language ? language.id : 'plaintext';
- }
-
static pushToImportsArray(arr, toImport) {
arr.push(import(toImport));
}
@@ -124,10 +108,7 @@ export default class SourceEditor {
return model;
}
const diffModel = {
- original: monacoEditor.createModel(
- blobOriginalContent,
- SourceEditor.getModelLanguage(model.uri.path),
- ),
+ original: monacoEditor.createModel(blobOriginalContent, getBlobLanguage(model.uri.path)),
modified: model,
};
instance.setModel(diffModel);
@@ -155,7 +136,7 @@ export default class SourceEditor {
};
static instanceUpdateLanguage(inst, path) {
- const lang = SourceEditor.getModelLanguage(path);
+ const lang = getBlobLanguage(path);
const model = inst.getModel();
return monacoEditor.setModelLanguage(model, lang);
}
diff --git a/app/assets/javascripts/editor/utils.js b/app/assets/javascripts/editor/utils.js
index af4473413f4..6977db161e0 100644
--- a/app/assets/javascripts/editor/utils.js
+++ b/app/assets/javascripts/editor/utils.js
@@ -1,3 +1,6 @@
+import { editor as monacoEditor, languages as monacoLanguages } from 'monaco-editor';
+import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
+
export const clearDomElement = (el) => {
if (!el || !el.firstChild) return;
@@ -6,6 +9,22 @@ export const clearDomElement = (el) => {
}
};
-export default () => ({
- clearDomElement,
-});
+export const setupEditorTheme = () => {
+ const themeName = window.gon?.user_color_scheme || DEFAULT_THEME;
+ const theme = themes.find((t) => t.name === themeName);
+ if (theme) monacoEditor.defineTheme(themeName, theme.data);
+ monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME);
+};
+
+export const getBlobLanguage = (path) => {
+ const ext = `.${path.split('.').pop()}`;
+ const language = monacoLanguages
+ .getLanguages()
+ .find((lang) => lang.extensions.indexOf(ext) !== -1);
+ return language ? language.id : 'plaintext';
+};
+
+export const setupCodeSnippet = (el) => {
+ monacoEditor.colorizeElement(el);
+ setupEditorTheme();
+};
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue
index 1dc40f57efb..4d6a1d5462b 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue
@@ -29,8 +29,10 @@ export default {
},
computed: {
showMetadata() {
- return [PACKAGE_TYPE_NUGET, PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN].includes(
- this.packageEntity.packageType,
+ return (
+ [PACKAGE_TYPE_NUGET, PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN].includes(
+ this.packageEntity.packageType,
+ ) && this.packageEntity.metadata
);
},
showNugetMetadata() {
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
index 4ab64350d26..3d3fa62fd43 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
@@ -1,8 +1,4 @@
<script>
-/*
- * The commented part of this component needs to be re-enabled in the refactor process,
- * See here for more info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64939
- */
import {
GlBadge,
GlButton,
@@ -19,10 +15,9 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { objectToQuery } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
-// import DependencyRow from '~/packages/details/components/dependency_row.vue';
-import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
import { packageTypeToTrackCategory } from '~/packages/shared/utils';
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
+import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue';
import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue';
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
@@ -61,9 +56,8 @@ export default {
GlTabs,
GlSprintf,
PackageTitle,
- PackagesListLoader,
VersionRow,
- // DependencyRow,
+ DependencyRow,
PackageHistory,
AdditionalMetadata,
InstallationCommands,
@@ -141,7 +135,7 @@ export default {
return this.packageEntity.versions?.nodes?.length > 0;
},
packageDependencies() {
- return this.packageEntity.dependency_links || [];
+ return this.packageEntity.dependencyLinks?.nodes || [];
},
showDependencies() {
return this.packageEntity.packageType === PACKAGE_TYPE_NUGET;
@@ -268,8 +262,7 @@ export default {
:description="s__('PackageRegistry|There was a problem fetching the details for this package.')"
:svg-path="svgPath"
/>
-
- <div v-else class="packages-app">
+ <div v-else-if="!isLoading" class="packages-app">
<package-title :package-entity="packageEntity">
<template #delete-button>
<gl-button
@@ -303,20 +296,14 @@ export default {
/>
</gl-tab>
- <gl-tab v-if="showDependencies" title-item-class="js-dependencies-tab">
+ <gl-tab v-if="showDependencies">
<template #title>
<span>{{ __('Dependencies') }}</span>
- <gl-badge size="sm" data-testid="dependencies-badge">{{
- packageDependencies.length
- }}</gl-badge>
+ <gl-badge size="sm">{{ packageDependencies.length }}</gl-badge>
</template>
<template v-if="packageDependencies.length > 0">
- <!-- <dependency-row
- v-for="(dep, index) in packageDependencies"
- :key="index"
- :dependency="dep"
- /> -->
+ <dependency-row v-for="dep in packageDependencies" :key="dep.id" :dependency-link="dep" />
</template>
<p v-else class="gl-mt-3" data-testid="no-dependencies-message">
@@ -325,11 +312,7 @@ export default {
</gl-tab>
<gl-tab :title="__('Other versions')" title-item-class="js-versions-tab">
- <template v-if="isLoading && !hasVersions">
- <packages-list-loader />
- </template>
-
- <template v-else-if="hasVersions">
+ <template v-if="hasVersions">
<version-row v-for="v in packageEntity.versions.nodes" :key="v.id" :package-entity="v" />
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/dependency_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/dependency_row.vue
index 1a2202b23c8..95236eea0b5 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/dependency_row.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/dependency_row.vue
@@ -2,14 +2,17 @@
export default {
name: 'DependencyRow',
props: {
- dependency: {
+ dependencyLink: {
type: Object,
required: true,
},
},
computed: {
showVersion() {
- return Boolean(this.dependency.version_pattern);
+ return Boolean(this.dependencyLink.dependency?.versionPattern);
+ },
+ showTargetFramework() {
+ return Boolean(this.dependencyLink.metadata?.targetFramework);
},
},
};
@@ -18,10 +21,10 @@ export default {
<template>
<div class="gl-responsive-table-row">
<div class="table-section section-50">
- <strong class="gl-text-body">{{ dependency.name }}</strong>
- <span v-if="dependency.target_framework" data-testid="target-framework"
- >({{ dependency.target_framework }})</span
- >
+ <strong class="gl-text-body">{{ dependencyLink.dependency.name }}</strong>
+ <span v-if="showTargetFramework" data-testid="target-framework">
+ ({{ dependencyLink.metadata.targetFramework }})
+ </span>
</div>
<div
@@ -29,7 +32,7 @@ export default {
class="table-section section-50 gl-display-flex gl-md-justify-content-end"
data-testid="version-pattern"
>
- <span class="gl-text-body">{{ dependency.version_pattern }}</span>
+ <span class="gl-text-body">{{ dependencyLink.dependency.versionPattern }}</span>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
index 8fa7649d2d2..14aa14e9822 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
@@ -16,7 +16,7 @@ query getPackageDetails($id: ID!) {
name
}
}
- pipelines(first: 3) {
+ pipelines(first: 10) {
nodes {
ref
id
@@ -60,6 +60,23 @@ query getPackageDetails($id: ID!) {
}
}
}
+ dependencyLinks {
+ nodes {
+ id
+ dependency {
+ id
+ name
+ versionPattern
+ }
+ dependencyType
+ metadata {
+ ... on NugetDependencyLinkMetadata {
+ id
+ targetFramework
+ }
+ }
+ }
+ }
metadata {
... on ComposerMetadata {
targetSha
diff --git a/app/assets/javascripts/terraform/components/init_command_modal.vue b/app/assets/javascripts/terraform/components/init_command_modal.vue
new file mode 100644
index 00000000000..2cb10d4ae23
--- /dev/null
+++ b/app/assets/javascripts/terraform/components/init_command_modal.vue
@@ -0,0 +1,86 @@
+<script>
+import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+
+export default {
+ i18n: {
+ title: s__('Terraform|Terraform init command'),
+ explanatoryText: s__(
+ `Terraform|To get access to this terraform state from your local computer, run the following command at the command line. The first line requires a personal access token with API read and write access. %{linkStart}How do I create a personal access token?%{linkEnd}.`,
+ ),
+ closeText: __('Close'),
+ copyToClipboardText: __('Copy'),
+ },
+ components: {
+ GlModal,
+ GlSprintf,
+ GlLink,
+ ModalCopyButton,
+ },
+ inject: ['accessTokensPath', 'terraformApiUrl', 'username'],
+ props: {
+ modalId: {
+ type: String,
+ required: true,
+ },
+ stateName: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ closeModalProps() {
+ return {
+ text: this.$options.i18n.closeText,
+ attributes: [],
+ };
+ },
+ },
+ methods: {
+ getModalInfoCopyStr() {
+ return `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
+terraform init \\
+ -backend-config="address=${this.terraformApiUrl}/${this.stateName}" \\
+ -backend-config="lock_address=${this.terraformApiUrl}/${this.stateName}/lock" \\
+ -backend-config="unlock_address=${this.terraformApiUrl}/${this.stateName}/lock" \\
+ -backend-config="username=${this.username}" \\
+ -backend-config="password=$GITLAB_ACCESS_TOKEN" \\
+ -backend-config="lock_method=POST" \\
+ -backend-config="unlock_method=DELETE" \\
+ -backend-config="retry_wait_min=5"
+ `;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="initCommandModal"
+ :modal-id="modalId"
+ :title="$options.i18n.title"
+ :action-cancel="closeModalProps"
+ >
+ <p data-testid="init-command-explanatory-text">
+ <gl-sprintf :message="$options.i18n.explanatoryText">
+ <template #link="{ content }">
+ <gl-link :href="accessTokensPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <div class="gl-display-flex">
+ <pre class="gl-bg-gray gl-white-space-pre-wrap" data-testid="terraform-init-command">{{
+ getModalInfoCopyStr()
+ }}</pre>
+ <modal-copy-button
+ :title="$options.i18n.copyToClipboardText"
+ :text="getModalInfoCopyStr()"
+ :modal-id="$options.modalId"
+ data-testid="init-command-copy-clipboard"
+ css-classes="gl-align-self-start gl-ml-2"
+ />
+ </div>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/terraform/components/states_table_actions.vue b/app/assets/javascripts/terraform/components/states_table_actions.vue
index c4fd97188de..f8f7482422e 100644
--- a/app/assets/javascripts/terraform/components/states_table_actions.vue
+++ b/app/assets/javascripts/terraform/components/states_table_actions.vue
@@ -8,12 +8,14 @@ import {
GlIcon,
GlModal,
GlSprintf,
+ GlModalDirective,
} from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import addDataToState from '../graphql/mutations/add_data_to_state.mutation.graphql';
import lockState from '../graphql/mutations/lock_state.mutation.graphql';
import removeState from '../graphql/mutations/remove_state.mutation.graphql';
import unlockState from '../graphql/mutations/unlock_state.mutation.graphql';
+import InitCommandModal from './init_command_modal.vue';
export default {
components: {
@@ -25,6 +27,10 @@ export default {
GlIcon,
GlModal,
GlSprintf,
+ InitCommandModal,
+ },
+ directives: {
+ GlModalDirective,
},
props: {
state: {
@@ -36,6 +42,7 @@ export default {
return {
showRemoveModal: false,
removeConfirmText: '',
+ showCommandModal: false,
};
},
i18n: {
@@ -54,6 +61,7 @@ export default {
remove: s__('Terraform|Remove state file and versions'),
removeSuccessful: s__('Terraform|%{name} successfully removed'),
unlock: s__('Terraform|Unlock'),
+ copyCommand: s__('Terraform|Copy Terraform init command'),
},
computed: {
cancelModalProps() {
@@ -74,6 +82,9 @@ export default {
attributes: [{ disabled: this.disableModalSubmit }, { variant: 'danger' }],
};
},
+ commandModalId() {
+ return `init-command-modal-${this.state.name}`;
+ },
},
methods: {
hideModal() {
@@ -164,6 +175,9 @@ export default {
});
});
},
+ copyInitCommand() {
+ this.showCommandModal = true;
+ },
},
};
</script>
@@ -182,6 +196,14 @@ export default {
</template>
<gl-dropdown-item
+ v-gl-modal-directive="commandModalId"
+ data-testid="terraform-state-copy-init-command"
+ @click="copyInitCommand"
+ >
+ {{ $options.i18n.copyCommand }}
+ </gl-dropdown-item>
+
+ <gl-dropdown-item
v-if="state.latestVersion"
data-testid="terraform-state-download"
:download="`${state.name}.json`"
@@ -248,5 +270,11 @@ export default {
/>
</gl-form-group>
</gl-modal>
+
+ <init-command-modal
+ v-if="showCommandModal"
+ :modal-id="commandModalId"
+ :state-name="state.name"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/terraform/index.js b/app/assets/javascripts/terraform/index.js
index 3f986423836..1b8cab0d51e 100644
--- a/app/assets/javascripts/terraform/index.js
+++ b/app/assets/javascripts/terraform/index.js
@@ -24,11 +24,16 @@ export default () => {
},
});
- const { emptyStateImage, projectPath } = el.dataset;
+ const { emptyStateImage, projectPath, accessTokensPath, terraformApiUrl, username } = el.dataset;
return new Vue({
el,
apolloProvider: new VueApollo({ defaultClient }),
+ provide: {
+ accessTokensPath,
+ terraformApiUrl,
+ username,
+ },
render(createElement) {
return createElement(TerraformList, {
props: {
diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss
index 0808f364e87..8e1438eaf8a 100644
--- a/app/assets/stylesheets/themes/_dark.scss
+++ b/app/assets/stylesheets/themes/_dark.scss
@@ -184,6 +184,21 @@ body.gl-dark {
}
}
}
+
+ .gl-datepicker-theme {
+ .pika-prev,
+ .pika-next {
+ filter: invert(0.9);
+ }
+
+ .is-selected > .pika-button {
+ color: $gray-900;
+ }
+
+ :not(.is-selected) > .pika-button:hover {
+ background-color: $gray-200;
+ }
+ }
}
$border-white-normal: $border-color;
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
index 982234f7506..75623d33ef5 100644
--- a/app/finders/group_members_finder.rb
+++ b/app/finders/group_members_finder.rb
@@ -3,6 +3,7 @@
class GroupMembersFinder < UnionFinder
RELATIONS = %i(direct inherited descendants).freeze
DEFAULT_RELATIONS = %i(direct inherited).freeze
+ INVALID_RELATION_TYPE_ERROR_MSG = "is not a valid relation type. Valid relation types are #{RELATIONS.join(', ')}."
RELATIONS_DESCRIPTIONS = {
direct: 'Members in the group itself',
@@ -42,6 +43,8 @@ class GroupMembersFinder < UnionFinder
attr_reader :user, :group
def groups_by_relations(include_relations)
+ check_relation_arguments!(include_relations)
+
case include_relations.sort
when [:inherited]
group.ancestors
@@ -86,6 +89,12 @@ class GroupMembersFinder < UnionFinder
def members_of_groups(groups)
GroupMember.non_request.of_groups(groups)
end
+
+ def check_relation_arguments!(include_relations)
+ unless include_relations & RELATIONS == include_relations
+ raise ArgumentError, "#{(include_relations - RELATIONS).first} #{INVALID_RELATION_TYPE_ERROR_MSG}"
+ end
+ end
end
GroupMembersFinder.prepend_mod_with('GroupMembersFinder')
diff --git a/app/helpers/projects/terraform_helper.rb b/app/helpers/projects/terraform_helper.rb
index 621d97ffb69..fb35224fad3 100644
--- a/app/helpers/projects/terraform_helper.rb
+++ b/app/helpers/projects/terraform_helper.rb
@@ -5,7 +5,10 @@ module Projects::TerraformHelper
{
empty_state_image: image_path('illustrations/empty-state/empty-serverless-lg.svg'),
project_path: project.full_path,
- terraform_admin: current_user&.can?(:admin_terraform_state, project)
+ terraform_admin: current_user&.can?(:admin_terraform_state, project),
+ access_tokens_path: profile_personal_access_tokens_path,
+ username: current_user&.username,
+ terraform_api_url: "#{Settings.gitlab.url}/api/v4/projects/#{project.id}/terraform/state"
}
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 5cc8474b71d..7758620f605 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -891,7 +891,7 @@ module Ci
end
def valid_dependency?
- return false if artifacts_expired?
+ return false if artifacts_expired? && !pipeline.artifacts_locked?
return false if erased?
true
diff --git a/app/models/integration.rb b/app/models/integration.rb
index a9c865569d0..5c4d03f1fa8 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -274,7 +274,7 @@ class Integration < ApplicationRecord
end
def self.closest_group_integration(type, scope)
- group_ids = scope.ancestors(hierarchy_order: :asc).select(:id)
+ group_ids = scope.ancestors.select(:id)
array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
where(type: type, group_id: group_ids, inherit_from_id: nil)
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
index 3216ec42427..081e51c1028 100644
--- a/app/models/namespaces/traversal/linear.rb
+++ b/app/models/namespaces/traversal/linear.rb
@@ -178,10 +178,6 @@ module Namespaces
depth_sql = "ABS(#{traversal_ids.count} - array_length(traversal_ids, 1))"
skope = skope.select(skope.arel_table[Arel.star], "#{depth_sql} as depth")
.order(depth: hierarchy_order)
- # The SELECT includes an extra depth attribute. We then wrap the SQL
- # in a standard SELECT to avoid mismatched attribute errors when
- # trying to chain future ActiveRelation commands.
- skope = self.class.without_sti_condition.from(skope, self.class.table_name)
end
skope
diff --git a/app/models/project.rb b/app/models/project.rb
index 9f90cef9c12..85bb1dea48f 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -914,9 +914,7 @@ class Project < ApplicationRecord
.base_and_ancestors(upto: top, hierarchy_order: hierarchy_order)
end
- def ancestors(hierarchy_order: nil)
- namespace&.self_and_ancestors(hierarchy_order: hierarchy_order)
- end
+ alias_method :ancestors, :ancestors_upto
def ancestors_upto_ids(...)
ancestors_upto(...).pluck(:id)
diff --git a/app/views/projects/packages/packages/show.html.haml b/app/views/projects/packages/packages/show.html.haml
index 2e1590cb826..42c31b3272f 100644
--- a/app/views/projects/packages/packages/show.html.haml
+++ b/app/views/projects/packages/packages/show.html.haml
@@ -6,7 +6,7 @@
.row
.col-12
- - if Feature.enabled?(:package_details_apollo)
+ - if Feature.enabled?(:package_details_apollo, default_enabled: :yaml)
#js-vue-packages-detail-new{ data: package_details_data(@project, @package) }
- else
#js-vue-packages-detail{ data: package_details_data(@project, @package, true) }
diff --git a/config/feature_flags/development/ci_pending_builds_maintain_ci_minutes_data.yml b/config/feature_flags/development/ci_pending_builds_maintain_ci_minutes_data.yml
index 1a247d3b2af..f073e94e322 100644
--- a/config/feature_flags/development/ci_pending_builds_maintain_ci_minutes_data.yml
+++ b/config/feature_flags/development/ci_pending_builds_maintain_ci_minutes_data.yml
@@ -1,7 +1,7 @@
---
name: ci_pending_builds_maintain_ci_minutes_data
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64443
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332951
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338149
milestone: '14.2'
type: development
group: group::pipeline execution
diff --git a/config/feature_flags/development/ci_pending_builds_maintain_shared_runners_data.yml b/config/feature_flags/development/ci_pending_builds_maintain_shared_runners_data.yml
index 5a8b89edfad..16b318509dc 100644
--- a/config/feature_flags/development/ci_pending_builds_maintain_shared_runners_data.yml
+++ b/config/feature_flags/development/ci_pending_builds_maintain_shared_runners_data.yml
@@ -1,7 +1,7 @@
---
name: ci_pending_builds_maintain_shared_runners_data
-introduced_by_url:
-rollout_issue_url:
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64644
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338152
milestone: '14.1'
type: development
group: group::pipeline execution
diff --git a/config/feature_flags/development/ci_queueing_denormalize_shared_runners_information.yml b/config/feature_flags/development/ci_queueing_denormalize_shared_runners_information.yml
index 6eefaeea3a1..326beaf6740 100644
--- a/config/feature_flags/development/ci_queueing_denormalize_shared_runners_information.yml
+++ b/config/feature_flags/development/ci_queueing_denormalize_shared_runners_information.yml
@@ -1,7 +1,7 @@
---
name: ci_queueing_denormalize_shared_runners_information
-introduced_by_url:
-rollout_issue_url:
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66082
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338289
milestone: '14.2'
type: development
group: group::pipeline execution
diff --git a/config/feature_flags/development/instance_level_integration_overrides.yml b/config/feature_flags/development/instance_level_integration_overrides.yml
index f99b85b3c05..6b1f3dd4276 100644
--- a/config/feature_flags/development/instance_level_integration_overrides.yml
+++ b/config/feature_flags/development/instance_level_integration_overrides.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66723
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336750
milestone: '14.2'
type: development
-group: group::ecosystem
+group: group::integrations
default_enabled: false
diff --git a/config/feature_flags/development/jira_issue_details_edit_labels.yml b/config/feature_flags/development/jira_issue_details_edit_labels.yml
index bccd7374907..c43d01bf969 100644
--- a/config/feature_flags/development/jira_issue_details_edit_labels.yml
+++ b/config/feature_flags/development/jira_issue_details_edit_labels.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65298
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/335069
milestone: '14.1'
type: development
-group: group::ecosystem
+group: group::integrations
default_enabled: false
diff --git a/config/feature_flags/development/jira_issue_details_edit_status.yml b/config/feature_flags/development/jira_issue_details_edit_status.yml
index 9d64707a79f..311e243c570 100644
--- a/config/feature_flags/development/jira_issue_details_edit_status.yml
+++ b/config/feature_flags/development/jira_issue_details_edit_status.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60092
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330628
milestone: '14.1'
type: development
-group: group::ecosystem
+group: group::integrations
default_enabled: false
diff --git a/config/feature_flags/development/package_details_apollo.yml b/config/feature_flags/development/package_details_apollo.yml
index aa8ee47df0c..fbab4c2c7c8 100644
--- a/config/feature_flags/development/package_details_apollo.yml
+++ b/config/feature_flags/development/package_details_apollo.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334786
milestone: '14.1'
type: development
group: group::package
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/report_on_long_redis_durations.yml b/config/feature_flags/development/report_on_long_redis_durations.yml
new file mode 100644
index 00000000000..0f93c591d63
--- /dev/null
+++ b/config/feature_flags/development/report_on_long_redis_durations.yml
@@ -0,0 +1,8 @@
+---
+name: report_on_long_redis_durations
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67512
+rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1183
+milestone: '14.2'
+type: development
+group: team::Scalability
+default_enabled: false
diff --git a/config/feature_flags/development/web_hooks_disable_failed.yml b/config/feature_flags/development/web_hooks_disable_failed.yml
index a54034d73e8..3a7c85edafc 100644
--- a/config/feature_flags/development/web_hooks_disable_failed.yml
+++ b/config/feature_flags/development/web_hooks_disable_failed.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60837
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329849
milestone: '13.12'
type: development
-group: group::ecosystem
+group: group::integrations
default_enabled: false
diff --git a/config/feature_flags/ops/api_kaminari_count_with_limit.yml b/config/feature_flags/ops/api_kaminari_count_with_limit.yml
index a987d5c65b1..a55e3e67710 100644
--- a/config/feature_flags/ops/api_kaminari_count_with_limit.yml
+++ b/config/feature_flags/ops/api_kaminari_count_with_limit.yml
@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/23
rollout_issue_url:
milestone: '11.8'
type: ops
-group: group::ecosystem
+group: group::integrations
default_enabled: false
diff --git a/doc/user/infrastructure/img/terraform_list_view_actions_v13_8.png b/doc/user/infrastructure/img/terraform_list_view_actions_v13_8.png
deleted file mode 100644
index 7d619b6ad7e..00000000000
--- a/doc/user/infrastructure/img/terraform_list_view_actions_v13_8.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/infrastructure/terraform_state.md b/doc/user/infrastructure/terraform_state.md
index 57db2b47de4..d6c0c9ac6de 100644
--- a/doc/user/infrastructure/terraform_state.md
+++ b/doc/user/infrastructure/terraform_state.md
@@ -83,6 +83,14 @@ local machine, this is a simple way to get started:
-backend-config="retry_wait_min=5"
```
+If you already have a GitLab-managed Terraform state, you can use the `terraform init` command
+with the prepopulated parameters values:
+
+1. On the top bar, select **Menu > Projects** and find your project.
+1. On the left sidebar, select **Infrastructure > Terraform**.
+1. Next to the environment you want to use, select the [Actions menu](#managing-state-files)
+ **{ellipsis_v}** and select **Copy Terraform init command**.
+
You can now run `terraform plan` and `terraform apply` as you normally would.
### Get started using GitLab CI
@@ -222,7 +230,7 @@ An example setup is shown below:
```plaintext
example_remote_state_address=https://gitlab.com/api/v4/projects/<TARGET-PROJECT-ID>/terraform/state/<TARGET-STATE-NAME>
example_username=<GitLab username>
- example_access_token=<GitLab Personal Acceess Token>
+ example_access_token=<GitLab Personal Access Token>
```
1. Define the data source by adding the following code block in a `.tf` file (such as `data.tf`):
@@ -362,10 +370,8 @@ contains these fields:
state file is locked.
- **Pipeline**: A link to the most recent pipeline and its status.
- **Details**: Information about when the state file was created or changed.
-- **Actions**: Actions you can take on the state file, including downloading,
- locking, unlocking, or [removing](#remove-a-state-file) the state file and versions:
-
- ![Terraform state list](img/terraform_list_view_actions_v13_8.png)
+- **Actions**: Actions you can take on the state file, including copying the `terraform init` command,
+ downloading, locking, unlocking, or [removing](#remove-a-state-file) the state file and versions.
NOTE:
Additional improvements to the
diff --git a/lib/api/entities/ci/job_request/dependency.rb b/lib/api/entities/ci/job_request/dependency.rb
index 2c6ed417714..2672a4a245b 100644
--- a/lib/api/entities/ci/job_request/dependency.rb
+++ b/lib/api/entities/ci/job_request/dependency.rb
@@ -6,7 +6,7 @@ module API
module JobRequest
class Dependency < Grape::Entity
expose :id, :name, :token
- expose :artifacts_file, using: Entities::Ci::JobArtifactFile, if: ->(job, _) { job.artifacts? }
+ expose :artifacts_file, using: Entities::Ci::JobArtifactFile, if: ->(job, _) { job.available_artifacts? }
end
end
end
diff --git a/lib/gitlab/checks/changes_access.rb b/lib/gitlab/checks/changes_access.rb
index a1c2f8d8280..9ecc93f871b 100644
--- a/lib/gitlab/checks/changes_access.rb
+++ b/lib/gitlab/checks/changes_access.rb
@@ -48,16 +48,28 @@ module Gitlab
commits_by_id = commits.index_by(&:id)
result = []
- pending = [newrev]
+ pending = Set[newrev]
# We go up the parent chain of our newrev and collect all commits which
# are new. In case a commit's ID cannot be found in the set of new
# commits, then it must already be a preexisting commit.
- pending.each do |rev|
- commit = commits_by_id[rev]
+ while pending.any?
+ rev = pending.first
+ pending.delete(rev)
+
+ # Remove the revision from commit candidates such that we don't walk
+ # it multiple times. If the hash doesn't contain the revision, then
+ # we have either already walked the commit or it's not new.
+ commit = commits_by_id.delete(rev)
next if commit.nil?
- pending.push(*commit.parent_ids)
+ # Only add the parent ID to the pending set if we actually know its
+ # commit to guards us against readding an ID which we have already
+ # queued up before.
+ commit.parent_ids.each do |parent_id|
+ pending.add(parent_id) if commits_by_id.has_key?(parent_id)
+ end
+
result << commit
end
diff --git a/lib/gitlab/instrumentation/redis_interceptor.rb b/lib/gitlab/instrumentation/redis_interceptor.rb
index 8a64abb9f62..0f21a16793d 100644
--- a/lib/gitlab/instrumentation/redis_interceptor.rb
+++ b/lib/gitlab/instrumentation/redis_interceptor.rb
@@ -5,8 +5,21 @@ module Gitlab
module RedisInterceptor
APDEX_EXCLUDE = %w[brpop blpop brpoplpush bzpopmin bzpopmax xread xreadgroup].freeze
+ # These are temporary to help with investigating
+ # https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1183
+ DURATION_ERROR_THRESHOLD = 1.25.seconds
+
+ class MysteryRedisDurationError < StandardError
+ attr_reader :backtrace
+
+ def initialize(backtrace)
+ @backtrace = backtrace
+ end
+ end
+
def call(*args, &block)
start = Gitlab::Metrics::System.monotonic_time # must come first so that 'start' is always defined
+ start_real_time = Time.now
instrumentation_class.instance_count_request
instrumentation_class.redis_cluster_validate!(args.first)
@@ -27,6 +40,13 @@ module Gitlab
instrumentation_class.add_duration(duration)
instrumentation_class.add_call_details(duration, args)
end
+
+ if duration > DURATION_ERROR_THRESHOLD && Feature.enabled?(:report_on_long_redis_durations, default_enabled: :yaml)
+ Gitlab::ErrorTracking.track_exception(MysteryRedisDurationError.new(caller),
+ command: command_from_args(args),
+ duration: duration,
+ timestamp: start_real_time.iso8601(5))
+ end
end
def write(command)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b1db6f42ac0..7c36f531da8 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -11513,7 +11513,7 @@ msgstr ""
msgid "DevopsAdoption|Edit subgroups"
msgstr ""
-msgid "DevopsAdoption|Feature adoption is based on usage in the previous calendar month. Last updated: %{timestamp}."
+msgid "DevopsAdoption|Feature adoption is based on usage in the previous calendar month. Data is updated at the beginning of each month. Last updated: %{timestamp}."
msgstr ""
msgid "DevopsAdoption|Fuzz Testing"
@@ -32641,6 +32641,9 @@ msgstr ""
msgid "Terraform|Cancel"
msgstr ""
+msgid "Terraform|Copy Terraform init command"
+msgstr ""
+
msgid "Terraform|Details"
msgstr ""
@@ -32692,12 +32695,18 @@ msgstr ""
msgid "Terraform|States"
msgstr ""
+msgid "Terraform|Terraform init command"
+msgstr ""
+
msgid "Terraform|The report %{name} failed to generate."
msgstr ""
msgid "Terraform|The report %{name} was generated in your pipelines."
msgstr ""
+msgid "Terraform|To get access to this terraform state from your local computer, run the following command at the command line. The first line requires a personal access token with API read and write access. %{linkStart}How do I create a personal access token?%{linkEnd}."
+msgstr ""
+
msgid "Terraform|To remove the State file and its versions, type %{name} to confirm:"
msgstr ""
diff --git a/spec/features/groups/packages_spec.rb b/spec/features/groups/packages_spec.rb
index 752303fdd78..9a7950266a5 100644
--- a/spec/features/groups/packages_spec.rb
+++ b/spec/features/groups/packages_spec.rb
@@ -52,6 +52,8 @@ RSpec.describe 'Group Packages' do
it_behaves_like 'package details link'
end
+ it_behaves_like 'package details link'
+
it 'allows you to navigate to the project page' do
find('[data-testid="root-link"]', text: project.name).click
diff --git a/spec/features/projects/packages_spec.rb b/spec/features/projects/packages_spec.rb
index fa4c57c305d..30298f79312 100644
--- a/spec/features/projects/packages_spec.rb
+++ b/spec/features/projects/packages_spec.rb
@@ -45,6 +45,8 @@ RSpec.describe 'Packages' do
it_behaves_like 'package details link'
end
+ it_behaves_like 'package details link'
+
context 'deleting a package' do
let_it_be(:project) { create(:project) }
let_it_be(:package) { create(:package, project: project) }
diff --git a/spec/features/projects/terraform_spec.rb b/spec/features/projects/terraform_spec.rb
index d080d101285..2c63f2bfc02 100644
--- a/spec/features/projects/terraform_spec.rb
+++ b/spec/features/projects/terraform_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe 'Terraform', :js do
it 'displays a table with terraform states' do
expect(page).to have_selector(
- '[data-testid="terraform-states-table-name"]',
+ "[data-testid='terraform-states-table-name']",
count: project.terraform_states.size
)
end
@@ -64,7 +64,7 @@ RSpec.describe 'Terraform', :js do
expect(page).to have_content(additional_state.name)
find("[data-testid='terraform-state-actions-#{additional_state.name}']").click
- find('[data-testid="terraform-state-remove"]').click
+ find("[data-testid='terraform-state-remove']").click
fill_in "terraform-state-remove-input-#{additional_state.name}", with: additional_state.name
click_button 'Remove'
@@ -72,6 +72,21 @@ RSpec.describe 'Terraform', :js do
expect { additional_state.reload }.to raise_error ActiveRecord::RecordNotFound
end
end
+
+ context 'when clicking on copy Terraform init command' do
+ it 'shows the modal with the init command' do
+ visit project_terraform_index_path(project)
+
+ expect(page).to have_content(terraform_state.name)
+
+ page.within("[data-testid='terraform-state-actions-#{terraform_state.name}']") do
+ click_button class: 'gl-dropdown-toggle'
+ click_button 'Copy Terraform init command'
+ end
+
+ expect(page).to have_content("To get access to this terraform state from your local computer, run the following command at the command line.")
+ end
+ end
end
end
@@ -87,11 +102,11 @@ RSpec.describe 'Terraform', :js do
context 'when user visits the index page' do
it 'displays a table without an action dropdown', :aggregate_failures do
expect(page).to have_selector(
- '[data-testid="terraform-states-table-name"]',
+ "[data-testid='terraform-states-table-name']",
count: project.terraform_states.size
)
- expect(page).not_to have_selector('[data-testid*="terraform-state-actions"]')
+ expect(page).not_to have_selector("[data-testid*='terraform-state-actions']")
end
end
end
diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb
index 3238f6744f7..0d797b7923c 100644
--- a/spec/finders/group_members_finder_spec.rb
+++ b/spec/finders/group_members_finder_spec.rb
@@ -38,6 +38,12 @@ RSpec.describe GroupMembersFinder, '#execute' do
}
end
+ it 'raises an error if a non-supported relation type is used' do
+ expect do
+ described_class.new(group).execute(include_relations: [:direct, :invalid_relation_type])
+ end.to raise_error(ArgumentError, "invalid_relation_type is not a valid relation type. Valid relation types are direct, inherited, descendants.")
+ end
+
using RSpec::Parameterized::TableSyntax
where(:subject_relations, :subject_group, :expected_members) do
diff --git a/spec/frontend/editor/utils_spec.js b/spec/frontend/editor/utils_spec.js
new file mode 100644
index 00000000000..0f85ab582bd
--- /dev/null
+++ b/spec/frontend/editor/utils_spec.js
@@ -0,0 +1,84 @@
+import { editor as monacoEditor } from 'monaco-editor';
+import * as utils from '~/editor/utils';
+import { DEFAULT_THEME } from '~/ide/lib/themes';
+
+describe('Source Editor utils', () => {
+ let el;
+
+ const stubUserColorScheme = (value) => {
+ if (window.gon == null) {
+ window.gon = {};
+ }
+ window.gon.user_color_scheme = value;
+ };
+
+ describe('clearDomElement', () => {
+ beforeEach(() => {
+ setFixtures('<div id="foo"><div id="bar">Foo</div></div>');
+ el = document.getElementById('foo');
+ });
+
+ it('removes all child nodes from an element', () => {
+ expect(el.children.length).toBe(1);
+ utils.clearDomElement(el);
+ expect(el.children.length).toBe(0);
+ });
+ });
+
+ describe('setupEditorTheme', () => {
+ beforeEach(() => {
+ jest.spyOn(monacoEditor, 'defineTheme').mockImplementation();
+ jest.spyOn(monacoEditor, 'setTheme').mockImplementation();
+ });
+
+ it.each`
+ themeName | expectedThemeName
+ ${'solarized-light'} | ${'solarized-light'}
+ ${DEFAULT_THEME} | ${DEFAULT_THEME}
+ ${'non-existent'} | ${DEFAULT_THEME}
+ `(
+ 'sets the $expectedThemeName theme when $themeName is set in the user preference',
+ ({ themeName, expectedThemeName }) => {
+ stubUserColorScheme(themeName);
+ utils.setupEditorTheme();
+
+ expect(monacoEditor.setTheme).toHaveBeenCalledWith(expectedThemeName);
+ },
+ );
+ });
+
+ describe('getBlobLanguage', () => {
+ it.each`
+ path | expectedLanguage
+ ${'foo.js'} | ${'javascript'}
+ ${'foo.js.rb'} | ${'ruby'}
+ ${'foo.bar'} | ${'plaintext'}
+ `(
+ 'sets the $expectedThemeName theme when $themeName is set in the user preference',
+ ({ path, expectedLanguage }) => {
+ const language = utils.getBlobLanguage(path);
+
+ expect(language).toEqual(expectedLanguage);
+ },
+ );
+ });
+
+ describe('setupCodeSnipet', () => {
+ beforeEach(() => {
+ jest.spyOn(monacoEditor, 'colorizeElement').mockImplementation();
+ jest.spyOn(monacoEditor, 'setTheme').mockImplementation();
+ setFixtures('<pre id="foo"></pre>');
+ el = document.getElementById('foo');
+ });
+
+ it('colorizes the element and applies the preference theme', () => {
+ expect(monacoEditor.colorizeElement).not.toHaveBeenCalled();
+ expect(monacoEditor.setTheme).not.toHaveBeenCalled();
+
+ utils.setupCodeSnippet(el);
+
+ expect(monacoEditor.colorizeElement).toHaveBeenCalledWith(el);
+ expect(monacoEditor.setTheme).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/dependency_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/dependency_row_spec.js.snap
index 39469bf4fd0..f83df7b11f4 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/dependency_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/dependency_row_spec.js.snap
@@ -10,13 +10,15 @@ exports[`DependencyRow renders full dependency 1`] = `
<strong
class="gl-text-body"
>
- Test.Dependency
+ Ninject.Extensions.Factory
</strong>
<span
data-testid="target-framework"
>
- (.NETStandard2.0)
+
+ (.NETCoreApp3.1)
+
</span>
</div>
@@ -27,7 +29,7 @@ exports[`DependencyRow renders full dependency 1`] = `
<span
class="gl-text-body"
>
- 2.3.7
+ 3.3.2
</span>
</div>
</div>
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
index 1bd2058cf5b..5119512564f 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
@@ -1,4 +1,4 @@
-import { GlEmptyState } from '@gitlab/ui';
+import { GlEmptyState, GlBadge, GlTabs, GlTab } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -10,6 +10,7 @@ import createFlash from '~/flash';
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue';
+import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue';
import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue';
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
@@ -21,6 +22,7 @@ import {
PACKAGE_TYPE_COMPOSER,
DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
+ PACKAGE_TYPE_NUGET,
} from '~/packages_and_registries/package_registry/constants';
import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
@@ -30,6 +32,7 @@ import {
packageDetailsQuery,
packageData,
packageVersions,
+ dependencyLinks,
emptyPackageDetailsQuery,
packageDestroyMutation,
packageDestroyMutationError,
@@ -85,6 +88,8 @@ describe('PackagesApp', () => {
show: jest.fn(),
},
},
+ GlTabs,
+ GlTab,
},
});
}
@@ -100,6 +105,9 @@ describe('PackagesApp', () => {
const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal');
const findVersionRows = () => wrapper.findAllComponents(VersionRow);
const noVersionsMessage = () => wrapper.findByTestId('no-versions-message');
+ const findDependenciesCountBadge = () => wrapper.findComponent(GlBadge);
+ const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
+ const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
afterEach(() => {
wrapper.destroy();
@@ -401,4 +409,43 @@ describe('PackagesApp', () => {
expect(noVersionsMessage().exists()).toBe(true);
});
});
+ describe('dependency links', () => {
+ it('does not show the dependency links for a non nuget package', async () => {
+ createComponent();
+
+ expect(findDependenciesCountBadge().exists()).toBe(false);
+ });
+
+ it('shows the dependencies tab with 0 count when a nuget package with no dependencies', async () => {
+ createComponent({
+ resolver: jest.fn().mockResolvedValue(
+ packageDetailsQuery({
+ packageType: PACKAGE_TYPE_NUGET,
+ dependencyLinks: { nodes: [] },
+ }),
+ ),
+ });
+
+ await waitForPromises();
+
+ expect(findDependenciesCountBadge().exists()).toBe(true);
+ expect(findDependenciesCountBadge().text()).toBe('0');
+ expect(findNoDependenciesMessage().exists()).toBe(true);
+ });
+
+ it('renders the correct number of dependency rows for a nuget package', async () => {
+ createComponent({
+ resolver: jest.fn().mockResolvedValue(
+ packageDetailsQuery({
+ packageType: PACKAGE_TYPE_NUGET,
+ }),
+ ),
+ });
+ await waitForPromises();
+
+ expect(findDependenciesCountBadge().exists()).toBe(true);
+ expect(findDependenciesCountBadge().text()).toBe(dependencyLinks().length.toString());
+ expect(findDependencyRows()).toHaveLength(dependencyLinks().length);
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js
index de561bdf8c9..9aed5b90c73 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js
@@ -1,22 +1,23 @@
-import { shallowMount } from '@vue/test-utils';
-import { dependencyLinks } from 'jest/packages/mock_data';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue';
+import { dependencyLinks } from '../../mock_data';
describe('DependencyRow', () => {
let wrapper;
- const { withoutFramework, withoutVersion, fullLink } = dependencyLinks;
+ const [fullDependencyLink] = dependencyLinks();
+ const { dependency, metadata } = fullDependencyLink;
- function createComponent({ dependencyLink = fullLink } = {}) {
- wrapper = shallowMount(DependencyRow, {
+ function createComponent(dependencyLink = fullDependencyLink) {
+ wrapper = shallowMountExtended(DependencyRow, {
propsData: {
- dependency: dependencyLink,
+ dependencyLink,
},
});
}
- const dependencyVersion = () => wrapper.find('[data-testid="version-pattern"]');
- const dependencyFramework = () => wrapper.find('[data-testid="target-framework"]');
+ const dependencyVersion = () => wrapper.findByTestId('version-pattern');
+ const dependencyFramework = () => wrapper.findByTestId('target-framework');
afterEach(() => {
wrapper.destroy();
@@ -32,7 +33,10 @@ describe('DependencyRow', () => {
describe('version', () => {
it('does not render any version information when not supplied', () => {
- createComponent({ dependencyLink: withoutVersion });
+ createComponent({
+ ...fullDependencyLink,
+ dependency: { ...dependency, versionPattern: undefined },
+ });
expect(dependencyVersion().exists()).toBe(false);
});
@@ -41,13 +45,16 @@ describe('DependencyRow', () => {
createComponent();
expect(dependencyVersion().exists()).toBe(true);
- expect(dependencyVersion().text()).toBe(fullLink.version_pattern);
+ expect(dependencyVersion().text()).toBe(dependency.versionPattern);
});
});
describe('target framework', () => {
it('does not render any framework information when not supplied', () => {
- createComponent({ dependencyLink: withoutFramework });
+ createComponent({
+ ...fullDependencyLink,
+ metadata: { ...metadata, targetFramework: undefined },
+ });
expect(dependencyFramework().exists()).toBe(false);
});
@@ -56,7 +63,7 @@ describe('DependencyRow', () => {
createComponent();
expect(dependencyFramework().exists()).toBe(true);
- expect(dependencyFramework().text()).toBe(`(${fullLink.target_framework})`);
+ expect(dependencyFramework().text()).toBe(`(${metadata.targetFramework})`);
});
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index 9e5457aa0fe..98ff29ef728 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -51,6 +51,41 @@ export const packageFiles = () => [
},
];
+export const dependencyLinks = () => [
+ {
+ dependencyType: 'DEPENDENCIES',
+ id: 'gid://gitlab/Packages::DependencyLink/77',
+ __typename: 'PackageDependencyLink',
+ dependency: {
+ id: 'gid://gitlab/Packages::Dependency/3',
+ name: 'Ninject.Extensions.Factory',
+ versionPattern: '3.3.2',
+ __typename: 'PackageDependency',
+ },
+ metadata: {
+ id: 'gid://gitlab/Packages::Nuget::DependencyLinkMetadatum/77',
+ targetFramework: '.NETCoreApp3.1',
+ __typename: 'NugetDependencyLinkMetadata',
+ },
+ },
+ {
+ dependencyType: 'DEPENDENCIES',
+ id: 'gid://gitlab/Packages::DependencyLink/78',
+ __typename: 'PackageDependencyLink',
+ dependency: {
+ id: 'gid://gitlab/Packages::Dependency/4',
+ name: 'Ninject.Extensions.Factory',
+ versionPattern: '3.3.2',
+ __typename: 'PackageDependency',
+ },
+ metadata: {
+ id: 'gid://gitlab/Packages::Nuget::DependencyLinkMetadatum/78',
+ targetFramework: '.NETCoreApp3.1',
+ __typename: 'NugetDependencyLinkMetadata',
+ },
+ },
+];
+
export const packageVersions = () => [
{
createdAt: '2021-08-10T09:33:54Z',
@@ -145,6 +180,9 @@ export const packageDetailsQuery = (extendPackage) => ({
nodes: packageVersions(),
__typename: 'PackageConnection',
},
+ dependencyLinks: {
+ nodes: dependencyLinks(),
+ },
__typename: 'PackageDetailsType',
...extendPackage,
},
diff --git a/spec/frontend/terraform/components/init_command_modal_spec.js b/spec/frontend/terraform/components/init_command_modal_spec.js
new file mode 100644
index 00000000000..dbdff899bac
--- /dev/null
+++ b/spec/frontend/terraform/components/init_command_modal_spec.js
@@ -0,0 +1,79 @@
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import InitCommandModal from '~/terraform/components/init_command_modal.vue';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+
+const accessTokensPath = '/path/to/access-tokens-page';
+const terraformApiUrl = 'https://gitlab.com/api/v4/projects/1';
+const username = 'username';
+const modalId = 'fake-modal-id';
+const stateName = 'production';
+const modalInfoCopyStr = `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
+terraform init \\
+ -backend-config="address=${terraformApiUrl}/${stateName}" \\
+ -backend-config="lock_address=${terraformApiUrl}/${stateName}/lock" \\
+ -backend-config="unlock_address=${terraformApiUrl}/${stateName}/lock" \\
+ -backend-config="username=${username}" \\
+ -backend-config="password=$GITLAB_ACCESS_TOKEN" \\
+ -backend-config="lock_method=POST" \\
+ -backend-config="unlock_method=DELETE" \\
+ -backend-config="retry_wait_min=5"
+ `;
+
+describe('InitCommandModal', () => {
+ let wrapper;
+
+ const propsData = {
+ modalId,
+ stateName,
+ };
+ const provideData = {
+ accessTokensPath,
+ terraformApiUrl,
+ username,
+ };
+
+ const findExplanatoryText = () => wrapper.findByTestId('init-command-explanatory-text');
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findInitCommand = () => wrapper.findByTestId('terraform-init-command');
+ const findCopyButton = () => wrapper.findComponent(ModalCopyButton);
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(InitCommandModal, {
+ propsData,
+ provide: provideData,
+ stubs: {
+ GlSprintf,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('on rendering', () => {
+ it('renders the explanatory text', () => {
+ expect(findExplanatoryText().text()).toContain('personal access token');
+ });
+
+ it('renders the personal access token link', () => {
+ expect(findLink().attributes('href')).toBe(accessTokensPath);
+ });
+
+ it('renders the init command with the username and state name prepopulated', () => {
+ expect(findInitCommand().text()).toContain(username);
+ expect(findInitCommand().text()).toContain(stateName);
+ });
+
+ it('renders the copyToClipboard button', () => {
+ expect(findCopyButton().exists()).toBe(true);
+ });
+ });
+
+ describe('when copy button is clicked', () => {
+ it('copies init command to clipboard', () => {
+ expect(findCopyButton().props('text')).toBe(modalInfoCopyStr);
+ });
+ });
+});
diff --git a/spec/frontend/terraform/components/states_table_actions_spec.js b/spec/frontend/terraform/components/states_table_actions_spec.js
index 61f6e9f0f7b..34e7d597cd8 100644
--- a/spec/frontend/terraform/components/states_table_actions_spec.js
+++ b/spec/frontend/terraform/components/states_table_actions_spec.js
@@ -3,6 +3,7 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import InitCommandModal from '~/terraform/components/init_command_modal.vue';
import StateActions from '~/terraform/components/states_table_actions.vue';
import lockStateMutation from '~/terraform/graphql/mutations/lock_state.mutation.graphql';
import removeStateMutation from '~/terraform/graphql/mutations/remove_state.mutation.graphql';
@@ -73,12 +74,14 @@ describe('StatesTableActions', () => {
return wrapper.vm.$nextTick();
};
- const findActionsDropdown = () => wrapper.find(GlDropdown);
+ const findActionsDropdown = () => wrapper.findComponent(GlDropdown);
+ const findCopyBtn = () => wrapper.find('[data-testid="terraform-state-copy-init-command"]');
+ const findCopyModal = () => wrapper.findComponent(InitCommandModal);
const findLockBtn = () => wrapper.find('[data-testid="terraform-state-lock"]');
const findUnlockBtn = () => wrapper.find('[data-testid="terraform-state-unlock"]');
const findDownloadBtn = () => wrapper.find('[data-testid="terraform-state-download"]');
const findRemoveBtn = () => wrapper.find('[data-testid="terraform-state-remove"]');
- const findRemoveModal = () => wrapper.find(GlModal);
+ const findRemoveModal = () => wrapper.findComponent(GlModal);
beforeEach(() => {
return createComponent();
@@ -125,6 +128,25 @@ describe('StatesTableActions', () => {
});
});
+ describe('copy command button', () => {
+ it('displays a copy init command button', () => {
+ expect(findCopyBtn().text()).toBe('Copy Terraform init command');
+ });
+
+ describe('when clicking the copy init command button', () => {
+ beforeEach(() => {
+ findCopyBtn().vm.$emit('click');
+
+ return waitForPromises();
+ });
+
+ it('opens the modal', async () => {
+ expect(findCopyModal().exists()).toBe(true);
+ expect(findCopyModal().isVisible()).toBe(true);
+ });
+ });
+ });
+
describe('download button', () => {
it('displays a download button', () => {
expect(findDownloadBtn().text()).toBe('Download JSON');
diff --git a/spec/helpers/projects/terraform_helper_spec.rb b/spec/helpers/projects/terraform_helper_spec.rb
index 8833e23c47d..9c2f009be26 100644
--- a/spec/helpers/projects/terraform_helper_spec.rb
+++ b/spec/helpers/projects/terraform_helper_spec.rb
@@ -22,6 +22,18 @@ RSpec.describe Projects::TerraformHelper do
expect(subject[:project_path]).to eq(project.full_path)
end
+ it 'includes access token path' do
+ expect(subject[:access_tokens_path]).to eq(profile_personal_access_tokens_path)
+ end
+
+ it 'includes username' do
+ expect(subject[:username]).to eq(current_user.username)
+ end
+
+ it 'includes terraform state api url' do
+ expect(subject[:terraform_api_url]).to eq("#{Settings.gitlab.url}/api/v4/projects/#{project.id}/terraform/state")
+ end
+
it 'indicates the user is a terraform admin' do
expect(subject[:terraform_admin]).to eq(true)
end
diff --git a/spec/lib/gitlab/checks/changes_access_spec.rb b/spec/lib/gitlab/checks/changes_access_spec.rb
index 576bab241db..4a74dfcec34 100644
--- a/spec/lib/gitlab/checks/changes_access_spec.rb
+++ b/spec/lib/gitlab/checks/changes_access_spec.rb
@@ -160,6 +160,36 @@ RSpec.describe Gitlab::Checks::ChangesAccess do
it_behaves_like 'a listing of new commits'
end
+
+ context 'with criss-cross merges' do
+ let(:new_commits) do
+ [
+ create_commit(newrev, %w[a1 b1]),
+ create_commit('a1', %w[a2 b2]),
+ create_commit('a2', %w[a3 b3]),
+ create_commit('a3', %w[c]),
+ create_commit('b1', %w[b2 a2]),
+ create_commit('b2', %w[b3 a3]),
+ create_commit('b3', %w[c]),
+ create_commit('c', [])
+ ]
+ end
+
+ let(:expected_commits) do
+ [
+ create_commit(newrev, %w[a1 b1]),
+ create_commit('a1', %w[a2 b2]),
+ create_commit('b1', %w[b2 a2]),
+ create_commit('a2', %w[a3 b3]),
+ create_commit('b2', %w[b3 a3]),
+ create_commit('a3', %w[c]),
+ create_commit('b3', %w[c]),
+ create_commit('c', [])
+ ]
+ end
+
+ it_behaves_like 'a listing of new commits'
+ end
end
def create_commit(id, parent_ids)
diff --git a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
index 09280402e2b..cd1828791c3 100644
--- a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
@@ -111,4 +111,35 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
end
end
end
+
+ context 'when a command takes longer than DURATION_ERROR_THRESHOLD' do
+ let(:threshold) { 0.5 }
+
+ before do
+ stub_const("#{described_class}::DURATION_ERROR_THRESHOLD", threshold)
+ end
+
+ context 'when report_on_long_redis_durations is disabled' do
+ it 'does nothing' do
+ stub_feature_flags(report_on_long_redis_durations: false)
+
+ expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
+
+ Gitlab::Redis::SharedState.with { |r| r.mget('foo', 'foo') { sleep threshold + 0.1 } }
+ end
+ end
+
+ context 'when report_on_long_redis_durations is enabled' do
+ it 'tracks an exception and continues' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(an_instance_of(described_class::MysteryRedisDurationError),
+ command: 'mget',
+ duration: be > threshold,
+ timestamp: a_string_matching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{5}/))
+
+ Gitlab::Redis::SharedState.with { |r| r.mget('foo', 'foo') { sleep threshold + 0.1 } }
+ end
+ end
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 3e16de44cea..0f6ba0c67b1 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -3743,7 +3743,21 @@ RSpec.describe Ci::Build do
context 'when artifacts of depended job has been expired' do
let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
- it { expect(job).not_to have_valid_build_dependencies }
+ context 'when pipeline is not locked' do
+ before do
+ build.pipeline.unlocked!
+ end
+
+ it { expect(job).not_to have_valid_build_dependencies }
+ end
+
+ context 'when pipeline is locked' do
+ before do
+ build.pipeline.artifacts_locked!
+ end
+
+ it { expect(job).to have_valid_build_dependencies }
+ end
end
context 'when artifacts of depended job has been erased' do
@@ -4763,8 +4777,24 @@ RSpec.describe Ci::Build do
let!(:pre_stage_job_invalid) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test2', stage_idx: 1) }
let!(:job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w(test1 test2) }) }
- it 'returns invalid dependencies' do
- expect(job.invalid_dependencies).to eq([pre_stage_job_invalid])
+ context 'when pipeline is locked' do
+ before do
+ build.pipeline.unlocked!
+ end
+
+ it 'returns invalid dependencies when expired' do
+ expect(job.invalid_dependencies).to eq([pre_stage_job_invalid])
+ end
+ end
+
+ context 'when pipeline is not locked' do
+ before do
+ build.pipeline.artifacts_locked!
+ end
+
+ it 'returns no invalid dependencies when expired' do
+ expect(job.invalid_dependencies).to eq([])
+ end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 5b045814891..7e1673a5299 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -6,7 +6,6 @@ RSpec.describe Project, factory_default: :keep do
include ProjectForksHelper
include GitHelpers
include ExternalAuthorizationServiceHelpers
- include ReloadHelpers
using RSpec::Parameterized::TableSyntax
let_it_be(:namespace) { create_default(:namespace).freeze }
@@ -3022,72 +3021,31 @@ RSpec.describe Project, factory_default: :keep do
end
end
- shared_context 'project with ancestors' do
+ describe '#ancestors_upto' do
let_it_be(:parent) { create(:group) }
let_it_be(:child) { create(:group, parent: parent) }
let_it_be(:child2) { create(:group, parent: child) }
let_it_be(:project) { create(:project, namespace: child2) }
- end
- shared_examples '#ancestors' do
- before do
- reload_models(parent, child, child2)
+ it 'returns all ancestors when no namespace is given' do
+ expect(project.ancestors_upto).to contain_exactly(child2, child, parent)
end
- it 'returns all ancestors' do
- expect(project.ancestors).to contain_exactly(child2, child, parent)
+ it 'includes ancestors upto but excluding the given ancestor' do
+ expect(project.ancestors_upto(parent)).to contain_exactly(child2, child)
end
describe 'with hierarchy_order' do
it 'returns ancestors ordered by descending hierarchy' do
- expect(project.ancestors(hierarchy_order: :desc).to_a).to eq([parent, child, child2])
+ expect(project.ancestors_upto(hierarchy_order: :desc)).to eq([parent, child, child2])
end
- end
- end
-
- describe '#ancestors' do
- include_context 'project with ancestors'
-
- include_examples '#ancestors'
- end
-
- describe '#ancestors_upto' do
- include_context 'project with ancestors'
-
- include_examples '#ancestors'
-
- it 'includes ancestors upto but excluding the given ancestor' do
- expect(project.ancestors_upto(parent)).to contain_exactly(child2, child)
- end
- describe 'with hierarchy_order' do
it 'can be used with upto option' do
expect(project.ancestors_upto(parent, hierarchy_order: :desc)).to eq([child, child2])
end
end
end
- describe '#ancestors' do
- let_it_be(:parent) { create(:group) }
- let_it_be(:child) { create(:group, parent: parent) }
- let_it_be(:child2) { create(:group, parent: child) }
- let_it_be(:project) { create(:project, namespace: child2) }
-
- before do
- reload_models(parent, child, child2)
- end
-
- it 'returns all ancestors' do
- expect(project.ancestors).to contain_exactly(child2, child, parent)
- end
-
- describe 'with hierarchy_order' do
- it 'returns ancestors ordered by descending hierarchy' do
- expect(project.ancestors(hierarchy_order: :desc).to_a).to eq([parent, child, child2])
- end
- end
- end
-
describe '#root_ancestor' do
let(:project) { create(:project) }
diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb
index 4a58f341658..8a63715ed86 100644
--- a/spec/serializers/build_details_entity_spec.rb
+++ b/spec/serializers/build_details_entity_spec.rb
@@ -133,6 +133,7 @@ RSpec.describe BuildDetailsEntity do
let(:message) { subject[:callout_message] }
before do
+ build.pipeline.unlocked!
build.drop!(:missing_dependency_failure)
end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 1c6a25c93ea..2f37d0ea42d 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
module Ci
RSpec.describe RegisterJobService do
let_it_be(:group) { create(:group) }
- let_it_be(:project, reload: true) { create(:project, group: group, shared_runners_enabled: false, group_runners_enabled: false) }
- let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be_with_reload(:project) { create(:project, group: group, shared_runners_enabled: false, group_runners_enabled: false) }
+ let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project) }
let!(:shared_runner) { create(:ci_runner, :instance) }
let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
@@ -467,13 +467,27 @@ module Ci
context 'when depended job has not been completed yet' do
let!(:pre_stage_job) { create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
- it { expect(subject).to eq(pending_job) }
+ it { is_expected.to eq(pending_job) }
end
context 'when artifacts of depended job has been expired' do
let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
- it_behaves_like 'not pick'
+ context 'when the pipeline is locked' do
+ before do
+ pipeline.artifacts_locked!
+ end
+
+ it { is_expected.to eq(pending_job) }
+ end
+
+ context 'when the pipeline is unlocked' do
+ before do
+ pipeline.unlocked!
+ end
+
+ it_behaves_like 'not pick'
+ end
end
context 'when artifacts of depended job has been erased' do
@@ -490,8 +504,12 @@ module Ci
let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
before do
- allow_any_instance_of(Ci::Build).to receive(:drop!)
- .and_raise(ActiveRecord::StaleObjectError.new(pending_job, :drop!))
+ pipeline.unlocked!
+
+ allow_next_instance_of(Ci::Build) do |build|
+ expect(build).to receive(:drop!)
+ .and_raise(ActiveRecord::StaleObjectError.new(pending_job, :drop!))
+ end
end
it 'does not drop nor pick' do
diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb
index 4f6092c6fb6..96be30b9f1f 100644
--- a/spec/support/shared_examples/features/packages_shared_examples.rb
+++ b/spec/support/shared_examples/features/packages_shared_examples.rb
@@ -34,10 +34,8 @@ RSpec.shared_examples 'package details link' do |property|
expect(page).to have_css('.packages-app h1[data-testid="title"]', text: package.name)
- page.within(%Q([name="#{package.name}"])) do
- expect(page).to have_content('Installation')
- expect(page).to have_content('Registry setup')
- end
+ expect(page).to have_content('Installation')
+ expect(page).to have_content('Registry setup')
end
end