Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-08-12 15:09:58 +00:00
parent 2fe5ea34a5
commit dc250651ab
52 changed files with 762 additions and 180 deletions

View file

@ -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);
}

View file

@ -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();
};

View file

@ -29,8 +29,10 @@ export default {
},
computed: {
showMetadata() {
return [PACKAGE_TYPE_NUGET, PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN].includes(
return (
[PACKAGE_TYPE_NUGET, PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN].includes(
this.packageEntity.packageType,
) && this.packageEntity.metadata
);
},
showNugetMetadata() {

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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>
@ -181,6 +195,14 @@ export default {
<gl-icon class="gl-mr-0" name="ellipsis_v" />
</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"
@ -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>

View file

@ -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: {

View file

@ -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;

View file

@ -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')

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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) }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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 ""

View file

@ -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

View file

@ -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) }

View file

@ -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

View file

@ -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

View file

@ -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();
});
});
});

View file

@ -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>

View file

@ -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);
});
});
});

View file

@ -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})`);
});
});
});

View file

@ -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,
},

View file

@ -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);
});
});
});

View file

@ -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');

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -3743,9 +3743,23 @@ 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) }
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
let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
@ -4763,11 +4777,27 @@ 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
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
describe '#execute_hooks' do
before do
build.clear_memoization(:build_data)

View file

@ -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)
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 '#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 'returns ancestors ordered by descending hierarchy' do
expect(project.ancestors_upto(hierarchy_order: :desc)).to eq([parent, child, child2])
end
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) }

View file

@ -133,6 +133,7 @@ RSpec.describe BuildDetailsEntity do
let(:message) { subject[:callout_message] }
before do
build.pipeline.unlocked!
build.drop!(:missing_dependency_failure)
end

View file

@ -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,14 +467,28 @@ 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) }
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
let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
@ -490,9 +504,13 @@ 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!)
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
expect(subject).to be_nil

View file

@ -34,11 +34,9 @@ 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
end
end
RSpec.shared_examples 'when there are no packages' do