Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f850f5c033
commit
0fc7ea6bc8
|
|
@ -1 +1 @@
|
|||
4d333b2bb343d59410a80873e6185b8f8a2061a3
|
||||
6bd8110489a44adeacff1897a757d8ea52bf4f87
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { FIND_FILE_BUTTON_CLICK } from '~/tracking/constants';
|
|||
import { updateElementsVisibility } from '~/repository/utils/dom';
|
||||
import blobControlsQuery from '~/repository/queries/blob_controls.query.graphql';
|
||||
import { getRefType } from '~/repository/utils/ref_type';
|
||||
import OpenMrBadge from '~/repository/components/header_area/open_mr_badge.vue';
|
||||
import { TEXT_FILE_TYPE, DEFAULT_BLOB_INFO } from '../../constants';
|
||||
import OverflowMenu from './blob_overflow_menu.vue';
|
||||
|
||||
|
|
@ -36,6 +37,7 @@ export default {
|
|||
},
|
||||
buttonClassList: 'sm:gl-w-auto gl-w-full sm:gl-mt-0 gl-mt-3',
|
||||
components: {
|
||||
OpenMrBadge,
|
||||
GlButton,
|
||||
OverflowMenu,
|
||||
},
|
||||
|
|
@ -192,6 +194,11 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<div v-if="showBlobControls" class="gl-flex gl-flex-wrap gl-items-center gl-gap-3">
|
||||
<open-mr-badge
|
||||
v-if="glFeatures.filterBlobPath"
|
||||
:project-path="projectPath"
|
||||
:blob-path="filePath"
|
||||
/>
|
||||
<gl-button
|
||||
v-gl-tooltip.html="findFileTooltip"
|
||||
:aria-keyshortcuts="findFileShortcutKey"
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ export default {
|
|||
data-testid="default-actions-container"
|
||||
:toggle-text="$options.i18n.dropdownLabel"
|
||||
text-sr-only
|
||||
category="tertiary"
|
||||
>
|
||||
<permalink-dropdown-item :permalink-path="blobInfo.permalinkPath" />
|
||||
<blob-button-group
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
<script>
|
||||
import { GlBadge } from '@gitlab/ui';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
import getOpenMrCountsForBlobPath from '~/repository/queries/open_mr_counts.query.graphql';
|
||||
import { nDaysBefore } from '~/lib/utils/datetime/date_calculation_utility';
|
||||
import { toYmd } from '~/analytics/shared/utils';
|
||||
import { logError } from '~/lib/logger';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
|
||||
const OPEN_MR_AGE_LIMIT_DAYS = 30;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlBadge,
|
||||
},
|
||||
inject: ['currentRef'],
|
||||
props: {
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
blobPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
openMrsCount: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
openMRsCountText() {
|
||||
return sprintf(__('%{count} open'), { count: this.openMrsCount });
|
||||
},
|
||||
createdAfter() {
|
||||
const lookbackDate = nDaysBefore(new Date(), OPEN_MR_AGE_LIMIT_DAYS - 1, { utc: true });
|
||||
return toYmd(lookbackDate);
|
||||
},
|
||||
isLoading() {
|
||||
return this.$apollo.queries.loading;
|
||||
},
|
||||
showBadge() {
|
||||
return !this.isLoading && this.openMrsCount > 0;
|
||||
},
|
||||
queryVariables() {
|
||||
return {
|
||||
projectPath: this.projectPath,
|
||||
targetBranch: [this.currentRef],
|
||||
blobPath: this.blobPath,
|
||||
createdAfter: this.createdAfter,
|
||||
};
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
openMrsCount: {
|
||||
query: getOpenMrCountsForBlobPath,
|
||||
variables() {
|
||||
return this.queryVariables;
|
||||
},
|
||||
update({ project: { mergeRequests: { count } = {} } = {} } = {}) {
|
||||
return count;
|
||||
},
|
||||
error(error) {
|
||||
logError(
|
||||
`Failed to fetch merge request count. See exception details for more information.`,
|
||||
error,
|
||||
);
|
||||
Sentry.captureException(error);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-badge v-if="showBadge" variant="success" icon="merge-request">
|
||||
{{ openMRsCountText }}
|
||||
</gl-badge>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
query getOpenMrCountsForBlobPath(
|
||||
$projectPath: ID!
|
||||
$targetBranch: [String!]
|
||||
$blobPath: String!
|
||||
$createdAfter: Time!
|
||||
) {
|
||||
project(fullPath: $projectPath) {
|
||||
id
|
||||
mergeRequests(
|
||||
state: opened
|
||||
targetBranches: $targetBranch
|
||||
blobPath: $blobPath
|
||||
createdAfter: $createdAfter
|
||||
) {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -107,7 +107,7 @@ export default {
|
|||
return this.getNoteableData.noteableType === 'MergeRequest';
|
||||
},
|
||||
iconBgClass() {
|
||||
return ICON_COLORS[this.note.system_note_icon_name] || 'gl-bg-strong gl-text-subtle';
|
||||
return ICON_COLORS[this.note.system_note_icon_name] || 'gl-text-subtle';
|
||||
},
|
||||
systemNoteIconName() {
|
||||
let icon = this.note.system_note_icon_name;
|
||||
|
|
@ -160,7 +160,7 @@ export default {
|
|||
iconBgClass,
|
||||
{
|
||||
'system-note-icon -gl-mt-1 gl-ml-2 gl-h-6 gl-w-6': isAllowedIcon,
|
||||
'system-note-dot -gl-top-1 gl-ml-4 gl-mt-3 gl-h-3 gl-w-3 gl-border-2 gl-border-solid gl-border-subtle gl-bg-gray-900':
|
||||
'system-note-dot -gl-top-1 gl-ml-4 gl-mt-3 gl-h-3 gl-w-3 gl-border-2 gl-border-solid gl-border-subtle':
|
||||
!isAllowedIcon,
|
||||
},
|
||||
]"
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ import { __ } from '~/locale';
|
|||
import NoteHeader from '~/notes/components/note_header.vue';
|
||||
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
||||
|
||||
const ALLOWED_ICONS = ['issue-close'];
|
||||
const ICON_COLORS = {
|
||||
'issue-close': '!gl-bg-blue-100 gl-text-blue-700 icon-info',
|
||||
'issue-close': 'system-note-icon-info',
|
||||
issues: 'system-note-icon-success',
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
@ -75,7 +75,7 @@ export default {
|
|||
return ICON_COLORS[this.note.systemNoteIconName] || '';
|
||||
},
|
||||
isAllowedIcon() {
|
||||
return ALLOWED_ICONS.includes(this.note.systemNoteIconName);
|
||||
return Object.keys(ICON_COLORS).includes(this.note.systemNoteIconName);
|
||||
},
|
||||
isTargetNote() {
|
||||
return this.targetNoteHash === this.noteAnchorId;
|
||||
|
|
@ -101,6 +101,13 @@ export default {
|
|||
deleteButtonClasses() {
|
||||
return this.singleLineDescription ? 'gl-top-5 gl-right-2 gl-mt-2' : 'gl-top-6 gl-right-3';
|
||||
},
|
||||
systemNoteIconName() {
|
||||
let icon = this.note.systemNoteIconName;
|
||||
if (this.note.systemNoteIconName === 'issues') {
|
||||
icon = 'issue-open-m';
|
||||
}
|
||||
return icon;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
renderGFM(this.$refs['gfm-content']);
|
||||
|
|
@ -126,13 +133,13 @@ export default {
|
|||
getIconColor,
|
||||
{
|
||||
'system-note-icon -gl-mt-1 gl-ml-2 gl-h-6 gl-w-6': isAllowedIcon,
|
||||
'system-note-dot -gl-top-1 gl-ml-4 gl-mt-3 gl-h-3 gl-w-3 gl-border-2 gl-border-solid gl-border-subtle gl-bg-gray-900':
|
||||
'system-note-dot -gl-top-1 gl-ml-4 gl-mt-3 gl-h-3 gl-w-3 gl-border-2 gl-border-solid gl-border-subtle':
|
||||
!isAllowedIcon,
|
||||
},
|
||||
]"
|
||||
class="gl-relative gl-float-left gl-flex gl-items-center gl-justify-center gl-rounded-full"
|
||||
>
|
||||
<gl-icon v-if="isAllowedIcon" :size="14" :name="note.systemNoteIconName" />
|
||||
<gl-icon v-if="isAllowedIcon" :size="14" :name="systemNoteIconName" />
|
||||
</div>
|
||||
<div class="gl-ml-7">
|
||||
<div class="gl-flex gl-items-start gl-justify-between">
|
||||
|
|
|
|||
|
|
@ -870,18 +870,17 @@ table.code {
|
|||
}
|
||||
}
|
||||
|
||||
.diff-file.linked-file .file-title {
|
||||
background-color: $blue-50;
|
||||
border-color: $blue-200;
|
||||
}
|
||||
.diff-file.linked-file {
|
||||
border-color: var(--blue-200);
|
||||
|
||||
.diff-file.linked-file .diff-content {
|
||||
border-color: $blue-200;
|
||||
.file-title {
|
||||
@apply gl-bg-feedback-info;
|
||||
}
|
||||
}
|
||||
|
||||
// This is not inside page_bundles because then it won't receive a proper color value for the dark theme
|
||||
.diff-file-row.is-active.is-linked {
|
||||
background-color: $blue-50;
|
||||
@apply gl-bg-feedback-info;
|
||||
}
|
||||
|
||||
.collapsed-file-warning {
|
||||
|
|
|
|||
|
|
@ -455,7 +455,7 @@ span.idiff {
|
|||
// The virtual scroller has a flat HTML structure so instead of the ::before
|
||||
// element stretching over multiple rows we instead create a repeating background image
|
||||
// for the line
|
||||
background: repeating-linear-gradient(to right, var(--gray-100, $gray-100), var(--gray-100, $gray-100) 1px, transparent 1px, transparent 14px);
|
||||
background: repeating-linear-gradient(to right, var(--gl-border-color-default), var(--gl-border-color-default) 1px, transparent 1px, transparent 14px);
|
||||
background-size: calc(var(--level) * 14px) 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 14px;
|
||||
|
|
|
|||
|
|
@ -364,11 +364,11 @@
|
|||
}
|
||||
|
||||
.diff-file-row.is-active {
|
||||
background-color: var(--gray-50, $gray-50);
|
||||
@apply gl-bg-strong;
|
||||
}
|
||||
|
||||
.diff-file-row.is-loading {
|
||||
color: var(--gl-text-color-disabled);
|
||||
@apply gl-text-disabled;
|
||||
}
|
||||
|
||||
.mr-info-list {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.system-note-dot {
|
||||
background-color: var(--gl-status-neutral-icon-color);
|
||||
}
|
||||
|
||||
// Vertical line gradient for specific system note icons
|
||||
.system-note-icon {
|
||||
|
|
@ -53,22 +56,22 @@
|
|||
background-color: var(--system-note-icon-background-color);
|
||||
|
||||
&.system-note-icon-success {
|
||||
--system-note-icon-color: var(--gl-status-success-text-color);
|
||||
--system-note-icon-color: var(--gl-status-success-icon-color);
|
||||
--system-note-icon-background-color: var(--gl-status-success-background-color);
|
||||
}
|
||||
|
||||
&.system-note-icon-danger {
|
||||
--system-note-icon-color: var(--gl-status-danger-text-color);
|
||||
--system-note-icon-color: var(--gl-status-danger-icon-color);
|
||||
--system-note-icon-background-color: var(--gl-status-danger-background-color);
|
||||
}
|
||||
|
||||
&.system-note-icon-info {
|
||||
--system-note-icon-color: var(--gl-status-info-text-color);
|
||||
--system-note-icon-color: var(--gl-status-info-icon-color);
|
||||
--system-note-icon-background-color: var(--gl-status-info-background-color);
|
||||
}
|
||||
|
||||
&.system-note-icon-warning {
|
||||
--system-note-icon-color: var(--gl-status-warning-text-color);
|
||||
--system-note-icon-color: var(--gl-status-warning-icon-color);
|
||||
--system-note-icon-background-color: var(--gl-status-warning-background-color);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
before_action do
|
||||
push_frontend_feature_flag(:inline_blame, @project)
|
||||
push_frontend_feature_flag(:blob_overflow_menu, current_user)
|
||||
push_frontend_feature_flag(:filter_blob_path, current_user)
|
||||
push_frontend_feature_flag(:blob_repository_vue_header_app, @project)
|
||||
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
|
||||
push_frontend_feature_flag(:directory_code_dropdown_updates, current_user)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class Projects::TreeController < Projects::ApplicationController
|
|||
push_frontend_feature_flag(:inline_blame, @project)
|
||||
push_frontend_feature_flag(:blob_repository_vue_header_app, @project)
|
||||
push_frontend_feature_flag(:blob_overflow_menu, current_user)
|
||||
push_frontend_feature_flag(:filter_blob_path, current_user)
|
||||
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
|
||||
push_frontend_feature_flag(:directory_code_dropdown_updates, current_user)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
push_frontend_feature_flag(:page_specific_styles, current_user)
|
||||
push_frontend_feature_flag(:blob_repository_vue_header_app, @project)
|
||||
push_frontend_feature_flag(:blob_overflow_menu, current_user)
|
||||
push_frontend_feature_flag(:filter_blob_path, current_user)
|
||||
push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)
|
||||
push_frontend_feature_flag(:directory_code_dropdown_updates, current_user)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,15 @@ module Resolvers
|
|||
"for example: `id_desc` or `name_asc`",
|
||||
default_value: 'name_asc'
|
||||
|
||||
argument :all_available, GraphQL::Types::Boolean,
|
||||
required: false,
|
||||
default_value: true,
|
||||
replace_null_with_default: true,
|
||||
description: <<~DESC
|
||||
When `true`, returns all accessible groups. When `false`, returns only groups where the user is a member.
|
||||
Unauthenticated requests always return all public groups. The `owned_only` argument takes precedence.
|
||||
DESC
|
||||
|
||||
private
|
||||
|
||||
def resolve_groups(**args)
|
||||
|
|
|
|||
|
|
@ -36,12 +36,18 @@ module Ci
|
|||
|
||||
def policies_allowed?(accessed_project, policies)
|
||||
return true if self_referential?(accessed_project)
|
||||
|
||||
unless accessed_project.ci_inbound_job_token_scope_enabled?
|
||||
# We capture policies even if the inbound scopes are disabled
|
||||
capture_job_token_policies(policies)
|
||||
return true
|
||||
end
|
||||
|
||||
return false unless inbound_accessible?(accessed_project)
|
||||
|
||||
# We capture policies even if the inbound scopes are disabled or the feature flag is disabled
|
||||
Ci::JobToken::Authorization.capture_job_token_policies(policies) if policies.present?
|
||||
# We capture policies even if the feature flag is disabled
|
||||
capture_job_token_policies(policies)
|
||||
|
||||
return true unless accessed_project.ci_inbound_job_token_scope_enabled?
|
||||
return true unless Feature.enabled?(:add_policies_to_ci_job_token, accessed_project)
|
||||
|
||||
policies_allowed_for_accessed_project?(accessed_project, policies)
|
||||
|
|
@ -140,6 +146,12 @@ module Ci
|
|||
def outbound_allowlist
|
||||
Ci::JobToken::Allowlist.new(current_project, direction: :outbound)
|
||||
end
|
||||
|
||||
def capture_job_token_policies(policies)
|
||||
return if policies.blank?
|
||||
|
||||
Ci::JobToken::Authorization.capture_job_token_policies(policies)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -233,17 +233,14 @@ module MergeRequests
|
|||
|
||||
def create_pipeline_for(merge_request, user, async: false, allow_duplicate: false)
|
||||
create_pipeline_params = params.slice(:push_options).merge(allow_duplicate: allow_duplicate)
|
||||
service = MergeRequests::CreatePipelineService.new(
|
||||
project: project, current_user: user, params: create_pipeline_params
|
||||
)
|
||||
|
||||
if async
|
||||
MergeRequests::CreatePipelineWorker.perform_async(
|
||||
project.id,
|
||||
user.id,
|
||||
merge_request.id,
|
||||
create_pipeline_params.deep_stringify_keys)
|
||||
service.execute_async(merge_request)
|
||||
else
|
||||
MergeRequests::CreatePipelineService
|
||||
.new(project: project, current_user: user, params: create_pipeline_params)
|
||||
.execute(merge_request)
|
||||
service.execute(merge_request)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ module MergeRequests
|
|||
|
||||
::MergeRequests::CreatePipelineWorker.perform_async(
|
||||
project.id, current_user.id, merge_request.id,
|
||||
params.merge(pipeline_creation_request: pipeline_creation_request)
|
||||
params.merge(pipeline_creation_request: pipeline_creation_request).deep_stringify_keys
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
- @with_header = true
|
||||
- page_classes = page_class.push(@html_class, user_application_color_mode).flatten.compact
|
||||
- body_classes = [system_message_class, @body_class]
|
||||
- body_classes = [system_message_class, @body_class, content_for(:body_class)]
|
||||
|
||||
!!! 5
|
||||
%html.gl-h-full{ lang: I18n.locale, class: page_classes }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'irb'
|
||||
|
||||
if Gitlab::Runtime.console?
|
||||
# Stop irb from writing a history file by default.
|
||||
module IrbNoHistory
|
||||
|
|
|
|||
|
|
@ -170,6 +170,148 @@ which causes the worker to process `unknown` artifacts
|
|||
[in batches that are five times larger](https://gitlab.com/gitlab-org/gitlab/-/issues/356319).
|
||||
This flag is not recommended for use.
|
||||
|
||||
#### `@final` artifacts not deleted from object store
|
||||
|
||||
An issue in GitLab 16.1 and 16.2 caused [`@final` artifacts to not be deleted from object storage](https://gitlab.com/gitlab-org/gitlab/-/issues/419920).
|
||||
|
||||
Administrators of GitLab instances that ran GitLab 16.1 or 16.2 for some time could see an increase
|
||||
in object storage used by artifacts. Follow this procedure to check for and remove these artifacts.
|
||||
|
||||
Removing the files is a two stage process:
|
||||
|
||||
1. [Identify which files have been orphaned](#list-orphaned-job-artifacts).
|
||||
1. [Delete the identified files from object storage](#delete-orphaned-job-artifacts).
|
||||
|
||||
##### List orphaned job artifacts
|
||||
|
||||
{{< tabs >}}
|
||||
|
||||
{{< tab title="Linux package (Omnibus)" >}}
|
||||
|
||||
```shell
|
||||
sudo gitlab-rake gitlab:cleanup:list_orphan_job_artifact_final_objects
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab title="Docker" >}}
|
||||
|
||||
```shell
|
||||
docker exec -it <container-id> bash
|
||||
gitlab-rake gitlab:cleanup:list_orphan_job_artifact_final_objects
|
||||
```
|
||||
|
||||
Either write to a persistent volume mounted in the container, or when the command completes: copy the output file out of the session.
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab title="Self-compiled (source)" >}}
|
||||
|
||||
```shell
|
||||
sudo -u git -H bundle exec rake gitlab:cleanup:list_orphan_job_artifact_final_objects RAILS_ENV=production
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab title="Helm chart (Kubernetes)" >}}
|
||||
|
||||
```shell
|
||||
# find the pod
|
||||
kubectl get pods --namespace <namespace> -lapp=toolbox
|
||||
|
||||
# open the Rails console
|
||||
kubectl exec -it -c toolbox <toolbox-pod-name> bash
|
||||
gitlab-rake gitlab:cleanup:list_orphan_job_artifact_final_objects
|
||||
```
|
||||
|
||||
When the command complete, copy the file out of the session onto persistent storage.
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
{{< /tabs >}}
|
||||
|
||||
The Rake task has some additional features that apply to all types of GitLab deployment:
|
||||
|
||||
- Scanning object storage can be interrupted. Progress is recorded in Redis, this is used to resume
|
||||
scanning artifacts from that point.
|
||||
- By default, the Rake task generates a CSV file:
|
||||
`/opt/gitlab/embedded/service/gitlab-rails/tmp/orphan_job_artifact_final_objects.csv`
|
||||
- Set an environment variable to specify a different filename:
|
||||
|
||||
```shell
|
||||
# Packaged GitLab
|
||||
sudo su -
|
||||
FILENAME='custom_filename.csv' gitlab-rake gitlab:cleanup:list_orphan_job_artifact_final_objects
|
||||
```
|
||||
|
||||
- If the output file exists already (the default, or the specified file) it appends entries to the file.
|
||||
- Each row contains the fields `object_path,object_size` comma separated, with no file header. For example:
|
||||
|
||||
```plaintext
|
||||
35/13/35135aaa6cc23891b40cb3f378c53a17a1127210ce60e125ccf03efcfdaec458/@final/1a/1a/5abfa4ec66f1cc3b681a4d430b8b04596cbd636f13cdff44277211778f26,201
|
||||
```
|
||||
|
||||
##### Delete orphaned job artifacts
|
||||
|
||||
{{< tabs >}}
|
||||
|
||||
{{< tab title="Linux package (Omnibus)" >}}
|
||||
|
||||
```shell
|
||||
sudo gitlab-rake gitlab:cleanup:delete_orphan_job_artifact_final_objects
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab title="Docker" >}}
|
||||
|
||||
```shell
|
||||
docker exec -it <container-id> bash
|
||||
gitlab-rake gitlab:cleanup:delete_orphan_job_artifact_final_objects
|
||||
```
|
||||
|
||||
- Copy the output file out of the session when the command completes, or write it to a volume that has been mounted by the container.
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab title="Self-compiled (source)" >}}
|
||||
|
||||
```shell
|
||||
sudo -u git -H bundle exec rake gitlab:cleanup:delete_orphan_job_artifact_final_objects RAILS_ENV=production
|
||||
```
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab title="Helm chart (Kubernetes)" >}}
|
||||
|
||||
```shell
|
||||
# find the pod
|
||||
kubectl get pods --namespace <namespace> -lapp=toolbox
|
||||
|
||||
# open the Rails console
|
||||
kubectl exec -it -c toolbox <toolbox-pod-name> bash
|
||||
gitlab-rake gitlab:cleanup:delete_orphan_job_artifact_final_objects
|
||||
```
|
||||
|
||||
- When the command complete, copy the file out of the session onto persistent storage.
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
{{< /tabs >}}
|
||||
|
||||
The following applies to all types of GitLab deployment:
|
||||
|
||||
- Specify the input filename using the `FILENAME` variable. By default the script looks for:
|
||||
`/opt/gitlab/embedded/service/gitlab-rails/tmp/orphan_job_artifact_final_objects.csv`
|
||||
- As the script deletes files, it writes out a CSV file with the deleted files:
|
||||
- the file is in the same directory as the input file
|
||||
- the filename is prefixed with `deleted_from--`. For example: `deleted_from--orphan_job_artifact_final_objects.csv`.
|
||||
- The rows in the file are: `object_path,object_size,object_generation/version`, for example:
|
||||
|
||||
```plaintext
|
||||
35/13/35135aaa6cc23891b40cb3f378c53a17a1127210ce60e125ccf03efcfdaec458/@final/1a/1a/5abfa4ec66f1cc3b681a4d430b8b04596cbd636f13cdff44277211778f26,201,1711616743796587
|
||||
```
|
||||
|
||||
### List projects and builds with artifacts with a specific expiration (or no expiration)
|
||||
|
||||
Using a [Rails console](../operations/rails_console.md), you can find projects that have job artifacts with either:
|
||||
|
|
|
|||
|
|
@ -731,6 +731,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="querygroupsallavailable"></a>`allAvailable` | [`Boolean`](#boolean) | When `true`, returns all accessible groups. When `false`, returns only groups where the user is a member. Unauthenticated requests always return all public groups. The `owned_only` argument takes precedence. |
|
||||
| <a id="querygroupsids"></a>`ids` | [`[ID!]`](#id) | Filter groups by IDs. |
|
||||
| <a id="querygroupsmarkedfordeletionon"></a>`markedForDeletionOn` | [`Date`](#date) | Date when the group was marked for deletion. |
|
||||
| <a id="querygroupsownedonly"></a>`ownedOnly` | [`Boolean`](#boolean) | Only include groups where the current user has an owner role. |
|
||||
|
|
@ -32322,6 +32323,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="organizationgroupsallavailable"></a>`allAvailable` | [`Boolean`](#boolean) | When `true`, returns all accessible groups. When `false`, returns only groups where the user is a member. Unauthenticated requests always return all public groups. The `owned_only` argument takes precedence. |
|
||||
| <a id="organizationgroupsids"></a>`ids` | [`[ID!]`](#id) | Filter groups by IDs. |
|
||||
| <a id="organizationgroupsmarkedfordeletionon"></a>`markedForDeletionOn` | [`Date`](#date) | Date when the group was marked for deletion. |
|
||||
| <a id="organizationgroupsownedonly"></a>`ownedOnly` | [`Boolean`](#boolean) | Only include groups where the current user has an owner role. |
|
||||
|
|
|
|||
|
|
@ -299,7 +299,7 @@ Parameters:
|
|||
| Attribute | Type | Required | Description |
|
||||
|--------------------------|-------------------|----------|-------------|
|
||||
| `skip_groups` | array of integers | no | Skip the group IDs passed |
|
||||
| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for administrators); Attributes `owned` and `min_access_level` have precedence |
|
||||
| `all_available` | boolean | no | When `true`, returns all accessible groups. When `false`, returns only groups where the user is a member. Defaults to `false` for users, `true` for administrators. Unauthenticated requests always return all public groups. The `owned` and `min_access_level` attributes take precedence. |
|
||||
| `search` | string | no | Return the list of authorized groups matching the search criteria |
|
||||
| `order_by` | string | no | Order groups by `name`, `path`, `id`, or `similarity`. Default is `name` |
|
||||
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
|
||||
|
|
@ -928,6 +928,7 @@ Parameters:
|
|||
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (administrators only) |
|
||||
| `owned` | boolean | no | Limit to groups explicitly owned by the current user |
|
||||
| `min_access_level` | integer | no | Limit to groups where current user has at least this [role (`access_level`)](members.md#roles) |
|
||||
| `all_available` | boolean | no | When `true`, returns all accessible groups. When `false`, returns only groups where the user is a member. Defaults to `false` for users, `true` for administrators. Unauthenticated requests always return all public groups. The `owned` and `min_access_level` attributes take precedence. |
|
||||
|
||||
```plaintext
|
||||
GET /groups/:id/subgroups
|
||||
|
|
@ -997,7 +998,7 @@ Parameters:
|
|||
| ------------------------ | ----------------- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/_index.md#namespaced-paths) of the immediate parent group |
|
||||
| `skip_groups` | array of integers | no | Skip the group IDs passed |
|
||||
| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for administrators). Attributes `owned` and `min_access_level` have precedence |
|
||||
| `all_available` | boolean | no | When `true`, returns all accessible groups. When `false`, returns only groups where the user is a member. Defaults to `false` for users, `true` for administrators. Unauthenticated requests always return all public groups. The `owned` and `min_access_level` attributes take precedence. |
|
||||
| `search` | string | no | Return the list of authorized groups matching the search criteria. Only descendant group short paths are searched (not full paths) |
|
||||
| `order_by` | string | no | Order groups by `name`, `path`, or `id`. Default is `name` |
|
||||
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
|
||||
|
|
|
|||
|
|
@ -114,6 +114,8 @@ For more information about our plans for language support in SAST, see the [cate
|
|||
| Scala | {{< icon name="dotted-circle" >}} No; tracked in [epic 15174](https://gitlab.com/groups/gitlab-org/-/epics/15174) | {{< icon name="check-circle" >}} Yes: [Semgrep](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep) with [GitLab-managed rules](rules.md#semgrep-based-analyzer) |
|
||||
| Swift (iOS) | {{< icon name="dotted-circle" >}} No | {{< icon name="check-circle" >}} Yes: [Semgrep](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep) with [GitLab-managed rules](rules.md#semgrep-based-analyzer) |
|
||||
| TypeScript | {{< icon name="check-circle" >}} Yes | {{< icon name="check-circle" >}} Yes: [Semgrep](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep) with [GitLab-managed rules](rules.md#semgrep-based-analyzer) |
|
||||
| YAML | {{< icon name="check-circle" >}} Yes | {{< icon name="check-circle" >}} Yes: [Semgrep](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep) with [GitLab-managed rules](rules.md#semgrep-based-analyzer) |
|
||||
| Java Properties | {{< icon name="check-circle" >}} Yes | {{< icon name="check-circle" >}} Yes: [Semgrep](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep) with [GitLab-managed rules](rules.md#semgrep-based-analyzer) |
|
||||
|
||||
**Footnotes:**
|
||||
|
||||
|
|
@ -819,7 +821,7 @@ flags are added to the scanner's CLI options.
|
|||
<code>--multi-core</code>
|
||||
</td>
|
||||
<td>
|
||||
Specify the number of CPU cores to utilize for scanning. This can significantly improve scan performance on multi-core systems. When setting this value, ensure that the number of cores specified does not exceed the total number of cores available to the container. Note that multi-core execution will require proportionally more memory than single-core execution. Exceeding the available cores or memory resources may lead to resource contention and suboptimal performance. Defaults to <code>1</code>.
|
||||
Multi-core scanning is enabled by default, automatically detecting and utilizing available CPU cores based on container information. On self-hosted runners, the maximum number of cores is capped at 4. You can override the automatic core detection by explicitly setting <code>--multi-core</code> to a specific value. Multi-core execution requires proportionally more memory than single-core execution. To disable multi-core scanning, set the environment variable <code>DISABLE_MULTI_CORE</code>. Note that exceeding available cores or memory resources may lead to resource contention and suboptimal performance.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ module API
|
|||
params :group_list_params do
|
||||
use :statistics_params
|
||||
optional :skip_groups, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Array of group ids to exclude from list'
|
||||
optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
|
||||
optional :all_available, type: Boolean, desc: 'When `true`, returns all accessible groups. When `false`, returns only groups where the user is a member.'
|
||||
optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values,
|
||||
desc: 'Limit by visibility'
|
||||
optional :search, type: String, desc: 'Search for a specific group'
|
||||
|
|
|
|||
|
|
@ -785,6 +785,9 @@ msgstr ""
|
|||
msgid "%{count} of %{total}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{count} open"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{count} project"
|
||||
msgid_plural "%{count} projects"
|
||||
msgstr[0] ""
|
||||
|
|
@ -21932,10 +21935,10 @@ msgstr ""
|
|||
msgid "DuoEnterpriseTrial|Gain deeper insights into GitLab Duo usage patterns"
|
||||
msgstr ""
|
||||
|
||||
msgid "DuoEnterpriseTrial|GitLab Duo Enterprise is is your end-to-end AI partner for faster, more secure software development."
|
||||
msgid "DuoEnterpriseTrial|GitLab Duo Enterprise is only available for purchase for Ultimate customers."
|
||||
msgstr ""
|
||||
|
||||
msgid "DuoEnterpriseTrial|GitLab Duo Enterprise is only available for purchase for Ultimate customers."
|
||||
msgid "DuoEnterpriseTrial|GitLab Duo Enterprise is your end-to-end AI partner for faster, more secure software development."
|
||||
msgstr ""
|
||||
|
||||
msgid "DuoEnterpriseTrial|Maintain control and keep your data safe"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import Shortcuts from '~/behaviors/shortcuts/shortcuts';
|
|||
import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater';
|
||||
import OverflowMenu from '~/repository/components/header_area/blob_overflow_menu.vue';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import OpenMrBadge from '~/repository/components/header_area/open_mr_badge.vue';
|
||||
import { blobControlsDataMock, refMock } from '../../mock_data';
|
||||
|
||||
jest.mock('~/repository/utils/dom');
|
||||
|
|
@ -24,17 +25,22 @@ let router;
|
|||
let wrapper;
|
||||
let mockResolver;
|
||||
|
||||
const createComponent = async (
|
||||
const createComponent = async ({
|
||||
props = {},
|
||||
blobInfoOverrides = {},
|
||||
glFeatures = { blobOverflowMenu: false },
|
||||
) => {
|
||||
routerOverride = {},
|
||||
} = {}) => {
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const projectPath = 'some/project';
|
||||
router = createRouter(projectPath, refMock);
|
||||
|
||||
await router.push({ name: 'blobPathDecoded', params: { path: '/some/file.js' } });
|
||||
await router.push({
|
||||
name: 'blobPathDecoded',
|
||||
params: { path: '/some/file.js' },
|
||||
...routerOverride,
|
||||
});
|
||||
|
||||
mockResolver = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
|
|
@ -75,6 +81,7 @@ const createComponent = async (
|
|||
};
|
||||
|
||||
describe('Blob controls component', () => {
|
||||
const findOpenMrBadge = () => wrapper.findComponent(OpenMrBadge);
|
||||
const findFindButton = () => wrapper.findByTestId('find');
|
||||
const findBlameButton = () => wrapper.findByTestId('blame');
|
||||
const findPermalinkButton = () => wrapper.findByTestId('permalink');
|
||||
|
|
@ -85,6 +92,36 @@ describe('Blob controls component', () => {
|
|||
await createComponent();
|
||||
});
|
||||
|
||||
describe('MR badge', () => {
|
||||
it('should render the baadge if `filter_blob_path` flag is on', async () => {
|
||||
await createComponent({ glFeatures: { filterBlobPath: true } });
|
||||
expect(findOpenMrBadge().exists()).toBe(true);
|
||||
expect(findOpenMrBadge().props('blobPath')).toBe('/some/file.js');
|
||||
expect(findOpenMrBadge().props('projectPath')).toBe('some/project');
|
||||
});
|
||||
|
||||
it('should not render the baadge if `filter_blob_path` flag is off', async () => {
|
||||
await createComponent({ glFeatures: { filterBlobPath: false } });
|
||||
expect(findOpenMrBadge().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showBlobControls', () => {
|
||||
it('should not render blob controls when filePath does not exist', async () => {
|
||||
await createComponent({
|
||||
routerOverride: { name: 'blobPathDecoded', params: null },
|
||||
});
|
||||
expect(wrapper.element).not.toBeVisible();
|
||||
});
|
||||
|
||||
it('should not render blob controls when route name is not blobPathDecoded', async () => {
|
||||
await createComponent({
|
||||
routerOverride: { name: 'blobPath', params: { path: '/some/file.js' } },
|
||||
});
|
||||
expect(wrapper.element).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FindFile button', () => {
|
||||
it('renders FindFile button', () => {
|
||||
expect(findFindButton().exists()).toBe(true);
|
||||
|
|
@ -114,13 +151,15 @@ describe('Blob controls component', () => {
|
|||
});
|
||||
|
||||
it('does not render blame button when blobInfo.storedExternally is true', async () => {
|
||||
await createComponent({}, { storedExternally: true });
|
||||
await createComponent({ blobInfoOverrides: { storedExternally: true } });
|
||||
|
||||
expect(findBlameButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render blame button when blobInfo.externalStorage is "lfs"', async () => {
|
||||
await createComponent({}, { storedExternally: true, externalStorage: 'lfs' });
|
||||
await createComponent({
|
||||
blobInfoOverrides: { storedExternally: true, externalStorage: 'lfs' },
|
||||
});
|
||||
|
||||
expect(findBlameButton().exists()).toBe(false);
|
||||
});
|
||||
|
|
@ -164,7 +203,7 @@ describe('Blob controls component', () => {
|
|||
|
||||
describe('BlobOverflow dropdown', () => {
|
||||
it('renders BlobOverflow component with correct props', async () => {
|
||||
await createComponent({}, {}, { blobOverflowMenu: true });
|
||||
await createComponent({ glFeatures: { blobOverflowMenu: true } });
|
||||
|
||||
expect(findOverflowMenu().exists()).toBe(true);
|
||||
expect(findOverflowMenu().props()).toEqual({
|
||||
|
|
@ -177,22 +216,26 @@ describe('Blob controls component', () => {
|
|||
});
|
||||
|
||||
it('passes the correct isBinary value to BlobOverflow when viewing a binary file', async () => {
|
||||
await createComponent(
|
||||
{ isBinary: true },
|
||||
{
|
||||
await createComponent({
|
||||
props: {
|
||||
isBinary: true,
|
||||
},
|
||||
blobInfoOverrides: {
|
||||
simpleViewer: {
|
||||
...blobControlsDataMock.repository.blobs.nodes[0].simpleViewer,
|
||||
fileType: 'podfile',
|
||||
},
|
||||
},
|
||||
{ blobOverflowMenu: true },
|
||||
);
|
||||
glFeatures: {
|
||||
blobOverflowMenu: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(findOverflowMenu().props('isBinary')).toBe(true);
|
||||
});
|
||||
|
||||
it('copies to clipboard raw blob text, when receives copy event', async () => {
|
||||
await createComponent({}, {}, { blobOverflowMenu: true });
|
||||
await createComponent({ glFeatures: { blobOverflowMenu: true } });
|
||||
|
||||
jest.spyOn(navigator.clipboard, 'writeText');
|
||||
findOverflowMenu().vm.$emit('copy');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
export const openMRQueryResult = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
project: {
|
||||
__typename: 'Project',
|
||||
id: '1234',
|
||||
mergeRequests: {
|
||||
count: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const zeroOpenMRQueryResult = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
project: {
|
||||
__typename: 'Project',
|
||||
id: '1234',
|
||||
mergeRequests: {
|
||||
count: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlBadge } from '@gitlab/ui';
|
||||
import OpenMrBadge from '~/repository/components/header_area/open_mr_badge.vue';
|
||||
import getOpenMrCountsForBlobPath from '~/repository/queries/open_mr_counts.query.graphql';
|
||||
import { logError } from '~/lib/logger';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { useFakeDate } from 'helpers/fake_date';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { openMRQueryResult, zeroOpenMRQueryResult } from './mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
jest.mock('~/lib/logger');
|
||||
jest.mock('~/sentry/sentry_browser_wrapper');
|
||||
|
||||
describe('OpenMrBadge', () => {
|
||||
let wrapper;
|
||||
let requestHandler;
|
||||
|
||||
const defaultProps = {
|
||||
projectPath: 'group/project',
|
||||
blobPath: 'path/to/file.js',
|
||||
};
|
||||
|
||||
function createComponent(props = {}, mockResolver = openMRQueryResult) {
|
||||
requestHandler = mockResolver;
|
||||
const mockApollo = createMockApollo([[getOpenMrCountsForBlobPath, mockResolver]]);
|
||||
|
||||
wrapper = shallowMount(OpenMrBadge, {
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
currentRef: 'main',
|
||||
},
|
||||
apolloProvider: mockApollo,
|
||||
});
|
||||
}
|
||||
|
||||
describe('rendering', () => {
|
||||
it('does not render badge when query is loading', () => {
|
||||
createComponent();
|
||||
expect(wrapper.findComponent(GlBadge).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render badge when there are no open MRs', async () => {
|
||||
createComponent({}, zeroOpenMRQueryResult);
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.findComponent(GlBadge).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders badge when when there are open MRs', async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
|
||||
const badge = wrapper.findComponent(GlBadge);
|
||||
expect(badge.exists()).toBe(true);
|
||||
expect(badge.props('variant')).toBe('success');
|
||||
expect(badge.props('icon')).toBe('merge-request');
|
||||
expect(wrapper.text()).toBe('3 open');
|
||||
});
|
||||
});
|
||||
|
||||
describe('computed properties', () => {
|
||||
useFakeDate();
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({});
|
||||
});
|
||||
|
||||
it('computes queryVariables correctly', () => {
|
||||
expect(requestHandler).toHaveBeenCalledWith({
|
||||
blobPath: 'path/to/file.js',
|
||||
createdAfter: '2020-06-07',
|
||||
projectPath: 'group/project',
|
||||
targetBranch: ['main'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('apollo query', () => {
|
||||
it('handles apollo error correctly', async () => {
|
||||
const mockError = new Error();
|
||||
createComponent({}, jest.fn().mockRejectedValueOnce(mockError));
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.findComponent(GlBadge).exists()).toBe(false);
|
||||
expect(logError).toHaveBeenCalledWith(
|
||||
'Failed to fetch merge request count. See exception details for more information.',
|
||||
mockError,
|
||||
);
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(mockError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Resolvers::GroupsResolver, feature_category: :groups_and_projects do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
include GraphqlHelpers
|
||||
|
||||
describe '#resolve' do
|
||||
|
|
@ -75,5 +77,23 @@ RSpec.describe Resolvers::GroupsResolver, feature_category: :groups_and_projects
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with `all_available` argument' do
|
||||
where :args, :expected_param do
|
||||
{} | { all_available: true }
|
||||
{ all_available: nil } | { all_available: true }
|
||||
{ all_available: true } | { all_available: true }
|
||||
{ all_available: false } | { all_available: false }
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'pass the correct parameter to the GroupsFinder' do
|
||||
expect(GroupsFinder).to receive(:new)
|
||||
.with(user, hash_including(**expected_param)).and_call_original
|
||||
|
||||
resolve(described_class, args: args, ctx: { current_user: user })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -109,30 +109,32 @@ RSpec.describe MergeRequests::BaseService, feature_category: :code_review_workfl
|
|||
end
|
||||
end
|
||||
|
||||
context 'async: true' do
|
||||
it 'enques a CreatePipelineWorker' do
|
||||
expect(MergeRequests::CreatePipelineService).not_to receive(:new)
|
||||
expect(MergeRequests::CreatePipelineWorker)
|
||||
.to receive(:perform_async)
|
||||
.with(project.id, user.id, merge_request.id, { "allow_duplicate" => false })
|
||||
.and_call_original
|
||||
context 'when async: true' do
|
||||
it 'executes MergeRequests::CreatePipelineService async' do
|
||||
service = instance_double(MergeRequests::CreatePipelineService)
|
||||
|
||||
Sidekiq::Testing.fake! do
|
||||
expect { subject.execute(merge_request, async: true) }.to change(MergeRequests::CreatePipelineWorker.jobs, :size).by(1)
|
||||
end
|
||||
expect(MergeRequests::CreatePipelineService)
|
||||
.to receive(:new)
|
||||
.with(project: project, current_user: user, params: { allow_duplicate: false })
|
||||
.and_return(service)
|
||||
|
||||
expect(service).to receive(:execute_async).with(merge_request)
|
||||
|
||||
subject.execute(merge_request, async: true)
|
||||
end
|
||||
|
||||
context 'allow_duplicate: true' do
|
||||
context 'when allow_duplicate: true' do
|
||||
it 'passes :allow_duplicate as true' do
|
||||
expect(MergeRequests::CreatePipelineService).not_to receive(:new)
|
||||
expect(MergeRequests::CreatePipelineWorker)
|
||||
.to receive(:perform_async)
|
||||
.with(project.id, user.id, merge_request.id, { "allow_duplicate" => true })
|
||||
.and_call_original
|
||||
service = instance_double(MergeRequests::CreatePipelineService)
|
||||
|
||||
Sidekiq::Testing.fake! do
|
||||
expect { subject.execute(merge_request, async: true, allow_duplicate: true) }.to change(MergeRequests::CreatePipelineWorker.jobs, :size).by(1)
|
||||
end
|
||||
expect(MergeRequests::CreatePipelineService)
|
||||
.to receive(:new)
|
||||
.with(project: project, current_user: user, params: { allow_duplicate: true })
|
||||
.and_return(service)
|
||||
|
||||
expect(service).to receive(:execute_async).with(merge_request)
|
||||
|
||||
subject.execute(merge_request, async: true, allow_duplicate: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -301,7 +301,9 @@ RSpec.describe MergeRequests::CreatePipelineService, :clean_gitlab_redis_cache,
|
|||
|
||||
describe '#execute_async' do
|
||||
it 'queues a merge request pipeline creation' do
|
||||
expect(MergeRequests::CreatePipelineWorker).to receive(:perform_async)
|
||||
expect(MergeRequests::CreatePipelineWorker).to receive(:perform_async).with(
|
||||
project.id, user.id, merge_request.id, { 'pipeline_creation_request' => anything }
|
||||
)
|
||||
expect(GraphqlTriggers).to receive(:merge_request_merge_status_updated).with(merge_request)
|
||||
|
||||
service.execute_async(merge_request)
|
||||
|
|
|
|||
|
|
@ -120,8 +120,8 @@ RSpec.describe MergeRequests::RefreshService, feature_category: :code_review_wor
|
|||
end
|
||||
|
||||
it 'triggers mergeRequestMergeStatusUpdated GraphQL subscription conditionally' do
|
||||
expect(GraphqlTriggers).to receive(:merge_request_merge_status_updated).with(@merge_request)
|
||||
expect(GraphqlTriggers).to receive(:merge_request_merge_status_updated).with(@another_merge_request)
|
||||
expect(GraphqlTriggers).to receive(:merge_request_merge_status_updated).twice.with(@merge_request)
|
||||
expect(GraphqlTriggers).to receive(:merge_request_merge_status_updated).twice.with(@another_merge_request)
|
||||
expect(GraphqlTriggers).not_to receive(:merge_request_merge_status_updated).with(@fork_merge_request)
|
||||
|
||||
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
|
||||
|
|
|
|||
|
|
@ -495,9 +495,14 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
|
|||
|
||||
shared_examples_for "creates a new pipeline" do
|
||||
it "creates a new pipeline" do
|
||||
expect(MergeRequests::CreatePipelineWorker)
|
||||
.to receive(:perform_async)
|
||||
.with(project.id, user.id, merge_request.id, { "allow_duplicate" => true })
|
||||
service = instance_double(MergeRequests::CreatePipelineService)
|
||||
|
||||
expect(MergeRequests::CreatePipelineService)
|
||||
.to receive(:new)
|
||||
.with(project: project, current_user: user, params: { allow_duplicate: true })
|
||||
.and_return(service)
|
||||
|
||||
expect(service).to receive(:execute_async).with(merge_request)
|
||||
|
||||
update_merge_request(target_branch: new_target_branch)
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue